| name | nodejs-backend-patterns |
| description | 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. |
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();
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") }));
app.use(compression());
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
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 },
},
},
});
await fastify.register(helmet);
await fastify.register(cors, { origin: true });
await fastify.register(compress);
fastify.post<{
Body: { name: string; email: string };
Reply: { id: string; name: string };
}>(
"/users",
{
schema: {
body: {
type: "object",
: [, ],
: {
: { : , : },
: { : , : },
},
},
},
},
(request, reply) => {
{ name, email } = request.;
{ : , name };
},
);
fastify.({ : , : });
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:
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: , : , : ) {
{
{ id } = req.;
user = ..(id);
res.(user);
} (error) {
(error);
}
}
() {
{
{ id } = req.;
: = req.;
user = ..(id, updates);
res.(user);
} (error) {
(error);
}
}
() {
{
{ id } = req.;
..(id);
res.().();
} (error) {
(error);
}
}
}
Service Layer:
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> {
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new ValidationError("Email already exists");
}
const hashedPassword = await bcrypt.hash(userData.password, );
user = ..({
...userData,
: hashedPassword,
});
{ password, ...userWithoutPassword } = user;
userWithoutPassword ;
}
(: ): <> {
user = ..(id);
(!user) {
();
}
{ password, ...userWithoutPassword } = user;
userWithoutPassword ;
}
(: , : ): <> {
user = ..(id, updates);
(!user) {
();
}
{ password, ...userWithoutPassword } = user;
userWithoutPassword ;
}
(: ): <> {
deleted = ..(id);
(!deleted) {
();
}
}
}
Repository Layer:
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 } = ..(query, [id]);
rows[] || ;
}
(: ): < | > {
query = ;
{ rows } = ..(query, [email]);
rows[] || ;
}
(: , : ): < | > {
fields = .(updates);
values = .(updates);
setClause = fields
.( )
.();
query = ;
{ rows } = ..(query, [id, ...values]);
rows[] || ;
}
(: ): <> {
query = ;
{ rowCount } = ..(query, [id]);
rowCount > ;
}
}
Pattern 2: Dependency Injection
DI Container:
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: , : T): {
: T;
..(key, {
(!instance) {
instance = ();
}
instance;
});
}
}
container = ();
container.(
,
({
: process..,
: (process.. || ),
: process..,
: process..,
: process..,
: ,
: ,
: ,
}),
);
container.(
,
(container.()),
);
container.(
,
(container.()),
);
container.(
,
(container.()),
);
container.(
,
(container.()),
);
Middleware Patterns
Authentication Middleware
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.(token, process..!) ;
req. = payload;
();
} (error) {
( ());
}
};
= () => {
(: , : , : ) => {
(!req.) {
( ());
}
hasRole = roles.( req.?.?.(role));
(!hasRole) {
( ());
}
();
};
};
Validation Middleware
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(),
: err.,
}));
( (, errors));
} {
(error);
}
}
};
};
{ z } ;
createUserSchema = z.({
: z.({
: z.().(),
: z.().(),
: z.().(),
}),
});
router.(, (createUserSchema), userController.);
Rate Limiting Middleware
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,
max: 100,
message: "Too many requests from this IP, please try again later",
standardHeaders: true,
legacyHeaders: false,
});
export const authLimiter = rateLimit({
store: new RedisStore({
: redis,
: ,
}),
: * * ,
: ,
: ,
});
Request Logging Middleware
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();
res.on("finish", () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
: req.[],
: req.,
});
});
();
};
{ logger };
Error Handling
Custom Error Classes
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 NotFoundError extends AppError {
constructor(: = ) {
(message, );
}
}
{
() {
(message, );
}
}
{
() {
(message, );
}
}
{
() {
(message, );
}
}
Global Error Handler
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 }),
});
}
logger.error({
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
});
message =
process.. ===
?
: err.;
res.().({
: ,
message,
});
};
= () => {
{
.((req, res, next)).(next);
};
};
Database Patterns
PostgreSQL with Connection Pool
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);
pool.on("connect", () => {
console.log("Database connected");
});
pool.on("error", (err) => {
console.error("Unexpected database error", err);
process.exit(-1);
});
= () => {
pool.();
.();
};
MongoDB with Mongoose
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 };
import { Schema, model, Document } from "mongoose";
{
: ;
: ;
: ;
: ;
: ;
}
userSchema = <>(
{
: { : , : },
: { : , : , : },
: { : , : },
},
{
: ,
},
);
userSchema.({ : });
= model<>(, userSchema);
Transaction Pattern
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");
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;
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],
);
client.(
,
[item., item.],
);
}
client.();
orderId;
} (error) {
client.();
error;
} {
client.();
}
}
}
Authentication & Authorization
JWT Authentication
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.({
: user.,
: user.,
});
refreshToken = .({
: user.,
});
{
token,
refreshToken,
: {
: user.,
: user.,
: user.,
},
};
}
() {
{
payload = jwt.(
refreshToken,
process..!,
) { : };
user = ..(payload.);
(!user) {
();
}
token = .({
: user.,
: user.,
});
{ token };
} (error) {
();
}
}
(: ): {
jwt.(payload, process..!, {
: ,
});
}
(: ): {
jwt.(payload, process..!, {
: ,
});
}
}
Caching Strategies
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);
(ttl) {
redis.(key, ttl, serialized);
} {
redis.(key, serialized);
}
}
(: ): <> {
redis.(key);
}
(: ): <> {
keys = redis.(pattern);
(keys. > ) {
redis.(...keys);
}
}
}
() {
() {
originalMethod = descriptor.;
descriptor. = () {
cache = ();
cacheKey = ;
cached = cache.(cacheKey);
(cached) {
cached;
}
result = originalMethod.(, args);
cache.(cacheKey, result, ttl);
result;
};
descriptor;
};
}
API Response Format
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,
: .(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