Next.js: API Routes & Route Handlers
Última atualização: 2026-08-26
Route Handlers formam a camada de API do Next.js — quando Server Actions não são suficientes, endpoints RESTful padrão continuam sendo a base da web moderna.
1. O Que Você Vai Aprender
- Arquivo
route.tsdefine endpoints GET/POST/PUT/DELETE - Zod valida corpos de requisição e parâmetros de consulta
- Formatação de resposta com
NextResponse(json/redirect/rewrite) middleware.tsconfigurando matchers e reescrita de requisições- Configuração CORS Cross-Origin e implementação de Limitação de Taxa
2. Uma História Real de uma Engenheira DevOps
(1) Ponto de Dor: A equipe implementou a lógica de autenticação oito vezes em cinco páginas
Ao revisar o código do TaskFlow, Diana percebeu que cada rota de API começava com as mesmas 15 linhas de código de autenticação — analisando a sessão do cookie, validando o token e retornando uma resposta 401. Oito endpoints de API × 15 linhas = 120 linhas de código duplicado. Para piorar, três endpoints não tinham verificações de autenticação, expondo diretamente dados de usuários a requisições de usuários não autenticados.
| Questão | Dados |
|---|---|
| Código de Autenticação Duplicado | 120 linhas (8 endpoints × 15 linhas) |
| Endpoints desprotegidos | 3 |
| Auditoria de segurança reprovada | 2 vezes |
| Tempo de Reparo | 4 horas por sessão |
(2) A Solução com Middleware
Use
middleware.tspara gerenciar centralizadamente autenticação, logging e CORS — um único arquivo controla todo o ciclo de vida de todas as requisições.
// middleware.ts — Single Sign-On + Log
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('session-token')?.value
// Inspecionar Autenticação de Requisição API
if (request.nextUrl.pathname.startsWith('/api/')) {
if (!token) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 })
}
}
// Adicionar cabeçalhos de segurança
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
return response
}
export const config = {
matcher: '/api/:path*',
}
(3) Ganhos
| Dimensão | Antes (sem middleware) | Depois (com middleware) |
|---|---|---|
| Código de Verificação | 120 linhas (espalhado) | 15 linhas (condensado) |
| Endpoints desprotegidos | 3 | 0 |
| Auditoria de Segurança | ❌ Reprovada | ✅ Aprovada |
| Carga de novo endpoint | 15 linhas de auth + lógica | Apenas lógica |
3. Fundamentos de Route Handlers
Route Handlers são definidos no arquivo app/api/**/route.ts, com uma função assíncrona exportada correspondente a cada método HTTP:
| Método HTTP | Função Exportada | Arquivo de Rota |
|---|---|---|
| GET | export async function GET() |
app/api/items/route.ts |
| POST | export async function POST() |
app/api/items/route.ts |
| PUT | export async function PUT() |
app/api/items/[id]/route.ts |
| DELETE | export async function DELETE() |
app/api/items/[id]/route.ts |
| PATCH | export async function PATCH() |
app/api/items/[id]/route.ts |
(1) Endpoint GET Básico
// app/api/items/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const items = await db.item.findMany()
return NextResponse.json(items)
}
(2) Parâmetros de Rota Dinâmica
// app/api/items/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const item = await db.item.findUnique({ where: { id: Number(params.id) } })
if (!item) {
return NextResponse.json({ error: 'Item não encontrado' }, { status: 404 })
}
return NextResponse.json(item)
}
(3) POST para Criar um Recurso
// app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const body = await request.json()
const item = await db.item.create({ data: body })
return NextResponse.json(item, { status: 201 })
}
▶ Exemplo: Route Handler CRUD Completo (Dificuldade: ⭐⭐)
Saída:
POST app/api/items/route.ts → Cria um novo recurso, retorna item criado com status 201.
// app/api/todos/route.ts — GET + POST
import { NextRequest, NextResponse } from 'next/server'
const API = 'https://jsonplaceholder.typicode.com'
export async function GET() {
const todos = await fetch(`${API}/todos?_limit=5`).then(r => r.json())
return NextResponse.json(todos)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const todo = await fetch(`${API}/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(todo, { status: 201 })
}
Saída:
GET app/api/todos/route.ts → Retorna dados JSON com status 200.
POST app/api/todos/route.ts → Cria um novo recurso, retorna item criado com status 201.
// app/api/todos/[id]/route.ts — GET + PUT + DELETE
import { NextRequest, NextResponse } from 'next/server'
const API = 'https://jsonplaceholder.typicode.com'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const todo = await fetch(`${API}/todos/${params.id}`).then(r => r.json())
if (!todo || todo.id === undefined) {
return NextResponse.json({ error: 'Não encontrado' }, { status: 404 })
}
return NextResponse.json(todo)
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const updated = await fetch(`${API}/todos/${params.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(updated)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await fetch(`${API}/todos/${params.id}`, { method: 'DELETE' })
return NextResponse.json({ success: true })
}
4. Validação de Requisições com Zod
Para validar dados de requisição em APIs REST, também recomendamos usar Zod — é mais conciso e type-safe do que verificações manuais com if.
(1) Validação do Corpo da Requisição
// app/api/items/route.ts — Verificação Zod do Corpo da Requisição POST
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const createItemSchema = z.object({
name: z.string().min(2).max(100),
price: z.number().positive('Preço deve ser positivo'),
category: z.enum(['electronics', 'clothing', 'food']),
tags: z.array(z.string()).max(5).optional(),
})
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = createItemSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Falha na validação', details: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const item = await db.item.create({ data: validated.data })
return NextResponse.json(item, { status: 201 })
}
(2) Validação de Parâmetros de Consulta
// app/api/items/route.ts — Zod Valida Parâmetros de Consulta
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
search: z.string().optional(),
})
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl
const query = querySchema.safeParse({
page: searchParams.get('page'),
limit: searchParams.get('limit'),
search: searchParams.get('search'),
})
if (!query.success) {
return NextResponse.json(
{ error: 'Parâmetros de consulta inválidos', details: query.error.flatten().fieldErrors },
{ status: 400 }
)
}
const { page, limit, search } = query.data
const items = await db.item.findMany({
skip: (page - 1) * limit,
take: limit,
where: search ? { name: { contains: search } } : undefined,
})
return NextResponse.json({ items, page, limit })
}
▶ Exemplo: API Completa com Validação Zod (Dificuldade: ⭐⭐)
Saída:
GET app/api/items/route.ts → Valida parâmetros de consulta com Zod, retorna resultados filtrados como JSON.
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'
const userSchema = z.object({
name: z.string().min(2, 'Nome muito curto'),
email: z.string().email('Email inválido'),
role: z.enum(['admin', 'user', 'viewer']).default('user'),
})
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = userSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Falha na validação', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const user = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
revalidateTag('users')
return NextResponse.json(user, { status: 201 })
}
Saída:
POST app/api/users/route.ts → Valida corpo da requisição com Zod, cria recurso, retorna 201.
5. Formato de Resposta NextResponse
NextResponse é uma ferramenta de resposta dedicada fornecida pelo Next.js que suporta múltiplos formatos de resposta:
| Método | Finalidade | Exemplo |
|---|---|---|
NextResponse.json(data, opts?) |
Resposta JSON | NextResponse.json({ id: 1 }, { status: 201 }) |
NextResponse.redirect(url) |
Redirecionamento | NextResponse.redirect(new URL('/login', request.url)) |
NextResponse.next() |
Continuar a cadeia de middleware | return NextResponse.next() |
NextResponse.rewrite(url) |
Reescrita de URL no Servidor | NextResponse.rewrite(new URL('/fallback', request.url)) |
(1) Resposta JSON
// app/api/auth/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ status: 'healthy', uptime: process.uptime() })
}
(2) Redirecionamento
// app/api/redirect/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const target = request.nextUrl.searchParams.get('to') ?? '/'
return NextResponse.redirect(new URL(target, request.url))
}
(3) Rewrite (Reescrita de URL)
// middleware.ts — Reescreve /products/old-slug para um novo caminho
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/products/old-')) {
const newPath = request.nextUrl.pathname.replace('/old-', '/')
return NextResponse.rewrite(new URL(newPath, request.url))
}
return NextResponse.next()
}
▶ Exemplo: Comparação de Formatos de Resposta (Dificuldade: ⭐)
Saída:
Middleware intercepts requests and rewrites URLs based on conditions.
// app/api/response-demo/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const format = request.nextUrl.searchParams.get('format') ?? 'json'
switch (format) {
case 'json':
return NextResponse.json({ message: 'Olá', timestamp: Date.now() })
case 'redirect':
return NextResponse.redirect(new URL('/api/response-demo?format=json', request.url))
case 'rewrite':
return NextResponse.rewrite(new URL('/api/response-demo?format=json', request.url))
default:
return NextResponse.json({ error: 'Formato desconhecido' }, { status: 400 })
}
}
Saída:
GET app/api/response-demo/route.ts → Retorna dados JSON com status 200.
6. Middleware
middleware.ts é um interceptador de requisições do Next.js — ele executa antes que cada requisição chegue a uma página ou API, e suporta correspondência de caminhos, reescrita de requisições, injeção de cabeçalhos e verificações de autenticação.
sequenceDiagram
participant Client as Navegador
participant MW as middleware.ts
participant Route as Route Handler/Página
Client->>MW: Requisição /api/todos
MW->>MW: Corresponder regras matcher
MW->>MW: Inspeção de Autenticação / Injeção de Cabeçalho
alt Aprovado
MW->>Route: NextResponse.next()
Route-->>Client: Resposta Normal
else Não verificado
MW-->>Client: NextResponse.json(401)
else Redirecionar
MW-->>Client: NextResponse.redirect(/login)
end
| Configuração | Tipo | Descrição |
|---|---|---|
matcher |
string[] | Padrão de caminho correspondente (suporta glob) |
request.nextUrl |
URL | O objeto URL para a requisição atual |
request.cookies |
Map | Cookies da requisição |
request.headers |
Headers | Cabeçalhos da Requisição |
NextResponse.next() |
Response | Continuar processamento normalmente |
NextResponse.redirect() |
Response | Redirecionar para outra URL |
(1) Configuração de Matcher
// middleware.ts — Configuração de matcher
export const config = {
matcher: [
'/api/:path*', // Todas as Rotas API
'/dashboard/:path*', // Todas as Páginas do dashboard
'/((?!_next|static|favicon.ico).*)', // Excluir recursos estáticos
],
}
(2) Padrões Comuns de Middleware
// middleware.ts — Resumo de Padrões Comuns
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. Inspeção de Autenticação
const token = request.cookies.get('session')?.value
const isAuthPage = pathname.startsWith('/login') || pathname.startsWith('/register')
if (!token && !isAuthPage && pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 2. Cabeçalhos de Segurança
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
// 3. Adicionar ID de requisição (Rastreamento)
const requestId = crypto.randomUUID()
response.headers.set('X-Request-Id', requestId)
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
▶ Exemplo: Logging no Middleware (Dificuldade: ⭐)
Saída:
Middleware intercepts requests and redirects based on conditions.
// middleware.ts — Logs de Requisição
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const start = Date.now()
const { method, nextUrl } = request
console.log(`[${new Date().toISOString()}] ${method} ${nextUrl.pathname}`)
const response = NextResponse.next()
// Log Assíncrono do Tempo de Resposta
response.headers.set('X-Response-Time', `${Date.now() - start}ms`)
return response
}
export const config = {
matcher: '/api/:path*',
}
Saída:
[${new Date(
7. CORS e Limitação de Taxa
(1) Configuração CORS
// app/api/cors-config/route.ts — Para um único endpoint CORS
import { NextRequest, NextResponse } from 'next/server'
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://your-app.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
}
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders })
}
export async function GET() {
return NextResponse.json(
{ data: 'CORS habilitado' },
{ headers: corsHeaders }
)
}
(2) Middleware CORS Global
// middleware.ts — CORS Global
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
// Requisições preflight são retornadas imediatamente
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: response.headers })
}
return response
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}
(3) Limitação de Taxa
// lib/rate-limit.ts — Limitador de Taxa Simples em Memória
const rateMap = new Map<string, { count: number; resetAt: number }>()
export function rateLimit(key: string, maxRequests: number, windowMs: number): boolean {
const now = Date.now()
const record = rateMap.get(key)
if (!record || now > record.resetAt) {
rateMap.set(key, { count: 1, resetAt: now + windowMs })
return true // Permitir
}
if (record.count >= maxRequests) {
return false // Restringir
}
record.count++
return true
}
// app/api/rate-limited/route.ts — Uso do Limitador de Taxa
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export async function GET(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
const allowed = rateLimit(ip, 10, 60_000) // 10 por minuto
if (!allowed) {
return NextResponse.json(
{ error: 'Muitas requisições' },
{ status: 429, headers: { 'Retry-After': '60' } }
)
}
return NextResponse.json({ message: 'Sucesso', timestamp: Date.now() })
}
▶ Exemplo: CORS + Limitação de Taxa Completo (Dificuldade: ⭐⭐⭐)
Saída:
GET app/api/rate-limited/route.ts → Retorna dados como JSON. Limitado por taxa para prevenir abusos.
// app/api/secure/route.ts — CORS + Limite de Taxa + Auth
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(2).max(200),
content: z.string().min(10),
})
export async function POST(request: NextRequest) {
// 1. Limite de Taxa
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
if (!rateLimit(ip, 20, 60_000)) {
return NextResponse.json({ error: 'Limite de taxa excedido' }, { status: 429 })
}
// 2. Verificação de Auth
const auth = request.headers.get('authorization')
if (!auth?.startsWith('Bearer ') || auth.slice(7) !== process.env.API_KEY) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 })
}
// 3. Validação
const body = await request.json()
const validated = postSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Falha na validação', details: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
// 4. Processar
const post = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify({ ...validated.data, userId: 1 }),
}).then(r => r.json())
return NextResponse.json(post, { status: 201, headers: {
'Access-Control-Allow-Origin': '*',
}})
}
export async function OPTIONS() {
return NextResponse.json({}, { headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}})
}
Saída:
POST app/api/secure/route.ts → Valida corpo da requisição com Zod, cria recurso, retorna 201.
8. Exemplo Completo: Serviço de API RESTful
// app/api/posts/route.ts — API CRUD de Posts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'
// ======== Esquemas ========
const createPostSchema = z.object({
title: z.string().min(2).max(200),
body: z.string().min(10).max(5000),
userId: z.number().int().positive(),
})
const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(50).default(10),
})
const API = 'https://jsonplaceholder.typicode.com'
// ======== GET /api/posts?page=1&limit=10 ========
export async function GET(request: NextRequest) {
const query = querySchema.safeParse({
page: request.nextUrl.searchParams.get('page'),
limit: request.nextUrl.searchParams.get('limit'),
})
if (!query.success) {
return NextResponse.json(
{ error: 'Consulta inválida', details: query.error.flatten().fieldErrors },
{ status: 400 }
)
}
const { page, limit } = query.data
const posts = await fetch(`${API}/posts?_page=${page}&_limit=${limit}`).then(r => r.json())
const total = 100 // Total do JSONPlaceholder
return NextResponse.json({
data: posts,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }
})
}
// ======== POST /api/posts ========
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = createPostSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Falha na validação', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const post = await fetch(`${API}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
revalidateTag('posts')
return NextResponse.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts — CRUD de Artigo Único
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const updatePostSchema = z.object({
title: z.string().min(2).max(200).optional(),
body: z.string().min(10).max(5000).optional(),
})
const API = 'https://jsonplaceholder.typicode.com'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await fetch(`${API}/posts/${params.id}`).then(r => {
if (!r.ok) throw new Error('Não encontrado')
return r.json()
}).catch(() => null)
if (!post) {
return NextResponse.json({ error: 'Post não encontrado' }, { status: 404 })
}
return NextResponse.json(post)
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const validated = updatePostSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Falha na validação', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const updated = await fetch(`${API}/posts/${params.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
return NextResponse.json(updated)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await fetch(`${API}/posts/${params.id}`, { method: 'DELETE' })
return NextResponse.json({ success: true, id: params.id })
}
// middleware.ts — Middleware Global de API
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export function middleware(request: NextRequest) {
const { pathname, origin } = request.nextUrl
if (!pathname.startsWith('/api/')) {
return NextResponse.next()
}
// 1. Limitação de Taxa
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
if (!rateLimit(ip, 30, 60_000)) {
return NextResponse.json({ error: 'Muitas requisições' }, {
status: 429,
headers: { 'Retry-After': '60' }
})
}
// 2. Logging
console.log(`[API] ${request.method} ${pathname}`)
// 3. CORS
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: response.headers })
}
return response
}
export const config = {
matcher: '/api/:path*',
}
❓ Perguntas Frequentes
P: Qual é a diferença entre um Route Handler e uma Server Action? R: Um Route Handler é um endpoint HTTP padrão definido usando
route.ts, adequado para integração com API de terceiros, webhooks e chamadas mobile. Uma Server Action é uma função do lado do servidor estilo RPC definida usando'use server', adequada para operações de formulário próprias. As principais diferenças são: Route Handlers exigem chamadas HTTP, enquanto Server Actions são chamadas de função; Route Handlers não incluem proteção CSRF, enquanto Server Actions a têm integrada.
P: Qual é a diferença entre
NextResponse.jsone simplesmentenew Response()? R:NextResponse.jsoné um método de conveniência doNextResponseque define automaticamenteContent-Type: application/jsone fornece melhor inferência de tipo TypeScript.new Response(JSON.stringify(data), { headers: {'Content-Type': 'application/json'} })é equivalente. Recomendamos usarNextResponse.jsonpara manter seu código conciso.
P: O
middleware.tspode ler o banco de dados? R: Sim, mas esteja atento ao desempenho — o middleware executa para cada requisição correspondente. Consultas ao banco de dados aumentam a latência. Recomenda-se realizar apenas operações leves no middleware (como analisar cookies, verificar cabeçalhos e redirecionar). Operações de banco de dados devem ser colocadas em route handlers ou server actions.
P: O Route Handler suporta Edge Runtime? R: Sim, suporta. Por padrão, o Route Handler executa no Node.js Runtime, mas você pode alternar para o Edge Runtime via
export const runtime = 'edge'. O Edge Runtime oferece menor latência, mas tem suporte limitado a APIs (não suporta módulos nativos do Node.js como fs e crypto). O Edge Runtime é adequado para proxy simples, verificação de autenticação e testes A/B.
P: Como posso proteger endpoints de API contra abusos? R: Três camadas de proteção: ① Limitação de taxa baseada em IP na camada de middleware; ② Autenticação com chave de API/JWT na camada de route handler; ③ Restrições globais de CORS nas origens permitidas. Em ambientes de produção, recomendamos usar um limitador de taxa baseado em Redis (como
upstash/ratelimit) para suportar compartilhamento de estado entre múltiplas instâncias.
P: Como obtenho o endereço IP do cliente em um Route Handler? R: Use
request.headerspara obtê-lo:request.headers.get('x-forwarded-for')?.split(',')[0]?.trim()ourequest.headers.get('x-real-ip'). A Vercel adiciona automaticamente esses cabeçalhos durante o deploy. Ao desenvolver localmente, pode retornar::1ou127.0.0.1.
📖 Resumo
- O arquivo
route.tsdefine endpoints RESTful usando funções como GET, POST, PUT e DELETE - Zod valida o corpo da requisição e os parâmetros de consulta, retornando um código de status 400 e informações detalhadas de erro
NextResponsesuporta quatro formatos de resposta: json(), redirect(), next() e rewrite()- middleware.ts trata autenticação, logging e CORS uniformemente antes que as requisições cheguem às páginas ou APIs
- A configuração
matchercontrola o escopo de correspondência de caminho do middleware - Configurando CORS via cabeçalhos de resposta no middleware ou route handlers
- Limitação de Taxa: Implementada usando um mapa em memória ou Redis (Upstash Ratelimit é recomendado para ambientes de produção)
📝 Exercícios
-
Exercício Básico (⭐): Crie
app/api/health/route.tspara retornar informações de verificação de saúde em JSON (status, timestamp, uptime). Adicione middleware para registrar cada chamada de API e seu tempo de resposta. -
Exercício Avançado (⭐⭐): Construa uma API CRUD completa:
app/api/books/route.ts(GET para listar + POST para criar) +app/api/books/[id]/route.ts(GET para ver detalhes + PUT para atualizar + DELETE para excluir). Use Zod para validar o corpo da requisição. Adicione autenticação por chave de API no middleware. -
Desafio (⭐⭐⭐): Implemente uma API de "serviço de URL curta":
app/api/shorten/route.ts(POST aceita uma URL e retorna uma URL curta),app/api/[code]/route.ts(GET recupera uma URL curta e realiza um redirecionamento 302). Adicione limitação de taxa (10 links curtos por minuto por IP), suporte CORS e logs de acesso no middleware. Armazene códigos curtos em um arquivo JSON ou em um mapa em memória.