//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Crie endpoints de API REST com segurança de tipo usando Manipuladores de Rota do App Router do Next.js 15+ com NextRequest, NextResponse, validação de requisição e códigos de status HTTP adequados.
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
type Post = {
id: string;
title: string;
content: string;
createdAt: string;
};
// GET /api/posts
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const page = parseInt(searchParams.get("page") ?? "1", 10);
const limit = parseInt(searchParams.get("limit") ?? "10", 10);
const posts = await db.post.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: "desc" },
});
return NextResponse.json({ posts, page, limit });
}
// POST /api/posts
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.title || !body.content) {
return NextResponse.json(
{ error: "title and content are required" },
{ status: 400 }
);
}
const post = await db.post.create({
data: { title: body.title, content: body.content },
});
return NextResponse.json(post, { status: 201 });
}// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
type Params = { params: Promise<{ id: string }> };
// GET /api/posts/:id
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params;
const post = await db.post.findUnique({ where: { id } });
if (!post) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}
return NextResponse.json(post);
}
// PATCH /api/posts/:id
export async function PATCH(request: NextRequest, { params }: Params) {
const { id } = await params;
const body = await request.json();
const post = await db.post.update({
where: { id },
data: body,
});
return NextResponse.json(post);
}
// DELETE /api/posts/:id
export async function DELETE(request: NextRequest, { params }: Params) {
const { id } = await params;
await db.post.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}// app/api/auth/me/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySession } from "@/lib/auth";
export async function GET(request: NextRequest) {
const token = request.cookies.get("session-token")?.value;
if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const session = await verifySession(token);
if (!session) {
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
}
const response = NextResponse.json({ user: session });
response.headers.set("X-Request-Id", crypto.randomUUID());
return response;
}GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) de um arquivo route.ts dentro do diretório app.NextRequest estende o Request padrão com propriedades de conveniência: .nextUrl para URL parseada, .cookies para acesso a cookies, e .geo / .ip em implantações de edge.NextResponse estende Response com helpers estáticos: .json(), .redirect(), e .rewrite().{ params: Promise<{ ... }> }, e você deve await params antes de acessar os valores.GET sem entrada dinâmica. Use request.nextUrl.searchParams, cookies(), ou headers() para optar por renderização dinâmica automaticamente.route.ts no mesmo diretório de um page.tsx causará conflito. Manipuladores de Rota e páginas não podem coexistir no mesmo segmento de rota.Resposta de Streaming:
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (const chunk of ["Hello", " ", "World"]) {
controller.enqueue(encoder.encode(chunk));
await new Promise((r) => setTimeout(r, 100));
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain" },
});
}Manipulação de FormData:
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Salvar buffer no armazenamento...
return NextResponse.json({ filename: file.name, size: file.size });
}Cabeçalhos CORS:
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}params mudou no Next.js 15: use Promise<{ id: string }> em vez do antigo { id: string } síncrono.request.json() retorna Promise<any>. Faça o cast ou valide o resultado com Zod para segurança de tipo.NextRequest (não Request) para acessar .nextUrl, .cookies, e outras extensões do Next.js.GET sem entrada dinâmica são cacheados estaticamente em tempo de build. Acesse searchParams, cookies(), ou headers() para torná-los dinâmicos, ou exporte const dynamic = "force-dynamic".route.ts e page.tsx não podem compartilhar o mesmo diretório. O Manipulador de Rota sombreará a página no mesmo segmento de rota. Coloque rotas de API sob app/api/ para evitar conflitos.request.json() lança erro em corpos vazios ou malformados. Envolva-o em try/catch para segurança.new Response(null, { status: 204 }) é necessário para respostas sem conteúdo. NextResponse.json(null, { status: 204 }) enviará um corpo JSON vazio, não uma resposta de conteúdo nulo verdadeira.fs, Buffer, ou crypto (use globalThis.crypto em vez disso).| Abordagem | Prós | Contras |
|---|---|---|
| Manipuladores de Rota | Nativo, zero-config, co-localizado | Sem validação embutida ou cadeia de middleware |
| Server Actions | Não precisa de rota de API, amigável para formulários | Não é RESTful, mais difícil de chamar externamente |
| tRPC | Segurança de tipo de ponta a ponta | Dependência extra, curva de aprendizado |
| Servidor customizado Express/Fastify | Ecossistema completo de middleware | Perde otimização estática automática |
| Hono no Edge Runtime | Leve, cadeia de middleware | Não suportado oficialmente pelo Next.js |
De uma aplicação SaaS de produção Next.js 15 / React 19 (SystemsArchitect.io).
// Exemplo de produção: rota de API com composição de segurança e rastreamento atômico de uso
// Arquivo: app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withCsrfProtection } from '@/lib/csrf';
import { withRateLimit } from '@/lib/rate-limit';
import { prisma } from '@/lib/prisma';
import { getSession } from '@/lib/auth';
export const maxDuration = 60; // Timeout da função Vercel em segundos
async function handler(request: NextRequest) {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { prompt, projectId } = body;
// Rastreamento atômico de uso: deduzir crédito antes da operação
const updated = await prisma.user.updateMany({
where: {
id: session.userId,
credits: { gt: 0 },
},
data: {
credits: { decrement: 1 },
},
});
if (updated.count === 0) {
return NextResponse.json({ error: 'No credits remaining' }, { status: 402 });
}
try {
const result = await generateContent(prompt, projectId);
return NextResponse.json({ result });
} catch (error) {
// Rollback: restaurar o crédito em caso de falha
await prisma.user.update({
where: { id: session.userId },
data: { credits: { increment: 1 } },
});
return NextResponse.json({ error: 'Generation failed' }, { status: 500 });
}
}
// Composição de middleware de segurança: verificação CSRF envolve o limitador de taxa envolve o manipulador
export const POST = withCsrfProtection(withRateLimit(handler));O que isso demonstra em produção:
withCsrfProtection(withRateLimit(handler)) compõe middlewares de segurança como funções de ordem superior. A verificação CSRF é executada primeiro. Se passar, o limitador de taxa é executado. Se isso passar, o manipulador é executado. Esse padrão de composição evita cadeias de middleware profundamente aninhadas.export const maxDuration = 60 define o timeout da função serverless do Vercel. O padrão é 10 segundos, o que é muito curto para geração de conteúdo de IA. Esta configuração é específica para implantações do Vercel.updateMany com uma condição where: { credits: { gt: 0 } }. Esta é uma operação atômica: o crédito só é deduzido se o usuário tiver créditos restantes. Isso evita condições de corrida onde duas requisições simultâneas poderiam ler credits: 1 e ambas prosseguir.updated.count === 0 significa que o usuário não foi encontrado ou tinha zero créditos. Essa única verificação lida com ambos os casos sem uma consulta de leitura separada.POST como a função composta (não handler diretamente) garante que cada requisição POST passe por ambas as camadas de segurança. Exportações nomeadas como GET, POST, PUT mapeiam diretamente para métodos HTTP em manipuladores de rota do Next.js.De uma aplicação SaaS de produção Next.js 15 / React 19 (SystemsArchitect.io).
// Exemplo de produção: limitação de taxa segura contra TOCTOU com upsert do Prisma
// Arquivo: src/lib/tools/usage-limits.ts
export async function checkAndIncrementUsage(
toolId: string,
userId: string | null,
ipAddress: string,
userTier: UserTier
): Promise<{ allowed: boolean; limit: number; used: number; resetsAt: string }> {
const limit = await getToolUsageLimitAsync(toolId, userTier);
if (limit === -1) {
await incrementToolUsage(toolId, userId, ipAddress);
return { allowed: true, limit: -1, used: 0, resetsAt: new Date(Date.now() + 86400000).toISOString() };
}
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
const sessionId = userId || ipAddress;
// Upsert atômico: incrementa primeiro, depois verifica
const updated = await prisma.toolUsage.upsert({
where: {
sessionId_ipAddress_toolName: { sessionId, ipAddress, toolName: toolId },
},
create: { toolName: toolId, userId, sessionId, ipAddress, usageCount: 1, lastUsedAt: new Date() },
update: { usageCount: { increment: 1 }, lastUsedAt: new Date() },
});
// Reset no limite do dia
if (updated.lastUsedAt < today) {
const reset = await prisma.toolUsage.update({
where: { id: updated.id },
data: { usageCount: 1, lastUsedAt: new Date() },
});
return { allowed: true, limit, used: reset.usageCount, resetsAt: tomorrow.toISOString() };
}
// Acima do limite? Rollback do incremento
if (updated.usageCount > limit) {
await prisma.toolUsage.update({
where: { id: updated.id },
data: { usageCount: { decrement: 1 } },
});
return { allowed: false, limit, used: updated.usageCount - 1, resetsAt: tomorrow.toISOString() };
}
return { allowed: true, limit, used: updated.usageCount, resetsAt: tomorrow.toISOString() };
}O que isso demonstra em produção:
upsert cria o registro no primeiro uso ou incrementa em usos subsequentes. Nenhuma consulta separada de "isso existe?".sessionId_ipAddress_toolName garante um contador por usuário por ferramenta por dia.userId || ipAddress como sessionId lida com usuários autenticados e anônimos.limit === -1 significa ilimitado (nível de administrador). Ainda rastreia o uso para análise, mas nunca nega.updated.lastUsedAt < today detecta registros desatualizados de ontem e redefine para 1.{ decrement: 1 } desfaz o incremento otimista quando o limite é excedido.GET, POST, PUT, PATCH, DELETE, HEAD e OPTIONS.route.ts dentro do diretório app.GET sem entrada dinâmica em tempo de build.searchParams, cookies() ou headers() opta pela renderização dinâmica automaticamente.export const dynamic = "force-dynamic" para forçar o comportamento dinâmico explicitamente.NextRequest estende Request com .nextUrl (URL parseada), .cookies, .geo e .ip.NextRequest para acessar essas conveniências específicas do Next.js.Request padrão funciona, mas carece desses helpers.type Params = { params: Promise<{ id: string }> };
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params;
}app/api/ para evitar conflitos com rotas de página.return new NextResponse(null, { status: 204 });NextResponse.json(null, { status: 204 }), pois isso envia um corpo JSON vazio.new NextResponse(null, { status: 204 }) para uma resposta de conteúdo nulo verdadeira.request.json() em um bloco try/catch.withCsrfProtection(withRateLimit(handler)) encadeia funções de ordem superior.updateMany com where: { credits: { gt: 0 } } verifica e decrementa em uma única consulta.credits: 1 e ambas prosseguir.updated.count === 0 significa que a dedução falhou (sem créditos ou usuário não encontrado).import { z } from "zod";
const bodySchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
const parsed = bodySchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.flatten() },
{ status: 400 }
);
}
// parsed.data é totalmente tipadoOPTIONS que retorna cabeçalhos CORS com status 204.Access-Control-Allow-Origin, Access-Control-Allow-Methods e Access-Control-Allow-Headers.Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