Inicio / Artículos / Compartir un único esquema Zod entre tu frontend de React y tu backend de Node

Compartir un único esquema Zod entre tu frontend de React y tu backend de Node

Aprenda cómo un único esquema Zod puede validar formularios de React, respuestas de API, cuerpos de solicitudes de Express y variables de entorno, al mismo tiempo que genera tipos TypeScript correspondientes.

1536 palabras

La validación es esencial en toda aplicación, pero los equipos a menudo acaban implementándola de forma fragmentada: una biblioteca para el frontend, otra diferente para el backend, y las mismas reglas copiadas en varios lugares. Zod se ha convertido en la opción preferida de los desarrolladores de JavaScript y TypeScript precisamente porque evita ese desorden: escribes un único esquema, y dicho esquema verifica tus datos y genera el tipo correspondiente en TypeScript, listo para usarse de manera idéntica tanto en el navegador como en el servidor.

1. ¿Qué es Zod?

Zod es una biblioteca de esquemas creada desde cero teniendo en cuenta a TypeScript. Describes la estructura de tus datos una sola vez, y Zod utiliza esa descripción para verificar valores en tiempo de ejecución y para derivar automáticamente un tipo en TypeScript; no hay una interfaz separada que escribir ni riesgo de que se desincronice con tus reglas de validación.

import { z } from 'zod';
const UserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age?: number }

Un único esquema como este cubre tres funciones simultáneamente: documenta la estructura de los datos, la aplica en tiempo de ejecución y proporciona el tipo estático del cual dependen el editor y el compilador.

2. Por qué Zod supera a las alternativas

La ventaja principal es la inferencia automática de tipos. Bibliotecas como Yup o Joi suelen requerir que mantengas un esquema de validación junto con una interfaz de TypeScript escrita a mano, confiando en que ambas no se desalineen a medida que cambia el código. Zod elimina por completo ese riesgo: el tipo se deriva directamente del esquema, por lo que no hay nada que sincronizar.

Zod también es ligero y no depende de bibliotecas externas, lo que lo hace igualmente adecuado tanto en un paquete frontend preocupado por el tamaño como en un servicio Node.js. Su API encadenable y componible también significa que incluso las validaciones complejas —objetos anidados, uniones, campos que dependen unos de otros— permanecen claras y fáciles de leer, en lugar de convertirse en un laberinto de funciones auxiliares improvisadas.

3. Usar Zod en una aplicación React

3.1 Validación de formularios con React Hook Form

Zod se integra directamente en React Hook Form a través del paquete @hookform/resolvers.

npm install zod react-hook-form @hookform/resolvers
// components/SignupForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const SignupSchema = z.object({
  name: z.string().min(2, 'Name is too short'),
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});
type SignupData = z.infer<typeof SignupSchema>;
export function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SignupData>({
    resolver: zodResolver(SignupSchema),
  });
  const onSubmit = (data: SignupData) => {
    console.log('Valid data:', data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} placeholder="Name" />
      {errors.name && <p>{errors.name.message}</p>}
      <input {...register('email')} placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}
      <input type="password" {...register('password')} placeholder="Password" />
      {errors.password && <p>{errors.password.message}</p>}
      <button type="submit">Sign Up</button>
    </form>
  );
}

No existe un seguimiento manual del estado de errores ni declaraciones duplicadas de tipos que mantener; un único esquema se encarga de la validación, proporciona los mensajes de error que aparecen junto a cada campo y define al mismo tiempo el tipo en TypeScript del objeto data enviado.

3.2 Validación de respuestas API

Zod es igualmente útil en el lado de entrada de tu aplicación; por ejemplo, al verificar que los datos devueltos por una API coincidan realmente con lo esperado, ya que no puedes confiar únicamente en los tipos en tiempo de compilación para garantizarlo.

import { z } from 'zod';
const PostSchema = z.object({
  id: z.number(),
  title: z.string(),
  body: z.string(),
});
const PostsResponseSchema = z.array(PostSchema);
async function fetchPosts() {
  const res = await fetch('/api/posts');
  const json = await res.json();
  const result = PostsResponseSchema.safeParse(json);
  if (!result.success) {
    console.error(result.error.flatten());
    throw new Error('Invalid API response shape');
  }
  return result.data; // fully typed Post[]
}

Este enfoque permite detectar respuestas mal formadas o inesperadas antes de que causen fallos silenciosos en su interfaz de usuario.

4. Uso de Zod en un backend Node.js / Express

4.1 Validación de los cuerpos de las solicitudes

