Next.js 15 Fullstack (App Router + Server Actions) · Lezione

Validazione delle richieste e risposte JSON tipizzate con Zod

Analizzi e convalidi i body delle richieste e i parametri delle query, restituendo risposte di errore tipizzate e ben strutturate.

Lezione 4 di 413 passaggi

Validazione delle richieste e risposte JSON tipizzate con Zod è una lezione Next.js 15 Fullstack (App Router + Server Actions) gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Next.js 15 Fullstack (App Router + Server Actions), e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Next.js 15 Fullstack (App Router + Server Actions) include 4 lezioni in totale.

Parti di questa lezione non sono ancora state tradotte e vengono mostrate in inglese.

Why Validate at the Edge?

In Next.js 15 Route Handlers (app/api/.../route.ts), the request body is just untyped JSON. TypeScript types vanish at runtime, so a client can send anything.

Zod lets you describe the expected shape once and get both:

  • A runtime check that rejects bad input.
  • A static type inferred from the schema, so your handler code is fully typed.

This lesson builds a typed POST handler that validates the body and query params, then returns clean, structured JSON errors.

Defining a Schema

Start by declaring the shape you expect. Zod schemas double as the single source of truth for both validation and types.

Use z.infer to derive a TypeScript type from the schema. There is no duplication: change the schema and the type updates automatically.

import { z } from 'zod';

export const CreateUserSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email address'),
  age: z.number().int().positive().optional(),
});

// Inferred type — fully typed, no duplication
export type CreateUserInput = z.infer<typeof CreateUserSchema>;

Parsing the Request Body

Inside a Route Handler, read JSON with await request.json(). Wrap it in a try/catch because malformed JSON throws before Zod ever runs.

Use schema.safeParse() instead of parse(). safeParse never throws — it returns a result object you can branch on.

import { NextRequest, NextResponse } from 'next/server';
import { CreateUserSchema } from './schema';

export async function POST(request: NextRequest) {
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return NextResponse.json(
      { error: 'Invalid JSON body' },
      { status: 400 },
    );
  }

  const result = CreateUserSchema.safeParse(body);
  // ...handle result
}

safeParse: Success vs Failure

safeParse returns a discriminated union:

  • On success: { success: true, data } where data is the typed, validated value.
  • On failure: { success: false, error } where error is a ZodError.

TypeScript narrows the type after you check result.success, so result.data is only accessible on the success branch.

const result = CreateUserSchema.safeParse(body);

if (!result.success) {
  return NextResponse.json(
    { error: 'Validation failed', issues: result.error.issues },
    { status: 422 },
  );
}

// result.data is now typed as CreateUserInput
const user = result.data;
console.log(user.email);

Shaping a Useful Error Response

Dumping the raw ZodError works but is noisy. A cleaner API returns a flat map of field to messages. Zod's error.flatten() gives you fieldErrors and formErrors ready for the client.

Pick a consistent status: 400 for unparseable input, 422 (Unprocessable Entity) for well-formed JSON that fails validation.

if (!result.success) {
  const { fieldErrors, formErrors } = result.error.flatten();
  return NextResponse.json(
    {
      error: 'Validation failed',
      fieldErrors, // { email: ['Invalid email address'] }
      formErrors,  // top-level errors
    },
    { status: 422 },
  );
}

Validating Query Parameters

Query params arrive as strings via request.nextUrl.searchParams. Use z.coerce to convert and validate in one step — for example turning ?page=2 into a real number.

Provide .default() values so missing params do not break the handler.

import { z } from 'zod';
import { NextRequest, NextResponse } from 'next/server';

const QuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

export async function GET(request: NextRequest) {
  const params = Object.fromEntries(request.nextUrl.searchParams);
  const result = QuerySchema.safeParse(params);

  if (!result.success) {
    return NextResponse.json(
      { error: 'Invalid query', fieldErrors: result.error.flatten().fieldErrors },
      { status: 400 },
    );
  }

  const { page, limit } = result.data; // numbers, defaulted
  return NextResponse.json({ page, limit });
}

Typed JSON Responses

NextResponse.json() is generic. Pass a type argument to lock down the success payload shape so your handler and your client stay in sync.

Define a shared response type and reuse it on both the server and the frontend fetch call.

import { NextResponse } from 'next/server';

type UserResponse = {
  id: string;
  name: string;
  email: string;
};

function ok(user: UserResponse) {
  // The generic enforces the body matches UserResponse
  return NextResponse.json<UserResponse>(user, { status: 201 });
}

A Reusable Validation Helper

Repeating the parse/branch logic in every handler gets tedious. Extract a small helper that validates a body against any schema and returns a typed result.

This keeps each Route Handler focused on business logic while centralizing the error-response format.

import { z } from 'zod';
import { NextResponse } from 'next/server';

