Claude Code for Express.js Advanced: Patterns for Production APIs — Claude Skills 360 Blog
Blog / Backend / Claude Code for Express.js Advanced: Patterns for Production APIs
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Published: June 8, 2027
Read time: 6 min read
By: Claude Skills 360

Express.js with TypeScript scales to production with the right patterns — RequestHandler<Params, ResBody, ReqBody, ReqQuery> types handler generics. asyncHandler(fn) wraps async functions to catch rejections. Zod validation middleware: validateBody(schema) returns a typed RequestHandler that calls schema.safeParse(req.body). express-rate-limit with RedisStore prevents abuse. helmet() sets security headers. multer({ storage: multerS3(...) }) handles S3 uploads. JWT: verify(token, secret) in middleware with req.user injection via declaration merging. pino with pino-http logs structured JSON. Dependency injection with tsyringe: @injectable() classes resolved by container.resolve(ServiceClass). Pagination: parsePagination(req.query) returns { page, pageSize, offset }. morgan or pino-http for request logging. Circuit breaker with opossum for external service calls. Claude Code generates Express middleware stacks, typed handlers, rate limiting, and upload pipelines.

CLAUDE.md for Express.js Advanced

## Express.js Advanced Stack
- Version: express >= 4.21, express >= 5 beta, typescript >= 5
- Typed handler: const handler: RequestHandler<{ id: string }, UserResponse, UpdateBody> = async (req, res) => { ... }
- Async: asyncHandler(async (req, res) => { const data = await service.get(req.params.id); res.json(data) })
- Validation: app.post("/users", validateBody(CreateUserSchema), handler)
- Error handler: app.use((err: Error, req, res, next): void => { res.status(500).json({ message: err.message }) })
- Rate limit: app.use("/api/", rateLimit({ windowMs: 15*60*1000, max: 100, store: new RedisStore({ client }) }))
- JWT: app.use("/api/protected", authenticate) — injects req.user

Core Utilities

// src/lib/express/types.ts — typed Express extensions
import type { Request, Response, NextFunction, RequestHandler } from "express"
import type { ZodSchema, z } from "zod"

// Augment Express Request to include validated data
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string
        email: string
        role: "user" | "admin"
      }
      pagination: {
        page: number
        pageSize: number
        offset: number
      }
    }
  }
}

// Async handler wrapper — catches promise rejections and passes to next()
export function asyncHandler<
  P = {},
  ResBody = any,
  ReqBody = any,
  ReqQuery = qs.ParsedQs,
>(
  fn: (
    req: Request<P, ResBody, ReqBody, ReqQuery>,
    res: Response<ResBody>,
    next: NextFunction,
  ) => Promise<void | Response>,
): RequestHandler<P, ResBody, ReqBody, ReqQuery> {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next)
  }
}

// Zod body validation middleware factory
export function validateBody<T extends ZodSchema>(schema: T) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const result = schema.safeParse(req.body)
    if (!result.success) {
      res.status(400).json({
        message: "Validation error",
        errors: result.error.flatten().fieldErrors,
      })
      return
    }
    req.body = result.data
    next()
  }
}

// Zod query validation middleware factory
export function validateQuery<T extends ZodSchema>(schema: T) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const result = schema.safeParse(req.query)
    if (!result.success) {
      res.status(400).json({
        message: "Query validation error",
        errors: result.error.flatten().fieldErrors,
      })
      return
    }
    req.query = result.data as any
    next()
  }
}

// Pagination middleware
export function paginationMiddleware(
  defaultPageSize = 10,
  maxPageSize = 100,
): RequestHandler {
  return (req, res, next) => {
    const page = Math.max(1, parseInt(String(req.query.page ?? "1"), 10) || 1)
    const requestedSize = parseInt(String(req.query.pageSize ?? String(defaultPageSize)), 10)
    const pageSize = Math.min(maxPageSize, Math.max(1, requestedSize || defaultPageSize))

    req.pagination = {
      page,
      pageSize,
      offset: (page - 1) * pageSize,
    }

    next()
  }
}

