Regras da API
Regras de API HTTP mantêm backends Node previsíveis para clientes, BFFs e integrações de parceiros entre serviços.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
app.get("/invoices", async (req, res) => {
const limit = Math.min(Number(req.query.limit ?? 20), 100);
const cursor = req.query.cursor as string | undefined;
const page = await listInvoices({ limit, cursor });
res.json({ data: page.items, nextCursor: page.nextCursor });
});
app.post("/invoices", async (req, res) => {
const key = req.headers["idempotency-key"];
if (!key) return res.status(400).json({ error: { code: "missing_idempotency_key", message: "required" } });
// ...
});Quando usar isto:
- APIs HTTP REST ou JSON públicas consumidas por web/mobile/parceiros.
- Múltiplas equipes implementam serviços que os clientes tratam uniformemente.
- Mutações de pagamento, pedidos ou faturamento precisam de retentativas seguras.
Exemplo de Trabalho
// src/errors/http-error.ts
export class HttpError extends Error {
constructor(
public status: number,
public code: string,
message: string,
) {
super(message);
}
}
export function errorHandler(err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) {
if (err instanceof HttpError) {
return res.status(err.status).json({ error: { code: err.code, message: err.message } });
}
res.status(500).json({ error: { code: "internal_error", message: "unexpected error" } });
}// src/routes/invoices.ts
app.get("/v1/invoices", async (req, res, next) => {
try {
const limit = Math.min(Math.max(Number(req.query.limit ?? 20), 1), 100);
const result = await repo.list({ limit, cursor: req.query.cursor as string | undefined });
res.json({ data: result.rows, nextCursor: result.nextCursor });
} catch (e) {
next(e);
}
});
app.post("/v1/invoices", async (req, res, next) => {
try {
const idempotencyKey = req.headers["idempotency-key"];
if (typeof idempotencyKey !== "string") {
throw new HttpError(400, "missing_idempotency_key", "Idempotency-Key header required");
}
const existing = await repo.findByIdempotencyKey(idempotencyKey);
if (existing) return res.status(200).json({ data: existing });
const created = await repo.create({ ...req.body, idempotencyKey });
res.status(201).json({ data: created });
} catch (e) {
next(e);
}
});O que isto demonstra:
- Envelope de sucesso uniforme
{ error: { code, message } }e{ data: ... }. - Paginação com
limitlimitado enextCursor. - POST idempotente retorna 200 na repetição, 201 na primeira criação.
Mergulho Profundo
Como Funciona
- Prefixo de versão
/v1/permite alterações que quebram em/v2/sem quebras silenciosas para o cliente. - O
codede erro é legível por máquina; amessageé segura para humanos (sem stack traces). - Chaves de idempotência armazenadas com índice único evitam cobranças duplicadas.
409 Conflictpara conflitos de estado (fatura já cancelada).
Guia de Códigos de Status
| Código | Uso |
|---|---|
| 400 | Falha na validação |
| 401 | Autenticação ausente/inválida |
| 403 | Autenticação ok, não permitido |
| 404 | Recurso não encontrado |
| 409 | Conflito / duplicado |
| 422 | Validação semântica (opcional) |
| 429 | Limite de taxa atingido |
| 500 | Erro inesperado do servidor |
Notas de TypeScript
- Compartilhe tipos de resposta em
packages/contractspara consumidores de monorepo. - Erros de parse do Zod mapeiam para 400 com
code: "validation_error".
Armadilhas
- Formas de erro diferentes por rota - Clientes não conseguem analisar de forma confiável. Correção: middleware de erro global apenas.
limit=999999sem limites - DB OOM. Correção: limite fixo de 100 no lado do servidor sempre.- Idempotência apenas na documentação - Retentativas cobram em dobro. Correção: restrição única no banco de dados por chave.
- Retornar 200 com corpo de erro - Quebra a semântica HTTP. Correção: códigos de status adequados.
- Expor IDs internos em erros -
postgres uuid constraintna mensagem. Correção: mensagem genérica, detalhes apenas nos logs.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| GraphQL | Consultas flexíveis do cliente | APIs simples de parceiros CRUD |
| gRPC interno | Desempenho serviço a serviço | API pública voltada para o navegador |
| Problem Details RFC 7807 | Organizações com muitos padrões | Clientes existentes com { error: { code } } |
FAQs
Paginação com cursor vs offset?
Prefira cursor para tabelas grandes (estável sob inserções). Offset ok para listas administrativas de baixo volume.
Idempotency-Key em PUT/PATCH?
Necessário em criações POST; PUT geralmente é naturalmente idempotente pelo ID do recurso; documentar por endpoint.
O envelope de sucesso deve sempre usar data?
Escolha um envelope para toda a organização; { data } é comum para consistência semelhante a JSON:API.
Erros de validação de esquema Fastify?
Mapeie a validação Fastify para o mesmo { error: { code, message } } em setErrorHandler.
Filtros de exceção NestJS?
Filtro personalizado mapeia HttpException para a forma de JSON de erro compartilhada.
Corpo da resposta de limite de taxa?
429 com cabeçalho Retry-After e error.code: "rate_limited".
Idempotência de DELETE?
Excluir duas vezes retorna 204 ou 404 consistentemente - documente qual.
Datas ISO8601 em JSON?
Sim, strings em UTC 2026-07-09T12:00:00.000Z; nunca ambíguas locais sem offset.
OpenAPI obrigatório?
Recomendado para APIs públicas; gere a partir de Zod/esquemas sempre que possível.
Versão no cabeçalho vs caminho?
O caminho /v1 é mais claro para cache e roteamento; cabeçalho é um complemento opcional.
Relacionado
- Lista de Verificação de Regras de Projeto Node - regras 11-13
- Regras de Segurança - validação na fronteira
- Regras de Logging e Observabilidade - registrar requestId com erros
Versões da Stack: Esta página foi escrita para Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, e NestJS 11.