Express.js Best Practices: Building Production-Ready Node.js Backend Applications

🎯 Introduction

Express.js is the de facto standard web framework for Node.js, powering millions of applications worldwide. Its minimalist, unopinionated design provides flexibility, but also requires developers to make crucial architectural decisions to build production-ready applications.

This comprehensive guide explores Express.js best practices across multiple dimensions:

  • Project Setup & Configuration: Optimal structure and environment management
  • Middleware Architecture: Building reusable, maintainable middleware pipelines
  • Routing Best Practices: Organizing routes for scalability
  • Error Handling: Robust error management strategies
  • Security: Protecting against common vulnerabilities
  • Performance Optimization: Making your Express app fast and efficient
  • Testing: Ensuring reliability through comprehensive testing
  • Deployment: Production-ready deployment strategies

💡 Core Philosophy: “Express.js provides the foundation—your architecture decisions determine whether you build a maintainable, scalable application or a tangled mess of code”

📦 Project Setup and Structure

express-api/
├── src/
│   ├── config/              # Configuration files
│   │   ├── database.ts
│   │   ├── redis.ts
│   │   └── logger.ts
│   ├── middleware/          # Custom middleware
│   │   ├── auth.ts
│   │   ├── errorHandler.ts
│   │   ├── requestLogger.ts
│   │   └── validation.ts
│   ├── routes/              # Route definitions
│   │   ├── index.ts
│   │   ├── users.ts
│   │   ├── posts.ts
│   │   └── auth.ts
│   ├── controllers/         # Request handlers
│   │   ├── userController.ts
│   │   ├── postController.ts
│   │   └── authController.ts
│   ├── services/            # Business logic
│   │   ├── userService.ts
│   │   ├── postService.ts
│   │   └── emailService.ts
│   ├── models/              # Data models
│   │   ├── User.ts
│   │   └── Post.ts
│   ├── types/               # TypeScript types
│   │   ├── express.d.ts
│   │   └── models.ts
│   ├── utils/               # Utility functions
│   │   ├── validation.ts
│   │   ├── crypto.ts
│   │   └── response.ts
│   ├── tests/               # Test files
│   │   ├── unit/
│   │   ├── integration/
│   │   └── e2e/
│   ├── app.ts               # Express app setup
│   └── server.ts            # Server entry point
├── .env.example             # Environment variables template
├── .env                     # Local environment (gitignored)
├── .eslintrc.json           # ESLint configuration
├── .prettierrc              # Prettier configuration
├── tsconfig.json            # TypeScript configuration
├── package.json
└── README.md

🎨 Project Architecture Flow

graph TD
    A[Client Request] --> B[Express Middleware Stack]
    B --> C[Authentication Middleware]
    C --> D[Validation Middleware]
    D --> E[Router]
    E --> F[Controller]
    F --> G[Service Layer]
    G --> H[Data Layer/Models]
    H --> I[Database]
    G --> J[External APIs]
    F --> K[Response Formatter]
    K --> L[Error Handler Middleware]
    L --> M[Client Response]

    style A fill:#ff6b6b
    style E fill:#4ecdc4
    style G fill:#feca57
    style I fill:#95e1d3
    style M fill:#74b9ff

⚙️ Configuration Best Practices

🔧 Environment Configuration

 1// src/config/index.ts
 2import dotenv from 'dotenv';
 3import path from 'path';
 4
 5// Load environment variables
 6dotenv.config({
 7  path: path.resolve(__dirname, `../../.env.${process.env.NODE_ENV || 'development'}`)
 8});
 9
10interface Config {
11  env: string;
12  port: number;
13  database: {
14    host: string;
15    port: number;
16    name: string;
17    user: string;
18    password: string;
19  };
20  jwt: {
21    secret: string;
22    expiresIn: string;
23  };
24  redis: {
25    host: string;
26    port: number;
27    password?: string;
28  };
29  cors: {
30    origin: string[];
31    credentials: boolean;
32  };
33  rateLimit: {
34    windowMs: number;
35    max: number;
36  };
37}
38
39// ✅ GOOD: Type-safe configuration with validation
40const config: Config = {
41  env: process.env.NODE_ENV || 'development',
42  port: parseInt(process.env.PORT || '3000', 10),
43  database: {
44    host: process.env.DB_HOST || 'localhost',
45    port: parseInt(process.env.DB_PORT || '5432', 10),
46    name: process.env.DB_NAME || 'myapp',
47    user: process.env.DB_USER || 'postgres',
48    password: process.env.DB_PASSWORD || ''
49  },
50  jwt: {
51    secret: process.env.JWT_SECRET || (() => {
52      throw new Error('JWT_SECRET is required');
53    })(),
54    expiresIn: process.env.JWT_EXPIRES_IN || '7d'
55  },
56  redis: {
57    host: process.env.REDIS_HOST || 'localhost',
58    port: parseInt(process.env.REDIS_PORT || '6379', 10),
59    password: process.env.REDIS_PASSWORD
60  },
61  cors: {
62    origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'],
63    credentials: true
64  },
65  rateLimit: {
66    windowMs: 15 * 60 * 1000, // 15 minutes
67    max: 100 // limit each IP to 100 requests per windowMs
68  }
69};
70
71// Validate required configuration
72function validateConfig(): void {
73  const required = [
74    'JWT_SECRET',
75    'DB_HOST',
76    'DB_NAME',
77    'DB_USER'
78  ];
79
80  const missing = required.filter(key => !process.env[key]);
81
82  if (missing.length > 0) {
83    throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
84  }
85}
86
87validateConfig();
88
89export default config;

