-
Notifications
You must be signed in to change notification settings - Fork 7
Using Zod
Summary: Zod is a schema declaration and validation library. StartER uses it to ensure data integrity in both the backend (API) and frontend (Forms).
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);
}}
>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);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.
-
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.
AI co-creation
Getting started
Explanations
How-To Guides
Reference
Digging deeper