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



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.ts para gerenciar centralizadamente autenticação, logging e CORS — um único arquivo controla todo o ciclo de vida de todas as requisições.

TS
// 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

TS
// 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

TS
// 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

TS
// 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:

TEXT 📖 Somente leitura
POST app/api/items/route.ts → Cria um novo recurso, retorna item criado com status 201.
TS
// 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:

TEXT 📖 Somente leitura
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.
TS
// 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

TS
// 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

TS
// 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:

TEXT 📖 Somente leitura
GET app/api/items/route.ts → Valida parâmetros de consulta com Zod, retorna resultados filtrados como JSON.
TS
// 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:

TEXT 📖 Somente leitura
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

TS
// app/api/auth/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  return NextResponse.json({ status: 'healthy', uptime: process.uptime() })
}

(2) Redirecionamento

TS
// 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)

TS
// 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:

TEXT 📖 Somente leitura
Middleware intercepts requests and rewrites URLs based on conditions.
TS
// 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:

TEXT 📖 Somente leitura
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.

100%
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

TS
// 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

TS
// 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:

TEXT 📖 Somente leitura
Middleware intercepts requests and redirects based on conditions.
TS
// 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:

TEXT 📖 Somente leitura
[${new Date(


7. CORS e Limitação de Taxa

(1) Configuração CORS

TS
// 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

TS
// 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

TS
// 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
}
TS
// 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:

TEXT 📖 Somente leitura
GET app/api/rate-limited/route.ts → Retorna dados como JSON. Limitado por taxa para prevenir abusos.
TS
// 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:

TEXT 📖 Somente leitura
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

TS
// 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 })
}
TS
// 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 })
}
TS
// 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.json e simplesmente new Response()? R: NextResponse.json é um método de conveniência do NextResponse que define automaticamente Content-Type: application/json e fornece melhor inferência de tipo TypeScript. new Response(JSON.stringify(data), { headers: {'Content-Type': 'application/json'} }) é equivalente. Recomendamos usar NextResponse.json para manter seu código conciso.

P: O middleware.ts pode 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.headers para obtê-lo: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ou request.headers.get('x-real-ip'). A Vercel adiciona automaticamente esses cabeçalhos durante o deploy. Ao desenvolver localmente, pode retornar ::1 ou 127.0.0.1.


📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Crie app/api/health/route.ts para 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.

  2. 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.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%