📝 Environment Variables Template

 1# .env.example
 2# Server
 3NODE_ENV=development
 4PORT=3000
 5
 6# Database
 7DB_HOST=localhost
 8DB_PORT=5432
 9DB_NAME=myapp
10DB_USER=postgres
11DB_PASSWORD=your_password_here
12
13# JWT
14JWT_SECRET=your_super_secret_jwt_key_here
15JWT_EXPIRES_IN=7d
16
17# Redis
18REDIS_HOST=localhost
19REDIS_PORT=6379
20REDIS_PASSWORD=
21
22# CORS
23CORS_ORIGIN=http://localhost:3000,http://localhost:3001
24
25# API Keys
26SENDGRID_API_KEY=
27AWS_ACCESS_KEY_ID=
28AWS_SECRET_ACCESS_KEY=
29
30# Logging
31LOG_LEVEL=info

🚀 Application Setup

✅ Main Application File (app.ts)

 1// src/app.ts
 2import express, { Application, Request, Response, NextFunction } from 'express';
 3import helmet from 'helmet';
 4import cors from 'cors';
 5import compression from 'compression';
 6import morgan from 'morgan';
 7import rateLimit from 'express-rate-limit';
 8import mongoSanitize from 'express-mongo-sanitize';
 9
10import config from './config';
11import routes from './routes';
12import { errorHandler, notFoundHandler } from './middleware/errorHandler';
13import logger from './config/logger';
14
15// ✅ GOOD: Separate app creation from server startup
16export function createApp(): Application {
17  const app = express();
18
19  // Security middleware
20  app.use(helmet({
21    contentSecurityPolicy: {
22      directives: {
23        defaultSrc: ["'self'"],
24        styleSrc: ["'self'", "'unsafe-inline'"],
25        scriptSrc: ["'self'"],
26        imgSrc: ["'self'", 'data:', 'https:'],
27      },
28    },
29  }));
30
31  // CORS configuration
32  app.use(cors(config.cors));
33
34  // Body parsing middleware
35  app.use(express.json({ limit: '10mb' }));
36  app.use(express.urlencoded({ extended: true, limit: '10mb' }));
37
38  // Compression middleware
39  app.use(compression());
40
41  // Data sanitization against NoSQL injection
42  app.use(mongoSanitize());
43
44  // Request logging
45  if (config.env === 'development') {
46    app.use(morgan('dev'));
47  } else {
48    app.use(morgan('combined', {
49      stream: {
50        write: (message: string) => logger.info(message.trim())
51      }
52    }));
53  }
54
55  // Rate limiting
56  const limiter = rateLimit({
57    windowMs: config.rateLimit.windowMs,
58    max: config.rateLimit.max,
59    message: 'Too many requests from this IP, please try again later.',
60    standardHeaders: true,
61    legacyHeaders: false,
62  });
63
64  app.use('/api/', limiter);
65
66  // Health check endpoint
67  app.get('/health', (req: Request, res: Response) => {
68    res.status(200).json({
69      status: 'OK',
70      timestamp: new Date().toISOString(),
71      uptime: process.uptime()
72    });
73  });
74
75  // API routes
76  app.use('/api/v1', routes);
77
78  // 404 handler
79  app.use(notFoundHandler);
80
81  // Global error handler (must be last)
82  app.use(errorHandler);
83
84  return app;
85}

🎯 Server Entry Point (server.ts)

 1// src/server.ts
 2import { createApp } from './app';
 3import config from './config';
 4import logger from './config/logger';
 5import { connectDatabase } from './config/database';
 6import { connectRedis } from './config/redis';
 7
 8// ✅ GOOD: Graceful startup with error handling
 9async function startServer(): Promise<void> {
10  try {
11    // Connect to database
12    await connectDatabase();
13    logger.info('Database connected successfully');
14
15    // Connect to Redis
16    await connectRedis();
17    logger.info('Redis connected successfully');
18
19    // Create Express app
20    const app = createApp();
21
22    // Start server
23    const server = app.listen(config.port, () => {
24      logger.info(`Server running on port ${config.port} in ${config.env} mode`);
25    });
26
27    // Graceful shutdown
28    const gracefulShutdown = async (signal: string) => {
29      logger.info(`${signal} received. Starting graceful shutdown...`);
30
31      server.close(() => {
32        logger.info('HTTP server closed');
33      });
34
35      // Close database connections
36      // await disconnectDatabase();
37      // await disconnectRedis();
38
39      process.exit(0);
40    };
41
42    process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
43    process.on('SIGINT', () => gracefulShutdown('SIGINT'));
44
45    // Handle unhandled rejections
46    process.on('unhandledRejection', (reason, promise) => {
47      logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
48      gracefulShutdown('UNHANDLED_REJECTION');
49    });
50
51    // Handle uncaught exceptions
52    process.on('uncaughtException', (error) => {
53      logger.error('Uncaught Exception:', error);
54      gracefulShutdown('UNCAUGHT_EXCEPTION');
55    });
56
57  } catch (error) {
58    logger.error('Failed to start server:', error);
59    process.exit(1);
60  }
61}
62
63startServer();

🛣️ Routing Best Practices

