API Routes - Introducción
NextJS te permite crear APIs REST directamente en tu aplicación, sin necesidad de un servidor separado como Express o Fastify.
¿Qué son API Routes?
Son endpoints HTTP que defines en tu aplicación NextJS:
// app/api/productos/route.ts
export async function GET() {
return Response.json({ productos: ['Camisa', 'Pantalón'] })
}
Accesible en: https://tudominio.com/api/productos
¿Por qué usar API Routes?
Ventajas
✅ Todo en un proyecto
- No necesitas servidor separado
- Frontend y backend en el mismo código
- Despliegas una sola vez
✅ TypeScript compartido
- Tipos compartidos entre frontend y backend
- Menos errores, mejor DX
✅ Acceso directo a tu base de datos
- Consultas directas desde las rutas
- Sin necesidad de crear capas intermedias
✅ Serverless por defecto
- Escala automáticamente
- Pagas solo por lo que usas
Desventajas
❌ Acoplamiento
- Frontend y backend juntos
- Más difícil separar después
❌ No ideal para APIs grandes
- Si tu API es compleja, considera un servidor dedicado
- GraphQL puede ser mejor para APIs complejas
Dos formas de crear APIs en NextJS
NextJS ofrece dos formas diferentes de crear endpoints:
1. Route Handlers (API Routes tradicionales)
Para APIs REST completas:
// app/api/productos/route.ts
export async function GET() {
const productos = await db.producto.findMany()
return Response.json(productos)
}
export async function POST(request: Request) {
const body = await request.json()
const producto = await db.producto.create({ data: body })
return Response.json(producto, { status: 201 })
}
Características:
- Control total del HTTP response
- Headers, status codes, cookies
- Métodos: GET, POST, PUT, DELETE, PATCH
- Perfecto para APIs públicas
2. Server Actions
Para mutaciones desde tu aplicación:
// app/actions.ts
'use server'
export async function crearProducto(formData: FormData) {
const producto = await db.producto.create({
data: {
nombre: formData.get('nombre'),
precio: Number(formData.get('precio'))
}
})
revalidatePath('/productos')
return producto
}
Características:
- Integración directa con formularios
- Revalidación de cache automática
- Tipos de TypeScript automáticos
- Más simple para casos comunes
¿Cuándo usar cada uno?
Usa Route Handlers para:
✅ APIs públicas
// Otros servicios consumen tu API
// GET /api/v1/productos
✅ Webhooks
// Stripe, GitHub, etc. te envían notificaciones
// POST /api/webhooks/stripe
✅ Proxy a APIs externas
// Ocultar API keys del cliente
// GET /api/weather → llama a API externa
✅ OAuth callbacks
// GitHub, Google login
// GET /api/auth/callback
✅ Subida de archivos
// POST /api/upload
✅ Control total de response
// Necesitas headers específicos, streaming, etc.
Usa Server Actions para:
✅ Formularios
// Crear, actualizar, eliminar datos desde forms
✅ Mutaciones simples
// Acciones que solo tu app usa
✅ Revalidación integrada
// Necesitas actualizar cache después de mutar
✅ La mayoría de casos
// Server Actions son más simples
Comparación lado a lado
Mismo ejemplo con ambos enfoques:
Con Route Handler
// app/api/productos/route.ts
import { NextRequest } from 'next/server'
import { revalidatePath } from 'next/cache'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validar
if (!body.nombre || !body.precio) {
return Response.json(
{ error: 'Datos inválidos' },
{ status: 400 }
)
}
// Crear
const producto = await db.producto.create({
data: {
nombre: body.nombre,
precio: body.precio
}
})
// Revalidar
revalidatePath('/productos')
return Response.json(producto, { status: 201 })
} catch (error) {
return Response.json(
{ error: 'Error del servidor' },
{ status: 500 }
)
}
}
Cliente:
'use client'
async function handleSubmit(e) {
e.preventDefault()
const res = await fetch('/api/productos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
nombre: 'Camisa',
precio: 25
})
})
const data = await res.json()
}
Con Server Action
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
const ProductoSchema = z.object({
nombre: z.string().min(3),
precio: z.number().positive()
})
export async function crearProducto(formData: FormData) {
// Validar
const datos = ProductoSchema.parse({
nombre: formData.get('nombre'),
precio: Number(formData.get('precio'))
})
// Crear
const producto = await db.producto.create({
data: datos
})
// Revalidar (integrado)
revalidatePath('/productos')
return producto
}
Cliente:
'use client'
import { crearProducto } from '@/app/actions'
export default function FormularioProducto() {
return (
<form action={crearProducto}>
<input name="nombre" required />
<input name="precio" type="number" required />
<button type="submit">Crear</button>
</form>
)
}
Server Actions es más simple para este caso.
Tabla de decisión
Característica | Route Handler | Server Action |
---|---|---|
Setup | Más código | Menos código |
Formularios | Manual con fetch | Integración directa |
Validación | Manual | Con Zod más fácil |
Types | Manual | Automático |
Cache | Manual revalidate | Integrado |
APIs públicas | ✅ Perfecto | ❌ No |
Webhooks | ✅ Sí | ❌ No |
Control HTTP | ✅ Total | ❌ Limitado |
Simplicidad | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Estructura de archivos
app/
├── api/ # Route Handlers
│ ├── productos/
│ │ ├── route.ts # /api/productos
│ │ └── [id]/
│ │ └── route.ts # /api/productos/[id]
│ ├── webhooks/
│ │ └── stripe/
│ │ └── route.ts # /api/webhooks/stripe
│ └── auth/
│ └── [...nextauth]/
│ └── route.ts # /api/auth/*
│
└── actions.ts # Server Actions
Recomendación
Para la mayoría de casos, empieza con Server Actions. Son más simples y cubren el 80% de necesidades.
Solo usa Route Handlers cuando:
- Necesitas una API pública
- Recibes webhooks
- Necesitas control total del HTTP response
- Integras con OAuth
Si empiezas con Server Actions y luego necesitas más control, siempre puedes migrar a Route Handlers.
Autenticación y seguridad
Ambos enfoques soportan autenticación:
Route Handler
export async function GET(request: Request) {
const token = request.headers.get('Authorization')
const user = await verifyToken(token)
// ...
}
Server Action
'use server'
import { auth } from '@/lib/auth'
export async function deleteProducto(id: string) {
const session = await auth()
if (!session) throw new Error('No autenticado')
// ...
}
Resumen
API Routes en NextJS:
- Dos formas: Route Handlers y Server Actions
- Route Handlers = APIs REST tradicionales
- Server Actions = Mutaciones simples desde tu app
Cuándo usar cada uno:
- Route Handlers: APIs públicas, webhooks, OAuth
- Server Actions: Todo lo demás (mayoría de casos)
Próximos pasos:
- Route Handlers - Aprende a crear APIs REST completas
- Request y Response - Maneja requests HTTP en detalle