🎯 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
🏗️ Recommended Project 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
- Modular Architecture: Separate concerns (routes, controllers, services, models)
- Error Handling: Centralized error handling with proper logging
- Security First: Use helmet, rate limiting, input validation, and sanitization
- Type Safety: Use TypeScript for better maintainability
- Testing: Write comprehensive unit and integration tests
- Performance: Implement caching, compression, and database optimization
- 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!)
📖 Recommended Project Commands
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
- Official Documentation: Express.js Guide
- Security: Express Security Best Practices
- Performance: Express Performance Best Practices
- TypeScript with Express: TypeScript Deep Dive
- Testing: Supertest Documentation
🎯 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:
- TypeScript Best Practices: A Comprehensive Guide to Type-Safe Development
- Building Production Kubernetes Platform with AWS EKS and CDK
- Microservices Architecture Patterns
Tags: #Express #Node.js #Backend #RestAPI #JavaScript #TypeScript #WebDevelopment #BestPractices #Security #Performance