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.

JS

Jatinder Sandhu

Published 30 June 2026

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:

PackageWhat it does
helmetSets security-related HTTP headers (XSS, HSTS, clickjacking)
corsControls which origins can call your API
morganRequest logging — helps you trace issues in production logs
express-rate-limitPrevents brute force attacks on auth endpoints
bcryptHashes 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.

ShareXLinkedIn

About the Author

Hi, I'm Jatinder Sandhu, a Full-Stack Developer with 6+ years of experience building websites, web applications, business management systems, and AI-powered solutions using technologies like Next.js, React, Node.js, and MongoDB.

I share practical technology guides, development tutorials, and business growth insights based on real-world experience working on client projects.

If you're looking to build a website, custom software, business automation system, or AI-powered solution, explore my portfolio at jatinder.malwaland.com.