Skip to content

Building REST APIs with Node.js and Express — Step-by-Step Guide

DodaTech Updated 2026-06-23 10 min read

In this tutorial, you'll learn about Building REST APIs with Node.js and Express. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Node.js with Express is a popular combination for building REST APIs, providing a lightweight, high-performance runtime with a minimalist web framework that scales from simple prototypes to enterprise applications.

What You'll Learn

You will build a complete REST API with Node.js and Express from scratch, including routing, middleware, request validation, database integration with PostgreSQL, error handling, authentication, and deployment.

Why Node.js and Express Matter

Node.js powers over 30 percent of web applications and handles thousands of requests per second with its event-driven, non-blocking architecture. Express is the most popular Node.js web framework, used by companies like IBM, Uber, and PayPal. At DodaTech, the Doda Browser sync service and DodaZIP update API are built with Express.

Real-World Use

DodaTech runs multiple Express APIs in production. Doda Browser sync API handles millions of bookmark sync requests daily, DodaZIP update server manages version distribution to thousands of clients, and internal Microservices for Durga Antivirus Pro threat analysis use Express for REST endpoints.

Node.js Express API Learning Path

flowchart LR
  A[JavaScript Basics] --> B[Node.js Fundamentals]
  B --> C[Express Setup]
  C --> D[Routing & Middleware]
  D --> E[Database Integration]
  E --> F[Validation & Error Handling]
  F --> G[Auth & Deployment]
  B:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

Prerequisites

Understand RESTful Api Design Best Practices and basic JavaScript Basics. Familiarity with HTTP Protocol Basics and JSON Data Format is required. Basic Command line knowledge helps.

Project Setup

Step 1: Initialize the Project

mkdir dodatech-api
cd dodatech-api
npm init -y
npm install express pg dotenv cors helmet morgan
npm install -D nodemon

Step 2: Project Structure

dodatech-api/
├── src/
│   ├── config/
│   │   └── database.js
│   ├── controllers/
│   │   └── userController.js
│   ├── middleware/
│   │   ├── auth.js
│   │   └── errorHandler.js
│   ├── models/
│   │   └── userModel.js
│   ├── routes/
│   │   └── userRoutes.js
│   ├── validators/
│   │   └── userValidator.js
│   └── app.js
├── .env
├── .env.example
└── package.json

Step 3: Create the Express App

// src/app.js
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
require("dotenv").config();

const userRoutes = require("./routes/userRoutes");
const errorHandler = require("./middleware/errorHandler");

const app = express();

// Global middleware
app.use(helmet());          // Security headers
app.use(cors());            // Cross-Origin Resource Sharing
app.use(morgan("dev"));     // Request logging
app.use(express.json());    // Parse JSON body

// Routes
app.use("/api/v1/users", userRoutes);

// Health check
app.get("/health", (req, res) => {
  res.json({ status: "ok", timestamp: new Date().toISOString() });
});

// Error handler
app.use(errorHandler);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`DodaTech API running on port ${PORT}`);
});

Database Integration with PostgreSQL

Step 1: Database Configuration

// src/config/database.js
const { Pool } = require("pg");

const pool = new Pool({
  host: process.env.DB_HOST || "localhost",
  port: process.env.DB_PORT || 5432,
  database: process.env.DB_NAME || "dodatech",
  user: process.env.DB_USER || "postgres",
  password: process.env.DB_PASSWORD
});

module.exports = {
  query: (text, params) => pool.query(text, params),
  pool
};

Step 2: Database Migration

-- migrations/001_create_users.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name VARCHAR(100) NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  role VARCHAR(20) DEFAULT 'member',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);

Creating Routes and Controllers

Step 1: Define Routes

// src/routes/userRoutes.js
const express = require("express");
const router = express.Router();
const userController = require("../controllers/userController");
const { validateCreateUser } = require("../validators/userValidator");
const { authenticate } = require("../middleware/auth");

