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í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ú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: