30 Regras de API para Next.js
Regras para construir APIs com Route Handlers e Server Actions do Next.js. Cobre design, segurança, validação, tratamento de erros e preocupações operacionais.
Busque em todas as páginas da documentação
Regras para construir APIs com Route Handlers e Server Actions do Next.js. Cobre design, segurança, validação, tratamento de erros e preocupações operacionais.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
1. Use Route Handlers para consumidores externos, Server Actions para formulários internos. Route Handlers (GET, POST em route.ts) servem clientes externos, webhooks e integrações de terceiros. Server Actions servem sua própria UI.
| Caso de Uso | Abordagem |
|---|---|
| Submissão de formulário da sua UI | Server Action |
| Webhook do Stripe/GitHub | Route Handler |
| API REST para aplicativo móvel | Route Handler |
| Mutação de clique de botão | Server Action |
| API pública com CORS habilitado | Route Handler |
2. Organize Route Handlers por recurso. Siga as convenções RESTful na sua estrutura de arquivos.
app/
api/
users/
route.ts # GET (listar), POST (criar)
[id]/
route.ts # GET (detalhe), PATCH (atualizar), DELETE
posts/
route.ts
[slug]/
route.ts
3. Use métodos HTTP apropriados. GET para leituras, POST para criações, PATCH para atualizações parciais, PUT para substituições completas, DELETE para remoções. Nunca use GET para mutações.
4. Retorne formas de resposta consistentes. Cada endpoint deve retornar a mesma estrutura para sucesso e erro.
// Sucesso
return NextResponse.json({ data: user }, { status: 200 });
// Erro
return NextResponse.json(
{ error: { code: "NOT_FOUND", message: "Usuário não encontrado" } },
{ status: 404 }
);
// Lista com paginação
return NextResponse.json({
data: users,
pagination: { page, pageSize, total },
});5. Use códigos de status HTTP apropriados.
| Código | Quando |
|---|---|
| 200 | Sucesso (GET, PATCH, DELETE) |
| 201 | Criado (POST) |
| 204 | Sem conteúdo (DELETE sem corpo) |
| 400 | Requisição inválida (falha na validação) |
| 401 | Não autorizado (sem autenticação) |
| 403 | Proibido (autenticação presente, permissões insuficientes) |
| 404 | Não encontrado |
| 409 | Conflito (recurso duplicado) |
| 422 | Entidade não processável (formato válido, dados inválidos) |
| 429 | Limite de taxa excedido |
| 500 | Erro interno do servidor |
6. Versionamento da sua API se ela tiver consumidores externos. Use versionamento baseado em caminho para APIs externas.
app/api/v1/users/route.ts
app/api/v2/users/route.ts
7. Mantenha os Route Handlers enxutos. Route Handlers devem analisar a entrada, chamar uma função de serviço e retornar uma resposta. A lógica de negócios pertence a lib/ ou services/.
// app/api/users/route.ts
import { createUser } from "@/services/users";
export async function POST(request: NextRequest) {
const body = await request.json();
const result = await createUser(body); // Lógica vive em outro lugar
return NextResponse.json({ data: result }, { status: 201 });
}8. Use helpers de requisição e resposta tipados. Crie funções utilitárias para padrões comuns.
// lib/api.ts
export function success<T>(data: T, status = 200) {
return NextResponse.json({ data }, { status });
}
export function error(code: string, message: string, status: number) {
return NextResponse.json({ error: { code, message } }, { status });
}
// Uso
return success(user, 201);
return error("NOT_FOUND", "Usuário não encontrado", 404);9. Aceite e valide parâmetros de consulta para endpoints GET. Use searchParams da URL e valide com Zod.
import { z } from "zod";
const querySchema = z.object({
page: z.coerce.number().min(1).default(1),
pageSize: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(),
sort: z.enum(["name", "createdAt", "updatedAt"]).default("createdAt"),
});
export async function GET(request: NextRequest) {
const params = Object.fromEntries(request.nextUrl.searchParams);
const parsed = querySchema.safeParse(params);
if (!parsed.success) {
return error("VALIDATION_ERROR", "Parâmetros inválidos", 400);
}
const { page, pageSize, search, sort } = parsed.data;
// ... buscar dados
}10. Valide corpos de requisição com Zod para POST/PATCH/PUT. Analise o corpo e retorne erros de validação estruturados.
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
role: z.enum(["user", "admin"]).default("user"),
});
export async function POST(request: NextRequest) {
const body = await request.json();
const parsed = createUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: { code: "VALIDATION_ERROR", details: parsed.error.flatten() } },
{ status: 400 }
);
}
const user = await db.user.create({ data: parsed.data });
return NextResponse.json({ data: user }, { status: 201 });
}11. Autentique todos os endpoints não públicos. Verifique a sessão ou chave de API no topo de cada Route Handler e Server Action.
export async function GET(request: NextRequest) {
const session = await auth();
if (!session) {
return error("UNAUTHORIZED", "Autenticação necessária", 401);
}
// ... prosseguir
}12. Autorize com base na propriedade do recurso. Autenticação prova identidade. Autorização prova acesso. Sempre verifique se o usuário possui ou tem permissão para acessar o recurso.
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth();
if (!session) return error("UNAUTHORIZED", "Autenticação necessária", 401);
const { id } = await params;
const post = await db.post.findUnique({ where: { id } });
if (!post) return error("NOT_FOUND", "Post não encontrado", 404);
if (post.authorId !== session.user.id && session.user.role !== "admin") {
return error("FORBIDDEN", "Não autorizado", 403);
}
await db.post.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}13. Valide assinaturas de webhook. Para Stripe, GitHub e outros provedores de webhook, sempre verifique a assinatura antes de processar.
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get("stripe-signature")!;
try {
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
// Processar evento
} catch {
return error("INVALID_SIGNATURE", "Assinatura de webhook inválida", 400);
}
}14. Limite a taxa dos seus endpoints de API. Proteja contra abusos com limitação de taxa. Use armazenamento em memória para desenvolvimento, Redis para produção.
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
});
export async function POST(request: NextRequest) {
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
const { success } = await ratelimit.limit(ip);
if (!success) {
return error("RATE_LIMITED", "Muitas requisições", 429);
}
// ... prosseguir
}15. Nunca exponha detalhes de erros internos. Registre o erro completo no lado do servidor. Retorne uma mensagem genérica para o cliente.
try {
const result = await riskyOperation();
return success(result);
} catch (err) {
console.error("Erro interno:", err); // Erro completo nos logs
return error("INTERNAL_ERROR", "Algo deu errado", 500); // Genérico para o cliente
}16. Use cabeçalhos CORS apenas quando necessário. Adicione CORS apenas para endpoints consumidos por frontends externos. CORS mal configurado é um risco de segurança.
export async function OPTIONS() {
return new NextResponse(null, {
headers: {
"Access-Control-Allow-Origin": "https://trusted-domain.com",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}17. Sanitize a entrada do usuário antes das consultas ao banco de dados. Mesmo com ORMs como Prisma (que parametrizam consultas), valide e sanitize a entrada. Nunca interpole a entrada do usuário em SQL bruto.
18. Defina cabeçalhos de cache apropriados. Endpoints GET públicos devem especificar o cache. Endpoints privados devem impedir o cache.
// Público, cacheável
return NextResponse.json({ data }, {
headers: { "Cache-Control": "public, max-age=60, s-maxage=300" },
});
// Privado, sem cache
return NextResponse.json({ data }, {
headers: { "Cache-Control": "private, no-cache, no-store" },
});19. Trate todos os casos de erro explicitamente. Verifique casos de não encontrado, não autorizado, erros de validação e estados de conflito. Não confie no tratamento de erros genérico para casos esperados.
20. Use try/catch em cada Route Handler. Envolva todo o corpo do handler para evitar que exceções não tratadas causem falha no processo.
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session) return error("UNAUTHORIZED", "Autenticação necessária", 401);
const data = await fetchData();
return success(data);
} catch (err) {
console.error("GET /api/data falhou:", err);
return error("INTERNAL_ERROR", "Algo deu errado", 500);
}
}21. Torne os handlers de webhook idempotentes. Webhooks podem ser entregues várias vezes. Armazene o ID do evento e ignore duplicatas.
export async function POST(request: NextRequest) {
const event = await verifyWebhook(request);
// Verificar se já foi processado
const existing = await db.webhookEvent.findUnique({
where: { eventId: event.id },
});
if (existing) return success({ received: true });
// Processar e registrar
await db.$transaction([
processEvent(event),
db.webhookEvent.create({ data: { eventId: event.id } }),
]);
return success({ received: true });
}22. Retorne cedo para estados inválidos. Verifique pré-condições no topo. Cada verificação retorna imediatamente se inválida. O caminho feliz é o código que chega ao final.
23. Registre dados estruturados, não strings. Use logging estruturado para facilitar a filtragem e alertas em produção.
console.error({
event: "api.users.create.failed",
userId: session.user.id,
error: err.message,
stack: err.stack,
timestamp: new Date().toISOString(),
});24. Defina timeouts para chamadas de API externas. Use AbortController com um timeout para evitar requisições pendentes.
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(externalUrl, { signal: controller.signal });
return success(await res.json());
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
return error("TIMEOUT", "Serviço externo excedeu o tempo limite", 504);
}
throw err;
} finally {
clearTimeout(timeout);
}25. Use streaming para respostas grandes. Retorne ReadableStream para exportações CSV, grandes arrays JSON ou dados em tempo real.
26. Paginação de endpoints de lista. Nunca retorne listas ilimitadas. Use tamanhos de página razoáveis por padrão (20-50) com um limite máximo (100).
27. Use pool de conexões de banco de dados. Crie um cliente Prisma singleton ou pool de conexões. Não crie uma nova conexão por requisição.
// lib/db.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const db = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;28. Use o runtime edge para endpoints sensíveis à latência. Verificações de autenticação simples, redirecionamentos e transformações leves se beneficiam do runtime edge.
export const runtime = "edge";
export async function GET(request: NextRequest) {
// Executa na edge, mais perto do usuário
}29. Monitore tempos de resposta e taxas de erro da API. Acompanhe latências P50, P95 e P99. Alerte sobre picos na taxa de erros. Use Vercel Analytics, Datadog ou similar.
30. Documente sua API. Mantenha uma especificação OpenAPI ou, no mínimo, um README com endpoints, exemplos de requisição/resposta e requisitos de autenticação. Seu eu futuro e sua equipe agradecerão.
// Sucesso
{ data: T }
// Erro
{ error: { code: string, message: string } }
// Lista
{ data: T[], pagination: { page, pageSize, total } }A consistência torna o processamento no lado do cliente previsível.
lib/ ou services/ para testabilidade e reutilizaçãoconst querySchema = z.object({
page: z.coerce.number().min(1).default(1),
pageSize: z.coerce.number().min(1).max(100).default(20),
});
export async function GET(request: NextRequest) {
const params = Object.fromEntries(request.nextUrl.searchParams);
const parsed = querySchema.safeParse(params);
if (!parsed.success) return error("VALIDATION_ERROR", "Parâmetros inválidos", 400);
}export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
// ...
return new NextResponse(null, { status: 204 });
}export function success<T>(data: T, status = 200) {
return NextResponse.json({ data }, { status });
}
export function error(code: string, message: string, status: number) {
return NextResponse.json({ error: { code, message } }, { status });
}"Algo deu errado" genérico para o clienteconst controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
return success(await res.json());
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
return error("TIMEOUT", "Serviço externo excedeu o tempo limite", 504);
}
throw err;
} finally {
clearTimeout(timeout);
}| Categoria | Regra Chave |
|---|---|
| Design | Formas de resposta consistentes, métodos HTTP e códigos de status apropriados |
| Segurança | Autenticação em todos os endpoints, validação de toda entrada, limite de taxa, verificação de webhooks |
| Erros | Try/catch em tudo, retorne cedo, registre dados estruturados |
| Performance | Paginação, pool de conexões, streaming de respostas grandes |
| Operações | Monitore latência, torne webhooks idempotentes, documente tudo |
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