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
- ✅ 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
This boilerplate follows a set of deliberate, opinionated patterns. Here is what was used and why.
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.
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.
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.
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.
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.
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.
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.
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.
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.
/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
git clone https://github.com/usmannadeem/nodejs-rest-api-2026.git
cd nodejs-rest-api-2026
npm installcp .env.example .env
# Edit .env with your valuesnpm run devnpm testnpm install -g pm2
pm2 start server.js -i max --name "my-api"
pm2 save
pm2 startup| 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 |
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
MIT