✅ Route Organization

 1// src/routes/index.ts
 2import { Router } from 'express';
 3import userRoutes from './users';
 4import postRoutes from './posts';
 5import authRoutes from './auth';
 6
 7const router = Router();
 8
 9// ✅ GOOD: Modular route organization
10router.use('/auth', authRoutes);
11router.use('/users', userRoutes);
12router.use('/posts', postRoutes);
13
14export default router;
 1// src/routes/users.ts
 2import { Router } from 'express';
 3import { authenticate } from '../middleware/auth';
 4import { validate } from '../middleware/validation';
 5import { userSchemas } from '../utils/validation';
 6import * as userController from '../controllers/userController';
 7
 8const router = Router();
 9
10// ✅ GOOD: Descriptive routes with middleware chain
11router
12  .route('/')
13  .get(
14    authenticate,
15    userController.getAllUsers
16  )
17  .post(
18    authenticate,
19    validate(userSchemas.createUser),
20    userController.createUser
21  );
22
23router
24  .route('/:id')
25  .get(
26    authenticate,
27    userController.getUserById
28  )
29  .patch(
30    authenticate,
31    validate(userSchemas.updateUser),
32    userController.updateUser
33  )
34  .delete(
35    authenticate,
36    userController.deleteUser
37  );
38
39// Nested resource routes
40router.get(
41  '/:id/posts',
42  authenticate,
43  userController.getUserPosts
44);
45
46export default router;

🎯 RESTful Route Conventions

 1// ✅ GOOD: Follow REST conventions
 2/*
 3GET    /api/v1/users           - Get all users
 4GET    /api/v1/users/:id       - Get user by ID
 5POST   /api/v1/users           - Create new user
 6PATCH  /api/v1/users/:id       - Update user (partial)
 7PUT    /api/v1/users/:id       - Replace user (full)
 8DELETE /api/v1/users/:id       - Delete user
 9
10GET    /api/v1/users/:id/posts - Get user's posts (nested resource)
11POST   /api/v1/users/:id/posts - Create post for user
12*/
13
14// ❌ AVOID: Non-RESTful routes
15/*
16GET    /api/v1/getUsers
17POST   /api/v1/deleteUser
18GET    /api/v1/user-posts
19*/

🎭 Middleware Best Practices

🔐 Authentication Middleware

 1// src/middleware/auth.ts
 2import { Request, Response, NextFunction } from 'express';
 3import jwt from 'jsonwebtoken';
 4import config from '../config';
 5import { AppError } from '../utils/errors';
 6
 7// ✅ GOOD: Extend Express Request type
 8declare global {
 9  namespace Express {
10    interface Request {
11      user?: {
12        id: string;
13        email: string;
14        role: string;
15      };
16    }
17  }
18}
19
20export const authenticate = async (
21  req: Request,
22  res: Response,
23  next: NextFunction
24): Promise<void> => {
25  try {
26    // Get token from header
27    const authHeader = req.headers.authorization;
28
29    if (!authHeader || !authHeader.startsWith('Bearer ')) {
30      throw new AppError('No token provided', 401);
31    }
32
33    const token = authHeader.substring(7);
34
35    // Verify token
36    const decoded = jwt.verify(token, config.jwt.secret) as {
37      id: string;
38      email: string;
39      role: string;
40    };
41
42    // Attach user to request
43    req.user = decoded;
44
45    next();
46  } catch (error) {
47    if (error instanceof jwt.JsonWebTokenError) {
48      next(new AppError('Invalid token', 401));
49    } else if (error instanceof jwt.TokenExpiredError) {
50      next(new AppError('Token expired', 401));
51    } else {
52      next(error);
53    }
54  }
55};
56
57// ✅ GOOD: Role-based authorization
58export const authorize = (...roles: string[]) => {
59  return (req: Request, res: Response, next: NextFunction): void => {
60    if (!req.user) {
61      return next(new AppError('Not authenticated', 401));
62    }
63
64    if (!roles.includes(req.user.role)) {
65      return next(new AppError('Not authorized', 403));
66    }
67
68    next();
69  };
70};

✅ Validation Middleware

 1// src/middleware/validation.ts
 2import { Request, Response, NextFunction } from 'express';
 3import Joi from 'joi';
 4import { AppError } from '../utils/errors';
 5
 6// ✅ GOOD: Generic validation middleware
 7export const validate = (schema: Joi.ObjectSchema) => {
 8  return (req: Request, res: Response, next: NextFunction): void => {
 9    const { error, value } = schema.validate(req.body, {
10      abortEarly: false,
11      stripUnknown: true
12    });
13
14    if (error) {
15      const errors = error.details.map(detail => ({
16        field: detail.path.join('.'),
17        message: detail.message
18      }));
19
20      return next(new AppError('Validation failed', 400, errors));
21    }
22
23    // Replace body with validated value
24    req.body = value;
25    next();
26  };
27};
28
29// src/utils/validation.ts
30import Joi from 'joi';
31
32export const userSchemas = {
33  createUser: Joi.object({
34    name: Joi.string().min(2).max(50).required(),
35    email: Joi.string().email().required(),
36    password: Joi.string().min(8).max(128).required(),
37    role: Joi.string().valid('user', 'admin').default('user')
38  }),
39
40  updateUser: Joi.object({
41    name: Joi.string().min(2).max(50),
42    email: Joi.string().email(),
43    password: Joi.string().min(8).max(128)
44  }).min(1)
45};