npm install zod express
// schemas/user-schema.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
// middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';
export function validate(schema: ZodSchema) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({ errors: result.error.flatten() });
    }
    req.body = result.data;
    next();
  };
}
// routes/users.ts
import { Router } from 'express';
import { validate } from '../middleware/validate';
import { CreateUserSchema } from '../schemas/user-schema';
const router = Router();
router.post('/users', validate(CreateUserSchema), (req, res) => {
  // req.body is now guaranteed to match CreateUserInput
  const { name, email, age } = req.body;
  res.status(201).json({ name, email, age });
});
export default router;

Esta configuración proporciona a cada ruta un paso de validación uniforme y declarativo, con el manejo de errores centralizado en lugar de repetirse como comprobaciones if incrustadas en los controladores.

4.2 Validación de variables de entorno

Un uso subestimado pero poderoso de Zod es verificar process.env al iniciar la aplicación, de modo que una mala configuración provoque un fallo inmediato en lugar de un error confuso más adelante.

// config/env.ts
import { z } from 'zod';
const EnvSchema = z.object({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  NODE_ENV: z.enum(['development', 'production', 'test']),
});
export const env = EnvSchema.parse(process.env);

Si falta alguna variable necesaria o tiene el formato incorrecto, el proceso se cae de inmediato con un mensaje de error legible; es mucho más fácil de diagnosticar que un fallo misterioso oculto dentro de una llamada a la base de datos.

5. La verdadera ventaja: un esquema único, compartido en toda la pila

Dado que los esquemas de Zod son simplemente valores de TypeScript, nada impide colocarlos en un paquete compartido —o en una carpeta compartida dentro de un monorepo— y reutilizar el esquema idéntico tanto en el cliente como en el servidor.

/packages
  /shared
    /schemas
      user-schema.ts   <-- used by both React app and Express API
  /web (React/Next.js)
  /api (Node/Express)
// packages/shared/schemas/user-schema.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});
export type CreateUserInput = z.infer<typeof CreateUserSchema>;

La aplicación React depende de este esquema para verificar el formulario de registro antes de enviarlo. La API Express también depende del mismo esquema para validar la carga recibida. Cuando el esquema evoluciona, por ejemplo, al añadir un campo obligatorio nuevo, ambas capas se adaptan juntas, y TypeScript detecta de inmediato cualquier código que aún no se haya ajustado a la nueva estructura. Esto evita toda una clase de errores en los que la validación del lado del cliente y del servidor terminan desalineándose con el paso del tiempo.

6. Mejores prácticas

  • Utilice safeParse cuando el fallo sea un resultado normal y esperado (entradas de formulario, respuestas de APIs de terceros), y reserve parse —que lanza excepciones— para casos que realmente nunca deberían ser inválidos, como las variables de entorno verificadas al iniciar la aplicación.
  • Mantenga los esquemas compartidos en un único paquete común siempre que controle tanto el frontend como el backend, para no tener que gestionar dos copias de las mismas reglas.
  • Utilice .transform() para limpiar los datos como parte de la validación en sí misma —eliminando espacios en blanco, forzando tipos— en lugar de ejecutar un proceso separado de normalización posteriormente.
  • Prefiera z.infer sobre interfaces escritas manualmente para cualquier elemento ya respaldado por un esquema, de modo que sus tipos y su lógica de validación nunca se desalineen.
  • Envíe error.flatten() o error.format() en las respuestas de error de la API, lo que permite al código del frontend asociar fácilmente cada error con el campo de formulario correspondiente.
  • 7. Conclusión

    Zod es más que una biblioteca de validación típica: cambia por completo la relación entre la validación y el tipado. Al generar tipos de TypeScript directamente a partir de los esquemas en tiempo de ejecución, elimina silenciosamente todo el problema de que las definiciones de tipo y las reglas de validación diverjan. Sumado a eso, su bajo consumo de recursos, su diseño componible y su comportamiento consistente tanto en el navegador como en Node, hacen que Zod sea la opción ideal para proyectos full-stack en TypeScript desarrollados con React y Node.js.

    Siguientes pasos:

    • Explora zod-to-openapi si necesitas generar documentación OpenAPI directamente a partir de tus esquemas
    • Investiga .refine() y .superRefine() para crear lógica de validación personalizada que abarque varios campos
    • Conoce tRPC, que utiliza nativamente los esquemas de Zod para ofrecer seguridad de tipos de extremo a extremo en tu API

    Lecturas relacionadas

  • Los logros discretos de TypeScript 6 y los hábitos de JavaScript de los desarrolladores senior — Descubra las características pasadas por alto de TypeScript 6, como la gestión explícita de recursos y los parámetros de tipo const, además de los modismos en JavaScript en los que confían diariamente los ingenieros senior.
  • req-guard-lite: Un limitador de velocidad minimalista y basado en TypeScript para Express — Aprenda cómo funciona un limitador de velocidad ligero y sin dependencias para Express, desde los valores predeterminados en memoria hasta la escalabilidad con Redis y generadores de claves personalizados.