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' })
}
Eliminar cookie
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ónrequest.json()
- Body como JSONrequest.formData()
- Body como FormDatarequest.url
- URL completaNextRequest.nextUrl.searchParams
- Query params
Response:
Response.json(data, options)
- Respuesta JSONnew Response(body, options)
- Respuesta customstatus
- Código HTTPheaders
- Headers de respuesta
Cookies:
cookies().get(name)
- Leer cookiecookies().set(name, value, options)
- Setear cookiecookies().delete(name)
- Eliminar cookie
Route Params:
{ params }
- Segundo argumentoparams.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