Skip to content

musman92/nodejs-rest-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Scalable REST API with Node.js & Express (2026)

A production-ready REST API boilerplate built with Node.js, Express, and MongoDB (Mongoose). Companion code for the blog post "Freelance Node.js Developer: Best REST APIs with Express" on usmannadeem.com.

Built by Usman Nadeem — Freelance Full-Stack Developer | usmannadeem.com


Features

  • ✅ Layered architecture (Routes → Controllers → Services → Models)
  • ✅ MongoDB with Mongoose (connection pooling, indexing, timestamps)
  • ✅ JWT authentication (access + refresh token pattern)
  • ✅ Zod request validation
  • ✅ Redis response caching middleware
  • ✅ Centralized error handling with AppError
  • ✅ Global rate limiting
  • ✅ Response compression
  • ✅ Pagination built into the service layer
  • ✅ Health check endpoint
  • ✅ Jest + Supertest test suite

Architecture & Patterns

This boilerplate follows a set of deliberate, opinionated patterns. Here is what was used and why.


1. Layered Architecture (Separation of Concerns)

Routes → Controllers → Services → Models

What it is: Each layer has one responsibility and only talks to the layer directly below it.

  • Routes — map HTTP verbs and URLs to controller functions. Nothing else.
  • Controllers — handle the request and response cycle. They call services and return JSON. No business logic lives here.
  • Services — contain all business logic (duplicate checks, pagination, data transformation). This is the brain of the application.
  • Models — define the database schema and interact with MongoDB via Mongoose.

Why: When a bug occurs, you know exactly which layer to look in. When requirements change, you update one layer without touching the others. This also makes unit testing straightforward — you can test a service function without spinning up an HTTP server.


2. app.js vs server.js Split

app.js configures and exports the Express app. server.js imports it and calls app.listen().

Why: Supertest (our testing library) imports app directly and makes HTTP requests without binding to a port. If app.listen() lived inside app.js, every test run would open a real network port, causing port conflicts and slower tests.


3. JWT — Access Token + Refresh Token Pattern

Short-lived access tokens (15 minutes) are stored in memory on the client. Long-lived refresh tokens (7 days) are stored in an HttpOnly cookie.

Why: Storing tokens in localStorage exposes them to XSS attacks. This two-token pattern limits the blast radius of a stolen token to 15 minutes while keeping the user logged in via the refresh token.


4. Zod for Request Validation

All incoming request bodies are validated against a Zod schema before reaching the controller.

Why: Zod has first-class TypeScript support, a clean chainable API, and returns structured error objects that are easy to format and return to the client. The validate middleware factory keeps route files clean — validation is a one-liner per route.


5. Operational vs Programmer Errors (AppError)

The custom AppError class flags errors as isOperational: true. The global error handler only exposes the message to the client if the error is operational.

Why: Operational errors are expected (404 not found, 409 conflict). Programmer errors are unexpected (null reference, syntax error). You want to tell the client about the former and hide the latter — leaking stack traces in production is a security risk.


6. Redis Caching Middleware

The cache(ttl) middleware intercepts res.json(), stores the response in Redis, and returns the cached version on subsequent requests until TTL expires.

Why: Read-heavy endpoints (product listings, user profiles) hit the database on every request without caching. Redis keeps response times under 5ms for cached routes regardless of database load. The middleware is designed to fail silently — if Redis is unavailable, the request continues to the database without error.


7. Mongoose Connection in server.js (not app.js)

connectDB() is called in server.js before app.listen(). The app starts only after the database connection is confirmed.

Why: If the app starts before the database is ready, the first requests will fail with connection errors. Waiting for connectDB() to resolve ensures the API is only available when it is actually ready to serve requests.


8. Rate Limiting at the Router Level

express-rate-limit is applied to all /api routes globally.

Why: Without rate limiting, a single client can exhaust your server or database with repeated requests. Applying it at the /api prefix level protects all endpoints without having to add it route by route, while leaving the /health endpoint unrestricted for monitoring tools.


9. Versioned Routes (/api/v1/)

All routes are prefixed with /api/v1/.

Why: When you ship a breaking change to your API — a renamed field, a changed response shape — clients pinned to /api/v1/ are unaffected. You introduce /api/v2/ alongside it and migrate clients gradually. This is non-negotiable for any API with external consumers.


10. Password Field Hidden by Default (select: false)

The password field on the User schema has select: false.

Why: Mongoose will never include the password in query results unless you explicitly request it with .select('+password'). This prevents accidentally leaking hashed passwords in API responses.


Project Structure

/src
  /config          # MongoDB connection
  /controllers     # Request/response handlers (no business logic)
  /services        # Business logic and DB queries
  /models          # Mongoose schemas
  /routes          # Express routers
  /middleware      # Auth, error handling, cache, validation
  /utils           # AppError, paginate, generateToken
  /validators      # Zod schemas
/tests             # Jest + Supertest integration tests
app.js             # Express app setup (exported for testing)
server.js          # DB connection + server start

Getting Started

1. Clone and install

git clone https://github.com/usmannadeem/nodejs-rest-api-2026.git
cd nodejs-rest-api-2026
npm install

2. Configure environment

cp .env.example .env
# Edit .env with your values

3. Run in development

npm run dev

4. Run tests

npm test

5. Run in production (with PM2)

npm install -g pm2
pm2 start server.js -i max --name "my-api"
pm2 save
pm2 startup

API Endpoints

Method Endpoint Auth Description
POST /api/v1/users No Create a user
GET /api/v1/users Yes Get all users (paginated)
GET /api/v1/users/:id Yes Get user by ID
PATCH /api/v1/users/:id Yes Update user
DELETE /api/v1/users/:id Yes Delete user
GET /health No Health check

Hire Me

Need a freelance Node.js developer to build or scale your API?

I'm Usman Nadeem, a freelance full-stack developer specializing in Node.js, Express, Laravel, Vue, and React. I've delivered backend systems for SaaS platforms, POS systems, and finance dashboards.

📧 Reach out via usmannadeem.com


License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

No contributors