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 };
function validate(schema, source = 'body') {
return (req, res, next) => {
const result = schema.safeParse(req[source]);
if (result.success) {
req[source] = result.data;
return next();
}
const fields = result.error.issues.reduce((acc, issue) => {
const key = issue.path.join('.') || '_root';
if (!acc[key]) acc[key] = issue.message;
return acc;
}, {});
return res.status(400).json({
error: 'Validation failed',
fields,
});
};
}
module.exports = { validate };
const express = require('express');
const { validate } = require('./validate');
const { signupSchema } = require('./signup.schema');
const { createUser, UserExistsError } = require('./users.service');
const router = express.Router();
async function signup(req, res, next) {
try {
const { email, username, password } = req.body;
const user = await createUser({ email, username, password });
return res.status(201).json({
id: user.id,
email: user.email,
username: user.username,
});
} catch (err) {
if (err instanceof UserExistsError) {
return res.status(409).json({
error: 'Account already exists',
fields: { email: 'This email is already registered' },
});
}
return next(err);
}
}
router.post('/signup', validate(signupSchema), signup);
module.exports = router;
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
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
Share this code
Here's the card — post it anywhere.