JWT Authentication

// src/middleware/auth.ts — JWT authentication middleware
import type { RequestHandler } from "express"
import { verify } from "jsonwebtoken"
import { db } from "../db"
import { users } from "../db/schema"
import { eq } from "drizzle-orm"

const JWT_SECRET = process.env.JWT_SECRET!

export const authenticate: RequestHandler = async (req, res, next) => {
  const token =
    req.headers.authorization?.replace("Bearer ", "") ??
    (req.cookies?.access_token as string | undefined)

  if (!token) {
    res.status(401).json({ message: "Authentication required" })
    return
  }

  try {
    const payload = verify(token, JWT_SECRET) as {
      sub: string
      email: string
      role: "user" | "admin"
    }

    // Optionally hydrate user from DB (adds latency, ensures up-to-date)
    // For performance, trust the JWT claims and skip DB lookup in hot paths
    req.user = {
      id: payload.sub,
      email: payload.email,
      role: payload.role,
    }

    next()
  } catch {
    res.status(401).json({ message: "Invalid or expired token" })
  }
}

export const requireAdmin: RequestHandler = (req, res, next) => {
  if (!req.user) {
    res.status(401).json({ message: "Authentication required" })
    return
  }

  if (req.user.role !== "admin") {
    res.status(403).json({ message: "Admin access required" })
    return
  }

  next()
}

export function requireRole(...roles: string[]): RequestHandler {
  return (req, res, next) => {
    if (!req.user) {
      res.status(401).json({ message: "Authentication required" })
      return
    }

    if (!roles.includes(req.user.role)) {
      res.status(403).json({ message: `Requires one of: ${roles.join(", ")}` })
      return
    }

    next()
  }
}

Application Setup

// src/app.ts — Express app configuration
import express from "express"
import helmet from "helmet"
import compression from "compression"
import cors from "cors"
import rateLimit from "express-rate-limit"
import RedisStore from "rate-limit-redis"
import { createClient } from "redis"
import pino from "pino"
import pinoHttp from "pino-http"
import { postsRouter } from "./routes/posts"
import { usersRouter } from "./routes/users"
import { authRouter } from "./routes/auth"
import type { Request, Response, NextFunction } from "express"

const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  transport: process.env.NODE_ENV === "development"
    ? { target: "pino-pretty" }
    : undefined,
})

const redisClient = createClient({ url: process.env.REDIS_URL })
redisClient.connect().catch(logger.error.bind(logger))

export function createApp() {
  const app = express()

  // ── Security ───────────────────────────────────────────────────────────
  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
      },
    },
  }))

  // ── Performance ────────────────────────────────────────────────────────
  app.use(compression())

  // ── CORS ───────────────────────────────────────────────────────────────
  app.use(cors({
    origin: (origin, callback) => {
      const allowed = [
        process.env.FRONTEND_URL ?? "http://localhost:3000",
        "http://localhost:3001",
      ]
      if (!origin || allowed.includes(origin)) callback(null, true)
      else callback(new Error("Not allowed by CORS"))
    },
    credentials: true,
  }))

  // ── Body parsing ───────────────────────────────────────────────────────
  app.use(express.json({ limit: "1mb" }))
  app.use(express.urlencoded({ extended: true }))

  // ── Logging ────────────────────────────────────────────────────────────
  app.use(pinoHttp({ logger }))

  // ── Rate limiting ──────────────────────────────────────────────────────
  const apiLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,  // 15 minutes
    max: 100,
    standardHeaders: true,
    legacyHeaders: false,
    store: new RedisStore({
      sendCommand: (...args: string[]) => redisClient.sendCommand(args),
    }),
    keyGenerator: (req) => req.user?.id ?? req.ip ?? "anonymous",
  })

  const authLimiter = rateLimit({
    windowMs: 60 * 60 * 1000,  // 1 hour
    max: 10,  // 10 login attempts per hour
    store: new RedisStore({
      sendCommand: (...args: string[]) => redisClient.sendCommand(args),
      prefix: "rl:auth:",
    }),
  })

  // ── Routes ─────────────────────────────────────────────────────────────
  app.get("/health", (_req, res) => {
    res.json({ ok: true, timestamp: new Date().toISOString() })
  })

  app.use("/api/auth", authLimiter, authRouter)
  app.use("/api/posts", apiLimiter, postsRouter)
  app.use("/api/users", apiLimiter, usersRouter)

  // ── 404 handler ────────────────────────────────────────────────────────
  app.use((_req, res) => {
    res.status(404).json({ message: "Route not found" })
  })

  // ── Error handler ──────────────────────────────────────────────────────
  app.use((err: Error, req: Request, res: Response, _next: NextFunction): void => {
    const log = (req as any).log ?? logger
    log.error({ err }, "Unhandled error")

    if (res.headersSent) return

    res.status(500).json({
      message: process.env.NODE_ENV === "production"
        ? "Internal server error"
        : err.message,
    })
  })

  return app
}

