Kanad
|

About

Projects

Blogs

Contact

|

©2025 / Kanad Shee/ Building Scalabale Products

©2025 / Kanad Shee

On This Page

Advance Backend Setup

Monday, August 4, 2025

•

By Kanad Shee

Node.js with Express, TypeScript, Winston Logger, and more

Advance Backend Setup

Backend (Node + TS)


Setting up a robust backend from scratch can feel overwhelming—especially when aiming for scalability, strong typing, and proper logging from the get-go. But with Node.js + Express + TypeScript, it’s entirely possible to craft a production-ready backend with clarity and control.

This project demonstrates how to build a modular, typed, and scalable backend using:

  • ⚙️ Node.js for the runtime
  • 🧭 Express for routing and middleware
  • 🧑‍💻 TypeScript for static typing and IDE support
  • 📑 Winston for structured, configurable logging

Key Features

Tools and Technologies

ToolPurpose
Node.jsRuntime environment
ExpressWeb framework / routing
TypeScriptStatic typing / developer DX
WinstonStructured logging
Jest + SupertestTesting & API tests
ESLint + PrettierLinting & formatting

Tech Stack

  • Node.js 18+ with TypeScript 5.4+
  • Express.js 4.19+ with comprehensive middleware
  • ESLint + Prettier for code quality
  • Jest + Supertest for testing
  • Winston for structured logging

Security

  • Helmet for security headers
  • CORS configuration
  • Rate limiting (express-rate-limit)
  • Input validation (express-validator)
  • API key authentication
  • Request ID tracking
  • Graceful shutdown handling

Dev Experience

  • Hot reload with tsx
  • Pre-commit hooks with Husky
  • Comprehensive error handling
  • Health check endpoints

Structure

src/
├── config/                 # Configuration files
│ └── config.ts             # Environment configuration
├── middleware/             # Custom middleware
│ ├── auth.ts               # Authentication middleware
│ ├── errorHandler.ts       # Global error handler
│ ├── notFoundHandler.ts    # 404 handler
│ └── requestLogger.ts      # Request logging
├── routes/                 # Route definitions
│ ├── api.ts                # Main API routes
│ ├── health.ts             # Health check routes
│ └── users.ts              # User routes
├── utils/                  # Utility functions
│ ├── AppError.ts           # Custom error classes
│ └── logger.ts             # Winston logger setup
├── app.ts                  # Express app configuration
└── index.ts                # Application entry point

The application follows a clean, modular architecture with:

  • Configuration management with environment validation
  • Custom middleware for auth, logging, and error handling
  • RESTful API routes with full CRUD operations
  • Comprehensive health checks for monitoring
  • Docker support for containerized deployment

Lets get into deep in all files:

  • First create a package.json file
npm init -y
  • Now replace this file with this:

    Here I have added all the necessary dependecies and dev dependencies needed for our project and setup:

// File name: package.json

{
  "name": "express-typescript-app",
  "version": "1.0.0",
  "description": "Production-grade Express.js application with TypeScript",
  "main": "dist/index.js",
  "scripts": {
    "start": "node dist/index.js",
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "build:watch": "tsc --watch",
    "clean": "rimraf dist",
    "lint": "eslint src/**/*.ts",
    "lint:fix": "eslint src/**/*.ts --fix",
    "format": "prettier --write src/**/*.ts",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "typecheck": "tsc --noEmit",
    "prepare": "husky install"
  },
  "keywords": ["express", "typescript", "node", "api"],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "express": "^4.19.2",
    "helmet": "^7.1.0",
    "cors": "^2.8.5",
    "compression": "^1.7.4",
    "morgan": "^1.10.0",
    "dotenv": "^16.4.5",
    "express-rate-limit": "^7.2.0",
    "express-validator": "^7.0.1",
    "winston": "^3.13.0",
    "express-async-errors": "^3.1.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.12.7",
    "@types/cors": "^2.8.17",
    "@types/compression": "^1.7.5",
    "@types/morgan": "^1.9.9",
    "@types/jest": "^29.5.12",
    "@types/supertest": "^6.0.2",
    "typescript": "^5.4.5",
    "tsx": "^4.7.2",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.2",
    "supertest": "^7.0.0",
    "eslint": "^8.57.0",
    "@typescript-eslint/eslint-plugin": "^7.7.1",
    "@typescript-eslint/parser": "^7.7.1",
    "prettier": "^3.2.5",
    "husky": "^9.0.11",
    "lint-staged": "^15.2.2",
    "rimraf": "^5.0.5",
    "nodemon": "^3.1.0"
  },
  "engines": {
    "node": ">=18.0.0",
    "npm": ">=8.0.0"
  },
  "lint-staged": {
    "*.{ts,js}": ["eslint --fix", "prettier --write"]
  }
}
  • Now move to terminal and run,
npm install
  • Now, we have to initialize typescript for our project:
npx tsc --init
  • This will create a tsconfig.json file for us:

  • And now replace that file with these options:

