How to Build REST API with Node.js and Express: A Complete Guide

We earn commissions when you shop through the links below.

If you’ve been wondering how to build REST API with Node.js and Express, you’re in the right place. Express remains the go-to framework for Node.js API development — it’s minimal, flexible, and has a massive ecosystem. In this guide I’ll walk you through building a fully functional REST API from scratch, covering project setup, routing, middleware, error handling, and deployment.

Why Node.js and Express for REST APIs?

Node.js handles concurrent connections well thanks to its non-blocking event loop, making it a solid choice for API servers. Express sits on top of Node.js and gives you routing, middleware support, and a clean request/response model without forcing an opinionated structure on you. The combo is fast to start with and scales reasonably well — which is why it’s still heavily used in production in 2026.

If you want to go deeper after this guide, Udemy has several highly-rated Node.js and Express courses that cover advanced patterns like authentication, rate limiting, and microservices architecture.

Project Setup

Start by initializing a Node.js project and installing dependencies:

mkdir my-api && cd my-api
npm init -y
npm install express
npm install --save-dev nodemon

Update your package.json scripts section:

{
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  }
}

Create your entry point at src/index.js:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.get('/', (req, res) => {
  res.json({ message: 'API is running' });
});

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

Run npm run dev and hit http://localhost:3000. You should get a JSON response back. That’s your foundation.

Structuring Your Project

Don’t put everything in index.js. A clean structure makes the codebase maintainable as it grows:

src/
  index.js          # Entry point
  routes/
    users.js        # User routes
    products.js     # Product routes
  controllers/
    userController.js
    productController.js
  middleware/
    errorHandler.js
    validate.js
  models/
    user.js

This separation of concerns — routes, controllers, models, middleware — is a pattern that holds up well whether you’re building a small API or something larger.

Building a CRUD Resource

Let’s build a users resource to demonstrate the full REST pattern. Create src/controllers/userController.js:

// In-memory store for demo purposes
let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
];
let nextId = 3;

exports.getUsers = (req, res) => {
  res.json(users);
};

exports.getUserById = (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
};

exports.createUser = (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  const newUser = { id: nextId++, name, email };
  users.push(newUser);
  res.status(201).json(newUser);
};

exports.updateUser = (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'User not found' });
  users[index] = { ...users[index], ...req.body };
  res.json(users[index]);
};

exports.deleteUser = (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'User not found' });
  users.splice(index, 1);
  res.status(204).send();
};

Now wire up the routes in src/routes/users.js:

const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');

router.get('/', userController.getUsers);
router.get('/:id', userController.getUserById);
router.post('/', userController.createUser);
router.put('/:id', userController.updateUser);
router.delete('/:id', userController.deleteUser);

module.exports = router;

Register the router in index.js:

const userRoutes = require('./routes/users');
app.use('/api/users', userRoutes);

You now have a fully working CRUD API. Test it with curl or Postman:

# Get all users
curl http://localhost:3000/api/users

# Create a user
curl -X POST http://localhost:3000/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name": "Carol", "email": "carol@example.com"}'

# Update a user
curl -X PUT http://localhost:3000/api/users/1 \
  -H 'Content-Type: application/json' \
  -d '{"name": "Alice Updated"}'

# Delete a user
curl -X DELETE http://localhost:3000/api/users/1

Adding Middleware

Middleware is where Express really shines. You can intercept requests to handle logging, authentication, validation, and error handling in a reusable way.

Here’s a simple request logger middleware:

// src/middleware/logger.js
const logger = (req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${req.method} ${req.url}`);
  next();
};

module.exports = logger;

And a centralized error handler — always put this last in your middleware stack:

// src/middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
  console.error(err.stack);
  const status = err.status || 500;
  res.status(status).json({
    error: err.message || 'Internal Server Error',
  });
};

module.exports = errorHandler;

Register both in index.js:

const logger = require('./middleware/logger');
const errorHandler = require('./middleware/errorHandler');

app.use(logger); // before routes
app.use('/api/users', userRoutes);
app.use(errorHandler); // after routes

To trigger the error handler from a controller, pass an error to next():

exports.getUserById = (req, res, next) => {
  try {
    const user = users.find(u => u.id === parseInt(req.params.id));
    if (!user) {
      const err = new Error('User not found');
      err.status = 404;
      return next(err);
    }
    res.json(user);
  } catch (err) {
    next(err);
  }
};

Environment Variables and Configuration

Never hardcode sensitive values. Install dotenv:

npm install dotenv

Create a .env file (and add it to .gitignore):

PORT=3000
NODE_ENV=development
DB_CONNECTION_STRING=mongodb://localhost:27017/mydb

Load it at the top of index.js:

require('dotenv').config();
const PORT = process.env.PORT || 3000;

Deploying Your API

Once your API is working locally, you need somewhere to run it. For straightforward Node.js deployments, I’d recommend Railway — it detects your Node.js app automatically, handles environment variables cleanly, and you can deploy from a GitHub push in minutes.

If you prefer more control over your infrastructure, DigitalOcean App Platform or a Droplet give you more flexibility. For a Droplet deployment, the basic steps are:

# On your server
git clone your-repo
cd your-repo
npm install --production
node src/index.js

# Or use PM2 to keep it running
npm install -g pm2
pm2 start src/index.js --name my-api
pm2 save
pm2 startup

Using PM2 keeps your process alive after server reboots and gives you basic process monitoring out of the box.

What to Add Next

The setup I’ve shown covers the core of how to build REST API with Node.js and Express. From here, depending on your use case, you’ll want to add:

  • A real database — MongoDB with Mongoose or PostgreSQL with Prisma or Knex
  • Authentication — JWT tokens with the jsonwebtoken package or session-based auth
  • Input validation — Zod or Joi for request body validation
  • Rate limitingexpress-rate-limit to protect your endpoints
  • API versioning — prefix routes with /api/v1/ so you can evolve the API without breaking clients
  • Testing — Jest and Supertest for integration tests against your Express app

Final Thoughts

Knowing how to build REST API with Node.js and Express is a foundational skill for any backend developer. The setup is approachable, the ecosystem is huge, and once you understand the request/response lifecycle and how middleware chains work, you can build production-ready APIs quickly. Start with the structure I’ve outlined here, add a real database, lock down your endpoints with authentication, and you’ll have something shippable.