export async function parseBody<T extends z.ZodTypeAny>(
  request: Request,
  schema: T,
): Promise<
  | { ok: true; data: z.infer<T> }
  | { ok: false; response: NextResponse }
> {
  let raw: unknown;
  try {
    raw = await request.json();
  } catch {
    return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) };
  }
  const result = schema.safeParse(raw);
  if (!result.success) {
    return {
      ok: false,
      response: NextResponse.json(
        { error: 'Validation failed', fieldErrors: result.error.flatten().fieldErrors },
        { status: 422 },
      ),
    };
  }
  return { ok: true, data: result.data };
}

Using the Helper in a Handler

With parseBody in place, a handler becomes short and readable. Validate, early-return on failure, then work with fully typed data.

This pattern scales: every endpoint follows the same shape, so error responses are consistent across your whole API.

import { NextRequest } from 'next/server';
import { CreateUserSchema } from './schema';
import { parseBody } from '@/lib/parse-body';
import { NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const parsed = await parseBody(request, CreateUserSchema);
  if (!parsed.ok) return parsed.response;

  const { name, email } = parsed.data; // typed
  const user = { id: crypto.randomUUID(), name, email };
  return NextResponse.json(user, { status: 201 });
}

Transforming and Refining

Zod can do more than reject — it can normalize. Use .transform() to reshape values (trim, lowercase) and .refine() for cross-field rules that a single field check cannot express.

The output type reflects transforms, so downstream code sees the cleaned data.

import { z } from 'zod';

const SignupSchema = z
  .object({
    email: z.string().email().transform((s) => s.toLowerCase().trim()),
    password: z.string().min(8),
    confirm: z.string(),
  })
  .refine((d) => d.password === d.confirm, {
    message: 'Passwords do not match',
    path: ['confirm'],
  });

type Signup = z.infer<typeof SignupSchema>;

Validation Logic You Can Actually Run

Strip away Next.js and the core idea is pure data validation. Here is a self-contained TypeScript program that mirrors the same parse-and-branch flow using Zod, with no server involved.

It shows both a valid and an invalid payload and prints the structured error map.

import { z } from 'zod';

const Schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.coerce.number().int().positive().optional(),
});

function validate(input: unknown) {
  const result = Schema.safeParse(input);
  if (!result.success) {
    return { status: 422, body: { fieldErrors: result.error.flatten().fieldErrors } };
  }
  return { status: 201, body: result.data };
}

console.log(JSON.stringify(validate({ name: 'Ada', email: 'ada@example.com', age: '30' })));
console.log(JSON.stringify(validate({ name: '', email: 'nope' })));

Quick Check

You are writing a POST Route Handler that validates the JSON body with a Zod schema. Which approach best returns a typed, structured error response without crashing on bad input?

Recap

You built a typed, validated Next.js 15 API endpoint:

  • Schema first: define a Zod schema and derive the type with z.infer.
  • safeParse, not parse: branch on result.success so nothing throws.
  • Structured errors: use error.flatten(); return 400 for bad JSON, 422 for validation failures.
  • Query params: coerce strings with z.coerce and supply .default() values.
  • Typed responses: NextResponse.json<T>() keeps server and client in sync.
  • Reuse: a parseBody helper centralizes the parse/error pattern across every handler.

The result is an edge API that is safe at runtime and fully typed at compile time.

Gratis per iniziare

Impara TypeScript con un tutor IA — gratis

Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.

Corsi
22
Lezioni
88

Domande Frequenti

La lezione «Validazione delle richieste e risposte JSON tipizzate con Zod» è gratuita?

Sì — il testo completo di «Validazione delle richieste e risposte JSON tipizzate con Zod» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Next.js 15 Fullstack (App Router + Server Actions), passa a CoddyKit PRO. Il corso Next.js 15 Fullstack (App Router + Server Actions) include 4 lezioni in totale.

Cosa imparerò in «Validazione delle richieste e risposte JSON tipizzate con Zod»?

Analizzi e convalidi i body delle richieste e i parametri delle query, restituendo risposte di errore tipizzate e ben strutturate. Eserciti Next.js 15 Fullstack (App Router + Server Actions) con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare Next.js 15 Fullstack (App Router + Server Actions)?

Non è richiesta alcuna esperienza precedente. Next.js 15 Fullstack (App Router + Server Actions) su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.

Quanto tempo richiede la lezione «Validazione delle richieste e risposte JSON tipizzate con Zod»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione Next.js 15 Fullstack (App Router + Server Actions)?

Sì. Ogni lezione Next.js 15 Fullstack (App Router + Server Actions) include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Progettazione di route handler RESTful con la Web Request API
  2. Compromessi tra runtime Node ed Edge
  3. Risposte in streaming e ReadableStream negli handler
  4. Validazione delle richieste e risposte JSON tipizzate con Zod
← Torna a Next.js 15 Fullstack (App Router + Server Actions)