Node.js/TypeScript REST API for managing candidate CVs/resumes with auth, PDF export, Redis caching, and Winston logging.
Version: 1.0.0 | Author: DatVT | License: ISC
| Layer | Tech |
|---|---|
| Runtime | Node.js + TypeScript 5.5 (strict, CommonJS) |
| Framework | Express 4.19 |
| Database | MongoDB + Mongoose 8.4 |
| Cache / Blacklist | Redis 4.6 (in-memory fallback) |
| Auth | JWT (access + refresh), Bcrypt (12 rounds) |
| Validation | Joi 17.13 |
| Puppeteer 22.13 + PDFKit 0.15 + Pug 3.0 | |
| Logging | Winston 3.19 + daily-rotate-file |
| Testing | Jest 29 + ts-jest |
npm run dev # ts-node + nodemon hot reload
npm run build # tsc + copy views/public → dist/
npm start # build + NODE_ENV=production node dist/server.js
npm test # jest --passWithNoTests
npm run env:setup # cp .env.example .env
npm run env:dev # cp .env.development .env
npm run env:prod # cp .env.production .env
npm run copy # copy views + public to dist/ (post-build fix)NODE_ENV=development
LOCAL_PORT=3001 # prod uses 3008
MONGO_URI=... # full URI, or use MONGOBD_USER + MONGOBD_PASSWORD
MONGOBD_USER=...
MONGOBD_PASSWORD=...
TOKEN_SECRET=... # 32+ chars, signs access tokens
TOKEN_REFRESH=... # signs refresh tokens
TOKEN_EXP_IN=... # access token expiry
SESSION_SECRET=...
REDIS_URL=redis://localhost:6379 # optional; fallback to in-memory if absent
src/
├── server.ts # Entry: Express setup, MongoDB connect, Redis init
├── alias.ts # module-alias: @ → src/ (dev) or dist/ (prod)
├── config/
│ ├── process.config.ts # Env var validation + export
│ ├── cors.config.ts # CORS: origin '*'
│ ├── session.config.ts # Express session config
│ ├── joi.config.ts # Shared Joi schemas (email, password, phone, etc.)
│ └── regex.config.ts # Password + phone regex
├── database/
│ └── mongo.db.ts # Singleton MongoDB connection manager
├── middlewares/
│ ├── verifyToken.middleware.ts # JWT extraction + blacklist check; attaches req.user._id
│ ├── rateLimit.middleware.ts # Redis-backed rate limit (100 req/15 min); mem fallback
│ ├── errors.middleware.ts # Global error handler; AppError-aware; stack in dev only
│ └── requestLogger.middleware.ts # Logs method, URL, status, duration via Winston
├── models/
│ ├── candidate.model.ts
│ ├── generalInformation.model.ts
│ ├── experience.model.ts
│ ├── education.model.ts
│ ├── project.model.ts
│ ├── certificate.model.ts
│ ├── award.model.ts
│ ├── reference.modal.ts
│ └── part/index.ts # Reusable sub-schemas (skills, languages, socialMedia)
├── routers/
│ ├── api/v1/ # All active routes (see API section)
│ └── api/v2/ # Auth v2 (WIP)
├── auth/
│ ├── auth.controller.ts
│ └── auth.service.ts
├── candidate/
│ ├── candidate.controller.ts
│ └── candidate.service.ts
├── candidate_profile/ # One controller+service+validate per CV section
│ ├── experience/
│ ├── education/
│ ├── general_information/
│ ├── awards/
│ ├── certificates/
│ ├── project/
│ ├── reference_information/
│ └── BaseController.ts # baseGetAll() + baseDelete() shared across sections
├── candidate_me/
│ └── index.ts # Public profile aggregation + PDF export
├── services/
│ ├── index.ts # Core DB ops: baseFindDocument, baseCreateDocument, etc.
│ ├── redis.ts # Redis client singleton (init/get/close/isAvailable)
│ └── createPDF.ts # Puppeteer PDF generation (createCV, pageRender)
├── utils/
│ ├── jwt.ts # jwtSign(), jwtVerify()
│ ├── bcrypt.ts # bcryptGenerateSalt(), bcryptCompareHash()
│ ├── tokenBlacklist.ts # Redis/mem blacklist; cleanup every 60s; key: blacklist:{token}
│ ├── helper.ts # asyncHandler, throwError, formatReturn, response helpers
│ ├── helper-auth.ts # extractTokenFromRequest() (header/body/query/cookie)
│ ├── valid.ts # validateSchema() (Joi), validateModel() (Mongoose)
│ ├── querySafe.ts # QuerySafe: blocks $ and javascript: to prevent NoSQL injection
│ ├── timeout.ts # withTimeout, withDBTimeout(5s), withRedisTimeout(2s)
│ └── index.ts # Re-exports all utils
├── errors/
│ └── index.ts # AppError hierarchy (see Errors section)
├── types/
│ ├── base.type.ts # BaseReturn interface, Collections enum
│ ├── candidate.type.ts # Types for PDF rendering
│ └── express.d.ts # Extends Express Request: user?: { _id: string }
├── logger/ # Winston setup: console + combined + error logs; JSON in prod
├── constant/ # App-wide constants
├── plugins/joi/ # Custom Joi plugins
├── views/ # Pug templates for PDF
└── public/ # Static assets + generated PDFs
- Request logger (Winston)
- Session middleware
- CORS (origin:
*) - Body parser (JSON + URL-encoded)
GET /health— exempt from rate limit- Rate limiter (Redis or mem)
- Static files (
public/) - API router
- Global error handler
Pug is set as view engine. Dev: port 3001, Prod: port 3008.
All v1 routes: /api/v1/... — JWT required except auth.
| Method | Path | Description |
|---|---|---|
| POST | /register |
Create user, bcrypt hash password |
| GET | /login |
Validate + return access + refresh tokens |
| POST | /logout |
Blacklist current token |
| POST | /refresh |
Rotate tokens; blacklist old refresh token |
| Method | Path | Description |
|---|---|---|
| GET | /:email |
Get profile by email |
| PUT | /update |
Full update |
| PATCH | /update |
Partial update |
Sections: education, experience, award, certificate, project, reference, generalInformation
| Method | Path | Description |
|---|---|---|
| GET | / |
List all for authenticated user |
| POST | /create |
Create entry |
| PUT | /update |
Update entry |
| DELETE | /delete/:id |
Delete by ID (ownership checked) |
generalInformation also has PATCH /update.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
None | Health check |
| GET | /api/me/:email |
None | Public profile |
| GET | /api/download-pdf |
Token via query | Export PDF |
POST /auth/register→ validate Joi → check duplicate email → bcrypt(password, 12) → create Candidate docPOST /auth/login→ find by email → bcryptCompare → signaccessToken(TOKEN_SECRET) +refreshToken(TOKEN_REFRESH) with payload{ _id: candidateId }- All protected requests →
verifyTokenmiddleware → extract Bearer token →jwtVerify→ check blacklist → attachreq.user._id POST /auth/refresh→ verify refresh token → blacklist old refresh token → issue new pairPOST /auth/logout→ add token to blacklist (Redis TTL = token remaining exp; mem fallback)
All models use Mongoose with timestamps: true and candidateId foreign key (except Candidate).
| Model | Key Fields |
|---|---|
| Candidate | email, password, firstName, lastName, gender, marital, birthday, address, phone, introduction, socialMedia |
| GeneralInformation | candidateId, position/career, professionalSkills[], personalSkills[], foreignLanguages[], workLocation, workForm |
| Experience | candidateId, company, position, startDate, endDate, isCurrent, description, skills[] |
| Education | candidateId, school, major, startDate, endDate, isCurrent, description |
| Project | candidateId, name, position, description, technology[], startDate, endDate, isWorking, images[], link |
| Certificate | candidateId, name, organization, startDate, endDate, isNoExpiration, link, images[], description |
| Award | candidateId, name, organization, issueDate, link, images[], description |
| Reference | candidateId, fullName, phone, company, position |
Reusable sub-schemas (models/part/index.ts): foreignLanguageSchema, professionalSkillsSchema, personalSkills, socialMediaSchema
services/index.ts exposes base DB operations used by all feature services:
baseFindDocument(model, query, findMany?)— query with NoSQL injection guardbaseCreateDocument(model, data, hooks?)— create with Mongoose validation + optional hooksbaseUpdateDocument(model, filter, data)— update with validationbaseDeleteDocument(model, id, candidateId)— delete with ownership checkbasePatchDocument(model, filter, data)— partial update
BaseController.ts wraps baseGetAll() (find all by candidateId) and baseDelete() shared across all CV section controllers.
AppError (base: statusCode, message, errorCode, isOperational)
├── ValidationError 400 VALIDATION_ERROR
├── BadRequestError 400 BAD_REQUEST
├── AuthenticationError 401 UNAUTHORIZED
│ ├── InvalidCredentialsError INVALID_CREDENTIALS
│ ├── TokenExpiredError TOKEN_EXPIRED
│ ├── TokenRevokedError TOKEN_REVOKED
│ └── InvalidTokenError INVALID_TOKEN
├── AuthorizationError 403 FORBIDDEN
├── NotFoundError 404 NOT_FOUND
└── ConflictError 409 CONFLICT
Global error middleware catches all AppError instances, logs via Winston, and returns { errorCode, message, stack? } (stack dev-only).
- NoSQL injection:
QuerySafeclass inutils/querySafe.tsblocks$operators andjavascript:patterns before any DB query - Password: bcrypt with 12 salt rounds
- Token blacklist: Redis
blacklist:{token}with TTL; in-memory Map fallback; cleanup job every 60s - Rate limiting: Redis-backed (100 req/15 min general, 150 req/15 min auth); Redis failure falls back to in-memory
- Token extraction: Supports Bearer header, request body, query param, and cookie
npm test # run all tests- Config:
jest.config.ts— presetts-jest, rootsrc/, module alias@/* → src/*, coverage fromsrc/**/*.{js,ts} - Test files:
src/__tests__/and co-located.test.ts
| Test File | Coverage |
|---|---|
| auth/auth.service.test.ts | register, login, email check (mocks: CandidateModel, bcrypt, JWT) |
| auth/auth.controller.test.ts | controller layer |
| auth/refreshToken.test.ts | token rotation |
| middlewares/verifyToken.test.ts | token extraction + blacklist check |
| middlewares/rateLimit.test.ts | rate limiting logic |
| middlewares/requestLogger.test.ts | request logging |
| utils/bcrypt.test.ts | hash + compare |
| utils/valid.test.ts | Joi + Mongoose validation |
| database/mongo.db.ts | DB connection |
Winston config in src/logger/:
- Console: all levels in dev; errors only in prod
- Combined log:
logs/combined-YYYY-MM-DD.log(daily rotation) - Error log:
logs/error-YYYY-MM-DD.log - Format: simple in dev, JSON in prod
| Alias | Resolves to |
|---|---|
@/* |
src/* (dev via tsconfig-paths) / dist/* (prod via module-alias) |
Always import as @/utils/helper, @/models/candidate.model, etc.
npm run build=tsc && npm run copycopystep:cp -R ./src/views ./src/public ./dist/— required because Pug templates and static assets are not compiled by tsc- If build succeeds but PDF/views break in prod: run
npm run copymanually dist/is gitignored; always rebuild before deploying