Skip to content

Using Zod

rocambille edited this page Jun 4, 2026 · 4 revisions

Summary: Zod is a schema declaration and validation library. StartER uses it to ensure data integrity in both the backend (API) and frontend (Forms).

React validation

In StartER, forms extract data using the native FormData object. Before sending this data or calling the action, use Zod to make sure the data is valid.

In your components (e.g., ItemForm.tsx), define your schema:

import { z } from "zod";

const itemSchema = z.object({
  title: z.string().min(1, "The title is required"),
});

Then, use it during submission in action={}:

<form
  action={(formData) => {
    const title = formData.get("title")?.toString() ?? "";

    // 1. Validation with Zod
    const parsed = itemSchema.safeParse({ title });

    if (!parsed.success) {
      // Display errors to the user
      alert(z.prettifyError(parsed.error));
      return;
    }

    // 2. Data is validated and typed (parsed.data)
    action(parsed.data);
  }}
>

Express validation

On the backend, Express middlewares use Zod to intercept invalid HTTP requests before they reach your actions.

In your module (e.g., itemValidator.ts), add a validation schema and create a validation middleware. You can do this "by hand" (see below) or use the createValidator helper from src/express/helpers/validation.ts (see the "tip" right after).

import { z } from "zod";
import type { RequestHandler } from "express";

// Business schema definition

const itemSchema = z.object({
  title: z.string().max(255, "The title must not exceed 255 characters"),
});

// Validation middleware

const validate: RequestHandler = (req, res, next) => {
  const parsed = itemSchema.safeParse(req.body);

  if (!parsed.success) {
    const { issues } = parsed.error;

    res.status(400).json(issues);

    return;
  }

  // Inject validated data replacing req.body for the next action
  req.body = parsed.data;

  next();
};

export default { validate };

Tip

You can also use the createValidator helper from src/express/helpers/validation.ts. The code:

// Validation middleware
const validate: RequestHandler = (req, res, next) => {
  const parsed = itemSchema.safeParse(req.body);

  if (!parsed.success) {
    const { issues } = parsed.error;

    res.status(400).json(issues);

    return;
  }

  // Inject validated data replacing req.body for the next action
  req.body = parsed.data;

  next();
};

export default { validate };

Becomes:

// Validation middleware using the helper
import { createValidator } from "../../helpers/validation";

export default createValidator(itemSchema);

This is the recommended approach in StartER.

Then insert this middleware into your API route declarations (in itemRoutes.ts):

import validateItem from "./itemValidator";
import itemActions from "./itemActions";

// The validation middleware runs BEFORE the `add` action
router.post("/api/items", validateItem.validate, itemActions.add);

Repository Output Parsing

While Validators (Input Schema) enforce business rules on incoming data, Repositories use Zod for Output Parsing (Output Schema).

By binding a Zod schema to a TypeScript type (z.ZodType<Item>), we safely cast raw primitives coming from SQLite without using as Type assertions.

Important

Keep your Input Schemas (Validators) and Output Schemas (Repositories) strictly separated. The Repository schema must NOT enforce constraints like .min(1), it only ensures the object shape matches the TypeScript contract.

For more details, see The Repository pattern.

Best practices and use cases

  • Shared code: you can theoretically share your Zod schemas between frontend and backend by placing them in a shared directory (e.g., src/types), although StartER advises by default to keep frontend validations dedicated to user experience and backend validations dedicated to database strictness.
  • Displaying errors: on the client side, z.treeifyError(parsed.error) easily structures error messages so they can be displayed below the faulty inputs.

See also

Clone this wiki locally