Next.js 15 Fullstack (App Router + Server Actions) · Leçon

Validation des requêtes et réponses JSON typées avec Zod

Analysez et validez les corps de requête et les paramètres de requête, puis renvoyez des réponses d’erreur typées et bien structurées.

Leçon 4 sur 413 étapes

Validation des requêtes et réponses JSON typées avec Zod est une leçon Next.js 15 Fullstack (App Router + Server Actions) gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Next.js 15 Fullstack (App Router + Server Actions), et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.

Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.

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.

Gratuit pour commencer

Apprends TypeScript avec un tuteur IA — gratuit

Écris et exécute du vrai code dans ton navigateur, obtiens de l'aide instantanée d'un tuteur IA disponible 24h/24, et reprends là où tu t'es arrêté sur le web ou dans l'app.

Cours
22
Leçons
88

Questions Fréquemment Posées

La leçon « Validation des requêtes et réponses JSON typées avec Zod » est-elle gratuite ?

Oui — le texte complet de « Validation des requêtes et réponses JSON typées avec Zod » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Next.js 15 Fullstack (App Router + Server Actions), passe à CoddyKit PRO. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Validation des requêtes et réponses JSON typées avec Zod » ?

Analysez et validez les corps de requête et les paramètres de requête, puis renvoyez des réponses d’erreur typées et bien structurées. Tu pratiques Next.js 15 Fullstack (App Router + Server Actions) avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Next.js 15 Fullstack (App Router + Server Actions) ?

Aucune expérience préalable n'est requise. Next.js 15 Fullstack (App Router + Server Actions) sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Validation des requêtes et réponses JSON typées avec Zod » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Next.js 15 Fullstack (App Router + Server Actions) ?

Oui. Chaque leçon Next.js 15 Fullstack (App Router + Server Actions) inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Concevoir des gestionnaires de routes RESTful avec l’API de requêtes Web
  2. Compromis entre l’environnement d’exécution Node et Edge
  3. Réponses en flux et ReadableStream dans les gestionnaires
  4. Validation des requêtes et réponses JSON typées avec Zod
← Retour à Next.js 15 Fullstack (App Router + Server Actions)