express

SKILL.md

Express.js - Backend API Patterns

Production patterns for Express.js REST APIs, middleware, and error handling


When to Use This Skill

Use this skill when:

  • Building Express.js backend applications
  • Implementing RESTful APIs
  • Setting up middleware chains
  • Handling authentication and authorization
  • Structuring Node.js server applications
  • Implementing error handling and validation

Don't use this skill when:

  • Using other frameworks (Fastify, Koa, Hono)
  • Building frontend applications
  • Working with Next.js API routes (use nextjs skill)

Critical Patterns

Pattern 1: Router Organization

When: Structuring API routes in larger applications

// ✅ GOOD: Organized router structure
// src/routes/products.ts
import { Router } from 'express';
import { ProductController } from '../controllers/product.controller';
import { validateBody } from '../middleware/validate';
import { productSchema } from '../schemas/product.schema';
import { requireAuth } from '../middleware/auth';

const router = Router();
const controller = new ProductController();

router.get('/', controller.findAll);
router.get('/:id', controller.findById);
router.post('/', requireAuth, validateBody(productSchema), controller.create);
router.put('/:id', requireAuth, validateBody(productSchema), controller.update);
router.delete('/:id', requireAuth, controller.delete);

export default router;

// src/routes/index.ts
import { Router } from 'express';
import productsRouter from './products';
import usersRouter from './users';
import authRouter from './auth';

const router = Router();

router.use('/products', productsRouter);
router.use('/users', usersRouter);
router.use('/auth', authRouter);

export default router;

// src/app.ts
import express from 'express';
import routes from './routes';

const app = express();
app.use('/api/v1', routes);

// ❌ BAD: All routes in one file
app.get('/api/products', ...);
app.post('/api/products', ...);
app.get('/api/users', ...);
// Becomes unmaintainable at scale

Pattern 2: Middleware Chain

When: Processing requests through multiple steps

// ✅ GOOD: Well-structured middleware chain
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { requestLogger } from './middleware/logger';
import { errorHandler } from './middleware/error';

const app = express();

// Security middleware (order matters!)
app.use(helmet()); // Security headers
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per window
}));

// Parsing middleware
app.use(express.json({ limit: '10kb' })); // Body size limit
app.use(express.urlencoded({ extended: true }));

// Logging
app.use(requestLogger);

// Routes
app.use('/api', routes);

// Error handling (must be last!)
app.use(errorHandler);

// ❌ BAD: Error handler before routes
app.use(errorHandler); // Won't catch route errors!
app.use('/api', routes);

Pattern 3: Async Error Handling

When: Handling errors in async route handlers

// ✅ GOOD: Async wrapper to catch errors
type AsyncHandler = (
  req: Request,
  res: Response,
  next: NextFunction
) => Promise<void>;

const asyncHandler = (fn: AsyncHandler) => {
  return (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

// Usage
router.get('/products/:id', asyncHandler(async (req, res) => {
  const product = await productService.findById(req.params.id);

  if (!product) {
    throw new NotFoundError('Product not found');
  }

  res.json(product);
}));

// ❌ BAD: Unhandled promise rejection
router.get('/products/:id', async (req, res) => {
  const product = await productService.findById(req.params.id); // If this throws, crash!
  res.json(product);
});

// ❌ BAD: Try-catch in every handler
router.get('/products/:id', async (req, res, next) => {
  try {
    const product = await productService.findById(req.params.id);
    res.json(product);
  } catch (error) {
    next(error);
  }
});

Pattern 4: Custom Error Classes

When: Creating structured error responses

// ✅ GOOD: Custom error classes
// src/errors/app-error.ts
export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number,
    public code: string
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(message, 404, 'NOT_FOUND');
  }
}

export class ValidationError extends AppError {
  constructor(message: string, public details?: Record<string, string[]>) {
    super(message, 400, 'VALIDATION_ERROR');
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

// Error handler middleware
export function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        ...(err instanceof ValidationError && { details: err.details }),
      },
    });
  }

  // Unknown error - log and return generic message
  console.error('Unexpected error:', err);
  res.status(500).json({
    error: {
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
    },
  });
}

