Ai nodejs-patterns
install
source · Clone the upstream repo
git clone https://github.com/wpank/ai
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/backend/nodejs-patterns" ~/.claude/skills/wpank-ai-nodejs-patterns && rm -rf "$T"
manifest:
skills/backend/nodejs-patterns/SKILL.mdsource content
Node.js Backend Patterns
Patterns for building scalable, maintainable Node.js backend applications with TypeScript.
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install nodejs-patterns
NEVER
- NEVER store secrets in code - Use environment variables, never hardcode credentials
- NEVER skip input validation - Validate all input at the middleware layer with Zod/Joi
- NEVER expose error details in production - Return generic messages, log details server-side
- NEVER use
type - TypeScript types prevent runtime errorsany - NEVER skip error handling - Always wrap async handlers, use global error middleware
- NEVER use sync operations - Use async/await for I/O, never
in handlersfs.readFileSync - NEVER trust client input - Sanitize, validate, and parameterize all queries
When to Use
- Building REST APIs with Express or Fastify
- Setting up middleware pipelines and error handling
- Implementing authentication and authorization
- Integrating databases with connection pooling and transactions
- Adding validation, caching, and rate limiting
Project Structure — Layered Architecture
src/ ├── controllers/ # Handle HTTP requests/responses ├── services/ # Business logic ├── repositories/ # Data access layer ├── models/ # Data models and types ├── middleware/ # Auth, validation, logging, errors ├── routes/ # Route definitions ├── config/ # Database, cache, env configuration └── utils/ # Helpers, custom errors, response formatting
Controllers handle HTTP concerns, services contain business logic, repositories abstract data access. Each layer only calls the layer below it.
Express Setup
import express 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" }));
Fastify Setup
import Fastify from "fastify"; import helmet from "@fastify/helmet"; import cors from "@fastify/cors"; const fastify = Fastify({ logger: { level: process.env.LOG_LEVEL || "info" }, }); await fastify.register(helmet); await fastify.register(cors, { origin: true }); // Type-safe routes with built-in schema validation fastify.post<{ Body: { name: string; email: string } }>( "/users", { schema: { body: { type: "object", required: ["name", "email"], properties: { name: { type: "string", minLength: 1 }, email: { type: "string", format: "email" }, }, }, }, }, async (request) => { const { name, email } = request.body; return { id: "123", name }; }, );
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 = "Resource not found") { super(message, 404); } } export class UnauthorizedError extends AppError { constructor(message = "Unauthorized") { super(message, 401); } } export class ForbiddenError extends AppError { constructor(message = "Forbidden") { super(message, 403); } }
Global Error Handler
import { Request, Response, NextFunction } from "express"; import { AppError, ValidationError } from "../utils/errors"; 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 }), }); } // Don't leak details in production const message = process.env.NODE_ENV === "production" ? "Internal server error" : err.message; res.status(500).json({ status: "error", message }); }; // Wrap async route handlers to forward errors export const asyncHandler = ( fn: (req: Request, res: Response, next: NextFunction) => Promise<any>, ) => (req: Request, res: Response, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next); };
Validation Middleware (Zod)
import { AnyZodObject, ZodError } from "zod"; 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((e) => ({ field: e.path.join("."), message: e.message, })); next(new ValidationError("Validation failed", errors)); } else { next(error); } } }; }; // Usage 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);
Authentication — JWT
Auth Middleware
import jwt from "jsonwebtoken"; interface JWTPayload { userId: string; email: string; } 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"); req.user = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload; next(); } catch { next(new UnauthorizedError("Invalid token")); } }; export const authorize = (...roles: string[]) => { return (req: Request, res: Response, next: NextFunction) => { if (!req.user) return next(new UnauthorizedError("Not authenticated")); if (!roles.some((r) => req.user?.roles?.includes(r))) { return next(new ForbiddenError("Insufficient permissions")); } next(); }; };
Auth Service
export class AuthService { constructor(private userRepository: UserRepository) {} async login(email: string, password: string) { const user = await this.userRepository.findByEmail(email); if (!user || !(await bcrypt.compare(password, user.password))) { throw new UnauthorizedError("Invalid credentials"); } return { token: jwt.sign( { userId: user.id, email: user.email }, process.env.JWT_SECRET!, { expiresIn: "15m" }, ), refreshToken: jwt.sign( { userId: user.id }, process.env.REFRESH_TOKEN_SECRET!, { expiresIn: "7d" }, ), user: { id: user.id, name: user.name, email: user.email }, }; } }
Database Patterns
PostgreSQL Connection Pool
import { Pool, PoolConfig } from "pg"; const pool = 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, }); pool.on("error", (err) => { console.error("Unexpected database error", err); process.exit(-1); }); export const closeDatabase = async () => { await pool.end(); };
Transaction Pattern
async createOrder(userId: string, items: OrderItem[]) { const client = await this.db.connect(); try { await client.query("BEGIN"); const { rows } = await client.query( "INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id", [userId, calculateTotal(items)], ); const orderId = 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], ); 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(); } }
Rate Limiting
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 }); export const apiLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: "rl:" }), windowMs: 15 * 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false, }); export const authLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: "rl:auth:" }), windowMs: 15 * 60 * 1000, max: 5, skipSuccessfulRequests: true, });
Caching with Redis
import Redis from "ioredis"; const redis = new Redis({ host: process.env.REDIS_HOST, retryStrategy: (times) => Math.min(times * 50, 2000), }); 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 ? await redis.setex(key, ttl, serialized) : 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) await redis.del(...keys); } }
API Response Helpers
export class ApiResponse { static success<T>(res: Response, data: T, message?: string, statusCode = 200) { return res.status(statusCode).json({ status: "success", message, data }); } 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
- Validate all input — Zod or Joi at the middleware layer
- Custom error classes — map to HTTP status codes, use global handler
- Never hardcode secrets — use environment variables
- Structured logging — Pino or Winston with request context
- Rate limiting — Redis-backed for distributed deployments
- Connection pooling — always for databases
- Dependency injection — constructor injection for testability
- Graceful shutdown — close DB pools, drain connections on SIGTERM
- Health checks —
endpoint for liveness/readiness probes/health