Middleware de validação
A verificação pertence à frente do handler. Construí-la significa escrever uma única função que funcione para qualquer rota, seja qual for o schema que essa rota precise.
O middleware do Express é uma função chamada a caminho de um handler, com três argumentos: a requisição, a resposta e next, que passa o controle adiante. Então o que precisamos é de uma função que produza um desses, já com um schema embutido.
validate(registerSchema) -> middleware que valida contra registerSchema
validate(loginSchema) -> middleware que valida contra loginSchemaUma função que retorna outra função é uma função de ordem superior, e aqui ela funciona como uma fábrica: chamada uma vez quando as rotas são configuradas, e o middleware que ela produz roda a cada requisição.
O formato da fábrica
Comece pelo esboço, em back-end/middleware/validate.ts:
import type { Request, Response, NextFunction } from 'express'
import * as z from 'zod'
export function validate(schema: z.ZodType) {
return (req: Request, res: Response, next: NextFunction): void => {
// Valida req.body contra o schema.
}
}Duas camadas, fazendo dois trabalhos em dois momentos diferentes.
A validate externa roda uma única vez, enquanto o app está iniciando, e seu único argumento é o schema. z.ZodType é a classe base da qual todo schema do Zod herda, então o parâmetro aceita um schema de objeto, um schema de string, qualquer um.
A função interna é a que o Express guarda e chama a cada requisição. Ela retorna void porque não devolve nenhum valor; ou ela responde à requisição, ou chama next.
A externa roda uma única vez, quando o app inicia, e seu trabalho é lembrar o schema. A interna roda a cada requisição, uma por uma, e seu trabalho é fazer a verificação.
Rejeitando uma requisição inválida
Dentro da função interna, rode o schema sobre o corpo da requisição:
const result = schema.safeParse(req.body)safeParse é o método certo para usar aqui, porque uma falha é algo a ser reportado, não uma exceção a ser tratada.
Quando ela falha, a resposta precisa dizer qual campo e o que estava errado:
if (!result.success) {
const errors = result.error.issues.map((issue) => ({
field: issue.path[0],
message: issue.message,
}))
res.status(400).json({ success: false, errors })
return
}Três coisas acontecem. Os issues viram pares de campo e mensagem, a resposta volta como um 400 carregando eles, e return interrompe a função.
Esse return é toda a segurança disso. res.json() envia uma resposta e continua executando, então sem ele a função segue até next() e o handler roda em cima de dados que falharam um instante antes.
res.json() só faz a primeira. O return faz a segunda, e deixar isso de fora é a versão clássica desse bug.
Passando adiante os dados válidos
Tudo depois desse return só roda quando a validação passou:
;(req as any).validatedData = result.data
next()Os dados já validados são anexados à requisição, e então next() passa o controle para o handler da rota, que lê req.validatedData e nunca toca em req.body.
result.data em vez de req.body faz diferença aqui. O Zod pode ter convertido uma string em número, removido espaços em branco, colocado um e-mail em minúsculas ou descartado uma chave que o schema não descrevia. O valor validado carrega tudo isso. O corpo bruto não carrega nada disso.
Aqui está o arquivo completo:
import type { Request, Response, NextFunction } from 'express'
import * as z from 'zod'
export function validate(schema: z.ZodType) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req.body)
if (!result.success) {
const errors = result.error.issues.map((issue) => ({
field: issue.path[0],
message: issue.message,
}))
res.status(400).json({ success: false, errors })
return
}
;(req as any).validatedData = result.data
next()
}
}next() é a passagem de bastão. Até ele ser chamado, a requisição fica parada nessa função e não vai adiante. É isso que faz o esquema funcionar: uma requisição que falha nunca é passada adiante, então o código que vem depois só vê dados que passaram na validação.
Coloque em prática
Esse middleware tem três bugs. Dois impedem que funcione; um é silencioso e pior.
export function validate(schema: z.ZodType) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req.body)
if (!result.success) {
res.status(400).json({ success: false, errors: result.error.issues })
}
;(req as any).validatedData = req.body
next()
}
}Compare suas respostas
1. Falta o return depois do 400. A resposta é enviada, a execução continua, next() roda, e o handler processa uma requisição que falhou na validação. O cliente vê uma rejeição e o trabalho acontece do mesmo jeito.
2. req.body no lugar onde deveria estar result.data. Mesmo depois de adicionar o return, o handler recebe o corpo bruto. Toda conversão, remoção de espaços e minúsculas feitas pelo schema são descartadas, e qualquer chave que o schema removeria continua anexada.
3. result.error.issues bruto na resposta. Funciona, então é o silencioso. Os objetos de issue do Zod descrevem seu schema para o cliente, incluindo tipos esperados e restrições que falharam, e não estão formatados para uma pessoa ler. Mapeie-os para pares de campo e mensagem.
O bug 2 é o que vale a pena refletir com calma. O middleware parece funcionar: requisições válidas passam, inválidas são rejeitadas, e os testes ficam verdes. O que desaparece silenciosamente é cada transformação que o schema estava fazendo, e isso só aparece muito mais tarde, como dados inconsistentes no banco de dados.
Para onde isso vai a seguir
O middleware está pronto e valida contra qualquer schema que receba. O que ele ainda não recebeu é um schema, nem uma rota para ficar na frente.
Construindo um endpoint validado escreve os dois, depois faz o schema crescer campo por campo até que todo input do formulário de cadastro esteja sendo checado.

