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.mdsource 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
- Use TypeScript: Type safety prevents runtime errors
- Implement proper error handling: Use custom error classes
- Validate input: Use libraries like Zod or Joi
- Use environment variables: Never hardcode secrets
- Implement logging: Use structured logging (Pino, Winston)
- Add rate limiting: Prevent abuse
- Use HTTPS: Always in production
- Implement CORS properly: Don't use
in production* - Use dependency injection: Easier testing and maintenance
- Write tests: Unit, integration, and E2E tests
- Handle graceful shutdown: Clean up resources
- Use connection pooling: For databases
- Implement health checks: For monitoring
- Use compression: Reduce response size
- Monitor performance: Use APM tools
Testing Patterns
See
javascript-testing-patterns skill for comprehensive testing guidance.
Resources
- Node.js Best Practices: https://github.com/goldbergyoni/nodebestpractices
- Express.js Guide: https://expressjs.com/en/guide/
- Fastify Documentation: https://www.fastify.io/docs/
- TypeScript Node Starter: https://github.com/microsoft/TypeScript-Node-Starter