Typed Router Example

// src/routes/posts.ts — typed Express router
import { Router } from "express"
import { z } from "zod"
import { asyncHandler, validateBody, validateQuery, paginationMiddleware } from "../lib/express/types"
import { authenticate } from "../middleware/auth"
import { db } from "../db"
import { posts } from "../db/schema"
import { eq, ilike, desc } from "drizzle-orm"

export const postsRouter = Router()

const CreatePostSchema = z.object({
  title: z.string().min(5).max(200),
  content: z.string().min(20),
  excerpt: z.string().max(500).optional(),
  published: z.boolean().default(false),
})

const PostQuerySchema = z.object({
  search: z.string().optional(),
  published: z.coerce.boolean().optional(),
})

postsRouter.get(
  "/",
  paginationMiddleware(10, 50),
  validateQuery(PostQuerySchema),
  asyncHandler(async (req, res) => {
    const { page, pageSize, offset } = req.pagination
    const { search, published } = req.query as z.infer<typeof PostQuerySchema>

    const where = []
    if (search) where.push(ilike(posts.title, `%${search}%`))
    if (published !== undefined) where.push(eq(posts.published, published))

    const result = await db.query.posts.findMany({
      where: where.length > 0 ? and(...where) : undefined,
      limit: pageSize,
      offset,
      orderBy: desc(posts.createdAt),
    })

    res.json({ posts: result, page, pageSize })
  }),
)

postsRouter.post(
  "/",
  authenticate,
  validateBody(CreatePostSchema),
  asyncHandler(async (req, res) => {
    const body = req.body as z.infer<typeof CreatePostSchema>
    const slug = body.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")

    const [post] = await db.insert(posts).values({
      ...body,
      slug,
      authorId: req.user!.id,
    }).returning()

    res.status(201).json(post)
  }),
)

For the Fastify alternative when a Node.js framework with better raw performance, JSON Schema validation built-in, a plugin system with lifecycle hooks, and TypeBox integration for type-safe JSON schemas is preferred — Fastify consistently benchmarks 2-3x faster than Express while Express has a much larger middleware ecosystem and longer community track record, see the Fastify guide. For the Hono alternative when deploying to edge runtimes (Cloudflare Workers, Deno Deploy) or Bun for maximum performance — Hono’s ergonomics are similar to Express but it’s built for edge from the ground up, see the Hono guide. The Claude Skills 360 bundle includes Express.js advanced skill sets covering middleware, auth, and rate limiting. Start with the free tier to try production Express API generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027
Backend

Claude Code for Hono Advanced: Edge-First Web Framework Patterns

Advanced Hono patterns with Claude Code — Hono RPC with hc typed client, factory createFactory for reusable middleware, validator middleware with Zod, streaming responses with streamText and streamSSE, WebSocket upgrader with upgradeWebSocket, Cloudflare Workers bindings with c.env, D1 database access, KV namespace, R2 bucket, middleware composition with compose, testing with app.request, and monorepo sharing of Hono RPC types.

6 min read Jun 6, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free