nodejs-backend-patterns
Install this skill
npx skills add wshobson/agentsWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
This skill provides a structured methodology for constructing server-side Node.js applications. It prioritizes the separation of concerns by implementing a layered architecture—splitting code into controllers for request handling, services for business logic, and repositories for data access. The content guides you through framework selection, specifically contrasting the lightweight nature of Express.js against the performance-focused, schema-driven approach of Fastify. You gain access to modular patterns for middleware management, error handling, and security hardening. By adopting these standards, your codebase achieves greater testability and maintainability, ensuring that business rules remain isolated from transport layers and database implementations. This repository of patterns acts as a technical foundation for engineers aiming to move beyond simple scripts toward professional, production-grade API development.
When to Use This Skill
- •Building RESTful APIs with strict input validation
- •Developing high-throughput microservices
- •Refactoring monolithic route files into distinct controllers
- •Implementing data persistence layers for SQL or NoSQL databases
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- “set up a clean backend architecture in Node
- “organize my express code into controllers and services
- “show me a production-ready Fastify setup
- “how should I structure my Node.js project folders
- “create a service-repository pattern for my API
Pro Tips
- 💡Always prioritize security from the outset; integrate middleware like Helmet and sanitize all input to prevent common vulnerabilities.
- 💡Design your API with clear, consistent endpoints and embrace HATEOAS principles where appropriate for better discoverability and client interaction.
- 💡Implement comprehensive error handling with a centralized approach to catch exceptions gracefully and provide meaningful error responses to clients.
What this skill does
- •Implementation of multi-layered architectural patterns
- •Fastify-based schema validation using JSON Schema
- •Express.js configuration for security and compression
- •Separation of business logic into dedicated service classes
- •Centralized error handling and request processing
When not to use it
- ✕When building simple serverless functions or single-file scripts
- ✕When working with non-Node.js runtimes
- ✕When avoiding dependencies for extremely small proof-of-concept projects
Example workflow
- Define TypeScript types for request and response models
- Configure the base framework instance with necessary security middleware
- Implement the repository layer for direct database interaction
- Develop service classes containing core business logic and validation
- Map incoming API routes to specific controller methods
- Register routes in the main application entry point
Prerequisites
- –Fundamental knowledge of JavaScript or TypeScript
- –Basic understanding of HTTP request methods
- –Node.js runtime environment installed
Pitfalls & limitations
- !Over-engineering small projects by adding too many layers
- !Blocking the event loop inside synchronous business logic
- !Incorrect management of asynchronous promise chains in controllers
FAQ
How it compares
This skill provides a standardized, battle-tested architectural blueprint rather than a generic prompt that might yield inconsistent, non-modular, or fragile code structures.
📄 Full skill instructions — original source: wshobson/agents
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 NotFoundError extends AppError {
constructor(message: string = "Resource not found") {
super(message, 404);
}
}
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 production9. **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
- **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
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/nodejs-backend-patterns/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/wshobson/agents/nodejs-backend-patterns/SKILL.md - Cursor:
~/.cursor/skills/wshobson/agents/nodejs-backend-patterns/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/wshobson/agents/nodejs-backend-patterns/SKILL.md
🚀 Install with CLI:npx skills add wshobson/agents