Sustituir as-Casts por el análisis Zod en cada límite de datos de Next.js
Por qué un cast de TypeScript no puede protegerte del cambio en la API, y cómo un esquema Zod valida los resultados de fetch, los formularios, los controladores de ruta y las acciones del servidor en Next.js.
Los componentes escritos a mano parecen seguros hasta que la producción envía un campo con otro nombre, un null en lugar de una cadena de texto, o un sobre de error en vez de un usuario. TypeScript no puede detectar eso: sus tipos desaparecen en tiempo de compilación, mientras que los datos de red solo existen en tiempo de ejecución; por lo tanto, as User es una afirmación y no una verificación. Esta guía muestra cómo un único esquema Zod puede tanto validar los datos recibidos como generar el tipo de TypeScript, y cómo aplicarlo en cada punto crítico de una aplicación React y Next.js: resultados de fetch, formularios, controladores de ruta y acciones del servidor.
El verdadero problema es el JSON no confiable
Cualquier carga útil que tu código no haya generado por sí mismo, ya sea una respuesta de fetch, un cuerpo de solicitud, entrada de acción del servidor o un webhook, merece ser sospechosa. Si omites la verificación en tiempo de ejecución, tendrás asignaciones ciegas, validadores que no se ajustan a las interfaces y tipos de cliente y servidor que no coinciden. Zod reduce todo esto a una sola definición: modifica el esquema y el tipo inferido cambia junto con él.
Define el esquema, deriva el tipo
Comienza con la importación:
import { z } from "zod";
El esquema a continuación describe un perfil de usuario; z.infer lo convierte en un tipo de TypeScript, y loadProfile procesa la respuesta mediante parse antes de devolverla.
export const UserProfileSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
displayName: z.string().min(1).optional(),
});export type UserProfile = z.infer<typeof UserProfileSchema>;async function loadProfile(id: string): Promise<UserProfile> {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return UserProfileSchema.parse(data);
}
Compara return data as UserProfile: el análisis lanza un error en cuanto la API incumple el contrato, mientras que la asignación forzada permite que los datos defectuosos sigan circulando hasta que algo falla lejos de la causa original.
En el código de la interfaz de usuario, safeParse suele ser mejor: devuelve un objeto con el resultado en lugar de lanzar una excepción, por lo que usted controla la solución alternativa:
const result = UserProfileSchema.safeParse(data);
if (!result.success) {
console.error(result.error.flatten());
return null;
}
Formularios que envían datos válidos al manejador de envío
Con zodResolver, React Hook Form valida los valores antes de que lleguen a handleSubmit. El archivo es un componente del cliente:
"use client";
Los mensajes de error de los campos también provienen del esquema, lo que mantiene alineados la retroalimentación de la interfaz y los tipos:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";const SignupSchema = z.object({
email: z.string().email("Enter a valid email"),
password: z.string().min(8, "At least 8 characters"),
});type SignupValues = z.infer<typeof SignupSchema>;export function SignupForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SignupValues>({
resolver: zodResolver(SignupSchema),
}); return (
<form onSubmit={handleSubmit((values) => console.log(values))}>
<input type="email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register("password")} />
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">Create account</button>
</form>
);
}
En una aplicación real, mueva SignupSchema a un módulo compartido en lugar de definirlo dentro del archivo del componente, para que el servidor pueda importar las mismas reglas.
Validación en los puntos de entrada de Next.js
Manejadores de ruta
Un manejador de ruta necesita NextResponse, Zod y el esquema de perfil compartido:
import { NextResponse } from "next/server";
import { z } from "zod";
import { UserProfileSchema } from "@/lib/schemas/user";
El manejador valida el cuerpo con safeParse y devuelve un código 400 con los errores detallados en caso de fallo. También analiza su propia respuesta contra UserProfileSchema, de modo que la salida cumple con los requisitos acordados con el cliente. El id codificado de antemano representa la inserción en la base de datos.
const CreateUserSchema = z.object({
email: z.string().email(),
displayName: z.string().min(1).max(80).optional(),
});export async function POST(request: Request) {
const parsed = CreateUserSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid body", details: parsed.error.flatten() },
{ status: 400 }
);
} const created = {
id: "11111111-1111-1111-1111-111111111111",
email: parsed.data.email,
displayName: parsed.data.displayName,
}; return NextResponse.json(UserProfileSchema.parse(created), { status: 201 });
}
Acciones del servidor
Un módulo de Acción del Servidor comienza con la directiva:
"use server";
La acción crea un objeto a partir de FormData y lo valida contra el mismo SignupSchema que se utiliza en el formulario. Devolver ok como un tipo literal (as const) permite a los llamantes filtrar los resultados de manera clara:
import { SignupSchema } from "@/lib/schemas/auth";export async function signupAction(formData: FormData) {
const parsed = SignupSchema.safeParse({
email: formData.get("email"),
password: formData.get("password"),
}); if (!parsed.success) {
return { ok: false as const, errors: parsed.error.flatten().fieldErrors };
} return { ok: true as const };
}
Un módulo de esquema compartido entre el cliente y el servidor soluciona el problema de “válido en el formulario, rechazado por el servidor”. Para ver un patrón similar fuera de Next.js, consulte compartir un esquema Zod entre una interfaz frontend de React y un backend de Node.
Hábitos que mantienen los esquemas mantenibles
- Mantenga los esquemas juntos, por ejemplo en
lib/schemas/*. - Derive variantes con
.extend,.picky.omiten lugar de duplicar campos. - Utilice
.transformpara tareas simples de limpieza como recortar cadenas o parsear fechas, nunca para reglas comerciales ocultas. - Use
z.discriminatedUnioncuando la estructura de los datos depende de un campo de estado. - Parsee las variables de entorno una sola vez, al iniciar el programa.
En la práctica, la composición se ve así. Un esquema base contiene campos compartidos:
const BaseUser = z.object({
email: z.string().email(),
displayName: z.string().optional(),
});
A partir de él, un esquema de actualización convierte cada campo en opcional mediante .partial(), y un DTO de respuesta agrega campos propiedad del servidor con .extend():
export const UpdateUserSchema = BaseUser.partial();
export const UserDtoSchema = BaseUser.extend({
id: z.string().uuid(),
createdAt: z.string().datetime(),
});
Una precaución: las versiones más recientes de Zod introdujeron formatos de nivel superior como z.email() y z.uuid(), además de cambiar la forma en que se exponen los errores. Las formas encadenadas mostradas aquí podrían estar obsoletas en su versión, por lo que consulte la documentación actual de Zod.
Puntos clave
- Los tipos describen la intención; solo el análisis en tiempo de ejecución lo aplica en el extremo de la red.
- Infiera tipos de TypeScript a partir de los esquemas de Zod para que ambos no se desvíen.
- Preferir
safeParsecuando se quiera manejar un fallo, yparsecuando el fallo debe generar una excepción.
as más riesgosa y asignele primero un esquema.Lecturas relacionadas
- Un formulario de fecha de nacimiento validada en Next.js con inputs controlados y callbacks — Cree un pequeño formulario cliente de Next.js que haga seguimiento de los ingresos con useState, rechace fechas de nacimiento inválidas, muestre errores accesibles y entregue datos limpios al componente padre.
- Separar las capas de dominio, datos y UI en un códigobase de Next.js App Router — Un estudio de caso de Pokédex que muestra cómo dividir una aplicación de Next.js App Router en capas de dominio, datos y presentación utilizando Prisma, Zod, autenticación por cookies y caché.
- Zod 4.5: compilar, validar y safeParse: Elegir un parser por límite — Cómo hacer pruebas de rendimiento con los parsers compilados de Zod 4.5 y la ruta rápida de validación booleana en cargas válidas e inválidas, así como determinar a dónde pertenece cada API sin perder las rutas de error.
- IDs marcados en TypeScript: ¿Qué codificaciones de hecho evitan un borrado incorrecto? — Seis formas de definir los tipos UserId e InvoiceId comparadas en una sola prueba: cuáles hacen que tsc rechace la llamada deleteInvoice(userId), y dónde los marcadores de Zod añaden seguridad en tiempo de ejecución.