// File Name: tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "CommonJS",
    "moduleResolution": "node",
    "rootDir": "./src",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": true,
    "noEmitOnError": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"],
      "@/controllers/*": ["src/controllers/*"],
      "@/middleware/*": ["src/middleware/*"],
      "@/routes/*": ["src/routes/*"],
      "@/services/*": ["src/services/*"],
      "@/types/*": ["src/types/*"],
      "@/utils/*": ["src/utils/*"],
      "@/config/*": ["src/config/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}

Linting

  • First create a eslint.json file and paste following configuration:
{
  "env": {
    "node": true,
    "es2022": true,
    "jest": true
  },
  "extends": [
    "eslint:recommended",
    "@typescript-eslint/recommended",
    "@typescript-eslint/recommended-requiring-type-checking"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "plugins": ["@typescript-eslint"],
  "rules": {
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/no-explicit-any": "warn",
    "@typescript-eslint/explicit-function-return-type": "warn",
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/await-thenable": "error",
    "@typescript-eslint/no-misused-promises": "error",
    "@typescript-eslint/prefer-nullish-coalescing": "error",
    "@typescript-eslint/prefer-optional-chain": "error",
    "no-console": "warn",
    "prefer-const": "error",
    "no-var": "error"
  },
  "ignorePatterns": ["dist/", "node_modules/", "*.js"]
}
  • Now, setup prettier by creating one more file .prettierrc:
{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false,
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "endOfLine": "lf"
}
  • Now create a jest config also for testing named jest.config.ts:
// File Name: jest.config.ts
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
  },
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/*.test.ts',
    '!src/**/*.spec.ts',
  ],
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  setupFilesAfterEnv: ['<rootDir>/src/tests/setup.ts'],
};

.gitignore

# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build outputs
dist/
build/

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Logs
logs/
*.log

# Runtime data
pids/
*.pid
*.seed
*.pid.lock

# Coverage directory used by tools like istanbul
coverage/
*.lcov

# nyc test coverage
.nyc_output

# Dependency directories
jspm_packages/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Testing
.jest/

# Temporary files
temp/
tmp/

  • Now in project ROOT create a src directory and inside that create some more directories and two files:
    • config
    • middleware
    • routes
    • tests
    • utils
    • app.ts
    • index.ts

Env Variables

# Environment Configuration
NODE_ENV=development
PORT=3000

# Security
API_KEY=your-super-secret-api-key-here

# Logging
LOG_LEVEL=info

# CORS Origins (comma-separated)
CORS_ORIGINS=http://localhost:3000,http://localhost:3001

# Rate Limiting
RATE_LIMIT_MAX=100

# Database (optional)
DATABASE_URL=postgresql://username:password@localhost:5432/myapp

# Redis (optional)
REDIS_URL=redis://localhost:6379

# JWT (optional)
JWT_SECRET=your-jwt-secret-key-here

Project Setup

  • Inside, src > config, create a file named: config.ts
// File Name: config.ts

// Environment validation without zod for simplicity
interface EnvConfig {
  NODE_ENV: 'development' | 'production' | 'test';
  PORT: number;
  API_KEY: string;
  LOG_LEVEL: 'error' | 'warn' | 'info' | 'debug';
  CORS_ORIGINS: string[];
  RATE_LIMIT_MAX: number;
  DATABASE_URL?: string | undefined;
  REDIS_URL?: string | undefined;
  JWT_SECRET?: string | undefined;
}

// Validate environment variables
const validateEnv = (): EnvConfig => {
  const requiredVars = ['API_KEY'];
  const missingVars = requiredVars.filter((varName) => !process.env[varName]);

  if (missingVars.length > 0) {
    throw new Error(
      `Missing required environment variables: ${missingVars.join(', ')}`,
    );
  }

  return {
    NODE_ENV: (process.env.NODE_ENV as EnvConfig['NODE_ENV']) || 'development',
    PORT: parseInt(process.env.PORT || '3000', 10),
    API_KEY: process.env.API_KEY!,
    LOG_LEVEL: (process.env.LOG_LEVEL as EnvConfig['LOG_LEVEL']) || 'info',
    CORS_ORIGINS: (process.env.CORS_ORIGINS || 'http://localhost:3000')
      .split(',')
      .map((origin) => origin.trim()),
    RATE_LIMIT_MAX: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
    DATABASE_URL: process.env.DATABASE_URL,
    REDIS_URL: process.env.REDIS_URL,
    JWT_SECRET: process.env.JWT_SECRET,
  };
};

const env = validateEnv();

export const config = {
  nodeEnv: env.NODE_ENV,
  port: env.PORT,
  apiKey: env.API_KEY,
  logLevel: env.LOG_LEVEL,
  corsOrigins: env.CORS_ORIGINS,
  rateLimitMax: env.RATE_LIMIT_MAX,
  databaseUrl: env.DATABASE_URL,
  redisUrl: env.REDIS_URL,
  jwtSecret: env.JWT_SECRET,
} as const;

export type Config = typeof config;

Utils

  • AppError.ts

  • logger.ts

  • Lets create the AppError.ts file:

// File Name: AppError.ts
export class AppError extends Error {
  public readonly statusCode: number;
  public readonly isOperational: boolean;
  public readonly timestamp: string;

