Tutoriales

API REST con Laravel y Vue 3: guía práctica con Sanctum

David Gonzalez
22 de febrero de 20266 min de lectura180 vistas
API REST con Laravel y Vue 3: guía práctica con Sanctum

Para construir una API REST con Laravel y Vue 3 necesitas cinco piezas: rutas en routes/api.php, Form Requests para validar, API Resources para dar forma al JSON, Sanctum para autenticar y, del lado de Vue, un cliente HTTP envuelto en composables. Aquí está cada pieza con el código mínimo que funciona en Laravel 11 y 12, y los errores que más tiempo hacen perder.

Rutas y controlador en routes/api.php

Desde Laravel 11 un proyecto nuevo ya no trae routes/api.php. Lo crea este comando, que además instala Sanctum y registra el prefijo /api:

php artisan install:api
php artisan make:model Product -m
php artisan make:controller Api/ProductController --api --model=Product
php artisan make:request StoreProductRequest
php artisan make:resource ProductResource
// routes/api.php
use App\Http\Controllers\Api\ProductController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('products', ProductController::class);
});

apiResource crea las cinco rutas REST (index, store, show, update y destroy) sin las de formularios HTML. Revísalas con php artisan route:list --path=api.

Validación con Form Requests y respuestas con API Resources

El Form Request saca la validación del controlador. Si falla y el cliente pide JSON, Laravel responde 422 con los errores por campo, sin código adicional.

// app/Http/Requests/StoreProductRequest.php
public function authorize(): bool
{
    return true; // aquí va tu política si solo algunos roles pueden crear
}

public function rules(): array
{
    return [
        'name'  => ['required', 'string', 'max:120'],
        'price' => ['required', 'integer', 'min:0'],
        'stock' => ['sometimes', 'integer', 'min:0'],
    ];
}
// app/Http/Resources/ProductResource.php
public function toArray(Request $request): array
{
    return [
        'id'    => $this->id,
        'name'  => $this->name,
        'price' => $this->price,
        'stock' => $this->stock,
    ];
}

// app/Http/Controllers/Api/ProductController.php
public function index()
{
    return ProductResource::collection(Product::latest()->paginate(20));
}

public function store(StoreProductRequest $request)
{
    return new ProductResource(Product::create($request->validated()));
}

Ojo: el archivo que genera make:request trae authorize() en false, y así toda petición responde 403. El Resource es tu contrato con el frontend: decide qué campos salen y con qué nombre, así un cambio en la tabla no rompe Vue. Al crear, Laravel responde 201 porque detecta que el modelo es nuevo; para que create() acepte los campos validados, declara $fillable en el modelo. Con paginate(), la respuesta trae data, links y meta (página actual, total, última página), lo que necesita el paginador. En tablas muy grandes, cursorPaginate() evita contar todos los registros.

Autenticación con Sanctum: cookies o tokens

SPA con cookies, si Vue y Laravel comparten dominio

Si el frontend vive en el mismo dominio o en un subdominio (app.tuempresa.com y api.tuempresa.com), usa la sesión de Laravel. Ningún token queda en localStorage, donde un script inyectado podría leerlo.

  1. En bootstrap/app.php, dentro de withMiddleware, llama a $middleware->statefulApi().
  2. En el .env, pon el dominio del frontend en SANCTUM_STATEFUL_DOMAINS y define SESSION_DOMAIN=.tuempresa.com.
  3. Publica la configuración con php artisan config:publish cors, activa supports_credentials y agrega login y logout a paths.
  4. Crea una ruta /login que valide con Auth::attempt() y regenere la sesión (Fortify la trae hecha).
  5. Desde Vue, pide /sanctum/csrf-cookie antes del login.

Tokens, para apps móviles o integraciones

Agrega el trait HasApiTokens al modelo User. En el login generas el token y el cliente lo envía en la cabecera Authorization: Bearer:

$token = $user->createToken('app-movil', ['products:read'])->plainTextToken;

El segundo parámetro limita qué puede hacer cada token. Ponle vencimiento en expiration de config/sanctum.php (por defecto no vence) y bórralo al cerrar sesión.

Errores y CORS sin sorpresas

El frontend debe enviar siempre Accept: application/json. Sin esa cabecera, un error de validación responde con una redirección, y una petición sin sesión intenta redirigir a la ruta login, que en una API casi nunca existe. Como respaldo, fuerza JSON en /api y personaliza el 404 para no exponer nombres de modelos:

// bootstrap/app.php (importa Illuminate\Http\Request y
// Symfony\Component\HttpKernel\Exception\NotFoundHttpException)
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->shouldRenderJsonWhen(
        fn (Request $request) => $request->is('api/*') || $request->expectsJson()
    );

    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json(['message' => 'Recurso no encontrado.'], 404);
        }
    });
})

En producción, APP_DEBUG=false: con debug activo, un error 500 devuelve la traza con rutas del servidor. Y en config/cors.php, si supports_credentials es true, allowed_origins debe listar los dominios exactos del frontend; el comodín no sirve con cookies.

Consumir la API desde Vue 3 con un composable

Centraliza la configuración en una sola instancia de axios. Con fetch también funciona, pero tendrías que leer la cookie XSRF-TOKEN y enviar la cabecera X-XSRF-TOKEN a mano.

// src/api.js
import axios from 'axios'

export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  headers: { Accept: 'application/json' },
  withCredentials: true, // solo en modo SPA con cookies
  withXSRFToken: true,
})

// src/composables/useProducts.js
import { ref } from 'vue'
import { api } from '../api'

export function useProducts() {
  const items = ref([])
  const meta = ref(null)
  const errors = ref({})

  async function load(page = 1) {
    const { data } = await api.get('/api/products', { params: { page } })
    items.value = data.data
    meta.value = data.meta
  }

  async function create(payload) {
    errors.value = {}
    try {
      await api.post('/api/products', payload)
      await load()
    } catch (e) {
      if (e.response?.status === 422) errors.value = e.response.data.errors
      else throw e
    }
  }

  return { items, meta, errors, load, create }
}

El componente solo llama a load() en onMounted y pinta errors junto a cada campo. Agrega un interceptor que mande al login cuando la API responda 401, o 419 si en modo SPA venció la sesión. Si esa SPA tiene páginas públicas que deben salir en Google, revisa el SEO técnico para aplicaciones SPA: aplica igual a Vue.

Cuándo conviene apoyo externo

Si tu equipo ya domina Laravel y Vue, esta base alcanza para muchos proyectos. Una segunda opinión vale la pena cuando:

  • la API tendrá clientes externos (apps móviles, aliados) y necesitas versionarla sin romper a nadie;
  • manejas datos personales o pagos y quieres revisar permisos, tokens y CORS antes de producción;
  • los listados ya son lentos y no sabes si el problema está en las consultas, los índices o el frontend;
  • heredaste un proyecto sin pruebas ni documentación.

Con la consultoría técnica por horas revisamos arquitectura y código junto a tu equipo, desde $150.000 la hora. Si prefieres que construyamos el sistema completo, conoce nuestro desarrollo de software a la medida o pide una cotización para tu proyecto.

Compartir artículo

¿Te gustó este artículo?

Contáctanos para recibir más contenido como este.

Contactar
1