In Quantum Metropolis, every system requires precise data validation — from access terminals to implant control panels. Zod is a validation library designed with TypeScript in mind that has become the standard in the Next.js ecosystem. Unlike other solutions, Zod automatically generates TypeScript types from validation schemas, eliminating code duplication.
In the year 2150, Quantum Metropolis systems require reliable data validation. Zod stands out from other validation libraries (Yup, Joi) for several reasons:
1npm install zod1import { z } from 'zod';Zod offers a set of methods for defining data types — like digital DNA describing the structure of information in the system:
1import { z } from 'zod';
2
3// Primitive types
4const nameSchema = z.string();
5const ageSchema = z.number();
6const isActiveSchema = z.boolean();
7
8// Object schema — we combine types into a structure
9const userSchema = z.object({
10 name: z.string(),
11 email: z.string().email(),
12 age: z.number().int().positive(),
13 isActive: z.boolean(),
14});Each
z.string(), z.number(), z.boolean() is a basic building block of a schema. The z.object() method combines them into a comprehensive structure — like a terminal schema in Quantum Metropolis.Zod offers two ways to validate data:
1import { z } from 'zod';
2
3const schema = z.object({
4 name: z.string().min(2),
5 email: z.string().email(),
6});
7
8// 1. parse() — throws an exception for invalid data
9try {
10 const data = schema.parse({ name: "Neo", email: "neo@quantum.io" });
11 console.log("Valid data:", data);
12} catch (error) {
13 console.error("Validation error:", error);
14}
15
16// 2. safeParse() — does NOT throw, returns a result object
17const result = schema.safeParse({ name: "", email: "bad-email" });
18
19if (result.success) {
20 console.log("Data:", result.data);
21} else {
22 console.log("Errors:", result.error.errors);
23 // [{ path: ["name"], message: "String must contain at least 2 character(s)" }, ...]
24}Rule: Use
safeParse() in forms and Server Actions (safer), and parse() when you want to fail fast on invalid data.Zod allows you to precisely define validation rules with custom messages — like configuring a security system on a terminal:
1import { z } from 'zod';
2
3const registrationSchema = z.object({
4 username: z.string()
5 .min(3, { message: "Username must be at least 3 characters" })
6 .max(20, { message: "Username can be at most 20 characters" }),
7 email: z.string()
8 .email({ message: "Invalid email address format" }),
9 age: z.number()
10 .int({ message: "Age must be a whole number" })
11 .min(13, { message: "You must be at least 13 years old" }),
12 password: z.string()
13 .min(8, { message: "Password must be at least 8 characters" })
14 .regex(/[A-Z]/, { message: "Password must contain an uppercase letter" })
15 .regex(/[0-9]/, { message: "Password must contain a digit" }),
16 acceptTerms: z.boolean()
17 .refine(val => val === true, {
18 message: "You must accept the system terms of use"
19 }),
20});The
.refine() method allows you to define custom, non-standard validation rules — useful when built-in methods aren't enough.One of Zod's most powerful features is the automatic generation of TypeScript types from schemas. You don't need to maintain separate interfaces:
1import { z } from 'zod';
2
3const implantSchema = z.object({
4 id: z.string().uuid(),
5 name: z.string().min(1),
6 powerLevel: z.number().min(0).max(100),
7 isActive: z.boolean(),
8 tags: z.array(z.string()),
9});
10
11// Type automatically generated from schema
12type Implant = z.infer<typeof implantSchema>;
13// Manual equivalent: { id: string; name: string; powerLevel: number; isActive: boolean; tags: string[] }
14
15// Now type and validation are SYNCHRONIZED
16function processImplant(data: Implant) {
17 console.log(data.name); // TypeScript knows the type!
18}Thanks to
z.infer, you have a single source of truth — the schema defines both validation and the TypeScript type.Zod works great with React Hook Form thanks to the
zodResolver adapter. This is the most commonly encountered pattern in Next.js applications:1import { useForm } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4
5// 1. Define the schema
6const loginSchema = z.object({
7 email: z.string()
8 .email({ message: "Invalid email address" }),
9 password: z.string()
10 .min(8, { message: "Password must be at least 8 characters" }),
11});
12
13// 2. Generate the type
14type LoginForm = z.infer<typeof loginSchema>;
15
16// 3. Use in the component
17export default function LoginPage() {
18 const {
19 register,
20 handleSubmit,
21 formState: { errors, isSubmitting }
22 } = useForm<LoginForm>({
23 resolver: zodResolver(loginSchema), // Zod as the validator
24 defaultValues: { email: '', password: '' }
25 });
26
27 const onSubmit = async (data: LoginForm) => {
28 // data is already validated and typed!
29 console.log("Logging in:", data);
30 };
31
32 return (
33 <form onSubmit={handleSubmit(onSubmit)}>
34 <input {...register('email')} placeholder="Email" />
35 {errors.email && <p>{errors.email.message}</p>}
36
37 <input {...register('password')} type="password" placeholder="Password" />
38 {errors.password && <p>{errors.password.message}</p>}
39
40 <button type="submit" disabled={isSubmitting}>
41 {isSubmitting ? 'Logging in...' : 'Log in'}
42 </button>
43 </form>
44 );
45}The key line is
resolver: zodResolver(loginSchema) — it connects the Zod schema with the React Hook Form validation system.In large applications, it's worth creating reusable schemas that can be combined and extended:
1import { z } from 'zod';
2
3// Base address schema — used in multiple places
4const addressSchema = z.object({
5 street: z.string().min(1, "Street is required"),
6 city: z.string().min(1, "City is required"),
7 postCode: z.string().regex(/^d{2}-d{3}$/, "Format: XX-XXX"),
8});
9
10// User schema — extending with address
11const userSchema = z.object({
12 name: z.string().min(2),
13 email: z.string().email(),
14 address: addressSchema, // Nesting
15 shippingAddress: addressSchema.optional(), // Optional
16});
17
18// .merge() — merging two schemas
19const baseProfile = z.object({ name: z.string(), bio: z.string() });
20const socialProfile = z.object({ twitter: z.string(), github: z.string() });
21const fullProfile = baseProfile.merge(socialProfile);
22
23// .extend() — adding fields to an existing schema
24const adminUser = userSchema.extend({
25 role: z.literal('admin'),
26 permissions: z.array(z.string()),
27});
28
29type AdminUser = z.infer<typeof adminUser>;The
.refine() method allows validation that can't be expressed declaratively — e.g. comparing two fields:1import { z } from 'zod';
2
3const passwordSchema = z.object({
4 password: z.string().min(8),
5 confirmPassword: z.string(),
6}).refine(data => data.password === data.confirmPassword, {
7 message: "Passwords do not match",
8 path: ["confirmPassword"], // Error assigned to confirmPassword field
9});
10
11// superRefine — when you need multiple errors simultaneously
12const advancedSchema = z.object({
13 accountType: z.enum(['personal', 'business']),
14 personalId: z.string().optional(),
15 companyId: z.string().optional(),
16}).superRefine((data, ctx) => {
17 if (data.accountType === 'personal' && !data.personalId) {
18 ctx.addIssue({
19 code: z.ZodIssueCode.custom,
20 message: "Personal ID is required for a personal account",
21 path: ['personalId'],
22 });
23 }
24 if (data.accountType === 'business' && !data.companyId) {
25 ctx.addIssue({
26 code: z.ZodIssueCode.custom,
27 message: "Company ID is required for a business account",
28 path: ['companyId'],
29 });
30 }
31});Zod is the standard in the Next.js ecosystem, but alternatives exist:
Zod is recommended for Next.js projects because it combines validation with TypeScript types and works on both server and client.
Zod is an essential tool in the Next.js developer's arsenal:
z.string(), z.number(), z.object() — defining schemasparse() and safeParse() — data validation.min(), .email(), .regex() — rules with error messages.refine() — custom validationzodResolver — integration with React Hook Formz.infer<typeof schema> — automatic type generation