  constructor(
    message: string,
    statusCode: number = 500,
    isOperational: boolean = true,
  ) {
    super(message);

    this.statusCode = statusCode;
    this.isOperational = isOperational;
    this.timestamp = new Date().toISOString();

    // Maintains proper stack trace for where our error was thrown
    Error.captureStackTrace(this, this.constructor);

    // Set the prototype explicitly
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

// Predefined error classes for common HTTP errors
export class BadRequestError extends AppError {
  constructor(message: string = 'Bad Request') {
    super(message, 400);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message: string = 'Unauthorized') {
    super(message, 401);
  }
}

export class ForbiddenError extends AppError {
  constructor(message: string = 'Forbidden') {
    super(message, 403);
  }
}

export class NotFoundError extends AppError {
  constructor(message: string = 'Not Found') {
    super(message, 404);
  }
}

export class ConflictError extends AppError {
  constructor(message: string = 'Conflict') {
    super(message, 409);
  }
}

export class ValidationError extends AppError {
  constructor(message: string = 'Validation Error') {
    super(message, 422);
  }
}

export class TooManyRequestsError extends AppError {
  constructor(message: string = 'Too Many Requests') {
    super(message, 429);
  }
}

export class InternalServerError extends AppError {
  constructor(message: string = 'Internal Server Error') {
    super(message, 500);
  }
}

export class ServiceUnavailableError extends AppError {
  constructor(message: string = 'Service Unavailable') {
    super(message, 503);
  }
}
  • Next create a logger.ts file:
// File Name: logger.ts

import winston from 'winston';
import { config } from '@/config/config';

// Custom log levels
const levels = {
  error: 0,
  warn: 1,
  info: 2,
  debug: 3,
};

// Custom colors for log levels
const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  debug: 'blue',
};

winston.addColors(colors);

// Create logger instance
export const logger = winston.createLogger({
  level: config.logLevel,
  levels,
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    winston.format.json(),
    winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
      let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;

      if (stack) {
        log += `\n${stack}`;
      }

      if (Object.keys(meta).length > 0) {
        log += `\n${JSON.stringify(meta, null, 2)}`;
      }

      return log;
    }),
  ),
  transports: [
    // Console transport with colors for development
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize({ all: true }),
        winston.format.simple(),
      ),
    }),
  ],
  exceptionHandlers: [new winston.transports.Console()],
  rejectionHandlers: [new winston.transports.Console()],
  exitOnError: false,
});

// Add file transports for production
if (config.nodeEnv === 'production') {
  logger.add(
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json(),
      ),
    }),
  );

  logger.add(
    new winston.transports.File({
      filename: 'logs/combined.log',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json(),
      ),
    }),
  );
}

// Create a stream object for Morgan HTTP logging
export const stream = {
  write: (message: string): void => {
    logger.info(message.trim());
  },
};

Middleware

  • auth.ts

  • errorHandler.ts

  • health.ts

  • notFoundHandler.ts

  • requestLogger.ts

  • First create the auth.ts file:

// File Name: auth.ts
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedError } from '@/utils/AppError';
import { config } from '@/config/config';
import { logger } from '@/utils/logger';

export const validateApiKey = (
  req: Request,
  res: Response,
  next: NextFunction,
): void => {
  try {
    const apiKey =
      req.headers['x-api-key'] ||
      req.headers['authorization']?.replace('Bearer ', '');

    if (!apiKey) {
      logger.warn('API key missing', {
        requestId: req.requestId,
        ip: req.ip,
        url: req.originalUrl,
      });
      throw new UnauthorizedError('API key is required');
    }

    if (apiKey !== config.apiKey) {
      logger.warn('Invalid API key attempted', {
        requestId: req.requestId,
        ip: req.ip,
        url: req.originalUrl,
        providedKey:
          typeof apiKey === 'string'
            ? apiKey.substring(0, 8) + '...'
            : 'invalid',
      });
      throw new UnauthorizedError('Invalid API key');
    }

    logger.debug('API key validated successfully', {
      requestId: req.requestId,
      ip: req.ip,
    });

    next();
  } catch (error) {
    next(error);
  }
};

// Optional: JWT validation middleware (if using JWT tokens)
export const validateJWT = (
  req: Request,
  res: Response,
  next: NextFunction,
): void => {
  try {
    const token = req.headers.authorization?.replace('Bearer ', '');

    if (!token) {
      throw new UnauthorizedError('JWT token is required');
    }

    // Add JWT validation logic here
    // Example: jwt.verify(token, config.jwtSecret)

    next();
  } catch (error) {
    next(error);
  }
};

// Rate limiting per user/API key
export const createUserRateLimit = (windowMs: number, max: number) => {
  const requests = new Map<string, { count: number; resetTime: number }>();

  return (req: Request, res: Response, next: NextFunction): void => {
    const identifier = (req.headers['x-api-key'] as string) || req.ip;
    const now = Date.now();
    const windowStart = now - windowMs;

    // Clean up old entries
    for (const [key, value] of requests.entries()) {
      if (value.resetTime < windowStart) {
        requests.delete(key);
      }
    }

    // Get or create request counter
    let requestData = requests.get(identifier as string);
    if (!requestData || requestData.resetTime < windowStart) {
      requestData = { count: 0, resetTime: now + windowMs };
      requests.set(identifier as string, requestData);
    }

    requestData.count++;

    if (requestData.count > max) {
      logger.warn('Rate limit exceeded', {
        identifier: String(identifier).substring(0, 8) + '...',
        count: requestData.count,
        max,
        requestId: req.requestId,
      });

      res.status(429).json({
        error: {
          message: 'Rate limit exceeded',
          retryAfter: Math.ceil((requestData.resetTime - now) / 1000),
        },
      });
      return;
    }

    next();
  };
};
  • Lets create the errorHandler.ts file:
// errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '@/utils/AppError';
import { logger } from '@/utils/logger';
import { config } from '@/config/config';

interface ErrorResponse {
  error: {
    message: string;
    status: string;
    statusCode: number;
    timestamp: string;
    path: string;
    method: string;
    stack?: string;
    requestId?: string;
  };
}

