Express + TypeScript cheatsheet.

Install

npm i express
npm i -D @types/express

Basic app

import express, { Request, Response, NextFunction } from "express";

const app = express();

app.use(express.json());

app.get("/", (req, res) => {
  res.json({ ok: true });
});

app.listen(3000);

Typed handler

app.get("/users/:id", (req: Request<{ id: string }>, res: Response<User>) => {
  res.json({ id: Number(req.params.id), name: "A", email: "[email protected]" });
});

Request<Params, ResBody, ReqBody, Query>:

Request<
  { id: string },             // params
  User,                       // res body type
  { name: string },           // req body
  { page?: string }           // query
>

RequestHandler type

import { RequestHandler } from "express";

const getUser: RequestHandler<{ id: string }, User> = (req, res) => {
  res.json({ id: Number(req.params.id), name: "A", email: "..." });
};

app.get("/users/:id", getUser);

Middleware

const logger: RequestHandler = (req, res, next) => {
  console.log(req.method, req.url);
  next();
};

app.use(logger);

Async handler wrapper

Express doesn’t catch async errors by default:

type AsyncHandler = (req: Request, res: Response, next: NextFunction) => Promise<unknown>;

const asyncHandler = (fn: AsyncHandler): RequestHandler =>
  (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

app.get("/x", asyncHandler(async (req, res) => {
  const data = await fetchSomething();
  res.json(data);
}));

Or use express-async-errors.

Error handler

class AppError extends Error {
  constructor(public status: number, msg: string) { super(msg); }
}

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof AppError) {
    return res.status(err.status).json({ error: err.message });
  }
  console.error(err);
  res.status(500).json({ error: "internal" });
});

Error middleware must have 4 args.

Augmenting Request

// types/express.d.ts
declare global {
  namespace Express {
    interface Request {
      user?: { id: number; email: string };
    }
  }
}

export {};
app.use((req, res, next) => {
  req.user = { id: 1, email: "[email protected]" };
  next();
});

app.get("/", (req, res) => {
  res.json({ id: req.user?.id });
});

Zod validation middleware

import { z } from "zod";

function validate<T>(schema: z.ZodSchema<T>) {
  return (req: Request, res: Response, next: NextFunction) => {
    const r = schema.safeParse(req.body);
    if (!r.success) return res.status(400).json(r.error.flatten());
    req.body = r.data;
    next();
  };
}

const NewUser = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

app.post("/users", validate(NewUser), (req, res) => {
  // req.body typed as z.infer<typeof NewUser>
});

Router

import { Router } from "express";

const router = Router();

router.get("/", listUsers);
router.post("/", validate(NewUser), createUser);
router.get("/:id", getUser);

app.use("/users", router);

CORS

npm i cors
npm i -D @types/cors
import cors from "cors";

app.use(cors({
  origin: ["https://app.example.com"],
  credentials: true,
}));

Helmet

npm i helmet
import helmet from "helmet";
app.use(helmet());

Body parsing

app.use(express.json({ limit: "1mb" }));
app.use(express.urlencoded({ extended: true }));
app.use(express.raw({ type: "application/octet-stream" }));

File upload (multer)

npm i multer
npm i -D @types/multer
import multer from "multer";

const upload = multer({ dest: "uploads/", limits: { fileSize: 10 * 1024 * 1024 } });

app.post("/upload", upload.single("file"), (req, res) => {
  req.file;                  // { fieldname, filename, path, ... }
});

Sessions / cookies

npm i cookie-parser express-session
npm i -D @types/cookie-parser @types/express-session
import session from "express-session";

app.use(session({
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true },
}));

Augment session:

declare module "express-session" {
  interface SessionData {
    userId?: number;
  }
}

Graceful shutdown

const server = app.listen(3000);

function shutdown() {
  console.log("shutting down");
  server.close(() => process.exit(0));
}

process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

Environment

import { z } from "zod";

const Env = z.object({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  NODE_ENV: z.enum(["development", "production", "test"]),
});

const env = Env.parse(process.env);

Common mistakes

  • Forgetting to await async middleware; errors silently lost.
  • req.body typed as any — wrap with Zod validation.
  • Catch-all app.use(err) without 4 args — middleware order matters.
  • Not setting express.json size limit — DoS vector.
  • Trusting req.query types — always strings.

Read this next

If you want my Express + Zod + Pino starter, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .