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.
71
64%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./plugins/javascript-typescript/skills/nodejs-backend-patterns/SKILL.mdComprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices.
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}`);
});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" });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 typesController 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;
}
}Use a DI container to wire up repositories, services, and controllers. For a full container implementation, see references/advanced-patterns.md.
// 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();
};
};// 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);// 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,
});// 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 };// 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);
}
}// 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);
};
};Node.js supports both SQL and NoSQL databases. Use connection pooling for all production databases.
Key patterns covered in references/advanced-patterns.md:
pg Pool configuration and graceful shutdownBEGIN/COMMIT/ROLLBACK with pg clientJWT-based auth with access tokens (short-lived, 15m) and refresh tokens (7d). Full AuthService implementation with bcrypt password comparison in references/advanced-patterns.md.
Redis-backed CacheService with get/set/delete/invalidatePattern, plus a @Cacheable decorator for method-level caching. See references/advanced-patterns.md.
Standardized ApiResponse helper with success, error, and paginated static methods. See references/advanced-patterns.md.
* in productionSee javascript-testing-patterns skill for comprehensive testing guidance.
91fe43e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.