Validazione API e tRPC
Validazione Request/Response API
Zod e’ perfetto per validare i dati in entrata e uscita delle API, garantendo type safety end-to-end.
Pattern Base
import { z } from "zod";
// Schema per la richiesta
const CreaUtenteRequest = z.object({
nome: z.string().min(1),
email: z.string().email(),
ruolo: z.enum(["admin", "utente"]).default("utente"),
});
// Schema per la risposta
const UtenteResponse = z.object({
id: z.string().uuid(),
nome: z.string(),
email: z.string().email(),
ruolo: z.enum(["admin", "utente"]),
createdAt: z.string().datetime(),
});
// Tipi inferiti
type CreaUtenteRequest = z.infer<typeof CreaUtenteRequest>;
type UtenteResponse = z.infer<typeof UtenteResponse>;
Middleware di Validazione per Express
Puoi creare un middleware generico che valida il body, i query params o i parametri:
import { Request, Response, NextFunction } from "express";
import { z, AnyZodObject, ZodError } from "zod";
function valida(schema: AnyZodObject) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (errore) {
if (errore instanceof ZodError) {
return res.status(400).json({
stato: "errore",
messaggi: errore.errors.map((e) => ({
campo: e.path.join("."),
messaggio: e.message,
})),
});
}
next(errore);
}
};
}
// Uso
const creaUtenteSchema = z.object({
body: z.object({
nome: z.string().min(1, "Nome obbligatorio"),
email: z.string().email("Email non valida"),
}),
query: z.object({}),
params: z.object({}),
});
app.post("/api/utenti", valida(creaUtenteSchema), (req, res) => {
// req.body e' gia' validato
const { nome, email } = req.body;
// ...
});
Validazione in Next.js API Routes
App Router (Route Handlers)
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";
const ProdottoSchema = z.object({
nome: z.string().min(1),
prezzo: z.number().positive(),
categoria: z.string(),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const dati = ProdottoSchema.parse(body);
// dati e' tipizzato e validato
const prodotto = await db.prodotto.create({ data: dati });
return NextResponse.json(prodotto, { status: 201 });
} catch (errore) {
if (errore instanceof z.ZodError) {
return NextResponse.json(
{ errori: errore.flatten().fieldErrors },
{ status: 400 }
);
}
return NextResponse.json(
{ errore: "Errore interno del server" },
{ status: 500 }
);
}
}
Validazione Query Params
const QuerySchema = z.object({
pagina: z.coerce.number().int().positive().default(1),
limite: z.coerce.number().int().min(1).max(100).default(20),
ordine: z.enum(["asc", "desc"]).default("desc"),
cerca: z.string().optional(),
});
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const query = QuerySchema.parse(Object.fromEntries(searchParams));
const prodotti = await db.prodotto.findMany({
skip: (query.pagina - 1) * query.limite,
take: query.limite,
orderBy: { createdAt: query.ordine },
where: query.cerca
? { nome: { contains: query.cerca } }
: undefined,
});
return NextResponse.json(prodotti);
}
Zod con tRPC
tRPC usa Zod nativamente per la validazione degli input. E’ il caso d’uso ideale per Zod.
Setup del Router
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
const appRouter = t.router({
// Query semplice
saluta: t.procedure
.input(z.object({ nome: z.string() }))
.query(({ input }) => {
return { messaggio: `Ciao, ${input.nome}!` };
}),
// Mutation con validazione
creaUtente: t.procedure
.input(z.object({
nome: z.string().min(2),
email: z.string().email(),
eta: z.number().int().min(13),
}))
.mutation(async ({ input }) => {
const utente = await db.utente.create({ data: input });
return utente;
}),
// Query con parametri opzionali
listaProdotti: t.procedure
.input(z.object({
categoria: z.string().optional(),
pagina: z.number().int().positive().default(1),
limite: z.number().int().min(1).max(100).default(20),
}))
.query(async ({ input }) => {
return await db.prodotto.findMany({
where: input.categoria
? { categoria: input.categoria }
: undefined,
skip: (input.pagina - 1) * input.limite,
take: input.limite,
});
}),
});
export type AppRouter = typeof appRouter;
Middleware con tRPC
const conAutenticazione = t.middleware(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: { utente: ctx.session.user },
});
});
const proceduraProtetta = t.procedure.use(conAutenticazione);
const appRouter = t.router({
profiloUtente: proceduraProtetta
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input, ctx }) => {
// ctx.utente e' disponibile e tipizzato
return await db.utente.findUnique({ where: { id: input.id } });
}),
aggiornaProfilo: proceduraProtetta
.input(z.object({
nome: z.string().min(1).optional(),
bio: z.string().max(500).optional(),
sito: z.string().url().optional(),
}))
.mutation(async ({ input, ctx }) => {
return await db.utente.update({
where: { id: ctx.utente.id },
data: input,
});
}),
});
Validazione Environment Variables
Un pattern fondamentale per qualsiasi progetto. Valida le variabili d’ambiente all’avvio:
const envSchema = z.object({
// Server
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.coerce.number().int().default(3000),
// Database
DATABASE_URL: z.string().url(),
DB_POOL_SIZE: z.coerce.number().int().positive().default(10),
// Autenticazione
JWT_SECRET: z.string().min(32, "JWT_SECRET deve avere almeno 32 caratteri"),
JWT_EXPIRES_IN: z.string().default("7d"),
// Servizi esterni
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().int().optional(),
SMTP_USER: z.string().optional(),
SMTP_PASS: z.string().optional(),
// Feature flags
ENABLE_CACHE: z.preprocess(
(val) => val === "true" || val === "1",
z.boolean().default(false)
),
});
// Valida e esporta
function validaEnv() {
const risultato = envSchema.safeParse(process.env);
if (!risultato.success) {
console.error("Variabili d'ambiente non valide:");
console.error(risultato.error.flatten().fieldErrors);
process.exit(1);
}
return risultato.data;
}
export const env = validaEnv();
// Ora puoi usare env.DATABASE_URL, env.PORT, ecc.
// Tutto e' tipizzato e validato!
Questo pattern garantisce che l’applicazione non si avvii se mancano variabili d’ambiente critiche, evitando errori difficili da diagnosticare in produzione.