Skip to content

Express API and validation

rocambille edited this page Jun 4, 2026 · 2 revisions

Summary: This page details advanced concepts of Express routing in StartER, including using factories to simplify URL parameter conversion and data validation with Zod.

Request manipulation and security

The backend must ensure that the data received via HTTP requests matches expectations before processing it. To reduce boilerplate code, StartER provides two utilities in src/express/helpers: createParamConverter and createValidator.

Param converter (createParamConverter)

"Param converters" transform a URL parameter (like :itemId) into a fully loaded business object (an entity) from the database.

The itemParamConverter.ts file uses the createParamConverter factory:

import { createParamConverter } from "../../helpers/paramConverter";
import itemRepository from "./itemRepository";

export default createParamConverter(itemRepository, "item");

This single line of code:

  1. Declares that we want to convert a parameter using itemRepository.
  2. Specifies that the found entity will be injected into req.item.
  3. Automatically handles errors: if the ID does not exist, it returns a 404 Not Found (or 204 No Content for a DELETE request).

TypeScript typing

To make req.item recognized by TypeScript, we extend the Request interface:

declare global {
  namespace Express {
    interface Request {
      item: Item;
    }
  }
}

Input validation (createValidator)

Before modifying data, StartER uses Zod via the createValidator factory to validate and sanitize req.body.

Example in itemValidator.ts:

import { z } from "zod";
import { createValidator } from "../../helpers/validation";

const itemDTOSchema = z.object({
  title: z.string().max(255),
  user_id: z.number(),
});

export default createValidator(itemDTOSchema, (req) => ({
  ...req.body,
  // Injection of trusted server-side data (authenticated user ID)
  user_id: req.me.id,
}));

Why use this factory?

  • Sanitization: req.body is replaced by the result of schema.parse(). Only fields defined in the schema are kept.
  • Formatting: Zod can transform types (e.g., string to number).
  • Fail fast: if data is invalid, the server immediately returns a 400 Bad Request with detailed Zod errors.
  • Trusted data: the optional second argument allows "merging" client data with server data (like req.me.id).

Usage in routes

Combining these tools keeps routes and actions extremely clean:

// src/express/modules/item/itemRoutes.ts
itemRoutes.param("itemId", itemParamConverter.convert);

itemRoutes.post("/api/items", itemValidator.validate, itemActions.add);
itemRoutes.put("/api/items/:itemId", itemValidator.validate, itemActions.edit);

Thanks to this structure, the edit action can assume that req.item exists and that req.body is perfectly valid and typed:

// src/express/modules/item/itemActions.ts
const edit: RequestHandler = (req, res) => {
  const item = { ...req.body, id: req.item.id };

  itemRepository.update(item);

  res.sendStatus(204);
};

Best practices

  • Systematically validate: never trust data sent by the client.
  • Server-side injection: prefer injecting the user ID (req.me.id) in the validator rather than receiving it from the client.
  • Idempotency: the converter returns 204 on a DELETE if the resource no longer exists, ensuring that repeated deletions do not cause unnecessary 404 errors.

See also

Clone this wiki locally