🚨 Error Handling Middleware

 1// src/middleware/errorHandler.ts
 2import { Request, Response, NextFunction } from 'express';
 3import logger from '../config/logger';
 4
 5// ✅ GOOD: Custom error class
 6export class AppError extends Error {
 7  public statusCode: number;
 8  public isOperational: boolean;
 9  public details?: any;
10
11  constructor(
12    message: string,
13    statusCode: number = 500,
14    details?: any,
15    isOperational: boolean = true
16  ) {
17    super(message);
18    this.statusCode = statusCode;
19    this.isOperational = isOperational;
20    this.details = details;
21
22    Error.captureStackTrace(this, this.constructor);
23  }
24}
25
26// ✅ GOOD: Comprehensive error handler
27export const errorHandler = (
28  err: Error | AppError,
29  req: Request,
30  res: Response,
31  next: NextFunction
32): void => {
33  let error = err;
34
35  // Convert non-AppError errors
36  if (!(error instanceof AppError)) {
37    error = new AppError(
38      err.message || 'Internal server error',
39      500,
40      undefined,
41      false
42    );
43  }
44
45  const appError = error as AppError;
46
47  // Log error
48  logger.error({
49    message: appError.message,
50    statusCode: appError.statusCode,
51    stack: appError.stack,
52    url: req.url,
53    method: req.method,
54    ip: req.ip
55  });
56
57  // Send response
58  res.status(appError.statusCode).json({
59    status: 'error',
60    message: appError.message,
61    ...(appError.details && { details: appError.details }),
62    ...(process.env.NODE_ENV === 'development' && {
63      stack: appError.stack
64    })
65  });
66};
67
68// ✅ GOOD: 404 handler
69export const notFoundHandler = (
70  req: Request,
71  res: Response,
72  next: NextFunction
73): void => {
74  next(new AppError(`Route ${req.originalUrl} not found`, 404));
75};
76
77// ✅ GOOD: Async error wrapper
78export const catchAsync = (
79  fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
80) => {
81  return (req: Request, res: Response, next: NextFunction): void => {
82    fn(req, res, next).catch(next);
83  };
84};

📊 Request Logging Middleware

 1// src/middleware/requestLogger.ts
 2import { Request, Response, NextFunction } from 'express';
 3import logger from '../config/logger';
 4
 5// ✅ GOOD: Detailed request logging
 6export const requestLogger = (
 7  req: Request,
 8  res: Response,
 9  next: NextFunction
10): void => {
11  const startTime = Date.now();
12
13  // Log request
14  logger.info({
15    type: 'request',
16    method: req.method,
17    url: req.url,
18    ip: req.ip,
19    userAgent: req.get('user-agent')
20  });
21
22  // Capture response
23  res.on('finish', () => {
24    const duration = Date.now() - startTime;
25
26    logger.info({
27      type: 'response',
28      method: req.method,
29      url: req.url,
30      statusCode: res.statusCode,
31      duration: `${duration}ms`
32    });
33  });
34
35  next();
36};

🎮 Controller Best Practices

✅ Controller Pattern

  1// src/controllers/userController.ts
  2import { Request, Response, NextFunction } from 'express';
  3import * as userService from '../services/userService';
  4import { catchAsync } from '../middleware/errorHandler';
  5import { AppError } from '../utils/errors';
  6
  7// ✅ GOOD: Thin controllers with service layer
  8export const getAllUsers = catchAsync(
  9  async (req: Request, res: Response, next: NextFunction) => {
 10    // Parse query parameters
 11    const page = parseInt(req.query.page as string) || 1;
 12    const limit = parseInt(req.query.limit as string) || 10;
 13    const sortBy = (req.query.sortBy as string) || 'createdAt';
 14    const order = (req.query.order as string) || 'desc';
 15
 16    // Call service
 17    const result = await userService.getAllUsers({
 18      page,
 19      limit,
 20      sortBy,
 21      order
 22    });
 23
 24    // Send response
 25    res.status(200).json({
 26      status: 'success',
 27      data: result.users,
 28      pagination: {
 29        page: result.page,
 30        limit: result.limit,
 31        total: result.total,
 32        pages: result.pages
 33      }
 34    });
 35  }
 36);
 37
 38export const getUserById = catchAsync(
 39  async (req: Request, res: Response, next: NextFunction) => {
 40    const { id } = req.params;
 41
 42    const user = await userService.getUserById(id);
 43
 44    if (!user) {
 45      return next(new AppError('User not found', 404));
 46    }
 47
 48    res.status(200).json({
 49      status: 'success',
 50      data: user
 51    });
 52  }
 53);
 54
 55export const createUser = catchAsync(
 56  async (req: Request, res: Response, next: NextFunction) => {
 57    const userData = req.body;
 58
 59    const user = await userService.createUser(userData);
 60
 61    res.status(201).json({
 62      status: 'success',
 63      data: user
 64    });
 65  }
 66);
 67
 68export const updateUser = catchAsync(
 69  async (req: Request, res: Response, next: NextFunction) => {
 70    const { id } = req.params;
 71    const updates = req.body;
 72
 73    const user = await userService.updateUser(id, updates);
 74
 75    if (!user) {
 76      return next(new AppError('User not found', 404));
 77    }
 78
 79    res.status(200).json({
 80      status: 'success',
 81      data: user
 82    });
 83  }
 84);
 85
 86export const deleteUser = catchAsync(
 87  async (req: Request, res: Response, next: NextFunction) => {
 88    const { id } = req.params;
 89
 90    await userService.deleteUser(id);
 91
 92    res.status(204).send();
 93  }
 94);
 95
 96export const getUserPosts = catchAsync(
 97  async (req: Request, res: Response, next: NextFunction) => {
 98    const { id } = req.params;
 99
100    const posts = await userService.getUserPosts(id);
101
102    res.status(200).json({
103      status: 'success',
104      data: posts
105    });
106  }
107);

🏢 Service Layer Best Practices

✅ Service Pattern

  1// src/services/userService.ts
  2import { User } from '../models/User';
  3import { AppError } from '../utils/errors';
  4import bcrypt from 'bcrypt';
  5
  6interface PaginationOptions {
  7  page: number;
  8  limit: number;
  9  sortBy: string;
 10  order: 'asc' | 'desc';
 11}
 12
 13interface PaginatedResult<T> {
 14  data: T[];
 15  page: number;
 16  limit: number;
 17  total: number;
 18  pages: number;
 19}
 20
 21// ✅ GOOD: Service contains business logic
 22export const getAllUsers = async (
 23  options: PaginationOptions
 24): Promise<PaginatedResult<User>> => {
 25  const { page, limit, sortBy, order } = options;
 26  const skip = (page - 1) * limit;
 27
 28  // In real implementation, use your ORM/database query
 29  const [users, total] = await Promise.all([
 30    User.find()
 31      .select('-password')
 32      .sort({ [sortBy]: order })
 33      .skip(skip)
 34      .limit(limit),
 35    User.countDocuments()
 36  ]);
 37
 38  return {
 39    data: users,
 40    page,
 41    limit,
 42    total,
 43    pages: Math.ceil(total / limit)
 44  };
 45};
 46
 47export const getUserById = async (id: string): Promise<User | null> => {
 48  const user = await User.findById(id).select('-password');
 49  return user;
 50};
 51
 52export const createUser = async (userData: Partial<User>): Promise<User> => {
 53  // Check if user exists
 54  const existingUser = await User.findOne({ email: userData.email });
 55
 56  if (existingUser) {
 57    throw new AppError('Email already in use', 400);
 58  }
 59
 60  // Hash password
 61  if (userData.password) {
 62    userData.password = await bcrypt.hash(userData.password, 12);
 63  }
 64
 65  // Create user
 66  const user = await User.create(userData);
 67
 68  // Remove password from response
 69  user.password = undefined;
 70
 71  return user;
 72};
 73
 74export const updateUser = async (
 75  id: string,
 76  updates: Partial<User>
 77): Promise<User | null> => {
 78  // Hash password if it's being updated
 79  if (updates.password) {
 80    updates.password = await bcrypt.hash(updates.password, 12);
 81  }
 82
 83  const user = await User.findByIdAndUpdate(
 84    id,
 85    updates,
 86    { new: true, runValidators: true }
 87  ).select('-password');
 88
 89  return user;
 90};
 91
 92export const deleteUser = async (id: string): Promise<void> => {
 93  const user = await User.findByIdAndDelete(id);
 94
 95  if (!user) {
 96    throw new AppError('User not found', 404);
 97  }
 98};
 99
100export const getUserPosts = async (userId: string): Promise<any[]> => {
101  // Check if user exists
102  const user = await User.findById(userId);
103
104  if (!user) {
105    throw new AppError('User not found', 404);
106  }
107
108  // Fetch user's posts (assuming you have a Post model)
109  // const posts = await Post.find({ author: userId });
110
111  return [];
112};

🔒 Security Best Practices

🛡️ Essential Security Middleware

 1// src/app.ts - Security configuration
 2import helmet from 'helmet';
 3import rateLimit from 'express-rate-limit';
 4import mongoSanitize from 'express-mongo-sanitize';
 5import hpp from 'hpp';
 6
 7// Helmet - Set security headers
 8app.use(helmet());
 9
10// Rate limiting
11const limiter = rateLimit({
12  windowMs: 15 * 60 * 1000, // 15 minutes
13  max: 100,
14  message: 'Too many requests from this IP'
15});
16app.use('/api/', limiter);
17
18// Data sanitization against NoSQL injection
19app.use(mongoSanitize());
20
21// Prevent parameter pollution
22app.use(hpp({
23  whitelist: ['duration', 'ratingsQuantity', 'ratingsAverage', 'price']
24}));

🔐 Authentication Best Practices

 1// src/services/authService.ts
 2import jwt from 'jsonwebtoken';
 3import bcrypt from 'bcrypt';
 4import crypto from 'crypto';
 5import { User } from '../models/User';
 6import { AppError } from '../utils/errors';
 7import config from '../config';
 8
 9// ✅ GOOD: Secure password hashing
