javascript 81 lines · 3 tabs

Reusable Zod Schema Validation Middleware for Express Signup Routes

Shared by codesnips Jul 2026
3 tabs
const { z } = require('zod');

const signupSchema = z
  .object({
    email: z
      .string()
      .email('Must be a valid email address')
      .transform((v) => v.trim().toLowerCase()),
    username: z
      .string()
      .min(3, 'Username must be at least 3 characters')
      .max(32, 'Username must be at most 32 characters'),
    password: z
      .string()
      .min(8, 'Password must be at least 8 characters')
      .regex(/[0-9]/, 'Password must contain a number'),
    confirmPassword: z.string(),
    acceptedTerms: z.literal(true, {
      errorMap: () => ({ message: 'You must accept the terms' }),
    }),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: 'Passwords do not match',
    path: ['confirmPassword'],
  });

module.exports = { signupSchema };
3 files · javascript Explain with highlit

This snippet shows how request validation is factored out of an Express route into a small, reusable middleware that runs a schema against a chosen part of the request and collects all field errors before the handler ever runs. The pattern keeps controllers focused on business logic and gives clients a single, predictable error shape instead of failing on the first bad field.

In signup.schema.js, the signup contract is declared with Zod. signupSchema describes each field independently — email uses .email(), password enforces a minimum length, and confirmPassword is cross-checked against password via a .refine() at the object level so mismatches surface as an error attached to the confirmPassword path. Because Zod validates the whole object by default rather than short-circuiting, every failing field is reported in one pass, which is exactly what a signup form needs to highlight all problems at once. The .transform() on email also normalizes input (trim + lowercase), so validation doubles as light sanitization.

In validate.js, validate(schema, source) returns an Express middleware. It runs schema.safeParse() against req[source] — usually body — so the same helper can validate query or params too. On success it writes the parsed, coerced value back with req[source] = result.data, meaning downstream handlers receive clean, typed data rather than the raw payload. On failure it walks result.error.issues and reduces them into a flat fields map keyed by the dotted field path, collapsing Zod's structured errors into a client-friendly object. Using safeParse instead of parse avoids throwing, keeping control flow explicit and side-effect free.

In auth.routes.js, the middleware is mounted directly in the route chain: router.post('/signup', validate(signupSchema), signup). By the time signup executes, req.body is guaranteed valid and normalized, so the handler can create the user without defensive checks. The centralized 400 response carries { error, fields }, giving the frontend a stable structure to map back onto form inputs.

The main trade-off is that all rules live in the schema, so complex conditional validation can grow awkward; for those cases Zod's superRefine or a dedicated service layer is preferable. For typical CRUD boundaries, though, this keeps validation declarative, testable, and DRY across many routes.


Related snips

Share this code

Here's the card — post it anywhere.

Reusable Zod Schema Validation Middleware for Express Signup Routes — share card
Link copied