Comparisons

Express vs Fastify: AI Rules Comparison

Express is the most widely used Node.js framework. Fastify is the performance-focused alternative with built-in validation and serialization. Each needs different AI rules for routing, middleware, validation, and error handling.

7 min read·April 26, 2025

res.json() in Fastify, app.use() middleware in Fastify — Express patterns that do not exist here

Routing, middleware vs plugins, JSON Schema validation, error handling, and rule templates

The Ubiquitous vs the Performant

Express is the default Node.js web framework. Most Node.js tutorials, blog posts, and Stack Overflow answers use Express. AI models have extensive Express training data. The result: without rules, AI generates Express patterns by default even in Fastify projects. Express is: minimalist (no opinions on structure, validation, or serialization), middleware-based (everything is middleware: auth, logging, parsing, CORS), and callback-oriented (though async/await works, many patterns still use callback style).

Fastify is the performance-focused alternative. It is 2-3x faster than Express in benchmarks (thanks to schema-based serialization and an optimized routing tree). Fastify is: opinionated (built-in JSON Schema validation and serialization), plugin-based (encapsulated plugins instead of middleware chains), and async-first (designed for async/await from the ground up). Fastify's request validation and response serialization are automatic when schemas are provided.

Without framework-specific rules: AI generates app.use() middleware in a Fastify project (Fastify uses plugins and hooks, not Express middleware). AI generates manual if-statement validation in Fastify (Fastify has built-in JSON Schema validation). AI generates res.json() in Fastify (Fastify uses return or reply.send()). The conventions are different enough that mixing them produces broken or suboptimal code.

Routing: app.get vs fastify.get

Express routing: app.get('/users/:id', (req, res) => { res.json(user); }). Route parameters: req.params.id. Query strings: req.query.page. Request body: req.body (requires body-parser middleware or express.json()). The handler receives (req, res, next) and must explicitly call res.json(), res.send(), or next(). Forgetting to respond: the request hangs indefinitely. AI rule: 'Express routes: app.get/post/put/delete. Handler: (req, res) => { res.json(data); }. Always call res.json/send/end or next. Body parsing: app.use(express.json()).'

Fastify routing: fastify.get('/users/:id', async (request, reply) => { return user; }). Simply returning a value sends it as JSON automatically (no reply.send needed for simple responses). Route parameters: request.params.id. Query: request.query.page. Body: request.body (built-in JSON parsing, no middleware needed). Async handlers: errors thrown are automatically caught and returned as 500 responses. AI rule: 'Fastify routes: fastify.get/post/put/delete. Async handler: return value sends as JSON. No need for reply.send() for simple responses. Body parsing is built-in.'

The routing rule prevents: AI generating (req, res) signatures in Fastify (it is (request, reply)), calling res.json() in Fastify (use return or reply.send()), adding express.json() middleware in Fastify (body parsing is built-in), and forgetting to return in Fastify async handlers (Express ignores the return, Fastify uses it). The handler pattern is different enough that one rule about the framework conventions prevents every route handler error.

  • Express: (req, res) => { res.json(data); } — must explicitly respond
  • Fastify: async (request, reply) => { return data; } — return sends as JSON
  • Express: express.json() middleware needed for body parsing
  • Fastify: body parsing built-in, no middleware required
  • Fastify async: thrown errors auto-caught as 500 — no try/catch boilerplate for basic errors
💡 Return Data = Send JSON

Express: must call res.json(data) explicitly — forgetting means the request hangs. Fastify: return data from an async handler and it sends as JSON automatically. Same result, different pattern. AI generating res.json in Fastify: works but misses the idiomatic return pattern.

Middleware vs Plugins and Hooks

Express middleware: functions that run in sequence on every request (or matching routes). app.use(cors()); app.use(express.json()); app.use(authMiddleware); app.use('/api', apiRouter). Middleware is global (affects all routes) or route-specific (mounted on a path). Middleware can: modify req/res, end the request, or call next() to continue. The middleware chain is the core abstraction in Express — everything is middleware.

