A secure task-manager backend built with NestJS, TypeORM and PostgreSQL. Users register and log in with JWT authentication, then manage their own projects and the tasks inside them.
The Week 8 work hardened validation, introduced a single error contract, completed the Swagger/OpenAPI documentation, validated the environment and added a runnable Postman collection.
- NestJS 12 (ESM, Express platform)
- TypeORM 1.x with PostgreSQL (
pg) class-validator/class-transformerDTO validationjsonwebtokenandbcryptfor authentication@nestjs/swaggerfor OpenAPI 3 documentationzodfor environment validation- Vitest + Supertest for unit and e2e tests
- JWT bearer authentication; passwords hashed with bcrypt
- Every project and task query is scoped to the authenticated user
- DTO validation with trimming, length limits, password complexity and enum checks
- Unknown request properties are rejected, not silently stripped
- Route
:idparameters validated before reaching the database - Custom exception hierarchy with stable machine-readable error codes
- One standard JSON envelope for validation, HTTP, database and unknown errors
- Swagger UI at
/apiand an exportable OpenAPI document - Validated environment variables with an example file
- Postman collection covering happy paths, auth and error cases
- Node.js 20+
- PostgreSQL 14+
npm install
cp .env.example .env # then fill in the real values
npm run migration:run # creates the schema
npm run start:dev # http://localhost:3000, Swagger at /apinpm run migration:run requires a reachable database and a valid .env,
because both the app and the TypeORM CLI validate the environment on startup.
| Variable | Required | Default | Purpose |
|---|---|---|---|
NODE_ENV |
no | development |
development, test or production |
PORT |
no | 3000 |
HTTP port |
DB_HOST |
no | localhost |
PostgreSQL host |
DB_PORT |
no | 5432 |
PostgreSQL port |
DB_USERNAME |
no | postgres |
PostgreSQL user |
DB_PASSWORD |
yes | — | PostgreSQL password |
DB_DATABASE |
no | tasks_db |
Database name |
JWT_SECRET |
yes | — | Signing secret, at least 16 characters |
SWAGGER_ENABLED |
no | true outside production |
true/false; Swagger is off in production unless set |
DATA_ROOT_DIR, TASK_FILE_NAME |
no | — | Only used by the unused file-storage module in src/db |
Configuration is validated once at startup by src/config/env.validation.ts.
A missing DB_PASSWORD or a short JWT_SECRET stops the process with a clear
message instead of failing later at runtime. .env is git-ignored;
.env.example lists every variable with placeholders.
| Command | Description |
|---|---|
npm run start:dev |
Watch mode |
npm run start:prod |
Run the compiled build |
npm run build |
Compile TypeScript |
npm run lint |
Type-aware oxlint over src and test |
npm run format |
Prettier |
npm test |
Unit tests |
npm run test:e2e |
API e2e tests |
npm run test:cov |
Unit tests with coverage |
npm run migration:run |
Apply migrations |
npm run migration:generate -- src/migrations/<Name> |
Generate a migration |
npm run migration:revert |
Revert the last migration |
npm run openapi:export |
Build and write docs/openapi.json |
npm run openapi:export boots the app to read the route metadata, so it needs
the same working database configuration as npm run start.
Base URL: http://localhost:3000. Protected routes expect
Authorization: Bearer <token>.
| Method | Path | Auth | Success |
|---|---|---|---|
GET |
/ |
no | 200 Hello World! |
POST |
/users/register |
no | 201 { message } |
POST |
/users/login |
no | 200 JWT as plain text |
GET |
/users/profile |
yes | 200 { id, name, email, role } |
PATCH |
/users/:id/role |
admin | 200 { id, role: "admin" } |
GET |
/projects |
yes | 200 projects + tasks |
POST |
/projects |
yes | 201 project |
GET |
/projects/:id |
yes | 200 project + tasks |
PUT |
/projects/:id |
yes | 200 project |
DELETE |
/projects/:id |
yes | 200 { id, deleted: true } |
GET |
/tasks |
yes | 200 tasks |
POST |
/tasks |
yes | 201 task |
GET |
/tasks/:id |
yes | 200 task |
PATCH |
/tasks/:id |
yes | 200 task |
DELETE |
/tasks/:id |
yes | 200 { id, deleted: true } |
Task status values: todo, in_progress, completed.
# Register
curl -X POST http://localhost:3000/users/register \
-H "Content-Type: application/json" \
-d '{"name":"Test User","email":"test@example.com","password":"Password123"}'
# Log in and capture the token (the body is the raw JWT)
TOKEN=$(curl -s -X POST http://localhost:3000/users/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Password123"}')
# Use it
curl http://localhost:3000/users/profile \
-H "Authorization: Bearer $TOKEN"Passwords must be 8–72 characters and contain an uppercase letter, a lowercase letter and a number. Emails and names are trimmed; emails are lowercased.
- Unknown body properties are rejected with
400. - Titles are trimmed, non-empty and capped at 100 characters (tasks) or 255 (projects).
projectIdand route ids must be positive integers.UpdateTaskDto/UpdateProjectDtoare partial: omitted fields are allowed, invalid values are not.
Database writes are tenant-scoped: a task can only be created in, read from,
updated in or deleted from a project owned by the authenticated user. Attempting
to use another user's project returns 404 rather than leaking its existence.
Every error response uses the same envelope:
{
"success": false,
"statusCode": 404,
"error": "Not Found",
"code": "NOT_FOUND",
"message": "Task with id \"99\" not found",
"details": [{ "field": "email", "message": "email must be an email" }],
"path": "/tasks/99",
"method": "GET",
"timestamp": "2026-11-04T10:00:00.000Z"
}details is only present for validation failures. Unknown 5xx errors return a
generic message; the stack trace is logged, never returned.
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR |
400 | Body or route parameter failed validation |
BAD_REQUEST |
400 | Malformed request |
INVALID_INPUT |
400 | Database rejected a value (missing/format/constraint) |
UNAUTHORIZED |
401 | Missing, invalid or expired token |
INVALID_CREDENTIALS |
401 | Wrong email or password |
FORBIDDEN |
403 | Authenticated but not allowed |
NOT_FOUND |
404 | Resource does not exist or is not owned by the caller |
DUPLICATE_RESOURCE |
409 | Unique value already exists (e.g. email) |
FOREIGN_KEY_VIOLATION |
409 | Referenced row does not exist |
BUSINESS_RULE_VIOLATION |
422 | Request is valid but breaks a business rule |
INTERNAL_SERVER_ERROR |
500 | Unexpected failure |
- Swagger UI:
http://localhost:3000/api - OpenAPI JSON:
http://localhost:3000/api-json - Exported document:
docs/openapi.json(regenerate withnpm run openapi:export)
Every endpoint is tagged (App, Users, Projects, Tasks), documents its
success schema, :id parameter, bearer requirement and the standard error
responses. Response DTOs exclude password fields.
Swagger is enabled outside production by default. Set SWAGGER_ENABLED=false to
hide it (for example on a public deployment) or true to force it on.
Files:
postman/task-manager.postman_collection.jsonpostman/task-manager.postman_environment.json
Import both into Postman and select the Task Manager - Local environment.
- Start the API (
npm run start:dev). - Run Users → Register — it generates a unique email.
- Run Users → Login — it stores the returned JWT in the
tokenvariable. - Run the project and task requests in order, then the Errors folder.
The collection uses collection-level bearer auth, so every protected request
picks up {{token}} automatically; public requests override it with noauth.
Request tests assert status codes, response shapes and the stored ids, so a full
run doubles as a smoke test.
Before sharing or publishing, add the real base URL to the environment. To publish the docs, open the collection in Postman and use Share → Publish documentation (a Postman account is required; publishing from the CLI is not supported). Alternatively commit the collection as done here — importing it requires no account.
npm test # 15 files, 36 unit tests
npm run test:e2e # 2 files, 28 e2e testsUnit coverage: exception filter envelope and database-error mapping, validation pipe flattening, id pipe, DTO rules for users/tasks/projects, JWT guard, service tenant checks.
The API e2e suite (test/api.e2e-spec.ts) boots the real controllers, services,
validation pipe and exception filter over HTTP with in-memory repository doubles.
It covers registration and normalization, duplicate email (service and database
constraint), structured validation errors, unknown properties, login, token
handling, admin authorization, route parameter validation, CRUD for projects and
tasks, cross-tenant rejection and the 404 envelope. It does not exercise SQL, so
run a migration check against a real database when changing entities.
docs/week8-compliance.md— requirement-by-requirement assessmentdocs/week8-phase-0-and-2.md— baseline and error handlingdocs/week8-phase-1.md— validation hardeningdocs/week8-phase-3.md— Swagger/OpenAPIdocs/week8-phase-4-and-5.md— environment validation, Postman and docsdocs/week8-phase-6.md— e2e coverage
JWT_SECREThas no fallback; signing and verification both read the validated configuration.- Passwords are bcrypt-hashed and never returned by the API.
- Unique index on
users.email; duplicate registration returns409. - All project/task reads and writes are scoped to the token's user id.
- Swagger is disabled in production unless explicitly enabled.
- The debug logging and direct
process.envreads from earlier weeks are gone.