Best Practices for API Development with Express and Nest.js

16 min read
API Development
Express.js
Nest.js
Node.js
REST API
Authentication
Security
Testing
Performance
SC
Written by Shailesh Chaudhari
Full-Stack Developer & Problem Solver
TL;DR: Complete guide to API development best practices with Express.js and Nest.js. Learn authentication, validation, error handling, testing, security, and performance optimization for scalable REST APIs.

Introduction to API Development

Hello everyone! I'm Shailesh Chaudhari, a full-stack developer who has built and maintained numerous APIs for web applications. Today, I'll guide you through the best practices for API development using both Express.js and Nest.js frameworks.

Building robust, scalable, and maintainable APIs requires more than just writing code—it requires following established patterns, implementing proper security measures, and designing for performance and maintainability. Whether you're using the flexibility of Express.js or the structure of Nest.js, these best practices will help you create production-ready APIs.

Why API Best Practices Matter

  • Scalability: Handle increased load and complexity
  • Security: Protect against common vulnerabilities
  • Maintainability: Easy to modify and extend
  • Reliability: Consistent error handling and responses
  • Performance: Optimized for speed and efficiency
  • Developer Experience: Easy to understand and work with

Project Structure and Organization

Express.js Project Structure

express-api/
├── src/
│   ├── controllers/
│   │   ├── user.controller.js
│   │   └── auth.controller.js
│   ├── middleware/
│   │   ├── auth.middleware.js
│   │   ├── validation.middleware.js
│   │   └── error.middleware.js
│   ├── models/
│   │   ├── user.model.js
│   │   └── index.js
│   ├── routes/
│   │   ├── user.routes.js
│   │   ├── auth.routes.js
│   │   └── index.js
│   ├── services/
│   │   ├── user.service.js
│   │   ├── auth.service.js
│   │   └── email.service.js
│   ├── utils/
│   │   ├── logger.js
│   │   ├── response.js
│   │   └── validation.js
│   ├── config/
│   │   ├── database.js
│   │   └── environment.js
│   └── app.js
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── docs/
├── package.json
└── server.js

Nest.js Project Structure

nest-api/
├── src/
│   ├── modules/
│   │   ├── users/
│   │   │   ├── users.controller.ts
│   │   │   ├── users.service.ts
│   │   │   ├── users.module.ts
│   │   │   ├── dto/
│   │   │   └── entities/
│   │   └── auth/
│   ├── common/
│   │   ├── decorators/
│   │   ├── guards/
│   │   ├── interceptors/
│   │   └── filters/
│   ├── config/
│   ├── shared/
│   └── main.ts
├── test/
├── docs/
├── package.json
└── tsconfig.json

Authentication and Authorization

JWT Authentication (Express.js)

// middleware/auth.middleware.js
const jwt = require('jsonwebtoken')
const User = require('../models/user.model')

const authenticate = async (req, res, next) => {
  try {
    const token = req.header('Authorization')?.replace('Bearer ', '')

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

    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    const user = await User.findById(decoded.userId)

    if (!user) {
      return res.status(401).json({
        success: false,
        message: 'Invalid token. User not found.'
      })
    }

    req.user = user
    next()
  } catch (error) {
    res.status(401).json({
      success: false,
      message: 'Invalid token.'
    })
  }
}

const authorize = (...roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: 'Authentication required.'
      })
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: 'Insufficient permissions.'
      })
    }

    next()
  }
}

module.exports = { authenticate, authorize }

JWT Authentication (Nest.js)

// common/guards/jwt-auth.guard.ts
import {
  Injectable,
  ExecutionContext,
  UnauthorizedException,
} from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    return super.canActivate(context)
  }

  handleRequest(err: any, user: any, info: any) {
    if (err || !user) {
      throw err || new UnauthorizedException('Invalid token')
    }
    return user
  }
}

// common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { ROLES_KEY } from '../decorators/roles.decorator'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(
      ROLES_KEY,
      [context.getHandler(), context.getClass()]
    )

    if (!requiredRoles) {
      return true
    }

    const { user } = context.switchToHttp().getRequest()
    return requiredRoles.some((role) => user.roles?.includes(role))
  }
}

// common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
import { ROLES_KEY } from './roles.decorator'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles)

