Onde a validação deve ficar
Três capítulos de schemas, todos rodados manualmente em objetos escritos poucas linhas acima deles. Dados reais chegam pela rede, e um schema só vale alguma coisa no lugar onde esses dados chegam.
A questão é onde colocar a verificação, e a resposta muda como o resto do código é escrito.
O caminho de um envio
Um formulário de cadastro envia um POST para o servidor. Entre a chegada da requisição e a execução do seu código, uma coisa acontece:
form ──POST──▶ validação ──▶ route handler ──▶ resposta
│
└──▶ 400 com os errosA validação fica no meio, e é ela quem decide qual dos dois desfechos a requisição vai ter.
Falha. O schema encontra problemas, a requisição para ali, e um 400 Bad Request volta carregando uma lista do que deu errado. O route handler nunca chega a rodar.
Passa. Os dados processados seguem para o route handler, que faz o trabalho de verdade e responde com 201 Created.
O handler só recebe dados que já bateram com o schema.
Nada chega ao ambiente atrás dela sem passar por ali, então esse ambiente pode parar de perguntar se as coisas foram verificadas.
Por que a verificação vai na frente
A alternativa é validar no topo de cada route handler, o que funciona mas não se sustenta.
// A versão que se desalinha com o tempo
import { registerSchema } from './schemas/userSchema.js'
app.post('/api/register', (req, res) => {
const result = registerSchema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({ success: false, errors: result.error.issues })
}
// ...o trabalho de verdade, eventualmente
})Toda rota repete esse bloco. Código repetido se desalinha: uma rota mapeia os erros e outra os retorna crus, uma esquece o return e continua a execução dentro do handler mesmo depois de responder, uma rota nova copia qualquer versão que estivesse mais perto.
Colocar a verificação na frente faz dela um único pedaço de código compartilhado por todas as rotas, e faz com que a validação de uma rota fique visível na sua definição, em vez de escondida no corpo dela.
Uma cópia pode ser corrigida uma vez só. Doze cópias significam descobrir qual delas tem o bug.
O que o handler recebe
Um handler que fica atrás da validação pode ser curto, porque ele pode assumir o formato dos dados que recebeu:
import { validate } from './middleware/validate.js'
import { registerSchema } from './schemas/userSchema.js'
app.post('/api/register', validate(registerSchema), (req, res) => {
const userData = req.validatedData
// Lógica de negócio, trabalhando com dados que já bateram com o schema.
res.status(201).json({ success: true, user: { email: userData.email } })
})Três coisas ficam entre o caminho e o handler agora: validate(registerSchema) é a verificação, e o handler só roda se ela passar.
O handler lê req.validatedData, não req.body. Essa distinção é o arranjo todo. req.body é o que chegou; req.validatedData é o que sobreviveu, já com as transformações aplicadas.
req.body é o que foi enviado. req.validatedData é o que passou pela verificação. Leia o segundo e o handler nunca precisa ficar em dúvida.
Coloque em prática
Aqui está uma rota que valida dentro do handler:
import * as z from 'zod'
import { saveSubscriber } from './services/subscribers.js'
const subscribeSchema = z.object({
email: z.email('Digite um endereço de e-mail válido'),
})
app.post('/api/subscribe', (req, res) => {
const result = subscribeSchema.safeParse(req.body)
if (!result.success) {
res.status(400).json({ success: false, errors: result.error.issues })
}
const { email } = req.body
saveSubscriber(email)
res.status(201).json({ success: true })
})Encontre três problemas nela.
Compare suas respostas
1. O return que falta. res.status(400).json(...) envia uma resposta e continua executando, então uma requisição inválida recebe um 400 e chega até saveSubscriber. O cliente vê uma rejeição enquanto o assinante é salvo mesmo assim.
2. Ela lê req.body em vez de result.data. Mesmo com o return corrigido, o valor usado é o bruto, então qualquer trim ou lowercase feito no schema é descartado, e tudo o que o schema removia volta a aparecer.
3. Ela retorna result.error.issues sem tratamento. Os objetos de issue do Zod carregam detalhes internos, incluindo o tipo esperado e a restrição que falhou, e são formatados para código, não para pessoas. Um cliente precisa de pares campo-e-mensagem.
Existe uma quarta coisa que não é um bug nessa rota, mas vira um em escala maior: tudo isso vive dentro do handler, então a próxima rota ganha uma cópia, e as cópias vão se desalinhando com o tempo.
Para onde isso vai a seguir
O plano está definido. Uma verificação na frente do handler, um 400 com erros úteis quando falha, dados processados anexados à requisição quando passa.
Validation middleware constrói isso: uma função que aceita qualquer schema e retorna algo que o Express pode colocar na frente de qualquer rota.

