DEV Community

Alex Chen
Alex Chen

Posted on

Node.js Express: Building Real APIs That Scale (2026)

Node.js Express: Building Real APIs That Scale (2026)

Express is simple by design — but building production-ready APIs requires more than app.get(). Here's the complete picture.

Project Structure That Scales

src/
├── app.js              # Express app setup (no server start!)
├── server.js           # Server entry point (imports app, starts listening)
├── config/
│   ├── index.js        # Config loader (env-based)
│   └── database.js     # DB connection config
├── routes/
│   ├── index.js        # Route aggregator
│   ├── users.route.js  # User endpoints
│   └── posts.route.js  # Post endpoints
├── controllers/
│   ├── user.controller.js
│   └── post.controller.js
├── middleware/
│   ├── auth.js         # Authentication
│   ├── validate.js     # Request validation
│   ├── rateLimit.js    # Rate limiting
│   ├── errorHandler.js # Global error handler
│   └── logger.js       # Request logging
├── services/
│   ├── user.service.js # Business logic
│   └── post.service.js
├── models/
│   ├── user.model.js   # Data access layer
│   └── post.model.js
├── utils/
│   ├── response.js     # Standardized API responses
│   ├── errors.js       # Custom error classes
│   └── asyncHandler.js # Try-catch wrapper
└── validators/
    ├── user.validator.js
    └── post.validator.js
Enter fullscreen mode Exit fullscreen mode

The Express App Setup

// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const morgan = require('morgan');
const routes = require('./routes');
const { errorHandler, notFoundHandler } = require('./middleware/errorHandler');
const { requestLogger } = require('./middleware/logger');
const rateLimiter = require('./middleware/rateLimit');

function createApp() {
  const app = express();

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

  // CORS configuration
  app.use(cors({
    origin: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'],
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
  }));

  // Compression (reduce payload size ~70%)
  app.use(compression({ threshold: 1024 }));

  // Body parsing with size limits
  app.use(express.json({ limit: '1mb' }));
  app.use(express.urlencoded({ extended: true, limit: '1mb' }));

  // Request logging
  if (process.env.NODE_ENV !== 'test') {
    app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
  }
  app.use(requestLogger);

  // Global rate limiting
  app.use('/api/', rateLimiter);

  // Health check endpoint (no auth required)
  app.get('/health', (req, res) => {
    res.json({
      status: 'ok',
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
      environment: process.env.NODE_ENV,
      version: process.env.npm_package_version || '1.0.0',
    });
  });

  // API routes
  app.use('/api/v1', routes);

  // 404 handler (must be after all routes)
  app.use(notFoundHandler);

  // Global error handler (must be LAST middleware)
  app.use(errorHandler);

  return app;
}

module.exports = createApp;
Enter fullscreen mode Exit fullscreen mode
// src/server.js
const createApp = require('./app');

const PORT = process.env.PORT || 3000;
const app = createApp();

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

module.exports = app; // For testing without starting server
Enter fullscreen mode Exit fullscreen mode

Middleware Patterns

// src/middleware/auth.js — JWT Authentication
const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const header = req.headers.authorization;

  if (!header?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid authorization header' });
  }

  const token = header.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // Attach user to request for downstream use
    req.user = {
      id: decoded.sub,
      email: decoded.email,
      role: decoded.role,
    };

    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Role-based authorization (composable!)
function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

module.exports = { authenticate, authorize };

// Usage:
// router.get('/admin', authenticate, authorize('admin'), adminDashboard);
// router.get('/profile', authenticate, getProfile); // Any authenticated user
Enter fullscreen mode Exit fullscreen mode
// src/middleware/validate.js — Schema Validation
const { validationResult, body, param, query } = require('express-validator');

function validate(rules) {
  return async (req, res, next) => {
    await Promise.all(rules.map(rule => rule.run(req)));
    const errors = validationResult(req);

    if (!errors.isEmpty()) {
      return res.status(422).json({
        error: 'Validation failed',
        details: errors.array().map(err => ({
          field: err.path,
          message: err.msg,
          value: err.value,
        })),
      });
    }

    next();
  };
}

// Pre-built validators for common cases:
const validators = {
  createUser: [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters'),
    body('name').trim().notEmpty().isLength({ max: 100 }),
  ],
  getUserById: [
    param('id').isUUID().withMessage('Invalid user ID format'),
  ],
  listPosts: [
    query('page').optional().isInt({ min: 1 }).toInt(),
    query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
    query('sort').optional().isIn(['createdAt', 'title', 'views']),
    query('order').optional().isIn(['asc', 'desc']),
  ],
};

module.exports = { validate, validators };
Enter fullscreen mode Exit fullscreen mode

Standardized Responses

// src/utils/response.js
class ApiResponse {
  static success(data, meta = {}) {
    return {
      success: true,
      data,
      ...Object.keys(meta).length && { meta },
      timestamp: new Date().toISOString(),
    };
  }