10export const hashPassword = async (password: string): Promise<string> => {
11  return await bcrypt.hash(password, 12);
12};
13
14export const comparePassword = async (
15  candidatePassword: string,
16  hashedPassword: string
17): Promise<boolean> => {
18  return await bcrypt.compare(candidatePassword, hashedPassword);
19};
20
21// ✅ GOOD: JWT token generation
22export const generateToken = (userId: string, email: string, role: string): string => {
23  return jwt.sign(
24    { id: userId, email, role },
25    config.jwt.secret,
26    { expiresIn: config.jwt.expiresIn }
27  );
28};
29
30// ✅ GOOD: Refresh token generation
31export const generateRefreshToken = (): string => {
32  return crypto.randomBytes(40).toString('hex');
33};
34
35// ✅ GOOD: Login service
36export const login = async (
37  email: string,
38  password: string
39): Promise<{ user: User; token: string }> => {
40  // Find user
41  const user = await User.findOne({ email }).select('+password');
42
43  if (!user) {
44    throw new AppError('Invalid email or password', 401);
45  }
46
47  // Check password
48  const isPasswordValid = await comparePassword(password, user.password);
49
50  if (!isPasswordValid) {
51    throw new AppError('Invalid email or password', 401);
52  }
53
54  // Generate token
55  const token = generateToken(user.id, user.email, user.role);
56
57  // Remove password from response
58  user.password = undefined;
59
60  return { user, token };
61};

🔑 Input Sanitization

 1// src/utils/sanitization.ts
 2import validator from 'validator';
 3import { AppError } from './errors';
 4
 5// ✅ GOOD: Sanitize user input
 6export const sanitizeInput = (input: string): string => {
 7  return validator.escape(validator.trim(input));
 8};
 9
10export const sanitizeEmail = (email: string): string => {
11  const normalized = validator.normalizeEmail(email) || email;
12  if (!validator.isEmail(normalized)) {
13    throw new AppError('Invalid email format', 400);
14  }
15  return normalized;
16};
17
18export const sanitizeUrl = (url: string): string => {
19  if (!validator.isURL(url)) {
20    throw new AppError('Invalid URL format', 400);
21  }
22  return url;
23};

⚡ Performance Optimization

🚀 Caching Strategy

 1// src/middleware/cache.ts
 2import { Request, Response, NextFunction } from 'express';
 3import Redis from 'ioredis';
 4import config from '../config';
 5
 6const redis = new Redis({
 7  host: config.redis.host,
 8  port: config.redis.port,
 9  password: config.redis.password
10});
11
12// ✅ GOOD: Response caching middleware
13export const cache = (duration: number = 300) => {
14  return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
15    // Only cache GET requests
16    if (req.method !== 'GET') {
17      return next();
18    }
19
20    const key = `cache:${req.originalUrl}`;
21
22    try {
23      // Check cache
24      const cached = await redis.get(key);
25
26      if (cached) {
27        res.set('X-Cache', 'HIT');
28        return res.json(JSON.parse(cached));
29      }
30
31      res.set('X-Cache', 'MISS');
32
33      // Store original send function
34      const originalSend = res.json.bind(res);
35
36      // Override send to cache response
37      res.json = (body: any): Response => {
38        redis.setex(key, duration, JSON.stringify(body));
39        return originalSend(body);
40      };
41
42      next();
43    } catch (error) {
44      // If cache fails, continue without caching
45      next();
46    }
47  };
48};
49
50// ✅ GOOD: Cache invalidation
51export const invalidateCache = async (pattern: string): Promise<void> => {
52  const keys = await redis.keys(`cache:${pattern}`);
53
54  if (keys.length > 0) {
55    await redis.del(...keys);
56  }
57};

📊 Database Query Optimization

 1// src/models/User.ts (Mongoose example)
 2import mongoose, { Schema, Document } from 'mongoose';
 3
 4// ✅ GOOD: Add indexes for frequently queried fields
 5const userSchema = new Schema({
 6  email: {
 7    type: String,
 8    required: true,
 9    unique: true,
10    lowercase: true,
11    index: true // Index for faster lookups
12  },
13  name: {
14    type: String,
15    required: true,
16    index: true
17  },
18  role: {
19    type: String,
20    enum: ['user', 'admin'],
21    default: 'user',
22    index: true
23  },
24  createdAt: {
25    type: Date,
26    default: Date.now,
27    index: true
28  }
29});
30
31// ✅ GOOD: Compound index for common queries
32userSchema.index({ email: 1, role: 1 });
33
34export const User = mongoose.model('User', userSchema);

🎯 Response Compression

 1// src/app.ts
 2import compression from 'compression';
 3
 4// ✅ GOOD: Compress responses
 5app.use(compression({
 6  filter: (req, res) => {
 7    if (req.headers['x-no-compression']) {
 8      return false;
 9    }
10    return compression.filter(req, res);
11  },
12  level: 6
13}));

🧪 Testing Best Practices

✅ Unit Testing Controllers

 1// src/tests/unit/userController.test.ts
 2import request from 'supertest';
 3import { createApp } from '../../app';
 4import * as userService from '../../services/userService';
 5
 6jest.mock('../../services/userService');
 7
 8describe('User Controller', () => {
 9  const app = createApp();
10
11  describe('GET /api/v1/users', () => {
12    it('should return all users with pagination', async () => {
13      const mockUsers = [
14        { id: '1', name: 'John Doe', email: 'john@example.com' },
15        { id: '2', name: 'Jane Doe', email: 'jane@example.com' }
16      ];
17
18      (userService.getAllUsers as jest.Mock).mockResolvedValue({
19        data: mockUsers,
20        page: 1,
21        limit: 10,
22        total: 2,
23        pages: 1
24      });
25
26      const response = await request(app)
27        .get('/api/v1/users')
28        .expect(200);
29
30      expect(response.body.status).toBe('success');
31      expect(response.body.data).toEqual(mockUsers);
32      expect(response.body.pagination.total).toBe(2);
33    });
34  });
35
36  describe('GET /api/v1/users/:id', () => {
37    it('should return user by id', async () => {
38      const mockUser = {
39        id: '1',
40        name: 'John Doe',
41        email: 'john@example.com'
42      };
43
44      (userService.getUserById as jest.Mock).mockResolvedValue(mockUser);
45
46      const response = await request(app)
47        .get('/api/v1/users/1')
48        .expect(200);
49
50      expect(response.body.data).toEqual(mockUser);
51    });
52
53    it('should return 404 if user not found', async () => {
54      (userService.getUserById as jest.Mock).mockResolvedValue(null);
55
56      const response = await request(app)
57        .get('/api/v1/users/999')
58        .expect(404);
59
60      expect(response.body.status).toBe('error');
61    });
62  });
63});

✅ Integration Testing

 1// src/tests/integration/auth.test.ts
 2import request from 'supertest';
 3import { createApp } from '../../app';
 4import { connectDatabase, disconnectDatabase } from '../../config/database';
 5import { User } from '../../models/User';
 6
 7describe('Auth Integration Tests', () => {
 8  const app = createApp();
 9
10  beforeAll(async () => {
11    await connectDatabase();
12  });
13
14  afterAll(async () => {
15    await disconnectDatabase();
16  });
17
18  beforeEach(async () => {
19    await User.deleteMany({});
20  });
21
22  describe('POST /api/v1/auth/register', () => {
23    it('should register a new user', async () => {
24      const userData = {
25        name: 'John Doe',
26        email: 'john@example.com',
27        password: 'password123'
28      };
29
30      const response = await request(app)
31        .post('/api/v1/auth/register')
32        .send(userData)
33        .expect(201);
34
35      expect(response.body.status).toBe('success');
36      expect(response.body.data.user.email).toBe(userData.email);
37      expect(response.body.data.token).toBeDefined();
38    });
39
40    it('should not register user with duplicate email', async () => {
41      const userData = {
42        name: 'John Doe',
43        email: 'john@example.com',
44        password: 'password123'
45      };
46
47      await request(app).post('/api/v1/auth/register').send(userData);
48
49      const response = await request(app)
50        .post('/api/v1/auth/register')
51        .send(userData)
52        .expect(400);
53
54      expect(response.body.status).toBe('error');
55    });
56  });
57});

📝 Logging Best Practices

🔍 Winston Logger Setup

 1// src/config/logger.ts
 2import winston from 'winston';
 3import config from './index';
 4
 5// ✅ GOOD: Structured logging with Winston
 6const logger = winston.createLogger({
 7  level: process.env.LOG_LEVEL || 'info',
 8  format: winston.format.combine(
 9    winston.format.timestamp(),
10    winston.format.errors({ stack: true }),
11    winston.format.json()
12  ),
13  defaultMeta: { service: 'express-api' },
14  transports: [
15    // Console transport
16    new winston.transports.Console({
17      format: winston.format.combine(
18        winston.format.colorize(),
19        winston.format.printf(({ timestamp, level, message, ...meta }) => {
20          return `${timestamp} [${level}]: ${message} ${
21            Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
22          }`;
23        })
24      )
25    }),
26
27    // File transports for production
28    ...(config.env === 'production'
29      ? [
30          new winston.transports.File({
31            filename: 'logs/error.log',
32            level: 'error'
33          }),
34          new winston.transports.File({
35            filename: 'logs/combined.log'
36          })
37        ]
38      : [])
39  ]
40});
41
42export default logger;

🚀 Deployment Best Practices

📦 Production Dependencies

 1{
 2  "name": "express-api",
 3  "version": "1.0.0",
 4  "scripts": {
 5    "dev": "tsx watch src/server.ts",
 6    "build": "tsc",
 7    "start": "node dist/server.js",
 8    "test": "jest",
 9    "test:watch": "jest --watch",
10    "test:coverage": "jest --coverage",
11    "lint": "eslint src/**/*.ts",
12    "format": "prettier --write \"src/**/*.ts\""
13  },
14  "dependencies": {
15    "express": "^4.18.2",
16    "helmet": "^7.1.0",
17    "cors": "^2.8.5",
18    "compression": "^1.7.4",
19    "express-rate-limit": "^7.1.5",
20    "express-mongo-sanitize": "^2.2.0",
21    "hpp": "^0.2.3",
22    "morgan": "^1.10.0",
23    "winston": "^3.11.0",
24    "dotenv": "^16.3.1",
25    "joi": "^17.11.0",
26    "bcrypt": "^5.1.1",
27    "jsonwebtoken": "^9.0.2",
28    "ioredis": "^5.3.2",
29    "mongoose": "^8.0.3"
30  },
31  "devDependencies": {
32    "@types/express": "^4.17.21",
33    "@types/node": "^20.10.6",
34    "@types/bcrypt": "^5.0.2",
35    "@types/jsonwebtoken": "^9.0.5",
36    "typescript": "^5.3.3",
37    "tsx": "^4.7.0",
38    "jest": "^29.7.0",
39    "@types/jest": "^29.5.11",
40    "ts-jest": "^29.1.1",
41    "supertest": "^6.3.3",
42    "@types/supertest": "^6.0.2",
43    "eslint": "^8.56.0",
44    "@typescript-eslint/eslint-plugin": "^6.17.0",
45    "@typescript-eslint/parser": "^6.17.0",
46    "prettier": "^3.1.1"
47  }
48}

