Route Handlers

Route Handlers te permiten crear API endpoints dentro de tu app NextJS. En vez de una API separada, tus endpoints viven junto a tu frontend.

Crear un endpoint

Crea un archivo route.ts dentro de una carpeta en app/:

code
app/
├── api/
│   ├── productos/
│   │   └── route.ts        → GET/POST /api/productos
│   └── productos/
│       └── [id]/
│           └── route.ts    → GET/PUT/DELETE /api/productos/123

GET

tsx
// app/api/productos/route.ts
import { NextResponse } from "next/server"

export async function GET() {
  const productos = await db.producto.findMany()

  return NextResponse.json(productos)
}

POST

tsx
export async function POST(request: Request) {
  const body = await request.json()

  const producto = await db.producto.create({
    data: {
      nombre: body.nombre,
      precio: body.precio,
    },
  })

  return NextResponse.json(producto, { status: 201 })
}

PUT y DELETE con params

tsx
// app/api/productos/[id]/route.ts
import { NextResponse } from "next/server"

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const producto = await db.producto.findUnique({ where: { id } })

  if (!producto) {
    return NextResponse.json(
      { error: "Producto no encontrado" },
      { status: 404 }
    )
  }

  return NextResponse.json(producto)
}

export async function PUT(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const body = await request.json()

  const producto = await db.producto.update({
    where: { id },
    data: body,
  })

  return NextResponse.json(producto)
}

export async function DELETE(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params

  await db.producto.delete({ where: { id } })

  return new Response(null, { status: 204 })
}

v16Cambio En v16, params en route handlers tambien es Promise (igual que en pages).

Query parameters

tsx
// GET /api/productos?categoria=ropa&orden=precio
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const categoria = searchParams.get("categoria")
  const orden = searchParams.get("orden") || "createdAt"

  const productos = await db.producto.findMany({
    where: categoria ? { categoria } : undefined,
    orderBy: { [orden]: "asc" },
  })

  return NextResponse.json(productos)
}

Headers y cookies

tsx
import { NextResponse } from "next/server"
import { cookies, headers } from "next/headers"

export async function GET() {
  // Leer headers
  const headersList = await headers()
  const userAgent = headersList.get("user-agent")

  // Leer cookies
  const cookieStore = await cookies()
  const token = cookieStore.get("auth-token")

  // Responder con headers custom
  return NextResponse.json(
    { data: "ok" },
    {
      headers: {
        "Cache-Control": "no-store",
        "X-Custom-Header": "valor",
      },
    }
  )
}

Validacion

Siempre valida los datos que llegan del cliente:

tsx
import { NextResponse } from "next/server"
import { z } from "zod"

const productoSchema = z.object({
  nombre: z.string().min(2),
  precio: z.number().positive(),
  categoriaId: z.string().uuid(),
})

export async function POST(request: Request) {
  const body = await request.json()
  const result = productoSchema.safeParse(body)

  if (!result.success) {
    return NextResponse.json(
      { errors: result.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const producto = await db.producto.create({
    data: result.data,
  })

  return NextResponse.json(producto, { status: 201 })
}

Ejemplo: API CRUD completa

tsx
// app/api/productos/route.ts
import { NextResponse } from "next/server"
import { z } from "zod"

const crearSchema = z.object({
  nombre: z.string().min(2),
  precio: z.number().positive(),
  descripcion: z.string().optional(),
})

// GET /api/productos — Lista todos
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const page = Number(searchParams.get("page")) || 1
  const limit = 20
  const skip = (page - 1) * limit

  const [productos, total] = await Promise.all([
    db.producto.findMany({ skip, take: limit, orderBy: { createdAt: "desc" } }),
    db.producto.count(),
  ])

  return NextResponse.json({
    data: productos,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit),
    },
  })
}

// POST /api/productos — Crea uno nuevo
export async function POST(request: Request) {
  const body = await request.json()
  const result = crearSchema.safeParse(body)

  if (!result.success) {
    return NextResponse.json(
      { errors: result.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const producto = await db.producto.create({ data: result.data })
  return NextResponse.json(producto, { status: 201 })
}
Route Handlers vs Server Actions

Usa Route Handlers cuando necesitas una API REST que otros servicios puedan consumir. Usa Server Actions cuando la mutation es solo desde tu frontend (formularios, botones).