A robust, production-ready Leave Management System API built using Node.js + TypeScript + NestJS and Prisma ORM.
By default, the project runs on SQLite for zero-config local evaluation. However, the database layer is fully compatible with PostgreSQL; transitioning simply requires swapping the database provider in schema.prisma and updating the DATABASE_URL environment variable.
- Framework: NestJS (TypeScript) - NestJS is selected as the preferred framework for its modularity, dependency injection container, and standard validation pipes (
class-validator/class-transformer), which ensure structured, maintainable code. - ORM & Database: Prisma with SQLite. Using Prisma allows us to use TypeScript-safe client queries and migrations. SQLite is chosen to facilitate instant local testing without needing Docker or a local PostgreSQL installation. Switching to PostgreSQL is fully supported out-of-the-box.
- Validations: Request bodies are strictly validated at the HTTP layer using NestJS global
ValidationPipewith whitelisting enabled.
- Design Decision: An employee's direct manager (specified by
managerIdin theUsertable) or any user with theADMINrole can approve their leave requests. An employee cannot approve their own leave request.
- Design Decision: Yes. The approver must have either the
MANAGERrole (and be the direct manager of the employee) or theADMINrole. RegularEMPLOYEEs or managers who are not the direct supervisor of the requester are rejected by the system.
- Design Decision: Half-days are fully supported.
- When submitting a half-day,
isHalfDaymust be set totrue, and bothstartDateandendDatemust be the same date. - The request counts as exactly
0.5working days. - Employees must choose a
halfDayOptionof eitherAMorPM. - Overlap validation allows an employee to submit both an
AMand aPMrequest on the same day, but blocks overlapping requests with identical options (e.g., twoAMrequests on the same day).
- When submitting a half-day,
- Design Decision: No. When a leave request is submitted:
- We query all registered public holidays from the
PublicHolidaytable. - The calculation engine iterates through each day in the requested range: weekend days (Saturday and Sunday) and registered public holiday dates are skipped.
- Only active working days are counted. If the calculated count is
0(e.g., a request containing only a weekend and a public holiday), the submission is rejected.
- We query all registered public holidays from the
- Design Decision: All dates (e.g.,
startDate,endDate, public holidays) are stored as ISO 8601 UTC date-only strings in the formatYYYY-MM-DD.- This avoids timezone offset shift issues (where a date becomes the previous/next day depending on local browser/server offsets).
- Chronological comparisons are handled using lexicographical string comparisons (e.g.,
'2026-06-01' < '2026-06-05'), which are supported natively by SQLite and PostgreSQL. - Parsing and manipulations use UTC methods (
Date.UTC) to guarantee timezone independence.
- Design Decision: Concurrency and race conditions are handled by wrapping the validation and creation inside an atomic database transaction (
prisma.$transaction).- Inside the transaction, we re-query the employee's current balance and fetch all active
PENDINGandAPPROVEDrequests. - If a concurrent request was created just milliseconds prior, the transaction reads the updated database state.
- If validations fail (insufficient balance or date overlap), the transaction aborts and rolls back.
- For PostgreSQL, this logic can be further hardened using
SELECT ... FOR UPDATErow-level locks on the user row, preventing concurrent transactions from checking balances simultaneously.
- Inside the transaction, we re-query the employee's current balance and fetch all active
- Design Proposal:
- Introduce an
ApprovalFlowtable that defines steps (e.g., Step 1: Manager, Step 2: Department Head, Step 3: HR). - Replace the single
statusandapproverIdon theLeaveRequesttable with a relation to a newLeaveRequestApprovalSteptable:model LeaveRequestApprovalStep { id String @id @default(uuid()) leaveRequestId String leaveRequest LeaveRequest @relation(fields: [leaveRequestId], references: [id]) stepNumber Int // 1, 2, 3... approverId String? // Nullable if assigned by role approverRole String // "MANAGER", "HR", "DEPT_HEAD" status String // "PENDING", "APPROVED", "REJECTED" comments String? updatedAt DateTime @updatedAt }
- When a request is submitted, we create the corresponding approval steps.
- The request itself remains in
PENDINGstatus. Only the step withstepNumber = 1is initially active. - When the active step is approved, we activate
stepNumber = 2. - The overall
LeaveRequeststatus is only marked asAPPROVEDonce the final step is completed. If any step isREJECTED, the request is immediately rejected.
- Introduce an
- Design Proposal:
- Data Level: Add a
tenantIdcolumn to all database tables. - API Level: Use a NestJS Middleware/Interceptor to extract the
tenantIdfrom the incoming request's authenticated JWT (or a request header likex-tenant-idfor testing). - Query Enforcement: Rather than manually appending
where: { tenantId }in every query (which is error-prone), use Prisma Client Extensions to apply a global query filter. This automatically injects tenant constraints to every query, update, and delete operation behind the scenes. - High-isolation clients: For strict isolation requirements, implement a database-per-tenant architecture where the database connection string is resolved dynamically based on the active tenant identifier.
- Data Level: Add a
POST /users: Create a new user (employee, manager, admin)GET /users: List all usersGET /users/:id: Get detailed user info, subordinates, and leave requests historyPATCH /users/:id/balance: Directly update an employee's leave balance (admin only)
POST /public-holidays: Register a public holiday (YYYY-MM-DDand description)GET /public-holidays: List all public holidaysDELETE /public-holidays/:date: Remove a public holiday
POST /leave-requests: Submit a leave request (performs working days calculation, overlap checks, balance checks)GET /leave-requests: List all leave requests (supports query filtersemployeeId,status, andmanagerIdto list requests from subordinates)GET /leave-requests/:id: View details of a specific requestPATCH /leave-requests/:id/approve: Approve a leave request (validates direct manager or ADMIN role, deducts leave balance atomically)PATCH /leave-requests/:id/reject: Reject a leave request
- Node.js: version 18 or above.
- npm: package manager.
npm installThis command creates the local SQLite database (dev.db), applies schema definitions, and seeds default users (Admin, Manager, two Employees) and 2026 public holidays.
npx prisma migrate dev# Starts development server (on port 3000 by default)
npm run start# Runs the full Jest test suite (unit and service tests)
npm run testYou can verify all workflows (date calculations, holiday exclusions, overlapping requests, AM/PM half-day exceptions, and approval access controls) using the provided automated script:
- Ensure the NestJS server is running:
npm run start - In a separate terminal window, run:
node scripts/test-api.js
This script will execute a series of tests against the server and print detailed success/failure states to the console.