🐳 Docker Configuration

 1# Dockerfile
 2FROM node:20-alpine AS builder
 3
 4WORKDIR /app
 5
 6# Copy package files
 7COPY package*.json ./
 8
 9# Install dependencies
10RUN npm ci
11
12# Copy source code
13COPY . .
14
15# Build TypeScript
16RUN npm run build
17
18# Production image
19FROM node:20-alpine
20
21WORKDIR /app
22
23# Copy package files
24COPY package*.json ./
25
26# Install production dependencies only
27RUN npm ci --production
28
29# Copy built files from builder
30COPY --from=builder /app/dist ./dist
31
32# Create non-root user
33RUN addgroup -g 1001 -S nodejs && \
34    adduser -S nodejs -u 1001
35
36USER nodejs
37
38EXPOSE 3000
39
40CMD ["node", "dist/server.js"]
 1# docker-compose.yml
 2version: '3.8'
 3
 4services:
 5  api:
 6    build: .
 7    ports:
 8      - '3000:3000'
 9    environment:
10      - NODE_ENV=production
11      - PORT=3000
12    env_file:
13      - .env
14    depends_on:
15      - postgres
16      - redis
17    restart: unless-stopped
18
19  postgres:
20    image: postgres:15-alpine
21    environment:
22      - POSTGRES_DB=myapp
23      - POSTGRES_USER=postgres
24      - POSTGRES_PASSWORD=password
25    volumes:
26      - postgres_data:/var/lib/postgresql/data
27    restart: unless-stopped
28
29  redis:
30    image: redis:7-alpine
31    restart: unless-stopped
32
33volumes:
34  postgres_data:

📊 Monitoring and Observability

📈 Health Checks and Metrics

 1// src/routes/health.ts
 2import { Router, Request, Response } from 'express';
 3import { checkDatabaseConnection } from '../config/database';
 4import { checkRedisConnection } from '../config/redis';
 5
 6const router = Router();
 7
 8// ✅ GOOD: Comprehensive health check
 9router.get('/health', async (req: Request, res: Response) => {
10  const health = {
11    status: 'OK',
12    timestamp: new Date().toISOString(),
13    uptime: process.uptime(),
14    checks: {
15      database: 'unknown',
16      redis: 'unknown',
17      memory: process.memoryUsage()
18    }
19  };
20
21  try {
22    // Check database
23    const dbConnected = await checkDatabaseConnection();
24    health.checks.database = dbConnected ? 'healthy' : 'unhealthy';
25
26    // Check Redis
27    const redisConnected = await checkRedisConnection();
28    health.checks.redis = redisConnected ? 'healthy' : 'unhealthy';
29
30    const statusCode = dbConnected && redisConnected ? 200 : 503;
31    res.status(statusCode).json(health);
32  } catch (error) {
33    health.status = 'ERROR';
34    res.status(503).json(health);
35  }
36});
37
38export default router;

📚 Summary and Quick Reference

🎯 Key Principles

  1. Modular Architecture: Separate concerns (routes, controllers, services, models)
  2. Error Handling: Centralized error handling with proper logging
  3. Security First: Use helmet, rate limiting, input validation, and sanitization
  4. Type Safety: Use TypeScript for better maintainability
  5. Testing: Write comprehensive unit and integration tests
  6. Performance: Implement caching, compression, and database optimization
  7. Monitoring: Health checks, logging, and metrics

🛠️ Essential Middleware Stack

 1// Recommended middleware order
 2app.use(helmet());              // Security headers
 3app.use(cors());                // CORS
 4app.use(express.json());        // Body parsing
 5app.use(compression());         // Response compression
 6app.use(mongoSanitize());      // NoSQL injection prevention
 7app.use(requestLogger);         // Request logging
 8app.use(rateLimit());          // Rate limiting
 9app.use('/api/v1', routes);    // Routes
10app.use(errorHandler);         // Error handling (last!)
 1# Development
 2npm run dev              # Start dev server with hot reload
 3
 4# Building
 5npm run build            # Compile TypeScript to JavaScript
 6
 7# Production
 8npm start               # Start production server
 9
10# Testing
11npm test                # Run tests
12npm run test:watch      # Run tests in watch mode
13npm run test:coverage   # Generate coverage report
14
15# Code Quality
16npm run lint            # Run ESLint
17npm run format          # Format code with Prettier
18
19# Docker
20docker-compose up       # Start all services
21docker-compose down     # Stop all services

🎨 API Response Format

 1// ✅ GOOD: Consistent response format
 2// Success response
 3{
 4  "status": "success",
 5  "data": { /* response data */ },
 6  "pagination": { /* if applicable */ }
 7}
 8
 9// Error response
10{
11  "status": "error",
12  "message": "Error message",
13  "details": [ /* validation errors if applicable */ ]
14}

🎓 Further Learning Resources

🎯 Conclusion

Express.js provides a solid foundation for building Node.js backend applications, but success depends on implementing proper architecture, security, and best practices. By following the patterns and practices outlined in this guide, you’ll build maintainable, scalable, and production-ready Express applications.

Key Takeaways:

  • Structure your application with clear separation of concerns
  • Implement comprehensive error handling and logging
  • Prioritize security at every layer
  • Write testable code with proper dependency injection
  • Use TypeScript for better maintainability and developer experience
  • Monitor your application’s health and performance

Related Posts:

Tags: #Express #Node.js #Backend #RestAPI #JavaScript #TypeScript #WebDevelopment #BestPractices #Security #Performance