export const globalErrorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction,
): void => {
  let error = err;

  // Convert non-AppError errors to AppError
  if (!(error instanceof AppError)) {
    // Handle specific error types
    if (error.name === 'ValidationError') {
      error = new AppError('Validation Error', 400);
    } else if (error.name === 'CastError') {
      error = new AppError('Invalid data format', 400);
    } else if (error.name === 'MongoError' && (error as any).code === 11000) {
      error = new AppError('Duplicate field value', 409);
    } else if (error.name === 'JsonWebTokenError') {
      error = new AppError('Invalid token', 401);
    } else if (error.name === 'TokenExpiredError') {
      error = new AppError('Token expired', 401);
    } else {
      // Generic server error
      error = new AppError('Something went wrong', 500, false);
    }
  }

  const appError = error as AppError;
  const statusCode = appError.statusCode || 500;

  // Log error details
  const logData = {
    error: {
      message: appError.message,
      stack: appError.stack,
      statusCode,
      url: req.originalUrl,
      method: req.method,
      ip: req.ip,
      userAgent: req.get('User-Agent'),
      timestamp: new Date().toISOString(),
    },
  };

  if (statusCode >= 500) {
    logger.error('Server Error:', logData);
  } else {
    logger.warn('Client Error:', logData);
  }

  // Prepare error response
  const errorResponse: ErrorResponse = {
    error: {
      message: appError.message,
      status: statusCode >= 500 ? 'error' : 'fail',
      statusCode,
      timestamp: appError.timestamp || new Date().toISOString(),
      path: req.originalUrl,
      method: req.method,
    },
  };

  // Include stack trace in development
  if (config.nodeEnv === 'development') {
    errorResponse.error.stack = appError.stack as string;
  }

  // Add request ID if available
  if (req.headers['x-request-id']) {
    errorResponse.error.requestId = req.headers['x-request-id'] as string;
  }

  res.status(statusCode).json(errorResponse);
};
  • We will also need a middleware to check server health. So create health.ts file:
// File Name: health.ts
import { Router, Request, Response } from 'express';
import { config } from '@/config/config';

const router = Router();

interface HealthResponse {
  status: 'ok' | 'error';
  timestamp: string;
  uptime: number;
  environment: string;
  version: string;
  memory: {
    used: number;
    free: number;
    total: number;
    usage: string;
  };
  system: {
    platform: string;
    arch: string;
    nodeVersion: string;
  };
  services?: {
    database?: 'connected' | 'disconnected' | 'error';
    redis?: 'connected' | 'disconnected' | 'error';
  };
}

// Basic health check
router.get('/', (req: Request, res: Response): void => {
  const memUsage = process.memoryUsage();
  const totalMemory = memUsage.heapTotal;
  const usedMemory = memUsage.heapUsed;
  const freeMemory = totalMemory - usedMemory;

  const healthData: HealthResponse = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: Math.floor(process.uptime()),
    environment: config.nodeEnv,
    version: process.env.npm_package_version || '1.0.0',
    memory: {
      used: Math.round(usedMemory / 1024 / 1024), // MB
      free: Math.round(freeMemory / 1024 / 1024), // MB
      total: Math.round(totalMemory / 1024 / 1024), // MB
      usage: `${Math.round((usedMemory / totalMemory) * 100)}%`,
    },
    system: {
      platform: process.platform,
      arch: process.arch,
      nodeVersion: process.version,
    },
  };

  res.status(200).json(healthData);
});

// Detailed health check with services
router.get('/detailed', async (req: Request, res: Response): Promise<void> => {
  const memUsage = process.memoryUsage();
  const totalMemory = memUsage.heapTotal;
  const usedMemory = memUsage.heapUsed;
  const freeMemory = totalMemory - usedMemory;

  const healthData: HealthResponse = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: Math.floor(process.uptime()),
    environment: config.nodeEnv,
    version: process.env.npm_package_version || '1.0.0',
    memory: {
      used: Math.round(usedMemory / 1024 / 1024), // MB
      free: Math.round(freeMemory / 1024 / 1024), // MB
      total: Math.round(totalMemory / 1024 / 1024), // MB
      usage: `${Math.round((usedMemory / totalMemory) * 100)}%`,
    },
    system: {
      platform: process.platform,
      arch: process.arch,
      nodeVersion: process.version,
    },
    services: {},
  };

  // Check database connection (if configured)
  if (config.databaseUrl) {
    try {
      // Add your database health check logic here
      // Example: await database.ping()
      healthData.services!.database = 'connected';
    } catch (error) {
      healthData.services!.database = 'error';
      healthData.status = 'error';
    }
  }

  // Check Redis connection (if configured)
  if (config.redisUrl) {
    try {
      // Add your Redis health check logic here
      // Example: await redis.ping()
      healthData.services!.redis = 'connected';
    } catch (error) {
      healthData.services!.redis = 'error';
      healthData.status = 'error';
    }
  }

  const statusCode = healthData.status === 'ok' ? 200 : 503;
  res.status(statusCode).json(healthData);
});

// Readiness probe (for Kubernetes)
router.get('/ready', (req: Request, res: Response): void => {
  // Add any readiness checks here
  // Example: check if database migrations are complete

  res.status(200).json({
    status: 'ready',
    timestamp: new Date().toISOString(),
  });
});

// Liveness probe (for Kubernetes)
router.get('/live', (req: Request, res: Response): void => {
  res.status(200).json({
    status: 'alive',
    timestamp: new Date().toISOString(),
  });
});

export default router;
  • Now, create the notFoundHandler.ts file:
// File Name: notFoundHandler.ts

import { Request, Response, NextFunction } from 'express';
import { NotFoundError } from '@/utils/AppError';

export const notFoundHandler = (
  req: Request,
  res: Response,
  next: NextFunction,
): void => {
  const message = `Route ${req.originalUrl} not found`;
  next(new NotFoundError(message));
};
  • And the last file requestLogger.ts to log each incoming request:
// requestLogger.ts
import { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import { logger } from '@/utils/logger';

// Extend Request interface to include requestId
declare global {
  namespace Express {
    interface Request {
      requestId: string;
      startTime: number;
    }
  }
}

export const requestLogger = (
  req: Request,
  res: Response,
  next: NextFunction,
): void => {
  // Generate unique request ID
  req.requestId =
    (req.headers['x-request-id'] as string) || crypto.randomUUID();
  req.startTime = Date.now();

  // Set request ID in response header
  res.setHeader('x-request-id', req.requestId);

  // Log request details
  logger.info('Incoming Request', {
    requestId: req.requestId,
    method: req.method,
    url: req.originalUrl,
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    timestamp: new Date().toISOString(),
  });

  // Override res.end to log response details
  const originalEnd = res.end;
  res.end = function (chunk?: any, encoding?: any): any {
    const responseTime = Date.now() - req.startTime;

    logger.info('Outgoing Response', {
      requestId: req.requestId,
      method: req.method,
      url: req.originalUrl,
      statusCode: res.statusCode,
      responseTime: `${responseTime}ms`,
      contentLength: res.get('content-length') || 0,
      timestamp: new Date().toISOString(),
    });

    originalEnd.call(this, chunk, encoding);
  };

  next();
};

Tests

  • So, create a file setup.ts inside tests directory inside src:
// File Name: setup.ts
// Test setup file
import 'dotenv/config';

// Set test environment
process.env.NODE_ENV = 'test';
process.env.API_KEY = 'test-api-key';
process.env.LOG_LEVEL = 'error'; // Suppress logs during tests

// Global test timeout
jest.setTimeout(10000);

// Mock console methods to reduce test noise
global.console = {
  ...console,
  log: jest.fn(),
  debug: jest.fn(),
  info: jest.fn(),
  warn: jest.fn(),
  error: jest.fn(),
};
  • So,the test setup is complete.

Routes

├── routes/ # Route definitions
│ ├── api.ts # Main API routes
│ ├── health.ts # Health check routes
│ └── users.ts # User routes
  • First create the users.ts file:
// File name: users.ts

import { Router, Request, Response, NextFunction } from 'express';
import { body, param, query, validationResult } from 'express-validator';
import { ValidationError, NotFoundError } from '@/utils/AppError';
import { logger } from '@/utils/logger';

const router = Router();

// Mock user data (replace with actual database)
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
  updatedAt: string;
}

let users: User[] = [
  {
    id: '1',
    name: 'John Doe',
    email: 'john@example.com',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  },
  {
    id: '2',
    name: 'Jane Smith',
    email: 'jane@example.com',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  },
];

// Validation middleware
const validateUser = [
  body('name')
    .trim()
    .isLength({ min: 2, max: 50 })
    .withMessage('Name must be between 2 and 50 characters'),
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Please provide a valid email'),
];

const validateUserId = [
  param('id').isLength({ min: 1 }).withMessage('User ID is required'),
];

const validateQuery = [
  query('page')
    .optional()
    .isInt({ min: 1 })
    .withMessage('Page must be a positive integer'),
  query('limit')
    .optional()
    .isInt({ min: 1, max: 100 })
    .withMessage('Limit must be between 1 and 100'),
  query('search')
    .optional()
    .trim()
    .isLength({ min: 1 })
    .withMessage('Search term cannot be empty'),
];

// Validation error handler
const handleValidationErrors = (
  req: Request,
  res: Response,
  next: NextFunction,
): void => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    const errorMessages = errors.array().map((error) => error.msg);
    throw new ValidationError(`Validation failed: ${errorMessages.join(', ')}`);
  }
  next();
};

// GET /api/users - Get all users with pagination and search
router.get(
  '/',
  validateQuery,
  handleValidationErrors,
  (req: Request, res: Response): void => {
    const page = parseInt(req.query.page as string) || 1;
    const limit = parseInt(req.query.limit as string) || 10;
    const search = req.query.search as string;

    let filteredUsers = users;

    // Apply search filter
    if (search) {
      filteredUsers = users.filter(
        (user) =>
          user.name.toLowerCase().includes(search.toLowerCase()) ||
          user.email.toLowerCase().includes(search.toLowerCase()),
      );
    }

    // Calculate pagination
    const startIndex = (page - 1) * limit;
    const endIndex = startIndex + limit;
    const paginatedUsers = filteredUsers.slice(startIndex, endIndex);

    logger.info('Users retrieved', {
      requestId: req.requestId,
      total: filteredUsers.length,
      page,
      limit,
      search: search || null,
    });

    res.status(200).json({
      data: paginatedUsers,
      pagination: {
        page,
        limit,
        total: filteredUsers.length,
        pages: Math.ceil(filteredUsers.length / limit),
        hasNext: endIndex < filteredUsers.length,
        hasPrev: page > 1,
      },
      search: search || null,
    });
  },
);

