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:

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

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

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

tsx
// Otros servicios consumen tu API
// GET /api/v1/productos

Webhooks

tsx
// Stripe, GitHub, etc. te envían notificaciones
// POST /api/webhooks/stripe

Proxy a APIs externas

tsx
// Ocultar API keys del cliente
// GET /api/weather → llama a API externa

OAuth callbacks

tsx
// GitHub, Google login
// GET /api/auth/callback

Subida de archivos

tsx
// POST /api/upload

Control total de response

tsx
// Necesitas headers específicos, streaming, etc.

Usa Server Actions para:

Formularios

tsx
// Crear, actualizar, eliminar datos desde forms

Mutaciones simples

tsx
// Acciones que solo tu app usa

Revalidación integrada

tsx
// Necesitas actualizar cache después de mutar

La mayoría de casos

tsx
// Server Actions son más simples

Comparación lado a lado

Mismo ejemplo con ambos enfoques:

Con Route Handler

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

tsx
'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

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

tsx
'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ísticaRoute HandlerServer Action
SetupMás códigoMenos código
FormulariosManual con fetchIntegración directa
ValidaciónManualCon Zod más fácil
TypesManualAutomático
CacheManual revalidateIntegrado
APIs públicasPerfectoNo
WebhooksNo
Control HTTPTotalLimitado
Simplicidad3/55/5

Estructura de archivos

plaintext
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

tsx
export async function GET(request: Request) {
  const token = request.headers.get('Authorization')
  const user = await verifyToken(token)
  // ...
}

Server Action

tsx
'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: