Request y Response

Los objetos Request y Response son estándares de Web APIs que NextJS usa en Route Handlers. Aquí aprenderás todo lo que puedes hacer con ellos.

Request Object

El objeto Request contiene toda la información sobre la petición HTTP que recibe tu API.

Anatomía de Request

export async function GET(request: Request) {
  // URL completa
  console.log(request.url)  // https://mitienda.com/api/productos?categoria=ropa
  
  // Método HTTP
  console.log(request.method)  // GET
  
  // Headers
  console.log(request.headers.get('Authorization'))
  
  // Body (solo POST, PUT, PATCH)
  const body = await request.json()
  
  return Response.json({ ok: true })
}

NextRequest (versión mejorada)

NextJS provee NextRequest con funcionalidades extra:

import { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  // URL parseada
  console.log(request.nextUrl.pathname)     // /api/productos
  console.log(request.nextUrl.searchParams) // URLSearchParams object
  
  // IP del cliente
  console.log(request.ip)  // 192.168.1.1
  
  // Geolocalización (con middleware)
  console.log(request.geo?.city)
  console.log(request.geo?.country)
  
  // Cookies (más fácil)
  const token = request.cookies.get('auth-token')
  
  return Response.json({ ok: true })
}

Headers

Leer headers

export async function GET(request: Request) {
  // Header específico
  const auth = request.headers.get('Authorization')
  const userAgent = request.headers.get('User-Agent')
  const contentType = request.headers.get('Content-Type')
  
  // Verificar si existe
  if (request.headers.has('Authorization')) {
    console.log('Usuario autenticado')
  }
  
  // Iterar todos los headers
  request.headers.forEach((value, key) => {
    console.log(`${key}: ${value}`)
  })
  
  return Response.json({ ok: true })
}

Headers comunes

export async function POST(request: Request) {
  // Authorization
  const token = request.headers.get('Authorization')?.replace('Bearer ', '')
  
  // Content-Type
  const contentType = request.headers.get('Content-Type')
  
  // User-Agent (navegador del usuario)
  const userAgent = request.headers.get('User-Agent')
  
  // Accept (qué formatos acepta el cliente)
  const accept = request.headers.get('Accept')
  
  // Origin (de dónde viene el request)
  const origin = request.headers.get('Origin')
  
  return Response.json({ token, contentType, userAgent })
}

Ejemplo: API key authentication

export async function GET(request: Request) {
  const apiKey = request.headers.get('X-API-Key')
  
  if (!apiKey) {
    return Response.json(
      { error: 'API key requerida' },
      { status: 401 }
    )
  }
  
  if (apiKey !== process.env.API_KEY) {
    return Response.json(
      { error: 'API key inválida' },
      { status: 401 }
    )
  }
  
  // API key válida, continuar
  const data = await fetchData()
  return Response.json(data)
}

Body (cuerpo de la petición)

JSON

export async function POST(request: Request) {
  // Parsear JSON del body
  const body = await request.json()
  
  console.log(body.nombre)
  console.log(body.precio)
  
  return Response.json({ received: body })
}

Cliente:

fetch('/api/productos', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    nombre: 'Camisa',
    precio: 25
  })
})

FormData

export async function POST(request: Request) {
  const formData = await request.formData()
  
  // Obtener valores
  const nombre = formData.get('nombre')
  const precio = formData.get('precio')
  const imagen = formData.get('imagen') as File
  
  console.log(nombre, precio, imagen.name)
  
  return Response.json({ ok: true })
}

Cliente:

const form = new FormData()
form.append('nombre', 'Camisa')
form.append('precio', '25')
form.append('imagen', file)

fetch('/api/productos', {
  method: 'POST',
  body: form,  // No especifiques Content-Type
})

Text

export async function POST(request: Request) {
  // Leer como texto plano
  const text = await request.text()
  
  console.log(text)  // String crudo
  
  return Response.json({ length: text.length })
}

Blob

export async function POST(request: Request) {
  // Leer como blob (archivos binarios)
  const blob = await request.blob()
  
  console.log(blob.type)
  console.log(blob.size)
  
  return Response.json({ ok: true })
}
⚠️
Solo puedes leer el body una vez
// ❌ Error: No puedes leer dos veces
export async function POST(request: Request) {
  const json = await request.json()
  const text = await request.text()  // Error!
}

// ✅ Bien: Solo lee una vez
export async function POST(request: Request) {
  const json = await request.json()
  // Trabaja solo con json
}

Una vez que lees el body con .json(), .text(), .formData(), etc., se consume y no puedes leerlo de nuevo.

URL y Query Params

NextRequest.nextUrl

import { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const { nextUrl } = request
  
  // Componentes de la URL
  console.log(nextUrl.href)       // https://mitienda.com/api/productos?cat=ropa&limit=10
  console.log(nextUrl.origin)     // https://mitienda.com
  console.log(nextUrl.pathname)   // /api/productos
  console.log(nextUrl.search)     // ?cat=ropa&limit=10
  
  // Search params (query parameters)
  const categoria = nextUrl.searchParams.get('categoria')
  const limit = nextUrl.searchParams.get('limit')
  
  return Response.json({ categoria, limit })
}

Leer query params

import { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  
  // Obtener un valor
  const categoria = searchParams.get('categoria')  // string | null
  
  // Obtener con valor por defecto
  const limit = Number(searchParams.get('limit')) || 10
  
  // Verificar si existe
  if (searchParams.has('destacado')) {
    console.log('Filtrar destacados')
  }
  
  // Obtener múltiples valores (array)
  const tags = searchParams.getAll('tag')
  // ?tag=nuevo&tag=oferta → ['nuevo', 'oferta']
  
  const productos = await db.producto.findMany({
    where: {
      categoriaId: categoria,
      tags: { hasSome: tags }
    },
    take: limit
  })
  
  return Response.json(productos)
}

Ejemplo: Paginación

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  
  const page = Number(searchParams.get('page')) || 1
  const limit = Number(searchParams.get('limit')) || 10
  const skip = (page - 1) * limit
  
  const [productos, total] = await Promise.all([
    db.producto.findMany({
      skip,
      take: limit,
      orderBy: { createdAt: 'desc' }
    }),
    db.producto.count()
  ])
  
  return Response.json({
    productos,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  })
}

Cliente:

// /api/productos?page=2&limit=20
const res = await fetch('/api/productos?page=2&limit=20')
const data = await res.json()

Ejemplo: Búsqueda y filtros

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  
  const q = searchParams.get('q')               // Búsqueda
  const categoria = searchParams.get('categoria')
  const minPrecio = Number(searchParams.get('min')) || 0
  const maxPrecio = Number(searchParams.get('max')) || Infinity
  const ordenar = searchParams.get('sort') || 'createdAt'
  
  const productos = await db.producto.findMany({
    where: {
      AND: [
        q ? {
          OR: [
            { nombre: { contains: q, mode: 'insensitive' } },
            { descripcion: { contains: q, mode: 'insensitive' } }
          ]
        } : {},
        categoria ? { categoriaId: categoria } : {},
        {
          precio: {
            gte: minPrecio,
            lte: maxPrecio
          }
        }
      ]
    },
    orderBy: { [ordenar]: 'desc' }
  })
  
  return Response.json(productos)
}

Cliente:

// /api/productos?q=camisa&categoria=ropa&min=20&max=50&sort=precio

Cookies

Leer cookies

import { cookies } from 'next/headers'

export async function GET() {
  const cookieStore = await cookies()
  
  // Obtener cookie
  const token = cookieStore.get('auth-token')
  console.log(token?.value)
  
  // Verificar si existe
  if (cookieStore.has('user-preferences')) {
    console.log('Usuario tiene preferencias guardadas')
  }
  
  // Obtener todas
  const allCookies = cookieStore.getAll()
  
  return Response.json({ ok: true })
}

Con NextRequest

import { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  // Más fácil con NextRequest
  const token = request.cookies.get('auth-token')?.value
  
  return Response.json({ token })
}

