Crear endpoints de API
Crea un endpoint protegido con autenticación
Crear endpoints de API
Crea un endpoint de API protegido que solo usuarios autenticados puedan usar. Al terminar tendrás:
- Un endpoint
GET /api/userque devuelve los datos del usuario - Un endpoint
POST /api/userque actualiza el perfil - Validación de sesión con Better Auth
- Llamadas desde el frontend con
fetch
Prerequisito: haber completado Autenticación de usuario (Google OAuth o email/password funcionando).
Paso 1: Crea un endpoint GET protegido
Crea el archivo app/api/user/route.ts. Este endpoint devuelve los datos del usuario autenticado:
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
export async function GET(request: NextRequest) {
// 1. Verificar sesión
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session?.user) {
return NextResponse.json(
{ message: "No autorizado. Inicia sesión primero." },
{ status: 401 }
);
}
// 2. Consultar la base de datos
const user = await db.user.findUnique({
where: { id: session.user.id },
select: {
id: true,
name: true,
email: true,
image: true,
role: true,
createdAt: true,
emailVerified: true,
subscription: {
select: {
plan: true,
status: true,
},
},
},
});
if (!user) {
return NextResponse.json(
{ message: "Usuario no encontrado" },
{ status: 404 }
);
}
// 3. Devolver respuesta
return NextResponse.json({ user });
}Qué hace cada parte:
| Paso | Descripción |
|---|---|
auth.api.getSession | Verifica la cookie de sesión y devuelve los datos del usuario |
!session?.user | Si no hay sesión válida, devuelve 401 |
db.user.findUnique | Consulta Prisma para obtener datos frescos de la DB |
select | Limita los campos devueltos (nunca expongas password o tokens) |
Paso 2: Añade un endpoint POST
Añade el handler POST en el mismo archivo para actualizar el perfil:
export async function POST(request: NextRequest) {
// 1. Verificar sesión
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session?.user) {
return NextResponse.json(
{ message: "No autorizado" },
{ status: 401 }
);
}
// 2. Validar el body
const body = await request.json();
const { name } = body;
if (!name || typeof name !== "string" || name.trim().length < 2) {
return NextResponse.json(
{ message: "El nombre debe tener al menos 2 caracteres" },
{ status: 400 }
);
}
// 3. Actualizar en la base de datos
const updatedUser = await db.user.update({
where: { id: session.user.id },
data: { name: name.trim() },
select: {
id: true,
name: true,
email: true,
},
});
return NextResponse.json({ user: updatedUser });
}Siempre válida los datos del body antes de escribir en la base de datos. Nunca confíes en el input del cliente.
Paso 3: Patrón de verificación de sesión
El patrón que usamos se repite en todas las API routes del boilerplate. Aquí está el esqueleto:
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
export async function GET(request: NextRequest) {
// Paso 1: Obtener sesión
const session = await auth.api.getSession({
headers: request.headers,
});
// Paso 2: Rechazar si no autenticado
if (!session?.user) {
return NextResponse.json(
{ message: "No autorizado" },
{ status: 401 }
);
}
// Paso 3: Tu lógica de negocio
// session.user.id -> ID del usuario
// session.user.email -> Email del usuario
// session.user.role -> "user" | "admin"
// Paso 4: Devolver respuesta
return NextResponse.json({ data: "..." });
}Así es como el endpoint de checkout de Stripe (app/api/stripe/checkout/route.ts) usa exactamente este patrón:
export async function POST(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session?.user) {
return NextResponse.json(
{ message: "No autorizado. Inicia sesión primero." },
{ status: 401 }
);
}
// ... crear checkout session con session.user.id
}Paso 4: Llama al endpoint desde el frontend
Desde un Server Component
En un Server Component, usa headers() de Next.js para pasar las cookies:
import { headers } from "next/headers";
export default async function PerfilPage() {
const headersList = await headers();
const res = await fetch(
`${process.env.NEXT_PUBLIC_APP_URL}/api/user`,
{
headers: {
cookie: headersList.get("cookie") || "",
},
}
);
if (!res.ok) {
return <p>Error al cargar el perfil</p>;
}
const { user } = await res.json();
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<p>Plan: {user.subscription?.plan || "Sin plan"}</p>
</div>
);
}En Server Components también puedes usar auth.api.getSession directamente sin pasar por la API route. La API route es más útil cuando necesitas exponer un endpoint para el frontend o para integraciónes externas.
Desde un Client Component
En un Client Component, fetch envía las cookies automáticamente:
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export function ProfileEditor({ initialName }: { initialName: string }) {
const [name, setName] = useState(initialName);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setMessage("");
const res = await fetch("/api/user", {
method: "POST",
headers: { "Content-Type": application/json" },
body: JSON.stringify({ name }),
});
const data = await res.json();
if (res.ok) {
setMessage("Perfil actualizado");
} else {
setMessage(data.message || "Error al actualizar");
}
setLoading(false);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Tu nombre"
/>
<Button type="submit" disabled={loading}>
{loading ? "Guardando..." : "Guardar"}
</Button>
{message && <p className="text-sm text-muted-foreground">{message}</p>}
</form>
);
}Paso 5: Prueba el endpoint
Prueba con el navegador
- Inicia sesión en tu app
- Abre http://localhost:3000/api/user
- Deberías ver el JSON con tus datos de usuario
Prueba sin autenticación
Abre una ventana de incógnito y visita la misma URL. Deberías ver:
{ "message": "No autorizado. Inicia sesión primero." }Con status code 401.
Prueba el POST con curl
Primero necesitas la cookie de sesión. Cópiala desde las DevTools del navegador (Application > Cookies > better-auth.session_token):
curl -X POST http://localhost:3000/api/user \
-H "Content-Type: application/json" \
-H "Cookie: better-auth.session_token=TU_TOKEN" \
-d '{"name": "Nuevo Nombre"}'Endpoint con validación de rol
Si necesitas un endpoint solo para admins, añade una verificación de rol:
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
export async function GET(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session?.user) {
return NextResponse.json({ message: "No autorizado" }, { status: 401 });
}
// Verificar rol de admin
if (session.user.role !== "admin") {
return NextResponse.json({ message: "Acceso denegado" }, { status: 403 });
}
const users = await db.user.findMany({
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
},
orderBy: { createdAt: "desc" },
});
return NextResponse.json({ users });
}Errores comunes
Error de CORS al llamar al endpoint
- Las API routes de Next.js no tienen problemas de CORS con llamadas desde la misma app
- Si llamas desde un dominio diferente, añade headers CORS en tu response
request.json() devuelve null o falla
- Verifica que envías
Content-Type: application/jsonen el header - Asegúrate de que el body es JSON válido
- Usa
try/catchalrededor derequest.json()para manejar errores de parsing
Resumen
Has aprendido a:
- Crear API routes protegidas con
auth.api.getSession - Validar la sesión y rechazar requests no autenticados
- Consultar y actualizar la base de datos con Prisma
- Llamar a los endpoints desde Server y Client Components
- Proteger endpoints por rol (admin)
Siguiente paso
- Página privada - Crea páginas protegidas que requieran autenticación