00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.