Express & Fastify
Rules
- Organize routes by domain:
routes/users.ts, routes/orders.ts — one router per resource
- Validate all input with Zod: parse
req.body, req.params, req.query before processing
- Centralized error handler: catch-all middleware (Express) or
setErrorHandler (Fastify)
- Return consistent error format:
{ error: { code: string, message: string, details?: unknown } }
- TypeScript: type request/response with generics —
Request<Params, ResBody, ReqBody, Query>
- Middleware order: CORS, body parser, auth, rate limiting, routes, error handler
- Use async route handlers — wrap with error catching or use
express-async-errors
- Fastify: use the plugin system to encapsulate routes and decorators —
fastify.register()
- Fastify schemas: define JSON Schema or use
@fastify/type-provider-zod for validation + serialization
- Health check:
GET /health — return 200 with DB/Redis connectivity status
- Graceful shutdown: listen for SIGTERM, stop accepting connections, finish in-flight requests
Patterns
import { Router } from "express";
import { z } from "zod";
const router = Router();
const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(1) });
router.post("/users", async (req, res, next) => {
try {
const body = CreateUserSchema.parse(req.body);
const user = await createUser(body);
res.status(201).json(user);
} catch (err) { next(err); }
});
import Fastify from "fastify";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "fastify-type-provider-zod";
const app = Fastify().withTypeProvider<ZodTypeProvider>();
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
app.post("/users", { schema: { body: CreateUserSchema } }, async (req) => {
return createUser(req.body);
});
Avoid
- Putting business logic in route handlers — extract to service/domain layer
- Forgetting async error handling in Express — unhandled rejections crash the process
- Importing all routes in one file — use
router.use() or Fastify plugins for modularity
- Skipping input validation because "the frontend validates" — always validate server-side