Validación de solicitudes y respuestas JSON tipadas con Zod
Analice y valide los cuerpos de las solicitudes y los parámetros de consulta, devolviendo respuestas de error tipadas y bien estructuradas.
Validación de solicitudes y respuestas JSON tipadas con Zod es una lección gratuita de Next.js 15 Fullstack (App Router + Server Actions) en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Next.js 15 Fullstack (App Router + Server Actions), y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Next.js 15 Fullstack (App Router + Server Actions) incluye 4 lecciones en total.
Partes de esta lección aún no han sido traducidas y se muestran en 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 }wheredatais the typed, validated value. - On failure:
{ success: false, error }whereerroris aZodError.
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.successso nothing throws. - Structured errors: use
error.flatten(); return 400 for bad JSON, 422 for validation failures. - Query params: coerce strings with
z.coerceand supply.default()values. - Typed responses:
NextResponse.json<T>()keeps server and client in sync. - Reuse: a
parseBodyhelper 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.
Aprende TypeScript con un tutor de IA — gratis
Escribe y ejecuta código real en tu navegador, obtén ayuda instantánea de un tutor de IA disponible 24/7 y continúa donde lo dejaste en la web o en la aplicación.
- Cursos
- 22
- Lecciones
- 88
Preguntas frecuentes
¿La lección «Validación de solicitudes y respuestas JSON tipadas con Zod» es gratis?
Sí — el texto completo de «Validación de solicitudes y respuestas JSON tipadas con Zod» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Next.js 15 Fullstack (App Router + Server Actions), actualiza a CoddyKit PRO. El curso de Next.js 15 Fullstack (App Router + Server Actions) incluye 4 lecciones en total.
¿Qué aprenderé en «Validación de solicitudes y respuestas JSON tipadas con Zod»?
Analice y valide los cuerpos de las solicitudes y los parámetros de consulta, devolviendo respuestas de error tipadas y bien estructuradas. Practicas Next.js 15 Fullstack (App Router + Server Actions) con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar Next.js 15 Fullstack (App Router + Server Actions)?
No se requiere experiencia previa. Next.js 15 Fullstack (App Router + Server Actions) en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.
¿Cuánto tiempo toma la lección «Validación de solicitudes y respuestas JSON tipadas con Zod»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de Next.js 15 Fullstack (App Router + Server Actions)?
Sí. Cada lección de Next.js 15 Fullstack (App Router + Server Actions) incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Diseño de handlers de rutas RESTful con la API de solicitudes web
- Diferencias entre los entornos de ejecución Node y Edge
- Respuestas en streaming y ReadableStream en handlers
- Validación de solicitudes y respuestas JSON tipadas con Zod