router.get("/", authenticate, userController.listUsers);
router.get("/:id", authenticate, userController.getUser);
router.post("/", validateCreateUser, userController.createUser);
router.put("/:id", authenticate, userController.updateUser);
router.delete("/:id", authenticate, userController.deleteUser);

module.exports = router;

Step 2: Implement Controller

// src/controllers/userController.js
const userModel = require("../models/userModel");

exports.listUsers = async (req, res, next) => {
  try {
    const { page = 1, limit = 20 } = req.query;
    const offset = (page - 1) * limit;

    const users = await userModel.findAll(limit, offset);
    const total = await userModel.count();

    res.json({
      success: true,
      data: users,
      pagination: {
        page: Number(page),
        limit: Number(limit),
        total: Number(total),
        pages: Math.ceil(total / limit)
      }
    });
  } catch (error) {
    next(error);
  }
};

exports.getUser = async (req, res, next) => {
  try {
    const user = await userModel.findById(req.params.id);

    if (!user) {
      return res.status(404).json({
        success: false,
        error: "User not found"
      });
    }

    res.json({ success: true, data: user });
  } catch (error) {
    next(error);
  }
};

exports.createUser = async (req, res, next) => {
  try {
    const { name, email, password } = req.body;

    const existing = await userModel.findByEmail(email);
    if (existing) {
      return res.status(409).json({
        success: false,
        error: "Email already exists"
      });
    }

    const user = await userModel.create({ name, email, password });

    res.status(201).json({ success: true, data: user });
  } catch (error) {
    next(error);
  }
};

exports.updateUser = async (req, res, next) => {
  try {
    const user = await userModel.update(req.params.id, req.body);

    if (!user) {
      return res.status(404).json({
        success: false,
        error: "User not found"
      });
    }

    res.json({ success: true, data: user });
  } catch (error) {
    next(error);
  }
};

exports.deleteUser = async (req, res, next) => {
  try {
    const deleted = await userModel.delete(req.params.id);

    if (!deleted) {
      return res.status(404).json({
        success: false,
        error: "User not found"
      });
    }

    res.status(204).send();
  } catch (error) {
    next(error);
  }
};

Step 3: Implement Model

// src/models/userModel.js
const db = require("../config/database");
const bcrypt = require("bcrypt");

const findAll = async (limit, offset) => {
  const result = await db.query(
    "SELECT id, name, email, role, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2",
    [limit, offset]
  );
  return result.rows;
};

const findById = async (id) => {
  const result = await db.query(
    "SELECT id, name, email, role, created_at FROM users WHERE id = $1",
    [id]
  );
  return result.rows[0] || null;
};

const findByEmail = async (email) => {
  const result = await db.query(
    "SELECT id, name, email FROM users WHERE email = $1",
    [email]
  );
  return result.rows[0] || null;
};

const create = async ({ name, email, password }) => {
  const saltRounds = 10;
  const passwordHash = await bcrypt.hash(password, saltRounds);

  const result = await db.query(
    `INSERT INTO users (name, email, password_hash)
     VALUES ($1, $2, $3)
     RETURNING id, name, email, role, created_at`,
    [name, email, passwordHash]
  );
  return result.rows[0];
};

const update = async (id, { name, email, role }) => {
  const result = await db.query(
    `UPDATE users
     SET name = COALESCE($1, name),
         email = COALESCE($2, email),
         role = COALESCE($3, role),
         updated_at = NOW()
     WHERE id = $4
     RETURNING id, name, email, role, created_at, updated_at`,
    [name, email, role, id]
  );
  return result.rows[0] || null;
};

const delete = async (id) => {
  const result = await db.query(
    "DELETE FROM users WHERE id = $1 RETURNING id",
    [id]
  );
  return result.rows.length > 0;
};

module.exports = { findAll, findById, findByEmail, create, update, delete };

Middleware

Authentication Middleware

// src/middleware/auth.js
const jwt = require("jsonwebtoken");

exports.authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return res.status(401).json({
      success: false,
      error: "Authentication required"
    });
  }

  const token = authHeader.split(" ")[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(403).json({
      success: false,
      error: "Invalid or expired token"
    });
  }
};