// Usage in controller
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('users')
export class UsersController {
  @Roles('admin')
  @Get()
  findAll() {
    // Only admins can access
  }

  @Roles('user', 'admin')
  @Get('profile')
  getProfile(@Req() req) {
    return req.user
  }
}

Password Security

// utils/password.js
const bcrypt = require('bcryptjs')

class PasswordUtils {
  static async hash(password) {
    const saltRounds = 12
    return await bcrypt.hash(password, saltRounds)
  }

  static async compare(password, hashedPassword) {
    return await bcrypt.compare(password, hashedPassword)
  }

  static validate(password) {
    const minLength = 8
    const hasUpperCase = /[A-Z]/.test(password)
    const hasLowerCase = /[a-z]/.test(password)
    const hasNumbers = /d/.test(password)
    const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password)

    if (password.length < minLength) {
      return 'Password must be at least 8 characters long'
    }

    if (!hasUpperCase) {
      return 'Password must contain at least one uppercase letter'
    }

    if (!hasLowerCase) {
      return 'Password must contain at least one lowercase letter'
    }

    if (!hasNumbers) {
      return 'Password must contain at least one number'
    }

    if (!hasSpecialChar) {
      return 'Password must contain at least one special character'
    }

    return null // Valid password
  }
}

module.exports = PasswordUtils

Input Validation and Sanitization

Express.js Validation

// middleware/validation.middleware.js
const Joi = require('joi')

const validate = (schema) => {
  return (req, res, next) => {
    const { error } = schema.validate(req.body, { abortEarly: false })

    if (error) {
      const errors = error.details.map(detail => ({
        field: detail.path.join('.'),
        message: detail.message
      }))

      return res.status(400).json({
        success: false,
        message: 'Validation failed',
        errors
      })
    }

    next()
  }
}

// Validation schemas
const userSchemas = {
  createUser: Joi.object({
    name: Joi.string().min(2).max(50).required(),
    email: Joi.string().email().required(),
    password: Joi.string().min(8).required(),
    role: Joi.string().valid('user', 'admin').default('user')
  }),

  updateUser: Joi.object({
    name: Joi.string().min(2).max(50),
    email: Joi.string().email(),
    role: Joi.string().valid('user', 'admin')
  }).min(1),

  login: Joi.object({
    email: Joi.string().email().required(),
    password: Joi.string().required()
  })
}

module.exports = { validate, userSchemas }

Nest.js Validation

// dto/create-user.dto.ts
import {
  IsEmail,
  IsString,
  MinLength,
  MaxLength,
  IsOptional,
  IsIn
} from 'class-validator'
import { Transform } from 'class-transformer'

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string

  @IsEmail()
  @Transform(({ value }) => value?.toLowerCase())
  email: string

  @IsString()
  @MinLength(8)
  password: string

  @IsOptional()
  @IsIn(['user', 'admin'])
  role?: string = 'user'
}

// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types'
import { CreateUserDto } from './create-user.dto'

export class UpdateUserDto extends PartialType(CreateUserDto) {}

// dto/login.dto.ts
import { IsEmail, IsString } from 'class-validator'

export class LoginDto {
  @IsEmail()
  email: string

  @IsString()
  password: string
}

// In controller
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
async create(@Body() createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto)
}

Error Handling

Express.js Error Handling

// middleware/error.middleware.js
const logger = require('../utils/logger')

const errorHandler = (err, req, res, next) => {
  let error = { ...err }
  error.message = err.message

  // Log error
  logger.error(err)

  // Mongoose bad ObjectId
  if (err.name === 'CastError') {
    const message = 'Resource not found'
    error = { message, statusCode: 404 }
  }

  // Mongoose duplicate key
  if (err.code === 11000) {
    const message = 'Duplicate field value entered'
    error = { message, statusCode: 400 }
  }

  // Mongoose validation error
  if (err.name === 'ValidationError') {
    const message = Object.values(err.errors).map(val => val.message).join(', ')
    error = { message, statusCode: 400 }
  }

  // JWT errors
  if (err.name === 'JsonWebTokenError') {
    const message = 'Invalid token'
    error = { message, statusCode: 401 }
  }

  if (err.name === 'TokenExpiredError') {
    const message = 'Token expired'
    error = { message, statusCode: 401 }
  }

  res.status(error.statusCode || 500).json({
    success: false,
    message: error.message || 'Server Error',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  })
}