  static paginated(items, page, limit, total) {
    return this.success(items, {
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
        hasNext: page * limit < total,
        hasPrev: page > 1,
      },
    });
  }

  static created(data) {
    return { success: true, data, timestamp: new Date().toISOString() };
  }
}

// src/utils/errors.js
class AppError extends Error {
  constructor(message, statusCode = 500, code = 'ERROR') {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true; // Distinguish from programming errors
    Error.captureStackTrace(this, this.constructor);
  }
}
class NotFoundError extends AppError {
  constructor(resource = 'Resource') { super(`${resource} not found`, 404, 'NOT_FOUND'); }
}
class ValidationError extends AppError {
  constructor(details) { super('Validation failed', 422, 'VALIDATION_ERROR', details); }
}
class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') { super(message, 401, 'UNAUTHORIZED'); }
}
class ForbiddenError extends AppError {
  constructor(message = 'Forbidden') { super(message, 403, 'FORBIDDEN'); }
}

// src/middleware/errorHandler.js
function errorHandler(err, req, res, _next) {
  const incidentId = crypto.randomUUID();

  // Log every error
  console.error(`[${incidentId}]`, {
    message: err.message,
    stack: err.stack,
    code: err.code,
    path: req.path,
    method: req.method,
    userId: req.user?.id,
  });

  const isProduction = process.env.NODE_ENV === 'production';
  const statusCode = err.statusCode || 500;
  const code = err.code || 'INTERNAL_ERROR';

  res.status(statusCode).json({
    success: false,
    error: {
      code,
      message: isProduction && statusCode === 500 
        ? 'An internal error occurred' 
        : err.message,
      ...(err.details && { details: err.details }),
      ...(isProduction && { incidentId }),
    },
    timestamp: new Date().toISOString(),
  });
}

function notFoundHandler(req, res) {
  res.status(404).json({
    success: false,
    error: { code: 'NOT_FOUND', message: `${req.method} ${req.path} not found` },
    timestamp: new Date().toISOString(),
  });
}
Enter fullscreen mode Exit fullscreen mode

Route & Controller Pattern

// src/routes/users.route.js
const router = require('express').Router();
const { asyncHandler } = require('../utils/asyncHandler');
const { authenticate, authorize } = require('../middleware/auth');
const { validate, validators } = require('../middleware/validate');
const userController = require('../controllers/user.controller');

router.post('/', validate(validators.createUser), asyncHandler(userController.create));
router.get('/', asyncHandler(userController.list));
router.get('/:id', validate(validators.getUserById), asyncHandler(userController.getById));
router.put('/:id', authenticate, validate(validators.updateUser), asyncHandler(userController.update));
router.delete('/:id', authenticate, authorize('admin'), asyncHandler(userController.delete));

module.exports = router;

// src/controllers/user.controller.js
const userService = require('../services/user.service');
const { ApiResponse } = require('../utils/response');
const { NotFoundError } = require('../utils/errors');

exports.create = async (req, res) => {
  const user = await userService.create(req.body);
  res.status(201).json(ApiResponse.created(user));
};

exports.list = async (req, res) => {
  const { page = 1, limit = 20, sort = 'createdAt', order = 'desc' } = req.query;
  const result = await userService.list({ page, limit, sort, order });
  res.json(ApiResponse.paginated(result.items, page, limit, result.total));
};

exports.getById = async (req, res) => {
  const user = await userService.findById(req.params.id);
  if (!user) throw new NotFoundError('User');
  res.json(ApiResponse.success(user));
};

// src/utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);
module.exports = asyncHandler;
Enter fullscreen mode Exit fullscreen mode

Performance Checklist

// ✅ Enable gzip/brotli compression → done via compression()
// ✅ Response caching (for GET endpoints):
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5-minute TTL

function cacheMiddleware(duration = 300) {
  return (req, res, next) => {
    if (req.method !== 'GET') return next();
    const key = req.originalUrl;
    const cached = cache.get(key);
    if (cached) return res.json(cached);
    res.originalJson = res.json.bind(res);
    res.json = (body) => { cache.set(key, body, duration); return res.originalJson(body); };
    next();
  };
}

// Usage: router.get('/posts', cacheMiddleware(60), listPosts)

// ✅ Connection pooling for DB:
const { Pool } = require('pg');
const pool = new Pool({ max: 20, idleTimeoutMillis: 30000 });

// ✅ Rate limiting per route:
const expressRateLimit = require('express-rate-limit');
const strictLimiter = expressRateLimit({ windowMs: 15 * 60 * 1000, max: 50 });
const authLimiter = expressRateLimit({ windowMs: 15 * 60 * 1000, max: 5 });

// ✅ Use streams for large responses:
app.get('/api/export', (req, res) => {
  res.setHeader('Content-Type', 'text/csv');
  bigDataStream.pipe(res); // Memory-efficient!
});
Enter fullscreen mode Exit fullscreen mode

What's your favorite Express pattern? What would you add to this guide?

Follow @armorbreak for more practical developer guides.

Top comments (0)