EmpiezaTuSaaS

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/user que devuelve los datos del usuario
  • Un endpoint POST /api/user que 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:

app/api/user/route.ts
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:

PasoDescripción
auth.api.getSessionVerifica la cookie de sesión y devuelve los datos del usuario
!session?.userSi no hay sesión válida, devuelve 401
db.user.findUniqueConsulta Prisma para obtener datos frescos de la DB
selectLimita 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:

app/api/user/route.ts
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:

Patrón para API routes protegidas
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:

app/api/stripe/checkout/route.ts
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:

app/perfil/page.tsx
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:

components/profile-editor.tsx
"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

  1. Inicia sesión en tu app
  2. Abre http://localhost:3000/api/user
  3. 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:

app/api/admin/users/route.ts
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

El endpoint devuelve 401 aunque estoy logueado

  • Verifica que la cookie better-auth.session_token existe en el navegador
  • Si usas fetch en un Client Component, las cookies se envían automáticamente (no necesitas credentials: "include" en same-origin)
  • Si llamas desde un Server Component, pasa el header cookie manualmente

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/json en el header
  • Asegúrate de que el body es JSON válido
  • Usa try/catch alrededor de request.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

On this page