// ❌ BAD: String errors or status codes everywhere
throw new Error('Not found'); // No status code!
res.status(404).json({ error: 'Not found' }); // Inconsistent format

Pattern 5: Request Validation

When: Validating incoming request data

// ✅ GOOD: Validation middleware with Zod
import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';
import { ValidationError } from '../errors/app-error';

export function validateBody<T extends z.ZodSchema>(schema: T) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);

    if (!result.success) {
      const details = result.error.flatten().fieldErrors;
      throw new ValidationError('Validation failed', details);
    }

    req.body = result.data; // Replace with validated data
    next();
  };
}

export function validateQuery<T extends z.ZodSchema>(schema: T) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.query);

    if (!result.success) {
      throw new ValidationError('Invalid query parameters');
    }

    req.query = result.data as any;
    next();
  };
}

// Schema definition
const createProductSchema = z.object({
  name: z.string().min(1).max(100),
  price: z.number().positive(),
  category: z.enum(['electronics', 'clothing', 'food']),
});

// Usage
router.post('/products',
  requireAuth,
  validateBody(createProductSchema),
  asyncHandler(async (req, res) => {
    // req.body is typed and validated!
    const product = await productService.create(req.body);
    res.status(201).json(product);
  })
);

// ❌ BAD: Manual validation in handler
router.post('/products', async (req, res) => {
  if (!req.body.name) {
    return res.status(400).json({ error: 'Name required' });
  }
  if (typeof req.body.price !== 'number') {
    return res.status(400).json({ error: 'Price must be number' });
  }
  // ... more validation scattered in handler
});

Code Examples

For complete, production-ready examples, see references/examples.md:

  • Controller pattern with pagination
  • Authentication middleware with JWT
  • Graceful shutdown handling
  • Service layer pattern

Anti-Patterns

Don't: Business Logic in Controllers

// ❌ BAD: All logic in controller
router.post('/orders', async (req, res) => {
  const items = await db.cartItem.findMany({ where: { userId: req.user.id } });
  const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  const order = await db.order.create({ data: { items, total } });
  await sendEmail(req.user.email, 'Order confirmed');
  res.json(order);
});

// ✅ GOOD: Controller calls service
router.post('/orders', asyncHandler(async (req, res) => {
  const order = await orderService.createFromCart(req.user.id);
  res.status(201).json({ data: order });
}));

Don't: Expose Stack Traces

// ❌ BAD: Sending error stack to client
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message, stack: err.stack });
});

// ✅ GOOD: Log internally, send safe message
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});

Don't: Skip Input Validation

// ❌ BAD: Trust user input
router.post('/users', async (req, res) => {
  const user = await db.user.create({ data: req.body }); // Dangerous!
});

// ✅ GOOD: Validate and sanitize
router.post('/users', validateBody(userSchema), async (req, res) => {
  const user = await db.user.create({ data: req.body });
});

Quick Reference

Task Pattern Example
Organize routes Router modules router.use('/products', productsRouter)
Catch async errors asyncHandler asyncHandler(async (req, res) => {})
Validate input Zod middleware validateBody(schema)
Custom errors Error classes throw new NotFoundError()
Auth middleware requireAuth router.use(requireAuth)
Parse JSON Built-in app.use(express.json())
Security headers Helmet app.use(helmet())

Resources

Official Documentation:

Related Skills:

  • nodejs: Node.js patterns
  • security: API security best practices
  • api-design: RESTful API design

Keywords

express, expressjs, nodejs, rest, api, middleware, routing, backend, server, error-handling, validation

Weekly Installs
2
First Seen
Feb 25, 2026
Installed on
trae-cn2
codebuddy2
github-copilot2
codex2
kiro-cli2
kimi-cli2