Zod: Validazione TypeScript-First per API, Form e Database

Hai mai ricevuto dati malformati da un’API esterna e visto la tua app crashare in produzione? O passato ore a debuggare un form che accettava valori impossibili? Zod risolve entrambi i problemi con una sintassi elegante e la type safety di TypeScript al runtime.

Cos’è Zod e Perché Usarlo

Zod è una libreria di validazione TypeScript-first che permette di definire schemi di dati e validarli sia a compile time che a runtime. A differenza di altri validatori, Zod inferisce automaticamente i tipi TypeScript dagli schemi — scrivi lo schema una volta sola e ottieni sia la validazione che i tipi.

npm install zod

Schema Base: Oggetti e Tipi Primitivi

Iniziamo con lo schema più comune: validare un oggetto utente come quello che potresti ricevere da un’API REST.

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number().int().positive(),
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().min(18).max(120).optional(),
  role: z.enum(['admin', 'user', 'moderator']),
  createdAt: z.string().datetime(),
});

// TypeScript inferisce automaticamente il tipo
type User = z.infer<typeof UserSchema>;
// Equivalente a:
// type User = {
//   id: number;
//   name: string;
//   email: string;
//   age?: number | undefined;
//   role: 'admin' | 'user' | 'moderator';
//   createdAt: string;
// }

parse() vs safeParse(): Quale Usare?

Zod offre due modalità di validazione principali. La scelta dipende dal contesto.

// parse() — lancia un'eccezione se la validazione fallisce
try {
  const user = UserSchema.parse(rawData);
  // user è tipato correttamente
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error(error.errors); // Array di errori dettagliati
  }
}

// safeParse() — ritorna un oggetto result senza eccezioni
const result = UserSchema.safeParse(rawData);

if (result.success) {
  const user = result.data; // tipato
} else {
  console.error(result.error.errors); // errori di validazione
}

💡 Pro tip: Usa parse() quando sei sicuro che i dati siano validi (es. all’avvio dell’app per validare le env variables). Usa safeParse() per input utente e dati da API esterne dove un errore è un caso normale da gestire.

Validare le Variabili d’Ambiente

Uno dei casi d’uso più potenti di Zod: validare le env variables all’avvio. Se manca una variabile critica, l’app si blocca immediatamente con un messaggio chiaro invece di crashare in produzione con un errore criptico.

// env.ts
import { z } from 'zod';

const EnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(32),
  PORT: z.string().transform(Number).pipe(z.number().int().positive()),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  STRIPE_SECRET: z.string().startsWith('sk_'),
});

export const env = EnvSchema.parse(process.env);
// Se manca DATABASE_URL → errore immediato all'avvio, non in produzione

Integrazione con Next.js App Router e Server Actions

Zod si integra perfettamente con le Next.js 16 Server Actions per validare i dati dei form lato server.

// app/actions/contact.ts
'use server';
import { z } from 'zod';

const ContactSchema = z.object({
  name: z.string().min(2, 'Nome troppo corto').max(100),
  email: z.string().email('Email non valida'),
  message: z.string().min(10, 'Messaggio troppo corto').max(1000),
});

export async function submitContact(formData: FormData) {
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  };

  const result = ContactSchema.safeParse(rawData);
  
  if (!result.success) {
    return { 
      error: result.error.flatten().fieldErrors 
      // { name: ['Nome troppo corto'], email: ['Email non valida'] }
    };
  }

  // result.data è tipato e validato
  await sendEmail(result.data);
  return { success: true };
}

Schema per API REST: Validare le Response

Validare le response di API esterne è uno dei casi più critici. Con Zod integrato con TypeScript 5, hai type safety completa end-to-end.

import { z } from 'zod';

const ProductSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  price: z.number().positive(),
  category: z.string(),
  tags: z.array(z.string()),
  metadata: z.record(z.string(), z.unknown()).optional(),
});

const ProductListSchema = z.array(ProductSchema);

async function fetchProducts(): Promise<z.infer<typeof ProductListSchema>> {
  const res = await fetch('/api/products');
  const data = await res.json();
  return ProductListSchema.parse(data); // Lancia se la risposta è malformata
}

Trasformazioni e Preprocessing

Zod non è solo validazione — puoi trasformare i dati durante il parsing. Utile per normalizzare input, convertire tipi e calcolare valori derivati.

const SlugSchema = z.string()
  .min(1)
  .transform(s => s.toLowerCase().trim().replace(/\s+/g, '-'));

const DateSchema = z.string().datetime()
  .transform(s => new Date(s));

const PriceSchema = z.string()
  .transform(s => parseFloat(s))
  .pipe(z.number().positive());

// Combina validazione e trasformazione
const FormSchema = z.object({
  title: z.string().transform(s => s.trim()),
  slug: SlugSchema,
  publishAt: DateSchema,
  price: PriceSchema,
});

FAQ e Domande Frequenti

Zod vs Yup vs Joi: quale scegliere nel 2026?

Per progetti TypeScript, Zod è la scelta migliore: inferenza automatica dei tipi, bundle size minore di Yup, API più ergonomica di Joi. Yup rimane valido per progetti React esistenti con react-hook-form. Joi è prevalentemente Node.js e non TypeScript-first.

Zod rallenta l’applicazione?

Il costo è trascurabile per la maggior parte delle applicazioni. Zod è ottimizzato per le performance e valida migliaia di oggetti al secondo. Il costo di validazione è ampiamente ripagato dalla sicurezza e dal debug time risparmiato.

Posso usare Zod con tRPC?

Sì, Zod è il validatore di default di tRPC. Tutti i procedure input vengono validati automaticamente con schemi Zod, ottenendo type safety completa client-server senza duplicare la logica di validazione.

Come gestisco schemi complessi con dipendenze tra campi?

Usa z.refine() o z.superRefine() per validazioni che dipendono da più campi. Esempio: password e conferma password devono essere uguali — z.object({...}).refine(data => data.password === data.confirmPassword).

🔧 Strumenti: zod.dev · react-hook-form + zodResolver · tRPC (usa Zod nativamente) · @conform-to/zod per Remix | 🎯 Takeaway: Definisci lo schema una volta, ottieni validazione runtime + tipi TypeScript gratis. Inizia validando le env variables — è il guadagno immediato più alto.

Condividi

Articoli Recenti

Categorie popolari