Fastify plugins and hooks: plugins are encapsulated modules that register routes, hooks, and decorators. fastify.register(authPlugin). Hooks are lifecycle events: onRequest, preValidation, preHandler, onSend, onResponse. Hooks replace middleware: fastify.addHook('onRequest', async (request, reply) => { /* auth check */ }). Plugins provide encapsulation (a plugin's decorators and hooks are scoped to its routes, not global by default). AI rule: 'Fastify: use fastify.register() for plugins, addHook() for lifecycle hooks. No app.use() middleware pattern.'

The middleware vs plugin rule prevents: AI adding app.use(cors()) in Fastify (use @fastify/cors plugin instead), writing middleware functions with (req, res, next) for Fastify (use hooks with (request, reply)), and applying global middleware that should be plugin-scoped. The mental model switch: Express = middleware chain. Fastify = plugin tree with lifecycle hooks. One architectural rule aligns the AI with the correct abstraction.

Validation: Manual vs JSON Schema Built-In

Express validation: no built-in validation. Developers add: express-validator (middleware-based), Zod (parse in the handler), or Joi (schema validation library). The validation is application-level — you choose the library and apply it manually. AI rule: 'Express: validate request body with Zod in the route handler. const parsed = schema.parse(req.body). Return 400 on validation error.' Common Express pattern: validation middleware or inline Zod parsing.

Fastify validation: built-in JSON Schema validation on routes. fastify.post('/users', { schema: { body: { type: 'object', properties: { email: { type: 'string', format: 'email' } }, required: ['email'] } } }, handler). Fastify validates the request against the schema automatically and returns 400 with a detailed error if validation fails. No validation code in the handler. Fastify also serializes the response using the schema (faster than JSON.stringify). AI rule: 'Fastify: define JSON Schema on the route options for body, querystring, params, and response. Validation and serialization are automatic.'

The validation rule is a major difference: Fastify routes should always have schemas (validation + performance). Express routes need manual validation code (Zod, express-validator). AI generating manual if-statement validation in Fastify: works but misses the built-in schema validation (no 400 auto-response, no serialization speedup). AI generating JSON Schema on Express routes: does nothing (Express does not process route schemas). The validation pattern is framework-determined.

  • Express: no built-in validation — use Zod, express-validator, or Joi manually
  • Fastify: JSON Schema on route options — automatic validation + 400 errors + serialization
  • Fastify schemas: body, querystring, params, headers, response — all validated automatically
  • Fastify serialization: schema-based response serialization is 2-3x faster than JSON.stringify
  • AI error: manual validation in Fastify misses built-in features. Schema on Express does nothing.
⚠️ Schema = Free Validation + 2-3x Faster Serialization

Fastify with JSON Schema on routes: automatic request validation (400 on failure) AND 2-3x faster response serialization. AI generating manual Zod validation in Fastify: works but misses both built-in features. One route option replaces validation code AND improves performance.

Error Handling Patterns

Express error handling: errors are passed to the next middleware via next(error). A global error handler: app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); }). The 4-argument signature identifies it as an error handler. Async errors require try/catch or express-async-errors (to automatically catch thrown errors). Without express-async-errors: an unhandled async error crashes the process.

Fastify error handling: async errors are automatically caught (Fastify wraps handlers). A custom error handler: fastify.setErrorHandler((error, request, reply) => { reply.status(error.statusCode || 500).send({ error: error.message }); }). Fastify also supports: onError hook (runs when an error occurs), custom error classes with statusCode property (Fastify reads the statusCode), and schema validation errors (automatically formatted as 400 responses). No try/catch needed for basic async error handling.

The error rule: 'Express: use express-async-errors or wrap async handlers in try/catch. Global error handler: 4-argument middleware. Errors via next(error).' 'Fastify: async errors are auto-caught. Custom error handler: setErrorHandler. Validation errors: automatic 400 from schema. Custom errors: set statusCode property.' AI generating next(error) in Fastify: next does not exist. AI forgetting try/catch in Express async: unhandled rejection crashes the process.

ℹ️ Auto-Catch vs Process Crash

Express async handler throws: without try/catch or express-async-errors, the error is unhandled and crashes the process. Fastify async handler throws: error auto-caught, 500 returned. Same thrown error, dramatically different default behavior. Fastify is safe by default.

Ready-to-Use Rule Templates

Express CLAUDE.md template: '# Server (Express). Framework: Express with TypeScript. Routes: app.get/post/put/delete with (req, res) handlers. Middleware: app.use() for cors, json parsing, auth. Validation: Zod schema in handlers — parse req.body, return 400 on failure. Errors: express-async-errors for auto-catch, global error handler with 4-arg middleware. Body parsing: app.use(express.json()). Response: res.json(data) or res.status(code).json(error). Never use Fastify patterns (register, addHook, return from handler).'

Fastify CLAUDE.md template: '# Server (Fastify). Framework: Fastify with TypeScript. Routes: fastify.get/post/put/delete with async (request, reply) handlers. Plugins: fastify.register() for modular features. Hooks: addHook for lifecycle (onRequest, preHandler). Validation: JSON Schema on route options for body, querystring, params — automatic validation and 400 errors. Serialization: response schema for faster JSON. Response: return value sends as JSON. Errors: auto-caught in async, setErrorHandler for custom handling. Never use Express patterns (app.use, next, res.json).'

The templates highlight the key differences: Express needs explicit response (res.json), manual validation (Zod), and explicit error handling (try/catch or express-async-errors). Fastify provides: auto-response (return), built-in validation (schema), and auto error handling (async catch). Copy the template for your framework — the positive rules guide and the negative rules prevent cross-contamination.

Comparison Summary

Summary of Express vs Fastify AI rules.

  • Routing: Express (req, res) => res.json() vs Fastify async (request, reply) => return data
  • Middleware: Express app.use() chain vs Fastify register() plugins + addHook() lifecycle
  • Validation: Express manual (Zod/Joi) vs Fastify JSON Schema built-in (auto 400 errors)
  • Serialization: Express JSON.stringify vs Fastify schema-based (2-3x faster)
  • Errors: Express needs try/catch or express-async-errors vs Fastify auto-catches async errors
  • Body parsing: Express needs express.json() middleware vs Fastify built-in
  • Performance: Fastify 2-3x faster than Express in benchmarks
  • Templates: one paragraph per framework prevents all cross-contamination errors