const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next)

module.exports = { errorHandler, asyncHandler }

Nest.js Error Handling

// common/filters/all-exceptions.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common'
import { Request, Response } from 'express'

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name)

  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()

    let status = HttpStatus.INTERNAL_SERVER_ERROR
    let message = 'Internal server error'
    let error = 'Internal Server Error'

    if (exception instanceof HttpException) {
      status = exception.getStatus()
      const exceptionResponse = exception.getResponse()

      if (typeof exceptionResponse === 'string') {
        message = exceptionResponse
      } else if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
        const responseObj = exceptionResponse as any
        message = responseObj.message || message
        error = responseObj.error || error
      }
    } else if (exception instanceof Error) {
      this.logger.error(
        `Exception: ${exception.message}`,
        exception.stack,
        'AllExceptionsFilter'
      )
    }

    const errorResponse = {
      statusCode: status,
      message,
      error,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
    }

    // Add stack trace in development
    if (process.env.NODE_ENV === 'development' && exception instanceof Error) {
      errorResponse['stack'] = exception.stack
    }

    response.status(status).json(errorResponse)
  }
}

// Custom exceptions
// common/exceptions/validation.exception.ts
import { BadRequestException } from '@nestjs/common'

export class ValidationException extends BadRequestException {
  constructor(errors: any) {
    super({
      message: 'Validation failed',
      errors,
    })
  }
}

// common/exceptions/not-found.exception.ts
import { NotFoundException } from '@nestjs/common'

export class NotFoundException extends NotFoundException {
  constructor(resource: string) {
    super(`${resource} not found`)
  }
}

Database Integration

MongoDB with Mongoose (Express.js)

// models/user.model.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Please add a name'],
    trim: true,
    maxlength: [50, 'Name cannot be more than 50 characters']
  },
  email: {
    type: String,
    required: [true, 'Please add an email'],
    unique: true,
    lowercase: true,
    match: [
      /^w+([.-]?w+)*@w+([.-]?w+)*(.w{2,3})+$/,
      'Please add a valid email'
    ]
  },
  password: {
    type: String,
    required: [true, 'Please add a password'],
    minlength: 8,
    select: false // Don't include password in queries by default
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  isActive: {
    type: Boolean,
    default: true
  }
}, {
  timestamps: true,
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
})

// Index for better performance
userSchema.index({ email: 1 })
userSchema.index({ createdAt: -1 })

// Hash password before saving
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next()

  const salt = await bcrypt.genSalt(12)
  this.password = await bcrypt.hash(this.password, salt)
  next()
})

// Instance methods
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password)
}

userSchema.methods.toJSON = function() {
  const userObject = this.toObject()
  delete userObject.password
  return userObject
}

module.exports = mongoose.model('User', userSchema)

TypeORM with Nest.js

// entities/user.entity.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  BeforeInsert,
  BeforeUpdate,
} from 'typeorm'
import * as bcrypt from 'bcrypt'

export enum UserRole {
  USER = 'user',
  ADMIN = 'admin',
}

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ length: 50 })
  name: string

  @Column({ unique: true, length: 100 })
  email: string

  @Column()
  password: string

  @Column({
    type: 'enum',
    enum: UserRole,
    default: UserRole.USER,
  })
  role: UserRole

  @Column({ default: true })
  isActive: boolean

  @CreateDateColumn()
  createdAt: Date

  @UpdateDateColumn()
  updatedAt: Date

  @BeforeInsert()
  @BeforeUpdate()
  async hashPassword() {
    if (this.password) {
      const saltRounds = 12
      this.password = await bcrypt.hash(this.password, saltRounds)
    }
  }

  async comparePassword(candidatePassword: string): Promise<boolean> {
    return bcrypt.compare(candidatePassword, this.password)
  }
}

// users/users.service.ts
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './user.entity'
import { CreateUserDto } from './dto/create-user.dto'

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async create(createUserDto: CreateUserDto): Promise<User> {
    const existingUser = await this.usersRepository.findOne({
      where: { email: createUserDto.email }
    })

    if (existingUser) {
      throw new ConflictException('User with this email already exists')
    }

    const user = this.usersRepository.create(createUserDto)
    return this.usersRepository.save(user)
  }

  async findOne(id: string): Promise<User> {
    const user = await this.usersRepository.findOne({ where: { id } })

    if (!user) {
      throw new NotFoundException('User not found')
    }

    return user
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.usersRepository.findOne({ where: { email } })
  }
}

API Response Standardization

Express.js Response Helper

// utils/response.js
class ApiResponse {
  static success(res, message = 'Success', data = null, statusCode = 200) {
    const response = {
      success: true,
      message,
      ...(data !== null && { data }),
      timestamp: new Date().toISOString()
    }

    return res.status(statusCode).json(response)
  }

  static error(res, message = 'Error', statusCode = 500, errors = null) {
    const response = {
      success: false,
      message,
      ...(errors && { errors }),
      timestamp: new Date().toISOString()
    }

    return res.status(statusCode).json(response)
  }

  static paginated(res, data, page, limit, total, message = 'Success') {
    const totalPages = Math.ceil(total / limit)
    const hasNext = page < totalPages
    const hasPrev = page > 1

    const response = {
      success: true,
      message,
      data,
      pagination: {
        page,
        limit,
        total,
        totalPages,
        hasNext,
        hasPrev,
        nextPage: hasNext ? page + 1 : null,
        prevPage: hasPrev ? page - 1 : null
      },
      timestamp: new Date().toISOString()
    }

    return res.status(200).json(response)
  }
}

module.exports = ApiResponse

Nest.js Response Interceptor

// common/interceptors/response.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'

export interface Response<T> {
  success: boolean
  message: string
  data: T
  timestamp: string
}

@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    return next.handle().pipe(
      map((data) => ({
        success: true,
        message: 'Success',
        data,
        timestamp: new Date().toISOString(),
      })),
    )
  }
}

// common/decorators/api-response.decorator.ts
import { applyDecorators } from '@nestjs/common'
import { ApiOperation, ApiResponse as SwaggerApiResponse } from '@nestjs/swagger'

export function ApiResponse(message: string, statusCode = 200) {
  return applyDecorators(
    ApiOperation({ summary: message }),
    SwaggerApiResponse({
      status: statusCode,
      description: message,
      schema: {
        type: 'object',
        properties: {
          success: { type: 'boolean', example: true },
          message: { type: 'string', example: message },
          data: { type: 'object' },
          timestamp: { type: 'string', format: 'date-time' },
        },
      },
    }),
  )
}

Testing Best Practices

Express.js Testing

// tests/integration/auth.test.js
const request = require('supertest')
const mongoose = require('mongoose')
const app = require('../src/app')
const User = require('../src/models/user.model')

describe('Auth Endpoints', () => {
  beforeAll(async () => {
    await mongoose.connect(process.env.TEST_DATABASE_URL)
  })

  afterAll(async () => {
    await mongoose.connection.close()
  })

  beforeEach(async () => {
    await User.deleteMany({})
  })

  describe('POST /api/auth/register', () => {
    it('should register a new user', async () => {
      const userData = {
        name: 'Test User',
        email: 'test@example.com',
        password: 'password123'
      }

      const response = await request(app)
        .post('/api/auth/register')
        .send(userData)
        .expect(201)

      expect(response.body.success).toBe(true)
      expect(response.body.data.user.email).toBe(userData.email)
      expect(response.body.data.token).toBeDefined()
    })

    it('should not register user with invalid email', async () => {
      const userData = {
        name: 'Test User',
        email: 'invalid-email',
        password: 'password123'
      }

      const response = await request(app)
        .post('/api/auth/register')
        .send(userData)
        .expect(400)

      expect(response.body.success).toBe(false)
      expect(response.body.errors).toBeDefined()
    })
  })

  describe('POST /api/auth/login', () => {
    it('should login with correct credentials', async () => {
      // Create test user
      const user = await User.create({
        name: 'Test User',
        email: 'test@example.com',
        password: 'password123'
      })

      const loginData = {
        email: 'test@example.com',
        password: 'password123'
      }

      const response = await request(app)
        .post('/api/auth/login')
        .send(loginData)
        .expect(200)

      expect(response.body.success).toBe(true)
      expect(response.body.data.token).toBeDefined()
    })
  })
})

Nest.js Testing

// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { getRepositoryToken } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { UsersService } from './users.service'
import { User } from './user.entity'

describe('UsersService', () => {
  let service: UsersService
  let repository: Repository<User>

  const mockUser = {
    id: '1',
    name: 'Test User',
    email: 'test@example.com',
    password: 'hashedpassword',
    role: 'user',
    isActive: true,
  }

  const mockRepository = {
    create: jest.fn(),
    save: jest.fn(),
    findOne: jest.fn(),
    find: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  }

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: mockRepository,
        },
      ],
    }).compile()

    service = module.get<UsersService>(UsersService)
    repository = module.get<Repository<User>>(getRepositoryToken(User))
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })

  describe('create', () => {
    it('should create a new user', async () => {
      const createUserDto = {
        name: 'Test User',
        email: 'test@example.com',
        password: 'password123',
      }

      mockRepository.create.mockReturnValue(mockUser)
      mockRepository.save.mockReturnValue(mockUser)
      mockRepository.findOne.mockReturnValue(null)

      const result = await service.create(createUserDto)

      expect(result).toEqual(mockUser)
      expect(mockRepository.create).toHaveBeenCalledWith(createUserDto)
      expect(mockRepository.save).toHaveBeenCalledWith(mockUser)
    })

    it('should throw error if user already exists', async () => {
      const createUserDto = {
        name: 'Test User',
        email: 'test@example.com',
        password: 'password123',
      }

      mockRepository.findOne.mockReturnValue(mockUser)

      await expect(service.create(createUserDto)).rejects.toThrow(
        'User with this email already exists'
      )
    })
  })
})

Security Best Practices

Rate Limiting

// Express.js rate limiting
const rateLimit = require('express-rate-limit')

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // Limit each IP to 5 requests per windowMs
  message: {
    success: false,
    message: 'Too many authentication attempts, please try again later.'
  },
  standardHeaders: true,
  legacyHeaders: false,
})

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: {
    success: false,
    message: 'Too many requests, please try again later.'
  },
})

// Apply to routes
app.use('/api/auth/', authLimiter)
app.use('/api/', apiLimiter)

CORS Configuration

// Express.js CORS
const cors = require('cors')

const corsOptions = {
  origin: function (origin, callback) {
    // Allow requests with no origin (mobile apps, etc.)
    if (!origin) return callback(null, true)

    const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || []

    if (allowedOrigins.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  },
  credentials: true,
  optionsSuccessStatus: 200
}

app.use(cors(corsOptions))

Helmet for Security Headers

// Express.js security headers
const helmet = require('helmet')

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

Performance Optimization

Caching Strategies

// Redis caching middleware (Express.js)
const redis = require('redis')
const client = redis.createClient()

const cache = (duration) => {
  return (req, res, next) => {
    const key = `__${req.originalUrl}__`

    client.get(key, (err, cachedData) => {
      if (err) return next()

      if (cachedData) {
        return res.json(JSON.parse(cachedData))
      }

      const originalJson = res.json
      res.json = (data) => {
        client.setex(key, duration, JSON.stringify(data))
        originalJson.call(res, data)
      }

      next()
    })
  }
}

// Usage
app.get('/api/users', cache(300), getUsers) // Cache for 5 minutes

Database Query Optimization

// Optimized queries with indexes
// In your model/schema
userSchema.index({ email: 1, isActive: 1 })
userSchema.index({ createdAt: -1 })

// Efficient queries
const getActiveUsers = async (page = 1, limit = 10) => {
  const skip = (page - 1) * limit

  const users = await User.find({ isActive: true })
    .select('name email createdAt') // Only select needed fields
    .sort({ createdAt: -1 })
    .skip(skip)
    .limit(limit)
    .lean() // Return plain objects for better performance

  const total = await User.countDocuments({ isActive: true })

  return { users, total }
}

API Documentation

Swagger with Express.js

// swagger.js
const swaggerJsdoc = require('swagger-jsdoc')
const swaggerUi = require('swagger-ui-express')

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'API Documentation',
      version: '1.0.0',
      description: 'API documentation for the application',
    },
    servers: [
      {
        url: 'http://localhost:3000',
        description: 'Development server',
      },
    ],
    components: {
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT',
        },
      },
    },
    security: [
      {
        bearerAuth: [],
      },
    ],
  },
  apis: ['./routes/*.js'], // Path to the API routes
}

const specs = swaggerJsdoc(options)
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs))

Swagger with Nest.js

// main.ts
import { NestFactory } from '@nestjs/core'
import { ValidationPipe } from '@nestjs/common'
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  // Swagger setup
  const config = new DocumentBuilder()
    .setTitle('API Documentation')
    .setDescription('API documentation for the application')
    .setVersion('1.0')
    .addBearerAuth(
      {
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT',
        name: 'JWT',
        description: 'Enter JWT token',
        in: 'header',
      },
      'JWT-auth'
    )
    .build()

  const document = SwaggerModule.createDocument(app, config)
  SwaggerModule.setup('api', app, document)

  // Validation
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }))

  await app.listen(3000)
}
bootstrap()

Deployment and Monitoring

Environment Configuration

// config/environment.js
const dotenv = require('dotenv')
const path = require('path')

// Load environment variables
const envFile = process.env.NODE_ENV === 'production'
  ? '.env.production'
  : '.env.development'

dotenv.config({ path: path.resolve(__dirname, '..', envFile) })

const config = {
  port: process.env.PORT || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  jwtSecret: process.env.JWT_SECRET,
  jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
  database: {
    url: process.env.DATABASE_URL,
    options: {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      maxPoolSize: 10,
    },
  },
  redis: {
    url: process.env.REDIS_URL,
  },
  cors: {
    origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
    credentials: true,
  },
  rateLimit: {
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: process.env.NODE_ENV === 'production' ? 100 : 1000,
  },
}

// Validate required environment variables
const requiredEnvVars = ['JWT_SECRET', 'DATABASE_URL']
requiredEnvVars.forEach(envVar => {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`)
  }
})

module.exports = config

Health Checks

// routes/health.routes.js
const express = require('express')
const mongoose = require('mongoose')
const router = express.Router()

router.get('/health', async (req, res) => {
  const healthcheck = {
    uptime: process.uptime(),
    message: 'OK',
    timestamp: Date.now(),
    services: {}
  }

  try {
    // Check database connection
    await mongoose.connection.db.admin().ping()
    healthcheck.services.database = 'OK'
  } catch (error) {
    healthcheck.services.database = 'ERROR'
    healthcheck.message = 'Database connection failed'
  }

  // Check Redis if used
  if (process.env.REDIS_URL) {
    try {
      const redis = require('redis')
      const client = redis.createClient({ url: process.env.REDIS_URL })
      await client.ping()
      healthcheck.services.redis = 'OK'
      await client.quit()
    } catch (error) {
      healthcheck.services.redis = 'ERROR'
    }
  }

  const statusCode = healthcheck.message === 'OK' ? 200 : 503
  res.status(statusCode).json(healthcheck)
})

module.exports = router

Conclusion: Building Production-Ready APIs

Building robust, scalable APIs with Express.js and Nest.js requires attention to detail across multiple areas—from authentication and validation to error handling and performance optimization. The frameworks provide excellent foundations, but following best practices ensures your APIs are secure, maintainable, and performant.

Start with a solid project structure, implement proper authentication and authorization, validate all inputs, handle errors gracefully, and write comprehensive tests. As your API grows, focus on performance optimization, proper documentation, and monitoring.

Remember that API development is an iterative process. Start with the core functionality, implement security measures early, and continuously improve based on usage patterns and feedback. The patterns and practices outlined in this guide will help you build APIs that can scale with your application's needs.

Whether you choose the flexibility of Express.js or the structure of Nest.js, the key to success lies in consistency, security, and maintainability. Follow these best practices, and you'll be well-equipped to build production-ready APIs that serve your applications effectively.

"APIs are the connective tissue of modern applications. Design them with care, security, and scalability in mind." - Shailesh Chaudhari

SC
Written by Shailesh Chaudhari
Full-Stack Developer & Problem Solver