正在加载,请稍候…

Building a REST API with Node.js and Express: Complete 2026 Guide

Step-by-step guide to building production-ready REST APIs with Node.js and Express. Covers routing, middleware, error handling, validation, and deployment best practices.

Building a REST API with Node.js and Express: Complete 2026 Guide

Node.js + Express remains the most popular backend stack for building REST APIs. In 2026, with Express 5 stable and the ecosystem matured, here's the definitive guide to building APIs that are fast, maintainable, and production-ready.

Project Setup

mkdir my-api && cd my-api
npm init -y
npm install express
npm install --save-dev typescript @types/express @types/node ts-node nodemon
npx tsc --init

Directory structure:

src/
  routes/
    users.ts
    products.ts
  controllers/
    userController.ts
  middleware/
    auth.ts
    errorHandler.ts
  models/
    User.ts
  app.ts
  server.ts

The Express App

// src/app.ts
import express, { Application } from 'express';
import userRoutes from './routes/users';
import { errorHandler } from './middleware/errorHandler';
import { notFound } from './middleware/notFound';

const app: Application = express();

// Built-in middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

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

// 404 handler (must come after routes)
app.use(notFound);

// Error handler (must be last)
app.use(errorHandler);

export default app;
// src/server.ts
import app from './app';

const PORT = process.env.PORT || 3000;

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

RESTful Routes

// src/routes/users.ts
import { Router } from 'express';
import {
  getUsers,
  getUserById,
  createUser,
  updateUser,
  deleteUser,
} from '../controllers/userController';
import { authenticate } from '../middleware/auth';
import { validateUser } from '../middleware/validate';

const router = Router();

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

export default router;

Controllers

// src/controllers/userController.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/userService';

const userService = new UserService();

export async function getUsers(req: Request, res: Response, next: NextFunction) {
  try {
    const { page = 1, limit = 20 } = req.query;
    const users = await userService.findAll({
      page: Number(page),
      limit: Number(limit),
    });
    res.json({
      success: true,
      data: users,
      meta: { page: Number(page), limit: Number(limit) },
    });
  } catch (error) {
    next(error); // Pass to error handler
  }
}

export async function getUserById(req: Request, res: Response, next: NextFunction) {
  try {
    const user = await userService.findById(req.params.id);
    if (!user) {
      return res.status(404).json({ success: false, message: 'User not found' });
    }
    res.json({ success: true, data: user });
  } catch (error) {
    next(error);
  }
}

export async function createUser(req: Request, res: Response, next: NextFunction) {
  try {
    const user = await userService.create(req.body);
    res.status(201).json({ success: true, data: user });
  } catch (error) {
    next(error);
  }
}

Middleware

Authentication Middleware

// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

interface JwtPayload {
  userId: string;
  email: string;
}

// Extend Request type
declare global {
  namespace Express {
    interface Request {
      user?: JwtPayload;
    }
  }
}

export function authenticate(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ success: false, message: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
    req.user = decoded;
    next();
  } catch {
    res.status(401).json({ success: false, message: 'Invalid token' });
  }
}

Validation Middleware (with Zod)

// src/middleware/validate.ts
import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';

const userSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  password: z.string().min(8),
  role: z.enum(['user', 'admin']).optional().default('user'),
});

export function validateUser(req: Request, res: Response, next: NextFunction) {
  const result = userSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      success: false,
      message: 'Validation failed',
      errors: result.error.flatten().fieldErrors,
    });
  }
  req.body = result.data; // Use parsed/coerced data
  next();
}

Global Error Handler

// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';

export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number = 500,
    public isOperational: boolean = true
  ) {
    super(message);
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

export function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      success: false,
      message: err.message,
    });
  }

  // Unexpected errors — log and hide details in production
  console.error(err);
  res.status(500).json({
    success: false,
    message: process.env.NODE_ENV === 'production'
      ? 'Internal server error'
      : err.message,
  });
}

REST API Best Practices

Use Consistent Response Format

// Success
{ "success": true, "data": { ... }, "meta": { "page": 1, "total": 100 } }

// Error
{ "success": false, "message": "User not found", "errors": { "email": ["Invalid format"] } }

HTTP Status Codes

Scenario Status Code
Success (GET) 200 OK
Created (POST) 201 Created
No content (DELETE) 204 No Content
Bad request 400 Bad Request
Unauthorized 401 Unauthorized
Forbidden 403 Forbidden
Not found 404 Not Found
Validation error 422 Unprocessable Entity
Server error 500 Internal Server Error

Pagination Pattern

// GET /api/users?page=2&limit=20&sort=createdAt&order=desc
router.get('/', async (req, res) => {
  const { page = 1, limit = 20, sort = 'createdAt', order = 'desc' } = req.query;
  
  const skip = (Number(page) - 1) * Number(limit);
  const [users, total] = await Promise.all([
    User.find().sort({ [sort as string]: order as 'asc' | 'desc' }).skip(skip).limit(Number(limit)),
    User.countDocuments()
  ]);

  res.json({
    success: true,
    data: users,
    meta: {
      page: Number(page),
      limit: Number(limit),
      total,
      totalPages: Math.ceil(total / Number(limit))
    }
  });
});

Rate Limiting & Security

npm install express-rate-limit helmet cors
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import cors from 'cors';

// Security headers
app.use(helmet());

// CORS
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
}));

// Rate limit: 100 requests per 15 minutes
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { success: false, message: 'Too many requests, please try again later.' },
});
app.use('/api/', limiter);

Environment Variables

# .env
PORT=3000
NODE_ENV=development
JWT_SECRET=your-super-secret-key-here
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
ALLOWED_ORIGINS=http://localhost:5173,https://myapp.com
// Load with dotenv
import dotenv from 'dotenv';
dotenv.config(); // Must be called before accessing process.env

Testing Your API

# Quick tests with curl
curl -X GET http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John","email":"john@example.com","password":"secret123"}'

# With auth token
curl -X GET http://localhost:3000/api/users \
  -H "Authorization: Bearer eyJhbGci..."

Summary

Key takeaways for production Node.js APIs:

  1. Separate concerns: routes → controllers → services → models
  2. Always use middleware for auth, validation, error handling
  3. Global error handler catches everything — don't let errors slip
  4. Validate all inputs with Zod or Joi before processing
  5. Use environment variables — never hardcode secrets
  6. Add rate limiting and security headers before going to production
  7. Consistent response format makes client-side code simpler

→ Test your API responses with JSON Viewer and parse JWTs with JWT Parser.