Pular para o conteúdo
TypeScript

Validação com Zod no TypeScript

Zod no TypeScript: schema, infer e validação na borda da API sem duplicar tipos.

Por que isso é importante

Validar com Zod no TypeScript evita tipo mentiroso: schema na borda e z.infer no restante. Parse na entrada — não confie em cast.

O gap entre compile time e runtime

Galera, o TypeScript faz um trabalho incrível em te proteger durante o desenvolvimento. Mas quando a aplicação tá rodando, ele desaparece. O JavaScript puro que sobra não tem ideia dos seus tipos. Se um endpoint externo manda um campo como number em vez de string, ninguém reclama. O app simplesmente faz algo imprevisível.

Antes do Zod, as opções eram: validação manual (if/else infinito), Joi (sem tipagem nativa), ou class-validator (verboso, decorators). Zod nasceu pra ser TypeScript-first, com inferência de tipos automática e API limpa.

Instalação e primeiro schema

npm install zod
import { z } from 'zod';

// Define o schema
const UserSchema = z.object({
  name: z.string().min(2, 'Nome muito curto'),
  email: z.string().email('Email inválido'),
  age: z.number().int().positive().optional(),
  role: z.enum(['admin', 'user', 'editor']),
});

// Infere o tipo automaticamente
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age?: number; role: 'admin' | 'user' | 'editor' }

// Valida dados
const result = UserSchema.parse({
  name: 'João',
  email: 'joao@email.com',
  role: 'admin',
});
// result é do tipo User, 100% validado

Percebe o poder? Você define o schema uma vez e extrai o tipo com z.infer. Zero duplicação. Se mudar o schema, o tipo atualiza automaticamente.

parse vs safeParse: quando usar cada um

O Zod oferece duas formas de validar. O parse() lança exceção se der errado. O safeParse() retorna um objeto de resultado sem lançar exceção. A escolha depende do contexto.

// parse() - lança ZodError se inválido
try {
  const user = UserSchema.parse(dadosIncertos);
  // user é User tipado
} catch (error) {
  if (error instanceof z.ZodError) {
    console.log(error.issues);
    // [{ code: 'too_small', minimum: 2, path: ['name'], message: 'Nome muito curto' }]
  }
}

// safeParse() - nunca lança exceção
const result = UserSchema.safeParse(dadosIncertos);

if (result.success) {
  const user = result.data; // User tipado
} else {
  const errors = result.error.issues;
  // Trata erros sem try/catch
}

Regra prática: use parse() na inicialização (variáveis de ambiente, configs). Use safeParse() em APIs e formulários onde você precisa retornar erros pro usuário.

Validando API requests com Zod

Dá pra integrar Zod direto no handler da API pra validar body, query params e headers. Isso elimina a necessidade de verificação manual campo a campo.

// schemas/user.ts
import { z } from 'zod';

export const CreateUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  password: z.string().min(8).regex(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
    'Senha precisa de maiúscula, minúscula e número'
  ),
});

export const UpdateUserSchema = CreateUserSchema.partial();
// Todos os campos ficam opcionais

export const QuerySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  search: z.string().optional(),
});

export type CreateUserInput = z.infer<typeof CreateUserSchema>;
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
export type QueryInput = z.infer<typeof QuerySchema>;
// routes/user.ts (Express)
import { CreateUserSchema, QuerySchema } from '../schemas/user';

app.post('/users', (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json({
      errors: result.error.issues.map(issue => ({
        field: issue.path.join('.'),
        message: issue.message,
      })),
    });
  }

  const user = result.data;
  // user tem tipo CreateUserInput, validado
});

app.get('/users', (req, res) => {
  const query = QuerySchema.parse(req.query);
  // query.page é number, query.limit é number
  // z.coerce converteu de string pra number automaticamente
});

Validando formulários no React

Zod combina perfeito com React Hook Form através do resolver @hookform/resolvers/zod. Você define o schema, conecta no form e a validação acontece automática, com mensagens de erro tipadas.

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const LoginSchema = z.object({
  email: z.string().email('Digite um email válido'),
  password: z.string().min(8, 'Mínimo 8 caracteres'),
});

type LoginForm = z.infer<typeof LoginSchema>;

