Copy .env.example to .env and adjust if needed:
cp .env.example .env
๐ **Quick Start (1-2 commands):**
```bash
# 1. Setup everything (database, dependencies, seed data)
npm run setup # Runs install, db:up, migrate, and seed
# 2. Start the API server
npm run dev
# Open http://localhost:3000/docs for API documentation- Fastify + TypeScript project with Dockerized PostgreSQL
- Prisma schema, migrations, and seed data (1000+ applicants)
- CRUD routes for companies, branches, jobs, applicants
- Assessment system with templates & submissions (answer snapshots)
- Matching algorithm with negative marking, recency boost, location bonus
- Deterministic tie-breaking & explainable results
- Top 5 candidates endpoint (
/jobs/:id/top-candidates) - API documentation at
/docs(Swagger UI with parameter schemas) - Request-level tests (happy path, validation errors, 404s)
- Structured error handling and correlation ID logging
- Production-ready README with setup, architecture, and scoring explanation
- 1-2 command setup (
npm run setup+npm run dev)
Runtime: Node.js 20+ with TypeScript (ES modules)
Framework: Fastify 5.6 (lightweight, fast) with Swagger/OpenAPI integration
Database: PostgreSQL via Prisma ORM with Docker Compose
Validation: Zod schemas for type-safe request/response validation
Logging: Pino with request correlation IDs and structured error handling
Testing: Vitest + Supertest for end-to-end API testing
Why these choices:
- Fastify: Superior performance vs Express, built-in validation, excellent TypeScript support
- Prisma: Type-safe ORM, great migrations, prevents SQL injection by design
- Zod: Runtime validation with TypeScript inference, perfect for APIs
- Docker Compose: Consistent local PostgreSQL environment
This shows how the main business entities relate to each other in the LoadUp recruitment system:
Company (1) โโ (N) Branch
โ
Job (N) โโ (1) AssessmentTemplate โโ (N) AssessmentItem
โ โ
Applicant (N) โโ (N) Submission โโ (N) SubmissionAnswer
Real-World Example:
- DHL Logistics (Company) has Hamburg Branch + Berlin Branch (Branches)
- "Delivery Driver" job (Job) uses "Driving Skills Test" (AssessmentTemplate)
- Anna Schmidt (Applicant) takes the test, creating a Submission with her answers
- The matching algorithm finds the best candidates for "Delivery Driver at Hamburg Branch"
Key Design Decisions:
- Jobs belong to companies but are matched against specific branches (flexible hiring)
- Assessment templates are reusable across multiple jobs (efficient test management)
- Submissions snapshot correctness and weights at submission time (audit trail)
- Scoring uses pure SQL aggregation for performance and determinism
- Companies:
POST|GET|DELETE /companies(with search & pagination) - Branches:
POST|GET|DELETE /branches(filter by companyId) - Jobs:
POST|GET|DELETE /jobs(linked to assessment templates) - Applicants:
POST|GET|DELETE /applicants(searchable by name/email/city)
- Templates:
POST /assessment-templates(create with nested items) - Submissions:
POST /submissions(validate against template, record answers)
- Match Candidates:
POST /matches(sophisticated scoring algorithm) - Top Candidates:
GET /jobs/:id/top-candidates(cached-friendly) - Platform Stats:
GET /stats(usage metrics)
Top Candidates Design:
/jobs/:id/top-candidatesis a thin wrapper around/matcheswith sensible defaults (limit=5). For larger scale, this endpoint is an ideal candidate for Redis caching with short TTL, as "top 5" queries are frequently repeated by hiring managers.
Full API Documentation: Available at http://localhost:3000/docs (Swagger UI)
Documentation Integration: OpenAPI schemas are generated from Zod validation definitions and auto-mounted via Fastify Swagger. This ensures validation rules and documentation stay perfectly synchronized - any schema change automatically updates both runtime validation and API documentation.
Formula: total = raw + recencyBoost + locationBonus
Components:
- Raw Score:
correctWeight - (negativeMarkingFraction ร wrongWeight) - Recency Boost:
raw ร recencyBoostPct / 100(if within window) - Location Bonus:
locationBonusFixed + (raw ร locationBonusPctOfRaw / 100)
Configurability (runtime, no code changes):
{
"negativeMarkingFraction": 0.25, // 25% penalty for wrong answers
"recencyWindowDays": 30, // Recent = last 30 days
"recencyBoostPct": 10, // 10% boost for recent submissions
"locationBonusFixed": 5, // +5 points for local candidates
"locationBonusPctOfRaw": 0 // Percentage bonus (disabled by default)
}Deterministic Tie-Breaking: total DESC โ raw DESC โ lastSubmissionAt DESC โ applicantId ASC
Full Explainability: Each result includes detailed breakdown of score components.
Note: Replace
jobIdandbranchIdwith actual values from your seeded data. Get real IDs via:curl "http://localhost:3000/jobs?limit=1"andcurl "http://localhost:3000/branches?limit=1"
Request:
curl -X POST http://localhost:3000/matches \
-H "Content-Type: application/json" \
-d '{
"jobId": "cltxyz123abc456",
"branchId": "cltabc789def012",
"limit": 5,
"filters": { "cityEqualsBranch": false },
"scoring": {
"negativeMarkingFraction": 0.25,
"recencyWindowDays": 30,
"recencyBoostPct": 10,
"locationBonusFixed": 5,
"locationBonusPctOfRaw": 0
}
}'Response (example with realistic seed data):
{
"matches": [
{
"applicantId": "cltdef456ghi789",
"applicantName": "Anna Schmidt",
"applicantEmail": "anna.schmidt@example.com",
"applicantLocation": "Berlin",
"totalScore": 87.4,
"explanation": "Raw: 72.0 (9 correct, 2 wrong, -2.8 penalty) + Recency: 14.4 (20% of raw, submitted 5 days ago) + Location: 1.0 (5 fixed + 0% of raw, Berlin != Hamburg)",
"rawScore": 72.0,
"recencyBoost": 14.4,
"locationBonus": 1.0
}
],
"meta": {
"jobId": "cltxyz123abc456",
"jobTitle": "Delivery Driver",
"branchId": "cltabc789def012",
"branchName": "DHL Logistics - Hamburg Branch",
"branchLocation": "Hamburg",
"totalCandidates": 847,
"scoringConfig": {
"negativeMarkingFraction": 0.25,
"recencyWindowDays": 30,
"recencyBoostPct": 10,
"locationBonusFixed": 5,
"locationBonusPctOfRaw": 0
}
}
}# Default: ~1000 applicants, 5 companies, realistic submissions
npm run seed
# Large dataset: 5000+ applicants for performance testing
SEED_APPLICANTS=5000 npm run seedConfigurable via Environment Variables (set in .env file):
SEED_COMPANIES=4 # Number of logistics companies (default: 4)
SEED_BRANCHES=2 # Branches per company (default: 2)
SEED_JOBS=2 # Jobs per company (default: 2)
SEED_APPLICANTS=1000 # Total applicants (default: 1000)
SEED_TEMPLATES=3 # Assessment templates (default: 3)
SEED_ITEMS=10 # Items per template (default: 10)Realistic Data Generated:
- German logistics companies (DB Schenker, DHL, Hermes, GLS)
- Geographic clustering (Berlin, Munich, Hamburg, Cologne)
- 85% submission coverage with weighted assessment items
- Time-distributed submissions for recency testing
# Run all tests
npm test
# Test categories
npm test health.stats.test.ts # Health checks & stats
npm test errors.validation.test.ts # Error handling & validation
npm test matches.happy.test.ts # Matching algorithm end-to-endLoadUp Challenge E2E/Request-Level Test Requirements:
- โ Health & Stats endpoints: Complete system health verification and platform metrics
- โ Matching happy path: Full candidate ranking workflow with realistic data and explainable results
- โ Validation & error cases: Input validation, HTTP status codes, and structured error responses
Comprehensive Coverage:
- โ Happy path: Full matching workflow with realistic data
- โ Edge cases: Validation errors, 404s, empty results, company mismatches
- โ Determinism: Same inputs produce same candidate ordering
- โ Algorithm: Scoring components, configurability, and tie-breaking logic
Error Handling:
- Consistent HTTP status codes (400, 404, 409, 500)
- Structured error responses with field-level validation details
- No stack trace leakage to clients
Logging & Observability:
- Request correlation IDs (
x-request-idheader support) - Structured JSON logging with Pino
- Error tracking with request context
Performance:
- Pagination caps (max 100 items per request)
- Database indexes on search fields and foreign keys
- SQL-based scoring (no N+1 queries)
- Handles 100,000+ applicants efficiently
Security:
- Input validation on all endpoints (Zod schemas)
- SQL injection prevention (Prisma parameterized queries)
- Basic operational hygiene
# Start development server (auto-reload)
npm run dev
# Database management
npm run db:up # Start PostgreSQL in Docker
npm run migrate # Apply schema migrations
npm run seed # Generate test data
# Production build
npm run build
npm startThe system is production-ready with:
- Docker Compose setup for local development
- Environment variable configuration
- TypeScript compilation for production builds
- Structured logging for monitoring
- Database migrations for schema management
Simplifications:
- Jobs belong to companies; branches specified at match time (vs separate JobOpening entity)
- One assessment template per job (vs template reuse across jobs)
- No authentication/authorization (focus on core matching logic)
Performance Trade-offs:
- OFFSET pagination (vs keyset) - simpler but slower for deep pages
- Eager scoring calculation (vs precomputed rankings) - flexible but compute-intensive
Scaling Considerations:
- Read replicas for high query volume
- Caching for
/top-candidatesqueries - Keyset pagination for large result sets
- Background job processing for bulk operations
- Authentication: JWT-based auth with role-based access control
- Caching: Redis for hot queries and session management
- Real-time: WebSocket updates for live candidate rankings
- Analytics: Advanced reporting and candidate insights
- API Versioning: Backward compatibility for client integrations
I used AI assistance (GitHub Copilot) during this challenge to speed up development.
Specifically, it helped me with:
- Structuring the project setup (Fastify, Prisma, Docker)
- Drafting boilerplate for routes, validation, and seed scripts
- Iterating on the SQL scoring logic
- Debugging test failures and Postgres syntax issues
- Polishing documentation (README)
All final design decisions, domain modeling, and implementation details were reviewed, tested, and integrated by me.
This project is licensed under the MIT License - see the LICENSE file for details.
Built with โค๏ธ for LoadUp Challenge