API bem projetada = facilita integrations, atrai desenvolvedores, escala sem refatoração.
API mal projetada = bugs constantes, performance ruim, impossível de manter.
Este artigo compara REST, GraphQL e Webhooks, com código completo em Next.js + TypeScript.
REST API (o padrão de facto)
REST (Representational State Transfer) = arquitetura baseada em recursos e verbos HTTP.
Princípios:
- Recursos identificados por URLs (
/api/users/123) - Verbos HTTP semânticos (GET, POST, PUT, DELETE)
- Stateless (sem sessão no servidor)
- Respostas cacheáveis
Exemplo completo (CRUD de usuários):
// app/api/users/route.ts
import { NextRequest } from 'next/server'
import { z } from 'zod'
// Schema de validação
const userSchema = z.object({
name: z.string().min(3).max(100),
email: z.string().email(),
role: z.enum(['USER', 'ADMIN'])
})
// GET /api/users (listar)
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const role = searchParams.get('role') // Filtro opcional
const where = role ? { role } : {}
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
}),
prisma.user.count({ where })
])
return Response.json({
data: users,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
})
}
// POST /api/users (criar)
export async function POST(req: NextRequest) {
const body = await req.json()
// Validação
const validation = userSchema.safeParse(body)
if (!validation.success) {
return Response.json(
{ error: 'Validation failed', details: validation.error.flatten() },
{ status: 400 }
)
}
const { name, email, role } = validation.data
// Verifica duplicação
const existing = await prisma.user.findUnique({ where: { email } })
if (existing) {
return Response.json(
{ error: 'Email already exists' },
{ status: 409 }
)
}
// Cria usuário
const user = await prisma.user.create({
data: { name, email, role }
})
return Response.json(user, { status: 201 })
}
// app/api/users/[id]/route.ts
// GET /api/users/123 (buscar)
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await prisma.user.findUnique({
where: { id: params.id },
include: {
bookings: true, // Relações
_count: {
select: { bookings: true }
}
}
})
if (!user) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
)
}
return Response.json(user)
}
// PATCH /api/users/123 (atualizar parcial)
export async function PATCH(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await req.json()
// Validação parcial (campos opcionais)
const updateSchema = userSchema.partial()
const validation = updateSchema.safeParse(body)
if (!validation.success) {
return Response.json(
{ error: 'Validation failed', details: validation.error.flatten() },
{ status: 400 }
)
}
const user = await prisma.user.update({
where: { id: params.id },
data: validation.data
})
return Response.json(user)
}
// DELETE /api/users/123 (deletar)
export async function DELETE(
req: NextRequest,
{ params }: { params: { id: string } }
) {
await prisma.user.delete({
where: { id: params.id }
})
return Response.json({ success: true }, { status: 204 })
}
Vantagens REST:
- ✅ Simples e intuitivo
- ✅ Cacheable (HTTP standard)
- ✅ Tooling maduro (Postman, Insomnia)
- ✅ Amplamente adotado
Desvantagens REST:
- ❌ Over-fetching (traz dados não usados)
- ❌ Under-fetching (precisa múltiplas requests)
- ❌ Versionamento complexo (/api/v1, /api/v2)
GraphQL (flexibilidade de queries)
GraphQL = Client define exatamente quais dados quer.
Setup (Next.js + Apollo Server):
npm install @apollo/server graphql
// app/api/graphql/route.ts
import { ApolloServer } from '@apollo/server'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import { gql } from 'graphql-tag'
// Schema (tipos e queries)
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
role: Role!
bookings: [Booking!]!
bookingsCount: Int!
}
type Booking {
id: ID!
date: String!
status: BookingStatus!
user: User!
}
enum Role {
USER
ADMIN
}
enum BookingStatus {
PENDING
CONFIRMED
COMPLETED
CANCELED
}
type Query {
users(page: Int, limit: Int, role: Role): UsersResponse!
user(id: ID!): User
bookings(userId: ID, status: BookingStatus): [Booking!]!
}
type Mutation {
createUser(name: String!, email: String!, role: Role!): User!
updateUser(id: ID!, name: String, email: String): User!
deleteUser(id: ID!): Boolean!
}
type UsersResponse {
data: [User!]!
meta: PaginationMeta!
}
type PaginationMeta {
page: Int!
limit: Int!
total: Int!
totalPages: Int!
}
`
// Resolvers (lógica)
const resolvers = {
Query: {
users: async (_: any, { page = 1, limit = 20, role }: any) => {
const where = role ? { role } : {}
const [data, total] = await Promise.all([
prisma.user.findMany({
where,
skip: (page - 1) * limit,
take: limit
}),
prisma.user.count({ where })
])
return {
data,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
},
user: async (_: any, { id }: { id: string }) => {
return prisma.user.findUnique({ where: { id } })
},
bookings: async (_: any, { userId, status }: any) => {
return prisma.booking.findMany({
where: {
...(userId && { userId }),
...(status && { status })
}
})
}
},
Mutation: {
createUser: async (_: any, { name, email, role }: any) => {
return prisma.user.create({
data: { name, email, role }
})
},
updateUser: async (_: any, { id, ...data }: any) => {
return prisma.user.update({
where: { id },
data
})
},
deleteUser: async (_: any, { id }: { id: string }) => {
await prisma.user.delete({ where: { id } })
return true
}
},
// Resolvers de campos (relações)
User: {
bookings: async (parent: any) => {
return prisma.booking.findMany({
where: { userId: parent.id }
})
},
bookingsCount: async (parent: any) => {
return prisma.booking.count({
where: { userId: parent.id }
})
}
},
Booking: {
user: async (parent: any) => {
return prisma.user.findUnique({
where: { id: parent.userId }
})
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
const handler = startServerAndCreateNextHandler(server)
export { handler as GET, handler as POST }
Query no client:
// Cliente GraphQL
const QUERY = gql`
query GetUsers($page: Int, $limit: Int) {
users(page: $page, limit: $limit) {
data {
id
name
email
bookingsCount # ← Conta sem trazer todos os bookings
}
meta {
total
totalPages
}
}
}
`
const { data } = await apolloClient.query({
query: QUERY,
variables: { page: 1, limit: 20 }
})
Vantagens GraphQL:
- ✅ Sem over-fetching (client pede exato o que quer)
- ✅ Sem under-fetching (1 request traz tudo)
- ✅ Fortemente tipado (schema)
- ✅ Introspection (auto-documentação)
Desvantagens GraphQL:
- ❌ Complexidade inicial (setup, tooling)
- ❌ Caching mais difícil (não usa HTTP cache)
- ❌ N+1 queries (precisa DataLoader)
- ❌ Over-engineering para MVPs simples
Webhooks (notificações assíncronas)
Webhook = API chama SEU servidor quando algo acontece.
Exemplo: Stripe webhook após pagamento.
// app/api/webhooks/stripe/route.ts
import { NextRequest } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
export async function POST(req: NextRequest) {
const body = await req.text()
const signature = req.headers.get('stripe-signature')!
let event: Stripe.Event
try {
// Valida assinatura (segurança)
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
} catch (err) {
console.error('Webhook signature verification failed:', err)
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
// Processa evento
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object as Stripe.PaymentIntent
await handlePaymentSuccess(paymentIntent)
break
case 'payment_intent.payment_failed':
const failedPayment = event.data.object as Stripe.PaymentIntent
await handlePaymentFailure(failedPayment)
break
case 'customer.subscription.created':
const subscription = event.data.object as Stripe.Subscription
await handleSubscriptionCreated(subscription)
break
case 'customer.subscription.deleted':
const deletedSub = event.data.object as Stripe.Subscription
await handleSubscriptionCanceled(deletedSub)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
// Sempre retorna 200 (ou Stripe reenvia)
return Response.json({ received: true })
}
async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) {
const bookingId = paymentIntent.metadata.bookingId
await prisma.booking.update({
where: { id: bookingId },
data: { status: 'CONFIRMED', paidAt: new Date() }
})
// Envia confirmação por email
await sendEmail({
to: paymentIntent.metadata.customerEmail,
subject: 'Pagamento confirmado!',
template: 'booking-confirmed'
})
}
Webhook próprio (notificar parceiros):
// lib/webhooks/send.ts
import crypto from 'crypto'
export async function sendWebhook(url: string, payload: any, secret: string) {
const timestamp = Date.now()
const signature = generateSignature(payload, timestamp, secret)
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': timestamp.toString()
},
body: JSON.stringify(payload)
})
if (!response.ok) {
throw new Error(`Webhook failed: ${response.status}`)
}
return response
}
function generateSignature(payload: any, timestamp: number, secret: string): string {
const data = `${timestamp}.${JSON.stringify(payload)}`
return crypto.createHmac('sha256', secret).update(data).digest('hex')
}
// Uso: notifica parceiro quando booking é confirmado
await sendWebhook(
partner.webhookUrl,
{
event: 'booking.confirmed',
data: { bookingId: '123', date: '2026-12-10' }
},
partner.webhookSecret
)
Retry lógico (se webhook falhar):
// lib/webhooks/retry.ts
export async function sendWebhookWithRetry(
url: string,
payload: any,
secret: string,
maxRetries = 3
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await sendWebhook(url, payload, secret)
console.log(`Webhook sent successfully (attempt ${attempt})`)
return
} catch (error) {
console.error(`Webhook failed (attempt ${attempt}/${maxRetries})`, error)
if (attempt === maxRetries) {
// Após 3 tentativas, salva para reprocessamento manual
await prisma.failedWebhook.create({
data: {
url,
payload,
error: error.message,
attempts: maxRetries
}
})
throw error
}
// Backoff exponencial (1s, 2s, 4s)
await sleep(1000 * Math.pow(2, attempt - 1))
}
}
}
Rate limiting (proteção de API)
Sem rate limit = API pode ser abusada (DDoS, scraping).
Implementação (Redis + upstash):
npm install @upstash/ratelimit @upstash/redis
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const redis = Redis.fromEnv()
// 10 requests por 10 segundos
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true
})
// Uso em API route
export async function GET(req: NextRequest) {
const ip = req.ip ?? '127.0.0.1'
const { success, limit, remaining, reset } = await ratelimit.limit(ip)
if (!success) {
return Response.json(
{
error: 'Rate limit exceeded',
limit,
remaining,
reset: new Date(reset)
},
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString()
}
}
)
}
// Continue processamento
const data = await fetchData()
return Response.json(data)
}
Rate limiting por tier (planos diferentes):
// lib/rate-limit-tier.ts
export function getRateLimitForPlan(plan: string) {
const limits = {
FREE: Ratelimit.slidingWindow(10, '10 s'), // 10/10s
PRO: Ratelimit.slidingWindow(100, '10 s'), // 100/10s
ENTERPRISE: Ratelimit.slidingWindow(1000, '10 s') // 1000/10s
}
return new Ratelimit({
redis,
limiter: limits[plan] || limits.FREE
})
}
API versioning (retrocompatibilidade)
Opção 1: URL versioning
/api/v1/users
/api/v2/users
// app/api/v1/users/route.ts
export async function GET() {
// Versão antiga (deprecated)
}
// app/api/v2/users/route.ts
export async function GET() {
// Versão nova
}
Opção 2: Header versioning
export async function GET(req: NextRequest) {
const version = req.headers.get('X-API-Version') || 'v1'
if (version === 'v2') {
return handleV2()
} else {
return handleV1()
}
}
Deprecation warning:
export async function GET() {
const data = await fetchUsers()
return Response.json(data, {
headers: {
'X-API-Deprecated': 'true',
'X-API-Sunset': '2027-01-01', // Data de desligamento
'Link': '</api/v2/users>; rel="successor-version"'
}
})
}
Documentação automática
Swagger/OpenAPI (REST):
npm install swagger-ui-react swagger-jsdoc
// lib/swagger.ts
import swaggerJsdoc from 'swagger-jsdoc'
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Conecta Prof API',
version: '1.0.0',
description: 'API para marketplace de professores'
},
servers: [
{ url: 'https://api.conectaprof.com' }
]
},
apis: ['./app/api/**/*.ts'] // Scan arquivos
}
export const swaggerSpec = swaggerJsdoc(options)
// app/api/users/route.ts
/**
* @swagger
* /api/users:
* get:
* summary: Lista usuários
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: Número da página
* responses:
* 200:
* description: Lista de usuários
*/
export async function GET(req: NextRequest) {
// ...
}
GraphQL Playground (auto-documentado):
Acesse /api/graphql e GraphQL Playground mostra schema interativo.
Quando usar cada padrão
Use REST se:
- API CRUD simples
- Precisa caching HTTP
- Integrações B2B (padrão da indústria)
- MVP rápido
Use GraphQL se:
- Client precisa flexibilidade (mobile, web, desktop)
- Múltiplas relações (evitar N requests)
- API pública com muitos consumers
- Real-time (subscriptions)
Use Webhooks se:
- Notificações assíncronas (pagamento aprovado, pedido enviado)
- Integrações entre sistemas
- Evitar polling (ineficiente)
Checklist de API production-ready
Segurança:
- HTTPS obrigatório
- Rate limiting implementado
- Autenticação (JWT ou session)
- Validação de input (Zod, Joi)
- CORS configurado
- Webhook signature validation
Performance:
- Pagination em listas
- Cache headers configurados
- Database índices otimizados
- N+1 queries resolvidos (DataLoader em GraphQL)
Developer Experience:
- Documentação (Swagger ou GraphQL Playground)
- Error messages claros
- Exemplos de requests/responses
- SDKs para linguagens principais (TypeScript, Python)
Monitoramento:
- Logging de requests (método, path, status, latência)
- Alertas de error rate
- Dashboard de métricas (RPS, P95 latency)
Próximos passos
- Escolher padrão (REST para MVP)
- Setup validação (Zod)
- Implementar rate limiting
- Configurar CORS
- Documentar endpoints (Swagger)
- Adicionar testes (supertest)
- Monitorar performance (Datadog/Sentry)
Lembre-se: API é contrato. Mudanças quebram clientes. Versione adequadamente.