export function LoginPage() {
  const { register, handleSubmit, formState: { errors } } = useForm<LoginForm>({
    resolver: zodResolver(LoginSchema),
  });

  const LoginForm) => {
    // data já está validado e tipado
    console.log(data.email, data.password);
  };

  return (
    <form
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="password" {...register('password')} />
      {errors.password && <span>{errors.password.message}</span>}

      <button type="submit">Entrar</button>
    </form>
  );
}

Schemas avançados: transform, refine e pipe

Zod vai muito além de validação simples. Dá pra transformar dados, adicionar validações customizadas e compor schemas de formas poderosas.

// transform: modifica o valor após validação
const SlugSchema = z.string()
  .transform(val => val.toLowerCase().replace(/\s+/g, '-'));

SlugSchema.parse('Meu Post Legal'); // 'meu-post-legal'

// refine: validação customizada
const PasswordSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
  message: 'Senhas não conferem',
  path: ['confirmPassword'],
});

// discriminatedUnion: schemas condicionais
const EventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
  z.object({ type: z.literal('keypress'), key: z.string() }),
  z.object({ type: z.literal('scroll'), deltaY: z.number() }),
]);

type Event = z.infer<typeof EventSchema>;
// TypeScript sabe o tipo exato baseado no campo 'type'

Tratamento de erros: formatando pro usuário

O ZodError carrega informações detalhadas sobre cada problema. Dá pra formatar de vários jeitos dependendo do destino: API response, toast de UI, log interno.

import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().positive(),
});

const result = schema.safeParse({ name: '', email: 'abc', age: -1 });

if (!result.success) {
  // Formato flat (lista de mensagens)
  const flat = result.error.flatten();
  // { fieldErrors: { name: [...], email: [...], age: [...] } }

  // Formato formatado (aninhado)
  const formatted = result.error.format();
  // { name: { _errors: ['...'] }, email: { _errors: ['...'] } }

  // Custom: mapa campo -> mensagem
  const errorMap = result.error.issues.reduce((acc, issue) => {
    const field = issue.path.join('.');
    acc[field] = issue.message;
    return acc;
  }, {} as Record<string, string>);
  // { name: 'String must contain at least 2 character(s)', ... }
}

Erros comuns com Zod

Atenção

Erro 1: Esquecer de usar z.coerce para query params. Dados de URL sempre chegam como string. Use z.coerce.number() em vez de z.number() para conversão automática.

Erro 2: Usar parse() em APIs sem try/catch. Se a validação falhar, o servidor retorna um 500 genérico. Sempre use safeParse() ou envolva em try/catch.

Erro 3: Criar tipos separados do schema. Se você tem um schema Zod E um type/interface manual, eles vão sair de sincronia. Use z.infer e pronto.

Erro 4: Não tratar mensagens de erro pro português. O Zod manda mensagens em inglês por padrão. Use o segundo argumento de cada validador para custom messages.

Passo a passo: integrando Zod no projeto

  1. Instale o Zod: npm install zod
  2. Crie uma pasta src/schemas/ para organizar seus schemas
  3. Defina schemas para cada entidade (User, Product, Order...)
  4. Exporte tipos com z.infer no mesmo arquivo do schema
  5. Use safeParse() nos handlers de API e formulários
  6. Configure mensagens de erro em português nos validadores
  7. Integre com React Hook Form via @hookform/resolvers/zod se usar React

Checklist: Zod no projeto TypeScript

Checklist: Validação com Zod

  • Zod instalado e configurado
  • Schemas criados para todas as entidades principais
  • Tipos inferidos com z.infer (sem duplicação manual)
  • APIs usando safeParse() com retorno de erros formatados
  • Formulários integrados via zodResolver
  • Mensagens de erro customizadas em português
  • Variáveis de ambiente validadas com Zod na inicialização
  • Testes unitários para schemas com edge cases

TypeScript Profissional: Projeto Completo

No CrazyStack você constrói um projeto completo com TypeScript

Node.js e React. Validação com Zod

tipagem forte e boas práticas do início ao deploy. Acesse:

/comprar?src=blog-como-usar-zod-typescript-validacao
]
}
]
}
]
}