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

Validação de requisições e respostas JSON tipadas com Zod

Analise e valide corpos de requisição e parâmetros de consulta, retornando respostas de erro tipadas e bem estruturadas.

Aula 4 de 413 etapas

Validação de requisições e respostas JSON tipadas com Zod é uma aula grátis de Next.js 15 Fullstack (App Router + Server Actions) no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Next.js 15 Fullstack (App Router + Server Actions), e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Next.js 15 Fullstack (App Router + Server Actions) inclui 4 aulas no total.

Partes desta aula ainda não foram traduzidas e aparecem em inglês.

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.

Grátis para começar

Aprenda TypeScript com um tutor de IA — grátis

Escreva e execute código real no seu navegador, obtenha ajuda instantânea de um tutor de IA 24/7 e continue de onde parou na web ou no app.

Cursos
22
Aulas
88

Perguntas Frequentes

A aula “Validação de requisições e respostas JSON tipadas com Zod” é grátis?

Sim — o texto completo de “Validação de requisições e respostas JSON tipadas com Zod” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Next.js 15 Fullstack (App Router + Server Actions), atualize para CoddyKit PRO. O curso de Next.js 15 Fullstack (App Router + Server Actions) inclui 4 aulas no total.

O que vou aprender em “Validação de requisições e respostas JSON tipadas com Zod”?

Analise e valide corpos de requisição e parâmetros de consulta, retornando respostas de erro tipadas e bem estruturadas. Você pratica Next.js 15 Fullstack (App Router + Server Actions) com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar Next.js 15 Fullstack (App Router + Server Actions)?

Nenhuma experiência prévia é necessária. Next.js 15 Fullstack (App Router + Server Actions) no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.

Quanto tempo leva a aula “Validação de requisições e respostas JSON tipadas com Zod”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de Next.js 15 Fullstack (App Router + Server Actions)?

Sim. Cada aula de Next.js 15 Fullstack (App Router + Server Actions) inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Projeto de manipuladores de rotas RESTful com a API de requisições Web
  2. Diferenças entre o ambiente de execução Node e Edge
  3. Respostas em fluxo e ReadableStream em manipuladores
  4. Validação de requisições e respostas JSON tipadas com Zod
← Voltar para Next.js 15 Fullstack (App Router + Server Actions)