// GET /api/users/:id - Get user by ID
router.get(
  '/:id',
  validateUserId,
  handleValidationErrors,
  (req: Request, res: Response): void => {
    const { id } = req.params;
    const user = users.find((u) => u.id === id);

    if (!user) {
      throw new NotFoundError(`User with ID ${id} not found`);
    }

    logger.info('User retrieved', {
      requestId: req.requestId,
      userId: id,
    });

    res.status(200).json({
      data: user,
    });
  },
);

// POST /api/users - Create new user
router.post(
  '/',
  validateUser,
  handleValidationErrors,
  (req: Request, res: Response): void => {
    const { name, email } = req.body;

    // Check if email already exists
    const existingUser = users.find((u) => u.email === email);
    if (existingUser) {
      throw new ValidationError('Email already exists');
    }

    const newUser: User = {
      id: (users.length + 1).toString(),
      name,
      email,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    users.push(newUser);

    logger.info('User created', {
      requestId: req.requestId,
      userId: newUser.id,
      email: newUser.email,
    });

    res.status(201).json({
      message: 'User created successfully',
      data: newUser,
    });
  },
);

// PUT /api/users/:id - Update user
router.put(
  '/:id',
  validateUserId,
  validateUser,
  handleValidationErrors,
  (req: Request, res: Response): void => {
    const { id } = req.params;
    const { name, email } = req.body;

    const userIndex = users.findIndex((u) => u.id === id);
    if (userIndex === -1) {
      throw new NotFoundError(`User with ID ${id} not found`);
    }

    // Check if email already exists (excluding current user)
    const existingUser = users.find((u) => u.email === email && u.id !== id);
    if (existingUser) {
      throw new ValidationError('Email already exists');
    }

    const existing = users[userIndex];

    if (!existing) {
      throw new NotFoundError(`User with ID ${id} not found`);
    }

    users[userIndex] = {
      id: existing.id,
      name,
      email,
      createdAt: existing.createdAt ?? new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    logger.info('User updated', {
      requestId: req.requestId,
      userId: id,
      email,
    });

    res.status(200).json({
      message: 'User updated successfully',
      data: users[userIndex],
    });
  },
);

// DELETE /api/users/:id - Delete user
router.delete(
  '/:id',
  validateUserId,
  handleValidationErrors,
  (req: Request, res: Response): void => {
    const { id } = req.params;

    const userIndex = users.findIndex((u) => u.id === id);
    if (userIndex === -1) {
      throw new NotFoundError(`User with ID ${id} not found`);
    }

    const deletedUser = users.splice(userIndex, 1)[0];

    logger.info('User deleted', {
      requestId: req.requestId,
      userId: id,
      email: deletedUser?.email,
    });

    res.status(200).json({
      message: 'User deleted successfully',
      data: deletedUser,
    });
  },
);

export default router;
  • Now, create the health.ts file:
// File name: health.ts
import { Router, Request, Response } from 'express';
import { config } from '@/config/config';

const router = Router();

interface HealthResponse {
  status: 'ok' | 'error';
  timestamp: string;
  uptime: number;
  environment: string;
  version: string;
  memory: {
    used: number;
    free: number;
    total: number;
    usage: string;
  };
  system: {
    platform: string;
    arch: string;
    nodeVersion: string;
  };
  services?: {
    database?: 'connected' | 'disconnected' | 'error';
    redis?: 'connected' | 'disconnected' | 'error';
  };
}

// Basic health check
router.get('/', (req: Request, res: Response): void => {
  const memUsage = process.memoryUsage();
  const totalMemory = memUsage.heapTotal;
  const usedMemory = memUsage.heapUsed;
  const freeMemory = totalMemory - usedMemory;

  const healthData: HealthResponse = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: Math.floor(process.uptime()),
    environment: config.nodeEnv,
    version: process.env.npm_package_version || '1.0.0',
    memory: {
      used: Math.round(usedMemory / 1024 / 1024), // MB
      free: Math.round(freeMemory / 1024 / 1024), // MB
      total: Math.round(totalMemory / 1024 / 1024), // MB
      usage: `${Math.round((usedMemory / totalMemory) * 100)}%`,
    },
    system: {
      platform: process.platform,
      arch: process.arch,
      nodeVersion: process.version,
    },
  };

  res.status(200).json(healthData);
});

// Detailed health check with services
router.get('/detailed', async (req: Request, res: Response): Promise<void> => {
  const memUsage = process.memoryUsage();
  const totalMemory = memUsage.heapTotal;
  const usedMemory = memUsage.heapUsed;
  const freeMemory = totalMemory - usedMemory;

  const healthData: HealthResponse = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: Math.floor(process.uptime()),
    environment: config.nodeEnv,
    version: process.env.npm_package_version || '1.0.0',
    memory: {
      used: Math.round(usedMemory / 1024 / 1024), // MB
      free: Math.round(freeMemory / 1024 / 1024), // MB
      total: Math.round(totalMemory / 1024 / 1024), // MB
      usage: `${Math.round((usedMemory / totalMemory) * 100)}%`,
    },
    system: {
      platform: process.platform,
      arch: process.arch,
      nodeVersion: process.version,
    },
    services: {},
  };

  // Check database connection (if configured)
  if (config.databaseUrl) {
    try {
      // Add your database health check logic here
      // Example: await database.ping()
      healthData.services!.database = 'connected';
    } catch (error) {
      healthData.services!.database = 'error';
      healthData.status = 'error';
    }
  }

  // Check Redis connection (if configured)
  if (config.redisUrl) {
    try {
      // Add your Redis health check logic here
      // Example: await redis.ping()
      healthData.services!.redis = 'connected';
    } catch (error) {
      healthData.services!.redis = 'error';
      healthData.status = 'error';
    }
  }

  const statusCode = healthData.status === 'ok' ? 200 : 503;
  res.status(statusCode).json(healthData);
});

// Readiness probe (for Kubernetes)
router.get('/ready', (req: Request, res: Response): void => {
  // Add any readiness checks here
  // Example: check if database migrations are complete

  res.status(200).json({
    status: 'ready',
    timestamp: new Date().toISOString(),
  });
});

// Liveness probe (for Kubernetes)
router.get('/live', (req: Request, res: Response): void => {
  res.status(200).json({
    status: 'alive',
    timestamp: new Date().toISOString(),
  });
});

export default router;
  • Next create api.ts file:
// File Name: api.ts
import { Router } from 'express';
import usersRouter from './users';
// Import other route modules here
// import productsRouter from './products';
// import ordersRouter from './orders';

const router = Router();

// API version info
router.get('/', (req, res) => {
  res.json({
    message: 'API v1',
    version: '1.0.0',
    timestamp: new Date().toISOString(),
    endpoints: {
      users: '/api/users',
      // Add other endpoints here
    },
  });
});

// Mount route modules
router.use('/users', usersRouter);
// router.use('/products', productsRouter);
// router.use('/orders', ordersRouter);

export default router;

App & Entry

  • Lets first create the app.ts which is our default app configuration:
// File Name: app.ts
import express, { Application, Request, Response, NextFunction } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';
import 'express-async-errors';

import { config } from '@/config/config';
import { logger } from '@/utils/logger';
import { globalErrorHandler } from '@/middleware/errorHandler';

// Import routes
import apiRouter from '@/routes/api';
import healthRouter from '@/routes/health';

import { requestLogger } from './middleware/requestLogger';
import { validateApiKey } from './middleware/auth';
import { notFoundHandler } from './middleware/notFoundHandler';

class App {
  public app: Application;

  constructor() {
    this.app = express();
    this.configureMiddleware();
    this.configureRoutes();
    this.configureErrorHandling();
  }

  private configureMiddleware(): void {
    // Security middleware
    this.app.use(
      helmet({
        contentSecurityPolicy: {
          directives: {
            defaultSrc: ["'self'"],
            styleSrc: ["'self'", "'unsafe-inline'"],
            scriptSrc: ["'self'"],
            imgSrc: ["'self'", 'data:', 'https:'],
          },
        },
        hsts: {
          maxAge: 31536000,
          includeSubDomains: true,
          preload: true,
        },
      }),
    );

    // CORS configuration
    this.app.use(
      cors({
        origin: config.corsOrigins,
        credentials: true,
        optionsSuccessStatus: 200,
      }),
    );

    // Rate limiting
    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: config.rateLimitMax,
      message: {
        error: 'Too many requests from this IP, please try again later.',
      },
      standardHeaders: true,
      legacyHeaders: false,
    });
    this.app.use('/api/', limiter);

    // Compression and parsing
    this.app.use(compression());
    this.app.use(express.json({ limit: '10mb' }));
    this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));

    // Logging
    if (config.nodeEnv !== 'test') {
      this.app.use(
        morgan('combined', {
          stream: { write: (message: string) => logger.info(message.trim()) },
        }),
      );
    }
    this.app.use(requestLogger);

    // Trust proxy for accurate IP addresses
    this.app.set('trust proxy', 1);
  }

  private configureRoutes(): void {
    // Health check route (no auth required)
    this.app.use('/health', healthRouter);

    // API routes with authentication
    this.app.use('/api', validateApiKey, apiRouter);

    // Root endpoint
    this.app.get('/', (req: Request, res: Response) => {
      res.status(200).json({
        message: 'Express TypeScript API Server',
        version: '1.0.0',
        timestamp: new Date().toISOString(),
        environment: config.nodeEnv,
      });
    });
  }

  private configureErrorHandling(): void {
    // 404 handler
    this.app.use(notFoundHandler);

    // Global error handler
    this.app.use(globalErrorHandler);
  }

  public listen(port: number): void {
    this.app.listen(port, () => {
      logger.info(`Server running on port ${port} in ${config.nodeEnv} mode`);
    });
  }
}

export default App;
  • Now, create the entry point for our server, index.ts:
// File Name: index.ts
import 'dotenv/config';
import App from './app';
import { config } from '@/config/config';
import { logger } from '@/utils/logger';

// Handle uncaught exceptions
process.on('uncaughtException', (error: Error) => {
  logger.error('Uncaught Exception:', error);
  process.exit(1);
});

// Handle unhandled promise rejections
process.on(
  'unhandledRejection',
  (reason: unknown, promise: Promise<unknown>) => {
    logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
    process.exit(1);
  },
);

// Graceful shutdown
process.on('SIGTERM', () => {
  logger.info('SIGTERM received. Shutting down gracefully...');
  process.exit(0);
});

process.on('SIGINT', () => {
  logger.info('SIGINT received. Shutting down gracefully...');
  process.exit(0);
});

// Start the application
const app = new App();
app.listen(config.port);

export default app;

  • Now create a new file health.test.ts inside routes > __tests__ folder for testing our routes health:
// File Name: health.test.ts
import request from 'supertest';
import App from '../../app';

describe('Health Routes', () => {
  let app: App;

  beforeAll(() => {
    app = new App();
  });

  describe('GET /health', () => {
    it('should return basic health status', async () => {
      const response = await request(app.app).get('/health').expect(200);

      expect(response.body).toMatchObject({
        status: 'ok',
        environment: 'test',
        version: expect.any(String),
        uptime: expect.any(Number),
        memory: {
          used: expect.any(Number),
          free: expect.any(Number),
          total: expect.any(Number),
          usage: expect.any(String),
        },
        system: {
          platform: expect.any(String),
          arch: expect.any(String),
          nodeVersion: expect.any(String),
        },
      });

      expect(response.body.timestamp).toMatch(
        /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
      );
    });
  });

  describe('GET /health/detailed', () => {
    it('should return detailed health status', async () => {
      const response = await request(app.app)
        .get('/health/detailed')
        .expect(200);

      expect(response.body).toMatchObject({
        status: 'ok',
        environment: 'test',
        services: expect.any(Object),
      });
    });
  });

  describe('GET /health/ready', () => {
    it('should return readiness status', async () => {
      const response = await request(app.app).get('/health/ready').expect(200);

      expect(response.body).toMatchObject({
        status: 'ready',
        timestamp: expect.any(String),
      });
    });
  });

  describe('GET /health/live', () => {
    it('should return liveness status', async () => {
      const response = await request(app.app).get('/health/live').expect(200);

      expect(response.body).toMatchObject({
        status: 'alive',
        timestamp: expect.any(String),
      });
    });
  });
});

Dockerizing the App:

  • So create a docker file named Dockerfile
# Multi-stage build for production optimization
FROM node:18-alpine AS builder

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install all dependencies (including devDependencies for build)
RUN npm ci

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM node:18-alpine AS production

# Create app directory
WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001

# Copy package files
COPY package*.json ./

# Install only production dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy built application from builder stage
COPY --from=builder /app/dist ./dist

# Create logs directory
RUN mkdir -p logs && chown -R nodejs:nodejs logs

# Change ownership of the app directory
RUN chown -R nodejs:nodejs /app

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))"

# Start the application
CMD ["node", "dist/index.js"]
  • Now create a docker compose file named docker-compose.yml:
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
      - PORT=3000
      - API_KEY=${API_KEY}
      - LOG_LEVEL=${LOG_LEVEL:-info}
      - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000}
      - RATE_LIMIT_MAX=${RATE_LIMIT_MAX:-100}
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
    volumes:
      - redis_data:/data
    restart: unless-stopped
    networks:
      - app-network

  # Optional: PostgreSQL database
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=${POSTGRES_DB:-myapp}
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped
    networks:
      - app-network

  # Optional: Nginx reverse proxy
  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - app-network

volumes:
  redis_data:
  postgres_data:

networks:
  app-network:
    driver: bridge

Get Started:

🛠 Getting Started:

  1. Install dependencies:
npm install
  1. Setup Environment:
cp .env.example .env
# Edit .env with your API_KEY
  1. Start Devlopment:
npm run dev
  1. Test the API:
# Health check (no auth)
curl http://localhost:3000/health

# API endpoints (requires API key)
curl -H "x-api-key: your-api-key" http://localhost:3000/api/users

Testing

Running Tests

# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage

Scripts

  • npm run dev - Start development server with hot reload
  • npm run build - Build for production
  • npm start - Start production server
  • npm run lint - Run ESLint
  • npm run lint:fix - Fix ESLint errors
  • npm run format - Format code with Prettier
  • npm test - Run tests
  • npm run test:watch - Run tests in watch mode
  • npm run test:coverage - Run tests with coverage
  • npm run typecheck - Check TypeScript types

Production

  • Docker support with multi-stage builds
  • Docker Compose for full stack deployment
  • PM2 process management
  • Nginx reverse proxy configuration
  • Health checks for Kubernetes/monitoring

This setup incorporates all the latest 2025 best practices including strict TypeScript configuration, modern security middleware, comprehensive error handling, structured logging, and production-ready deployment options. The code is clean, well-documented, and follows industry standards for scalable Node.js applications.

Best Practices

2025 Standards

  • ES2022 target for modern JavaScript features
  • Strict TypeScript configuration
  • Path aliases for clean imports
  • Environment validation with type safety
  • Graceful shutdown handling

Production Ready

  • Error boundaries for unhandled exceptions
  • Request ID tracking for distributed tracing
  • Memory and performance monitoring
  • Structured configuration management
  • Comprehensive health checks

Developer Experience

  • Hot reload in development
  • Automatic formatting and linting
  • Pre-commit hooks for code quality
  • Type-safe environment configuration
  • Detailed error messages

Architecture Decisions

Security First

  • Helmet for security headers
  • CORS configuration for cross-origin requests
  • Rate limiting to prevent abuse
  • Input validation on all endpoints
  • API key authentication for protected routes

Error Handling

  • Custom error classes for different HTTP status codes
  • Global error handler for consistent error responses
  • Async error handling with express-async-errors
  • Request ID tracking for debugging

Logging & Monitoring

  • Structured logging with Winston
  • Request/response logging with unique request IDs
  • Health check endpoints for monitoring
  • Performance metrics (response time, memory usage)

Code Quality

  • TypeScript for type safety
  • ESLint with TypeScript rules
  • Prettier for code formatting
  • Husky for git hooks
  • Jest for testing

🙏 Thank You

Thank you for taking the time to read this till the end.
If you have any suggestions, questions, or feedback — feel free to reach out.

Happy coding! 🚀