Guide · Node.js · Express
How to Build a REST API with Node.js and Express (2026 Guide)
A step-by-step guide to building a production-ready REST API with Node.js and Express — routing, middleware, validation, error handling, and deployment.
Jatinder Sandhu
What You Will Build
This guide walks you through building a production-ready REST API using Node.js and Express from scratch. By the end, you will have a working API with proper folder structure, routing, middleware, input validation, centralised error handling, and environment configuration — the same patterns I use in client projects.
I have built REST APIs for taxi booking platforms, visa consultancy portals, admin dashboards, and SaaS products. The architecture in this guide is what I reach for every time — it is simple, scalable, and easy for other developers to pick up. We will skip the “hello world” basics and go straight to the structure that survives production.
Prerequisites
- Node.js 18+ installed
- Basic JavaScript / ES6+ knowledge
- npm or yarn
- A REST client — Postman or Thunder Client (VS Code extension)
Step 1 — Project Setup and Folder Structure
Start by initialising the project and installing the core dependencies. The folder structure matters — a flat structure works for tutorials but fails at scale. Use a feature-based layout from day one.
# Initialise project
mkdir my-api && cd my-api
npm init -y
# Install core packages
npm install express dotenv cors helmet morgan
# Install dev dependencies
npm install -D nodemon
Here is the folder structure I use. Keep controllers, routes, middleware, and models cleanly separated — it makes the codebase easy to navigate as it grows.
my-api/
├── src/
├── controllers/ # Route logic
├── routes/ # Express routers
├── middleware/ # Auth, validation, error
├── models/ # DB models (Mongoose / Prisma)
├── utils/ # Helpers and shared logic
└── app.js # Express app setup
├── .env
├── .env.example
└── server.js # Entry point
Step 2 — Configure the Express App
Split your Express configuration from your server entry point. app.js handles middleware and routing. server.js starts the HTTP server. This separation makes testing easier — you can import the app without starting a server.
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const userRoutes = require('./routes/users');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// Security and logging
app.use(helmet());
app.use(cors());
app.use(morgan('dev'));
app.use(express.json());
// Routes
app.use('/api/users', userRoutes);
// Error handler — must be last
app.use(errorHandler);
module.exports = app;
// server.js
require('dotenv').config();
const app = require('./src/app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 3 — Routes and Controllers
Keep route definitions thin. The router file only maps HTTP methods and paths to controller functions. All business logic lives in the controller. This single rule keeps your routes file readable no matter how large the API grows.
// src/routes/users.js
const express = require('express');
const router = express.Router();
const { getUsers, getUserById, createUser, updateUser, deleteUser } = require('../controllers/users');
router.get('/', getUsers);
router.get('/:id', getUserById);
router.post('/', createUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
module.exports = router;
// src/controllers/users.js
const getUsers = async (req, res, next) => {
try {
// Fetch from database here
const users = []; // replace with DB query
res.json({ success: true, data: users });
} catch (err) {
next(err); // passes to error handler
}
};
module.exports = { getUsers }; // export all controllers
The key pattern here is the try/catch wrapping every async controller and passing errors to next(err). This sends the error to your centralised error handler rather than crashing the process or leaving the request hanging.
Step 4 — Middleware: Validation and Error Handling
Middleware is the most important concept in Express. Every request passes through a chain of middleware functions before reaching your controller. Use middleware for authentication, input validation, logging, rate limiting, and error handling.
For input validation, use express-validator. It keeps validation rules co-located with your routes and produces consistent error responses. Install it with npm install express-validator.
// src/middleware/validate.js
const { validationResult } = require('express-validator');
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array(),
});
}
next();
};
module.exports = validate;
// src/middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
success: false,
message: err.message || 'Internal server error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
module.exports = errorHandler;
The error handler has four parameters — Express identifies it as an error handler because of the four-argument signature (err, req, res, next). It checks for a custom statusCode on the error object (useful for throwing intentional 404s or 403s) and only exposes stack traces in development mode.
Step 5 — Consistent API Response Format
One of the most overlooked parts of API design is response consistency. Every endpoint should return the same shape — success flag, data, and where applicable, pagination metadata. This makes frontend integration predictable and error handling straightforward.
Success Response
{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"total": 42
}
}Error Response
{
"success": false,
"message": "User not found",
"errors": [
{ "field": "id", "msg": "..." }
]
}Create a small utility helper so you are not duplicating this structure in every controller:
// src/utils/response.js
const success = (res, data, statusCode = 200, meta = ) =>
res.status(statusCode).json({ success: true, data, ...meta })
const fail = (res, message, statusCode = 400) =>
res.status(statusCode).json({ success: false, message })
module.exports = { success, fail };
Step 6 — Environment Variables and Security
Never hardcode secrets, connection strings, or API keys. Use a .env file locally and inject environment variables through your hosting platform in production. Always commit a .env.example file with keys but no values so other developers know what is required.
# .env.example
PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your_jwt_secret_here
JWT_EXPIRES_IN=7d
The packages installed earlier cover the most common security concerns at the middleware level:
| Package | What it does |
|---|---|
| helmet | Sets security-related HTTP headers (XSS, HSTS, clickjacking) |
| cors | Controls which origins can call your API |
| morgan | Request logging — helps you trace issues in production logs |
| express-rate-limit | Prevents brute force attacks on auth endpoints |
| bcrypt | Hashes passwords — never store plain-text passwords |
Conclusion
Building a REST API with Node.js and Express is not complicated — but doing it right from the start saves weeks of refactoring later. The patterns in this guide — separated routing and controller logic, centralised error handling, consistent response format, and environment-based config — are production-proven across dozens of real projects.
Start with this structure, add a database layer (MongoDB with Mongoose or PostgreSQL with Prisma), wire up authentication with JWT, and you have the foundation for any serious backend. The next step is deployment — Railway and Render both support Node.js APIs with a single push.
Need a production-ready API built for your project?
I build scalable Node.js backends for SaaS products, marketplaces, and business applications. Get in touch to discuss your requirements.