Error Handler Middleware

// src/middleware/errorHandler.js
module.exports = (err, req, res, next) => {
  console.error("Error:", err);

  const statusCode = err.statusCode || 500;
  const message = err.message || "Internal server error";

  res.status(statusCode).json({
    success: false,
    error: message,
    ...(process.env.NODE_ENV === "development" && { stack: err.stack })
  });
};

Validation Middleware

// src/validators/userValidator.js
const Joi = require("joi");

const createUserSchema = Joi.object({
  name: Joi.string().min(2).max(100).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).max(128).required()
});

exports.validateCreateUser = (req, res, next) => {
  const { error } = createUserSchema.validate(req.body, { abortEarly: false });

  if (error) {
    const errors = error.details.map(d => ({
      field: d.path.join("."),
      message: d.message
    }));

    return res.status(422).json({
      success: false,
      error: "Validation failed",
      errors
    });
  }

  next();
};

Running the API

# Start in development mode
npm run dev

Expected output:

[nodemon] starting `node src/app.js`
DodaTech API running on port 3000

Test with curl:

curl -X POST HTTP://localhost:3000/API/v1/users \
  -H "Content-Type: application/JSON" \
  -d '{"name":"Alice","email":"alice@example.com","password":"securepass123"}'

Expected response:

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Alice",
    "email": "alice@example.com",
    "role": "member",
    "created_at": "2026-06-23T10:00:00.000Z"
  }
}

Common Errors

  1. Not handling async errorsExpress does not catch errors in async route handlers automatically. Wrap async handlers in a try-catch or use express-async-errors.

  2. Hardcoded configuration — Putting database credentials and API keys in the code. Use environment variables with dotenv and never commit .env files.

  3. Missing input validation — Accepting user input without validation. Use Joi or express-validator to validate every request body and query parameter.

  4. No Rate Limiting — Exposing endpoints without rate limits. Use express-rate-limit to prevent abuse, especially on auth endpoints.

  5. Returning Stack traces in production — Sending error Stack traces to clients. Use different error formatting for development and production environments.

  6. Not using HTTP status codes properly — Returning 200 for all responses including errors. Use 201 for creation, 204 for deletion, 400 for bad requests, 401 for unauthorized.

  7. Memory leaks from unclosed connections — Not closing database connections on shutdown. Implement graceful shutdown handlers that close database pools and server connections.

Practice Questions

  1. What is the purpose of middleware in Express?
  2. How do you handle errors in async Express route handlers?
  3. Why should you use environment variables for configuration?
  4. What is the difference between 401 and 403 status codes?
  5. How do you implement pagination in a REST API?

Challenge

Build a complete REST API for a task management application with Express and PostgreSQL. Include: CRUD for tasks with title, description, status, priority, and due date, user authentication with JWT, input validation with Joi, pagination and filtering by status and priority, Rate Limiting on auth endpoints, comprehensive error handling, and a Migration script for the database schema. Write a test suite using Jest and supertest.

FAQ

Which is better for building APIs: Express or Fastify? Express has a larger ecosystem and more middleware options. Fastify offers better performance (up to 2x faster) and built-in schema validation. Choose Express for community support and Fastify for high-performance needs.

How do I structure a large Express application? Use the MVC pattern with separate folders for routes, controllers, models, middleware, and validators. Group related functionality into feature modules. Use an app Factory Pattern to create the Express instance.

Should I use SQL or NoSQL with Express? Use PostgreSQL for structured data with relationships and complex queries. Use MongoDB for flexible schemas and rapid prototyping. Both work well with Express. Choose based on your data model.

How do I test Express APIs? Use supertest for HTTP integration tests and Jest as the test runner. Test each endpoint with valid and invalid inputs. Mock the database layer for unit tests or use a test database for integration tests.

How do I deploy an Express API? Deploy on cloud platforms like AWS Elastic Beanstalk, Google Cloud Run, Heroku, or DigitalOcean App Platform. Use PM2 for Process management. Set up a reverse Proxy with NGINX for production.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro