Awesome-omni-skill nodejs-backend-patterns

Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/nodejs-backend-patterns-antoine-bouteiller" ~/.claude/skills/diegosouzapw-awesome-omni-skill-nodejs-backend-patterns-654e0d && rm -rf "$T"
manifest: skills/development/nodejs-backend-patterns-antoine-bouteiller/SKILL.md
source content

Node.js Backend Patterns

Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices.

When to Use This Skill

  • Building REST APIs or GraphQL servers
  • Creating microservices with Node.js
  • Implementing authentication and authorization
  • Designing scalable backend architectures
  • Setting up middleware and error handling
  • Integrating databases (SQL and NoSQL)
  • Building real-time applications with WebSockets
  • Implementing background job processing

Core Frameworks

Express.js - Minimalist Framework

Basic Setup:

import express, { Request, Response, NextFunction } from 'express'
import helmet from 'helmet'
import cors from 'cors'
import compression from 'compression'

const app = express()

// Security middleware
app.use(helmet())
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }))
app.use(compression())

// Body parsing
app.use(express.json({ limit: '10mb' }))
app.use(express.urlencoded({ extended: true, limit: '10mb' }))

// Request logging
app.use((req: Request, res: Response, next: NextFunction) => {
  console.log(`${req.method} ${req.path}`)
  next()
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

Fastify - High Performance Framework

Basic Setup:

import Fastify from 'fastify'
import helmet from '@fastify/helmet'
import cors from '@fastify/cors'
import compress from '@fastify/compress'

const fastify = Fastify({
  logger: {
    level: process.env.LOG_LEVEL || 'info',
    transport: {
      target: 'pino-pretty',
      options: { colorize: true },
    },
  },
})

// Plugins
await fastify.register(helmet)
await fastify.register(cors, { origin: true })
await fastify.register(compress)

// Type-safe routes with schema validation
fastify.post<{
  Body: { name: string; email: string }
  Reply: { id: string; name: string }
}>(
  '/users',
  {
    schema: {
      body: {
        type: 'object',
        required: ['name', 'email'],
        properties: {
          name: { type: 'string', minLength: 1 },
          email: { type: 'string', format: 'email' },
        },
      },
    },
  },
  async (request, reply) => {
    const { name, email } = request.body
    return { id: '123', name }
  }
)

await fastify.listen({ port: 3000, host: '0.0.0.0' })

Architectural Patterns

Pattern 1: Layered Architecture

Structure:

src/
├── controllers/     # Handle HTTP requests/responses
├── services/        # Business logic
├── repositories/    # Data access layer
├── models/          # Data models
├── middleware/      # Express/Fastify middleware
├── routes/          # Route definitions
├── utils/           # Helper functions
├── config/          # Configuration
└── types/           # TypeScript types

Controller Layer:

// controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express'
import { UserService } from '../services/user.service'
import { CreateUserDTO, UpdateUserDTO } from '../types/user.types'

export class UserController {
  constructor(private userService: UserService) {}

  async createUser(req: Request, res: Response, next: NextFunction) {
    try {
      const userData: CreateUserDTO = req.body
      const user = await this.userService.createUser(userData)
      res.status(201).json(user)
    } catch (error) {
      next(error)
    }
  }

  async getUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { id } = req.params
      const user = await this.userService.getUserById(id)
      res.json(user)
    } catch (error) {
      next(error)
    }
  }

  async updateUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { id } = req.params
      const updates: UpdateUserDTO = req.body
      const user = await this.userService.updateUser(id, updates)
      res.json(user)
    } catch (error) {
      next(error)
    }
  }

  async deleteUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { id } = req.params
      await this.userService.deleteUser(id)
      res.status(204).send()
    } catch (error) {
      next(error)
    }
  }
}

Service Layer:

// services/user.service.ts
import { UserRepository } from '../repositories/user.repository'
import { CreateUserDTO, UpdateUserDTO, User } from '../types/user.types'
import { NotFoundError, ValidationError } from '../utils/errors'
import bcrypt from 'bcrypt'

export class UserService {
  constructor(private userRepository: UserRepository) {}

  async createUser(userData: CreateUserDTO): Promise<User> {
    // Validation
    const existingUser = await this.userRepository.findByEmail(userData.email)
    if (existingUser) {
      throw new ValidationError('Email already exists')
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(userData.password, 10)

    // Create user
    const user = await this.userRepository.create({
      ...userData,
      password: hashedPassword,
    })

    // Remove password from response
    const { password, ...userWithoutPassword } = user
    return userWithoutPassword as User
  }

  async getUserById(id: string): Promise<User> {
    const user = await this.userRepository.findById(id)
    if (!user) {
      throw new NotFoundError('User not found')
    }
    const { password, ...userWithoutPassword } = user
    return userWithoutPassword as User
  }

  async updateUser(id: string, updates: UpdateUserDTO): Promise<User> {
    const user = await this.userRepository.update(id, updates)
    if (!user) {
      throw new NotFoundError('User not found')
    }
    const { password, ...userWithoutPassword } = user
    return userWithoutPassword as User
  }

  async deleteUser(id: string): Promise<void> {
    const deleted = await this.userRepository.delete(id)
    if (!deleted) {
      throw new NotFoundError('User not found')
    }
  }
}

Repository Layer:

// repositories/user.repository.ts
import { Pool } from 'pg'
import { CreateUserDTO, UpdateUserDTO, UserEntity } from '../types/user.types'

export class UserRepository {
  constructor(private db: Pool) {}

  async create(userData: CreateUserDTO & { password: string }): Promise<UserEntity> {
    const query = `
      INSERT INTO users (name, email, password)
      VALUES ($1, $2, $3)
      RETURNING id, name, email, password, created_at, updated_at
    `
    const { rows } = await this.db.query(query, [userData.name, userData.email, userData.password])
    return rows[0]
  }

  async findById(id: string): Promise<UserEntity | null> {
    const query = 'SELECT * FROM users WHERE id = $1'
    const { rows } = await this.db.query(query, [id])
    return rows[0] || null
  }

  async findByEmail(email: string): Promise<UserEntity | null> {
    const query = 'SELECT * FROM users WHERE email = $1'
    const { rows } = await this.db.query(query, [email])
    return rows[0] || null
  }

  async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> {
    const fields = Object.keys(updates)
    const values = Object.values(updates)

    const setClause = fields.map((field, idx) => `${field} = $${idx + 2}`).join(', ')

    const query = `
      UPDATE users
      SET ${setClause}, updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
      RETURNING *
    `

    const { rows } = await this.db.query(query, [id, ...values])
    return rows[0] || null
  }

  async delete(id: string): Promise<boolean> {
    const query = 'DELETE FROM users WHERE id = $1'
    const { rowCount } = await this.db.query(query, [id])
    return rowCount > 0
  }
}

Pattern 2: Dependency Injection

DI Container:

// di-container.ts
import { Pool } from 'pg'
import { UserRepository } from './repositories/user.repository'
import { UserService } from './services/user.service'
import { UserController } from './controllers/user.controller'
import { AuthService } from './services/auth.service'

class Container {
  private instances = new Map<string, any>()

  register<T>(key: string, factory: () => T): void {
    this.instances.set(key, factory)
  }

  resolve<T>(key: string): T {
    const factory = this.instances.get(key)
    if (!factory) {
      throw new Error(`No factory registered for ${key}`)
    }
    return factory()
  }

  singleton<T>(key: string, factory: () => T): void {
    let instance: T
    this.instances.set(key, () => {
      if (!instance) {
        instance = factory()
      }
      return instance
    })
  }
}

export const container = new Container()

// Register dependencies
container.singleton(
  'db',
  () =>
    new Pool({
      host: process.env.DB_HOST,
      port: parseInt(process.env.DB_PORT || '5432'),
      database: process.env.DB_NAME,
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      max: 20,
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000,
    })
)

container.singleton('userRepository', () => new UserRepository(container.resolve('db')))

container.singleton('userService', () => new UserService(container.resolve('userRepository')))

container.register('userController', () => new UserController(container.resolve('userService')))

container.singleton('authService', () => new AuthService(container.resolve('userRepository')))

Middleware Patterns

Authentication Middleware

// middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { UnauthorizedError } from '../utils/errors'

interface JWTPayload {
  userId: string
  email: string
}

declare global {
  namespace Express {
    interface Request {
      user?: JWTPayload
    }
  }
}

export const authenticate = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const token = req.headers.authorization?.replace('Bearer ', '')

    if (!token) {
      throw new UnauthorizedError('No token provided')
    }

    const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload

    req.user = payload
    next()
  } catch (error) {
    next(new UnauthorizedError('Invalid token'))
  }
}

export const authorize = (...roles: string[]) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) {
      return next(new UnauthorizedError('Not authenticated'))
    }

    // Check if user has required role
    const hasRole = roles.some((role) => req.user?.roles?.includes(role))

    if (!hasRole) {
      return next(new UnauthorizedError('Insufficient permissions'))
    }

    next()
  }
}

Validation Middleware

// middleware/validation.middleware.ts
import { Request, Response, NextFunction } from 'express'
import { AnyZodObject, ZodError } from 'zod'
import { ValidationError } from '../utils/errors'

export const validate = (schema: AnyZodObject) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      await schema.parseAsync({
        body: req.body,
        query: req.query,
        params: req.params,
      })
      next()
    } catch (error) {
      if (error instanceof ZodError) {
        const errors = error.errors.map((err) => ({
          field: err.path.join('.'),
          message: err.message,
        }))
        next(new ValidationError('Validation failed', errors))
      } else {
        next(error)
      }
    }
  }
}

// Usage with Zod
import { z } from 'zod'

const createUserSchema = z.object({
  body: z.object({
    name: z.string().min(1),
    email: z.string().email(),
    password: z.string().min(8),
  }),
})

router.post('/users', validate(createUserSchema), userController.createUser)

Rate Limiting Middleware

// middleware/rate-limit.middleware.ts
import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
import Redis from 'ioredis'

const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT || '6379'),
})

export const apiLimiter = rateLimit({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:',
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
})

export const authLimiter = rateLimit({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:auth:',
  }),
  windowMs: 15 * 60 * 1000,
  max: 5, // Stricter limit for auth endpoints
  skipSuccessfulRequests: true,
})

Request Logging Middleware

// middleware/logger.middleware.ts
import { Request, Response, NextFunction } from 'express'
import pino from 'pino'

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: { colorize: true },
  },
})

export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
  const start = Date.now()

  // Log response when finished
  res.on('finish', () => {
    const duration = Date.now() - start
    logger.info({
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: `${duration}ms`,
      userAgent: req.headers['user-agent'],
      ip: req.ip,
    })
  })

  next()
}

export { logger }

Error Handling

Custom Error Classes

// utils/errors.ts
export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number = 500,
    public isOperational: boolean = true
  ) {
    super(message)
    Object.setPrototypeOf(this, AppError.prototype)
    Error.captureStackTrace(this, this.constructor)
  }
}

export class ValidationError extends AppError {
  constructor(
    message: string,
    public errors?: any[]
  ) {
    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 ConflictError extends AppError {
  constructor(message: string) {
    super(message, 409)
  }
}

Global Error Handler

// middleware/error-handler.ts
import { Request, Response, NextFunction } from 'express'
import { AppError } from '../utils/errors'
import { logger } from './logger.middleware'

export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      status: 'error',
      message: err.message,
      ...(err instanceof ValidationError && { errors: err.errors }),
    })
  }

  // Log unexpected errors
  logger.error({
    error: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
  })

  // Don't leak error details in production
  const message = process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message

  res.status(500).json({
    status: 'error',
    message,
  })
}

// Async error wrapper
export const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) => {
  return (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next)
  }
}

Database Patterns

PostgreSQL with Connection Pool

// config/database.ts
import { Pool, PoolConfig } from 'pg'

const poolConfig: PoolConfig = {
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
}

export const pool = new Pool(poolConfig)

// Test connection
pool.on('connect', () => {
  console.log('Database connected')
})

pool.on('error', (err) => {
  console.error('Unexpected database error', err)
  process.exit(-1)
})

// Graceful shutdown
export const closeDatabase = async () => {
  await pool.end()
  console.log('Database connection closed')
}

MongoDB with Mongoose

// config/mongoose.ts
import mongoose from 'mongoose'

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI!, {
      maxPoolSize: 10,
      serverSelectionTimeoutMS: 5000,
      socketTimeoutMS: 45000,
    })

    console.log('MongoDB connected')
  } catch (error) {
    console.error('MongoDB connection error:', error)
    process.exit(1)
  }
}

mongoose.connection.on('disconnected', () => {
  console.log('MongoDB disconnected')
})

mongoose.connection.on('error', (err) => {
  console.error('MongoDB error:', err)
})

export { connectDB }

// Model example
import { Schema, model, Document } from 'mongoose'

interface IUser extends Document {
  name: string
  email: string
  password: string
  createdAt: Date
  updatedAt: Date
}

const userSchema = new Schema<IUser>(
  {
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true },
  },
  {
    timestamps: true,
  }
)

// Indexes
userSchema.index({ email: 1 })

export const User = model<IUser>('User', userSchema)

Transaction Pattern

// services/order.service.ts
import { Pool } from 'pg'

export class OrderService {
  constructor(private db: Pool) {}

  async createOrder(userId: string, items: any[]) {
    const client = await this.db.connect()

    try {
      await client.query('BEGIN')

      // Create order
      const orderResult = await client.query('INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id', [userId, calculateTotal(items)])
      const orderId = orderResult.rows[0].id

      // Create order items
      for (const item of items) {
        await client.query('INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)', [
          orderId,
          item.productId,
          item.quantity,
          item.price,
        ])

        // Update inventory
        await client.query('UPDATE products SET stock = stock - $1 WHERE id = $2', [item.quantity, item.productId])
      }

      await client.query('COMMIT')
      return orderId
    } catch (error) {
      await client.query('ROLLBACK')
      throw error
    } finally {
      client.release()
    }
  }
}

Authentication & Authorization

JWT Authentication

// services/auth.service.ts
import jwt from 'jsonwebtoken'
import bcrypt from 'bcrypt'
import { UserRepository } from '../repositories/user.repository'
import { UnauthorizedError } from '../utils/errors'

export class AuthService {
  constructor(private userRepository: UserRepository) {}

  async login(email: string, password: string) {
    const user = await this.userRepository.findByEmail(email)

    if (!user) {
      throw new UnauthorizedError('Invalid credentials')
    }

    const isValid = await bcrypt.compare(password, user.password)

    if (!isValid) {
      throw new UnauthorizedError('Invalid credentials')
    }

    const token = this.generateToken({
      userId: user.id,
      email: user.email,
    })

    const refreshToken = this.generateRefreshToken({
      userId: user.id,
    })

    return {
      token,
      refreshToken,
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
      },
    }
  }

  async refreshToken(refreshToken: string) {
    try {
      const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET!) as { userId: string }

      const user = await this.userRepository.findById(payload.userId)

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

      const token = this.generateToken({
        userId: user.id,
        email: user.email,
      })

      return { token }
    } catch (error) {
      throw new UnauthorizedError('Invalid refresh token')
    }
  }

  private generateToken(payload: any): string {
    return jwt.sign(payload, process.env.JWT_SECRET!, {
      expiresIn: '15m',
    })
  }

  private generateRefreshToken(payload: any): string {
    return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {
      expiresIn: '7d',
    })
  }
}

Caching Strategies

// utils/cache.ts
import Redis from 'ioredis'

const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT || '6379'),
  retryStrategy: (times) => {
    const delay = Math.min(times * 50, 2000)
    return delay
  },
})

export class CacheService {
  async get<T>(key: string): Promise<T | null> {
    const data = await redis.get(key)
    return data ? JSON.parse(data) : null
  }

  async set(key: string, value: any, ttl?: number): Promise<void> {
    const serialized = JSON.stringify(value)
    if (ttl) {
      await redis.setex(key, ttl, serialized)
    } else {
      await redis.set(key, serialized)
    }
  }

  async delete(key: string): Promise<void> {
    await redis.del(key)
  }

  async invalidatePattern(pattern: string): Promise<void> {
    const keys = await redis.keys(pattern)
    if (keys.length > 0) {
      await redis.del(...keys)
    }
  }
}

// Cache decorator
export function Cacheable(ttl: number = 300) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value

    descriptor.value = async function (...args: any[]) {
      const cache = new CacheService()
      const cacheKey = `${propertyKey}:${JSON.stringify(args)}`

      const cached = await cache.get(cacheKey)
      if (cached) {
        return cached
      }

      const result = await originalMethod.apply(this, args)
      await cache.set(cacheKey, result, ttl)

      return result
    }

    return descriptor
  }
}

API Response Format

// utils/response.ts
import { Response } from 'express'

export class ApiResponse {
  static success<T>(res: Response, data: T, message?: string, statusCode = 200) {
    return res.status(statusCode).json({
      status: 'success',
      message,
      data,
    })
  }

  static error(res: Response, message: string, statusCode = 500, errors?: any) {
    return res.status(statusCode).json({
      status: 'error',
      message,
      ...(errors && { errors }),
    })
  }

  static paginated<T>(res: Response, data: T[], page: number, limit: number, total: number) {
    return res.json({
      status: 'success',
      data,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit),
      },
    })
  }
}

Best Practices

  1. Use TypeScript: Type safety prevents runtime errors
  2. Implement proper error handling: Use custom error classes
  3. Validate input: Use libraries like Zod or Joi
  4. Use environment variables: Never hardcode secrets
  5. Implement logging: Use structured logging (Pino, Winston)
  6. Add rate limiting: Prevent abuse
  7. Use HTTPS: Always in production
  8. Implement CORS properly: Don't use
    *
    in production
  9. Use dependency injection: Easier testing and maintenance
  10. Write tests: Unit, integration, and E2E tests
  11. Handle graceful shutdown: Clean up resources
  12. Use connection pooling: For databases
  13. Implement health checks: For monitoring
  14. Use compression: Reduce response size
  15. Monitor performance: Use APM tools

Testing Patterns

See

javascript-testing-patterns
skill for comprehensive testing guidance.

Resources