Ejemplo: Autenticación con cookies

import { cookies } from 'next/headers'

export async function GET() {
  const cookieStore = await cookies()
  const sessionId = cookieStore.get('session-id')?.value
  
  if (!sessionId) {
    return Response.json(
      { error: 'No autenticado' },
      { status: 401 }
    )
  }
  
  // Verificar sesión
  const session = await db.session.findUnique({
    where: { id: sessionId },
    include: { user: true }
  })
  
  if (!session) {
    return Response.json(
      { error: 'Sesión inválida' },
      { status: 401 }
    )
  }
  
  // Usuario autenticado
  return Response.json({ user: session.user })
}

Response Object

El objeto Response es lo que devuelves de tu Route Handler.

Response.json()

La forma más común:

export async function GET() {
  return Response.json({ 
    message: 'Hola mundo',
    timestamp: Date.now()
  })
}

Con status y headers:

export async function POST() {
  return Response.json(
    { message: 'Creado exitosamente' },
    {
      status: 201,
      headers: {
        'X-Custom-Header': 'valor',
      }
    }
  )
}

Response con texto plano

export async function GET() {
  return new Response('Hola mundo', {
    status: 200,
    headers: {
      'Content-Type': 'text/plain',
    }
  })
}

Response con HTML

export async function GET() {
  const html = `
    <!DOCTYPE html>
    <html>
      <head><title>Mi página</title></head>
      <body>
        <h1>Hola desde API</h1>
      </body>
    </html>
  `
  
  return new Response(html, {
    headers: {
      'Content-Type': 'text/html',
    }
  })
}

Response con archivo

import { readFile } from 'fs/promises'

export async function GET() {
  const file = await readFile('./public/documento.pdf')
  
  return new Response(file, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'attachment; filename="documento.pdf"',
    }
  })
}

Response con imagen

export async function GET() {
  const imageBuffer = await fetchImage()
  
  return new Response(imageBuffer, {
    headers: {
      'Content-Type': 'image/png',
      'Cache-Control': 'public, max-age=31536000',
    }
  })
}

Status Codes

Usa el código HTTP correcto:

// ✅ 200 - OK
return Response.json(data, { status: 200 })

// ✅ 201 - Created
return Response.json(newItem, { status: 201 })

// ✅ 204 - No Content
return new Response(null, { status: 204 })

// ❌ 400 - Bad Request
return Response.json({ error: 'Datos inválidos' }, { status: 400 })

// ❌ 401 - Unauthorized
return Response.json({ error: 'No autenticado' }, { status: 401 })

// ❌ 403 - Forbidden
return Response.json({ error: 'No tienes permiso' }, { status: 403 })

// ❌ 404 - Not Found
return Response.json({ error: 'No encontrado' }, { status: 404 })

// ❌ 409 - Conflict
return Response.json({ error: 'Ya existe' }, { status: 409 })

// ❌ 429 - Too Many Requests
return Response.json({ error: 'Límite excedido' }, { status: 429 })

// ❌ 500 - Internal Server Error
return Response.json({ error: 'Error del servidor' }, { status: 500 })

Response Headers

Headers comunes

export async function GET() {
  return Response.json(data, {
    headers: {
      // Cache
      'Cache-Control': 'public, max-age=3600',
      
      // CORS
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST',
      
      // Seguridad
      'X-Content-Type-Options': 'nosniff',
      'X-Frame-Options': 'DENY',
      
      // Custom
      'X-API-Version': '1.0',
      'X-Rate-Limit-Remaining': '99',
    }
  })
}

Setear cookies en response

export async function POST() {
  // Crear sesión
  const sessionId = generateSessionId()
  
  return Response.json(
    { message: 'Login exitoso' },
    {
      status: 200,
      headers: {
        'Set-Cookie': `session-id=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=86400`
      }
    }
  )
}

Con helper de NextJS:

import { cookies } from 'next/headers'

export async function POST() {
  const cookieStore = await cookies()
  
  // Setear cookie
  cookieStore.set('session-id', 'abc123', {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 86400, // 1 día
    path: '/',
  })
  
  return Response.json({ message: 'Login exitoso' })
}
import { cookies } from 'next/headers'

export async function POST() {
  const cookieStore = await cookies()
  
  // Eliminar cookie
  cookieStore.delete('session-id')
  
  return Response.json({ message: 'Logout exitoso' })
}

Redirect

import { redirect } from 'next/navigation'

export async function GET() {
  // Redirect permanente
  redirect('https://newdomain.com')
  
  // Con Response (más control)
  return Response.redirect('https://newdomain.com', 301)
}

Streaming Response

Para datos que llegan progresivamente:

export async function GET() {
  const encoder = new TextEncoder()
  
  const stream = new ReadableStream({
    async start(controller) {
      // Enviar chunk 1
      controller.enqueue(encoder.encode('Primera parte\n'))
      await delay(1000)
      
      // Enviar chunk 2
      controller.enqueue(encoder.encode('Segunda parte\n'))
      await delay(1000)
      
      // Enviar chunk 3
      controller.enqueue(encoder.encode('Tercera parte\n'))
      
      controller.close()
    }
  })
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/plain',
      'Transfer-Encoding': 'chunked',
    }
  })
}

Server-Sent Events (SSE)

export async function GET() {
  const encoder = new TextEncoder()
  
  const stream = new ReadableStream({
    async start(controller) {
      // Enviar eventos cada segundo
      const interval = setInterval(() => {
        const event = `data: ${JSON.stringify({ time: Date.now() })}\n\n`
        controller.enqueue(encoder.encode(event))
      }, 1000)
      
      // Limpiar después de 10 segundos
      setTimeout(() => {
        clearInterval(interval)
        controller.close()
      }, 10000)
    }
  })
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    }
  })
}

Cliente:

const eventSource = new EventSource('/api/events')

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data)
  console.log(data.time)
}

Dynamic Route Segments

Acceder a parámetros de la URL:

// app/api/productos/[id]/route.ts
type Params = {
  params: { id: string }
}

export async function GET(request: Request, { params }: Params) {
  const { id } = params
  
  const producto = await db.producto.findUnique({
    where: { id }
  })
  
  if (!producto) {
    return Response.json(
      { error: 'Producto no encontrado' },
      { status: 404 }
    )
  }
  
  return Response.json(producto)
}

Múltiples segmentos

// app/api/categorias/[catId]/productos/[prodId]/route.ts
type Params = {
  params: {
    catId: string
    prodId: string
  }
}

export async function GET(request: Request, { params }: Params) {
  const { catId, prodId } = params
  
  const producto = await db.producto.findFirst({
    where: {
      id: prodId,
      categoriaId: catId
    }
  })
  
  return Response.json(producto)
}

IP del cliente

import { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  // IP del cliente
  const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown'
  
  console.log(`Request desde: ${ip}`)
  
  return Response.json({ ip })
}

User Agent

export async function GET(request: Request) {
  const userAgent = request.headers.get('User-Agent')
  
  const isMobile = /mobile/i.test(userAgent || '')
  const isBot = /bot|crawler|spider/i.test(userAgent || '')
  
  return Response.json({ userAgent, isMobile, isBot })
}

Resumen

Request:

  • request.headers - Headers de la petición
  • request.json() - Body como JSON
  • request.formData() - Body como FormData
  • request.url - URL completa
  • NextRequest.nextUrl.searchParams - Query params

Response:

  • Response.json(data, options) - Respuesta JSON
  • new Response(body, options) - Respuesta custom
  • status - Código HTTP
  • headers - Headers de respuesta

Cookies:

  • cookies().get(name) - Leer cookie
  • cookies().set(name, value, options) - Setear cookie
  • cookies().delete(name) - Eliminar cookie

Route Params:

  • { params } - Segundo argumento
  • params.id - Segmentos dinámicos

Mejores prácticas:

  • Valida todos los inputs
  • Usa códigos HTTP correctos
  • Maneja errores apropiadamente
  • Lee el body solo una vez