Crear una API REST con Laravel: rutas, JSON, autenticación y seguridad
Laravel permite construir APIs REST utilizando rutas, controllers, validación, Eloquent, API Resources, middleware y autenticación. Pero una buena API no consiste solamente en devolver JSON. También necesita definir recursos, contratos HTTP, códigos de estado, permisos, errores, paginación, seguridad y comportamiento consistente.
¿Cómo crear una API REST con Laravel?
Para crear una API REST con Laravel defines rutas HTTP, diriges las solicitudes a controllers, validas los datos, consultas la base de datos, transformas los resultados a JSON y devuelves códigos HTTP apropiados.
Una API de productos, por ejemplo, podría exponer:
GET /api/productos
GET /api/productos/{producto}
POST /api/productos
PUT /api/productos/{producto}
PATCH /api/productos/{producto}
DELETE /api/productos/{producto}
Laravel puede resolver buena parte de la infraestructura, pero tú sigues definiendo qué representa cada recurso, quién puede modificarlo y qué debe devolver cada operación.
Las piezas principales de una API Laravel
Routes
Definen los endpoints HTTP.
Controllers
Coordinan cada solicitud y respuesta.
Validation
Comprueba los datos recibidos.
Eloquent
Consulta y persiste datos.
Resources
Transforman models en respuestas.
Sanctum
Puede proteger endpoints autenticados.
REST no es simplemente poner /api/ delante de una URL
Una API REST suele organizar su interfaz alrededor de recursos.
Por ejemplo:
/productos
/usuarios
/pedidos
/categorias
Los métodos HTTP expresan qué operación quieres realizar sobre esos recursos.
| Método | Uso habitual | Ejemplo |
|---|---|---|
| GET | Consultar | /productos |
| POST | Crear | /productos |
| PUT | Reemplazar / actualizar según contrato | /productos/10 |
| PATCH | Modificar parcialmente | /productos/10 |
| DELETE | Eliminar | /productos/10 |
Habilita el soporte de API de tu aplicación
En una instalación moderna de Laravel puedes preparar la capa API mediante Artisan.
php artisan install:api
Esto resulta especialmente útil cuando también necesitarás autenticación mediante Sanctum.
A partir de ahí, puedes trabajar con las rutas destinadas a tu API.
Especialmente si estás trabajando sobre una aplicación existente.
Define el contrato inicial de la API mediante rutas
Podemos comenzar con dos endpoints públicos.
<?php
use App\Http\Controllers\ProductoController;
use Illuminate\Support\Facades\Route;
Route::get(
'/productos',
[ProductoController::class, 'index']
);
Route::get(
'/productos/{producto}',
[ProductoController::class, 'show']
);
El primer endpoint representa la colección.
El segundo representa un producto específico.
Después puedes añadir escritura
Route::post(
'/productos',
[ProductoController::class, 'store']
);
Route::patch(
'/productos/{producto}',
[ProductoController::class, 'update']
);
Route::delete(
'/productos/{producto}',
[ProductoController::class, 'destroy']
);
Laravel también puede declarar este patrón mediante una ruta de recurso.
Route::apiResource(
'productos',
ProductoController::class
);
El objetivo no es utilizar menos líneas a cualquier precio.
Lo importante es que la estructura de endpoints sea coherente y predecible.
Revisa las rutas que realmente registró Laravel
Cuando una URL no responde como esperas, verificar las rutas registradas puede ahorrarte mucho tiempo.
php artisan route:list
Esto permite comprobar:
Método HTTP.
URI.
Controller.
Middleware.
Crea un controller dedicado al recurso
Puedes generar un controller orientado a API.
php artisan make:controller ProductoController --api
Allí puedes organizar métodos como:
index()
store()
show()
update()
destroy()
Esos nombres reflejan operaciones comunes sobre un recurso.
El controller coordina HTTP
No debería convertirse automáticamente en una clase con cientos de líneas de reglas de negocio.
Devuelve una colección paginada de productos
<?php
public function index()
{
$productos = Producto::query()
->where('activo', true)
->orderBy('id')
->paginate(20);
return ProductoResource::collection(
$productos
);
}
Ya tenemos varias decisiones importantes:
solamente productos activos, orden definido y paginación.
Evita comenzar con Producto::all()
Una API que funciona con veinte registros puede convertirse en un problema cuando la tabla alcanza cientos de miles.
Laravel con MySQL →Laravel puede resolver automáticamente el model asociado a un parámetro de ruta
Nuestra ruta contiene:
/productos/{producto}
El controller puede recibir directamente una instancia del model.
<?php
public function show(
Producto $producto
) {
return new ProductoResource(
$producto
);
}
Esto evita repetir manualmente una búsqueda por ID en cada acción.
Route Model Binding resuelve el model; no decide por sí solo los permisos de tu negocio.
Utiliza API Resources para separar el model de la representación JSON
Un model Eloquent puede contener campos que tu API no necesita exponer.
Puedes crear un Resource:
php artisan make:resource ProductoResource
Y definir qué representa públicamente un producto.
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class ProductoResource extends JsonResource
{
public function toArray(
Request $request
): array {
return [
'id' => $this->id,
'nombre' => $this->nombre,
'precio' => $this->precio,
'activo' => $this->activo
];
}
}
¿Por qué no devolver simplemente el model?
Porque la estructura de tu base de datos y el contrato público de tu API no deberían quedar obligatoriamente acoplados.
Mañana puedes modificar una tabla sin querer modificar automáticamente la respuesta consumida por una aplicación móvil.
Valida el request antes de crear registros
Una API no debería confiar automáticamente en el JSON enviado por el cliente.
Puedes validar:
<?php
$datos = $request->validate([
'nombre' => [
'required',
'string',
'max:150'
],
'sku' => [
'required',
'string',
'max:100'
],
'precio' => [
'required',
'numeric',
'min:0'
]
]);
Después puedes trabajar con esos datos.
$producto = Producto::create(
$datos
);
Una API debería enviar Accept: application/json
Esto ayuda a comunicar que el cliente espera respuestas JSON, incluyendo errores de validación.
Accept: application/json
Content-Type: application/json
Cuando las reglas crecen, sácalas del controller
Puedes crear una clase dedicada a validar la creación del recurso.
php artisan make:request StoreProductoRequest
Allí puedes declarar las reglas.
<?php
public function rules(): array
{
return [
'nombre' => [
'required',
'string',
'max:150'
],
'sku' => [
'required',
'string',
'max:100',
'unique:productos,sku'
],
'precio' => [
'required',
'numeric',
'min:0'
]
];
}
El controller queda más enfocado.
<?php
public function store(
StoreProductoRequest $request
) {
$producto = Producto::create(
$request->validated()
);
return (new ProductoResource($producto))
->response()
->setStatusCode(201);
}
Utiliza códigos de estado que expliquen qué ocurrió
| Código | Significado habitual |
|---|---|
| 200 | Solicitud correcta |
| 201 | Recurso creado |
| 204 | Operación correcta sin cuerpo de respuesta |
| 401 | No autenticado |
| 403 | Autenticado pero no autorizado |
| 404 | Recurso no encontrado |
| 409 | Conflicto de estado |
| 422 | Datos que no superan validación |
| 429 | Demasiadas solicitudes |
| 500 | Error interno inesperado |
El código de estado forma parte del contrato de tu API.
Actualiza únicamente los campos permitidos por el contrato
<?php
public function update(
UpdateProductoRequest $request,
Producto $producto
) {
$producto->update(
$request->validated()
);
return new ProductoResource(
$producto->refresh()
);
}
Aquí el Form Request define qué acepta la operación.
No deberías permitir que el cliente cambie campos internos simplemente porque existen en la tabla.
Una eliminación correcta puede responder sin contenido
<?php
public function destroy(
Producto $producto
) {
$producto->delete();
return response()->noContent();
}
El hecho de que el endpoint exista no significa que cualquier usuario deba poder ejecutarlo.
Antes de eliminar necesitas resolver autorización y reglas relacionadas con el negocio.
Pagina la API antes de que el volumen de datos se convierta en un problema
Imagina una tabla con 300.000 productos.
Un endpoint no debería devolverlos todos simplemente porque:
$productos = Producto::all();
resulta sencillo.
Utiliza paginación.
$productos = Producto::query()
->orderBy('id')
->paginate(20);
return ProductoResource::collection(
$productos
);
Así el cliente puede navegar por bloques de resultados en lugar de descargar la colección completa.
Profundizar en Eloquent →Los parámetros de consulta pueden permitir buscar sin crear un endpoint para cada filtro
Un cliente podría solicitar:
GET /api/productos?activo=1&categoria=5
El controller puede construir la consulta progresivamente.
$query = Producto::query();
if ($request->filled('activo')) {
$query->where(
'activo',
$request->boolean('activo')
);
}
if ($request->filled('categoria')) {
$query->where(
'categoria_id',
$request->integer('categoria')
);
}
$productos = $query
->orderBy('id')
->paginate(20);
Valida también los filtros
El hecho de que estén en una URL no significa que sean confiables.
Los nombres de columnas dinámicas deberían limitarse a valores permitidos
No conviene permitir que cualquier texto recibido determine directamente una columna SQL.
Puedes mantener una lista explícita.
$permitidos = [
'nombre',
'precio',
'created_at'
];
$orden = $request->input(
'orden',
'created_at'
);
if (! in_array(
$orden,
$permitidos,
true
)) {
$orden = 'created_at';
}
$productos = Producto::query()
->orderBy($orden)
->paginate(20);
Para columnas dinámicas, una allowlist suele ser una estrategia mucho más clara.
Una API puede incluir relaciones sin convertir cada respuesta en un volcado completo de MySQL
Supongamos que un producto pertenece a una categoría.
Podemos cargar la relación cuando sea necesaria.
$productos = Producto::query()
->with('categoria')
->paginate(20);
Y el Resource puede decidir cómo representar esa información.
Eso aumenta consultas, memoria, transferencia y acoplamiento.
El problema N+1 también puede aparecer dentro de tus Resources
Si transformas una colección y durante cada transformación accedes a una relación no cargada, puedes generar muchas consultas.
100 productos
+
1 consulta por categoría
=
muchas consultas adicionales
Por eso, la construcción de la consulta y la transformación JSON deberían diseñarse juntas.
Eloquent, relaciones y N+1 →Protege los endpoints privados con una estrategia de autenticación adecuada
Laravel Sanctum puede utilizarse para escenarios como:
SPA propia.
Aplicación móvil.
Tokens sencillos de API.
Puedes proteger una ruta con:
Route::get(
'/usuario',
function (Request $request) {
return $request->user();
}
)->middleware('auth:sanctum');
También puedes proteger un grupo de operaciones
Route::apiResource(
'productos',
ProductoController::class
)->only([
'index',
'show'
]);
Route::middleware(
'auth:sanctum'
)->group(function () {
Route::apiResource(
'productos',
ProductoController::class
)->except([
'index',
'show'
]);
});
En este ejemplo, consultar productos es público, pero crear, modificar y eliminar exige autenticación.
Una API de terceros puede utilizar Bearer tokens, pero una SPA propia tiene necesidades diferentes
Cuando utilizas tokens personales para una API, el cliente puede enviar:
Authorization: Bearer TU_TOKEN
Pero no deberías asumir que una SPA propia necesita guardar manualmente un token de larga duración en JavaScript.
La estrategia de autenticación depende de quién consume la API.
Después escoge el mecanismo de autenticación.
Tener un token válido no significa tener permiso para modificar cualquier recurso
Imagina:
DELETE /api/proyectos/945
El usuario puede estar correctamente autenticado.
Pero todavía debes comprobar si puede eliminar ese proyecto.
Autenticación
¿Quién eres?
Autorización
¿Puedes realizar esta acción sobre este recurso?
Los tokens pueden limitar capacidades, pero eso tampoco sustituye la autorización del recurso
Puedes imaginar capacidades como:
productos:read
productos:create
productos:update
Un token podría tener permiso para actualizar productos, pero tu aplicación todavía podría necesitar comprobar:
empresa, propietario, rol o estado del recurso.
CORS aparece cuando un frontend del navegador intenta consumir tu API desde otro origen
Por ejemplo:
https://app.ejemplo.com
↓
https://api.ejemplo.com
El navegador aplica reglas de origen que debes tener en cuenta.
CORS no es autenticación
Permitir un origen no demuestra la identidad del usuario.
CORS tampoco protege una API de clientes fuera del navegador
Un servidor, script o herramienta HTTP no funciona bajo las mismas restricciones del navegador.
Diseña respuestas de error que pueda interpretar una máquina y entender una persona
Una respuesta de conflicto podría ser:
{
"message": "No es posible eliminar el producto.",
"code": "PRODUCT_HAS_ACTIVE_ORDERS"
}
Esto resulta mucho más útil que devolver:
{
"error": true
}
No expongas información interna innecesaria
El cliente no necesita recibir:
Contraseñas de base de datos.
Stack traces.
Rutas internas del servidor.
SQL sensible.
Los detalles técnicos deberían registrarse donde corresponda para debugging.
Mantén una estructura de respuesta predecible
Si una colección devuelve:
{
"data": [
{
"id": 1,
"nombre": "Monitor"
}
]
}
no conviene que otro endpoint equivalente cambie arbitrariamente a:
{
"resultado": {
"productos": [...]
}
}
salvo que exista una razón clara para ese contrato.
La consistencia reduce lógica especial en cada cliente.
Una API puede necesitar transacciones cuando un request modifica varias tablas
Imagina un endpoint:
POST /api/pedidos
que debe:
Crear pedido.
Crear items.
Descontar stock.
Podrías envolver la operación en una transacción.
<?php
use Illuminate\Support\Facades\DB;
$pedido = DB::transaction(
function () use ($datos) {
$pedido = Pedido::create([
'usuario_id' => $datos['usuario_id'],
'total' => $datos['total']
]);
foreach ($datos['items'] as $item) {
$pedido->items()
->create($item);
}
return $pedido;
}
);
Piensa qué ocurrirá si el cliente repite una solicitud
Un usuario puede presionar dos veces un botón.
Una red puede cortar la respuesta después de que el servidor procesó la operación.
El cliente puede intentar nuevamente.
Para una consulta GET suele ser sencillo
Repetir la misma lectura normalmente no crea un nuevo recurso.
Para pagos, pedidos u operaciones sensibles necesitas más cuidado
Puede ser necesario diseñar mecanismos que eviten duplicados durante reintentos.
Una API pública necesita límites además de autenticación
Incluso un usuario válido puede generar un volumen excesivo de solicitudes.
El rate limiting puede ayudar a proteger:
Login.
Búsquedas costosas.
Generación con IA.
APIs públicas.
Procesamiento intensivo.
Los límites pueden depender del endpoint, usuario, plan o tipo de operación.
Un límite adecuado para consultar productos puede ser completamente distinto al de generar un informe pesado.
No necesitas versionar el primer endpoint por reflejo, pero sí pensar cómo evolucionará el contrato
Una estrategia frecuente es utilizar URLs como:
/api/v1/productos
/api/v2/productos
Pero versionar cada pequeño cambio puede generar mantenimiento innecesario.
La verdadera pregunta es:
¿este cambio rompe a los clientes existentes?
Una API segura necesita proteger mucho más que la contraseña
Valida entrada
Nunca confíes automáticamente en JSON, headers, query strings o IDs enviados por el cliente.
Autoriza cada operación sensible
Un usuario autenticado no debería acceder automáticamente a todos los registros.
Limita los campos modificables
Evita permitir que el cliente asigne roles, propietarios, saldos o estados internos sin reglas.
No expongas secretos
Claves API, credenciales y configuraciones sensibles pertenecen al servidor.
Controla la información devuelta
API Resources ayudan a crear una capa explícita entre models y JSON.
La API debe comprobar los permisos en el servidor.
Un controller no debería contener toda la lógica de una API
Al principio es normal que un controller haga cosas sencillas.
Pero imagina este proceso:
Validar request
↓
Comprobar stock
↓
Calcular precios
↓
Aplicar descuentos
↓
Crear pedido
↓
Crear items
↓
Reservar stock
↓
Emitir evento
Todo eso dentro de store() puede convertir rápidamente el controller en una clase difícil de probar y modificar.
Puedes separar el caso de uso
<?php
final class CrearPedido
{
public function ejecutar(
DatosPedido $datos
): Pedido {
// Reglas del proceso.
return $pedido;
}
}
Y dejar al controller principalmente como adaptador HTTP.
POO y arquitectura en PHP →Prueba la API como la utilizaría un cliente real
Puedes escribir tests HTTP que llamen a tus endpoints.
<?php
public function test_lista_productos(): void
{
Producto::factory()
->count(3)
->create();
$response = $this->getJson(
'/api/productos'
);
$response
->assertOk()
->assertJsonStructure([
'data'
]);
}
Prueba también los errores
No solamente el camino perfecto.
Deberías comprobar progresivamente:
Validación incorrecta.
Usuario no autenticado.
Usuario sin permiso.
Recurso inexistente.
Conflictos de negocio.
Una API lenta muchas veces es una base de datos lenta disfrazada de problema HTTP
Antes de culpar al framework, revisa:
N+1.
Índices.
Paginación.
Columnas innecesarias.
Servicios externos.
Consultas repetidas.
Una ruta muy elegante puede terminar ejecutando cientos de consultas SQL.
Una API Laravel profesional necesita comprender MySQL y Eloquent
Relaciones, índices, N+1 y transacciones.
Documenta la API pensando en quien tendrá que consumirla
El consumidor necesita saber:
Endpoint.
Método HTTP.
Autenticación.
Parámetros.
Body esperado.
Respuesta.
Códigos de error.
Una API sin documentación obliga al consumidor a descubrir el contrato mediante prueba y error.
OpenAPI puede ayudarte a formalizar ese contrato
Especialmente cuando existen múltiples clientes, equipos o integraciones externas.
Una API Laravel puede convertirse en backend de muchas interfaces diferentes
El mismo backend puede alimentar:
Frontend JavaScript.
Aplicación móvil.
Panel administrativo.
Integración empresarial.
Otro backend.
Aplicación con inteligencia artificial.
Web ───────────┐
│
Móvil ─────────┤
│
Sistema ───────┼──→ Laravel API ───→ MySQL
│
IA ────────────┤
│
Integración ───┘
Esta separación permite que la lógica central no dependa de una sola interfaz.
Laravel puede exponer datos propios y consumir modelos de inteligencia artificial al mismo tiempo
Por ejemplo, podrías construir:
Frontend
↓
Laravel API
↓
Autenticación
↓
MySQL
↓
Contexto autorizado
↓
Modelo de IA
↓
Respuesta JSON
Laravel puede controlar usuarios, permisos, consumo, historial y datos, mientras una API externa proporciona la capacidad de IA.
PHP con inteligencia artificial →Construye una API de inventario para practicar todo el flujo
Empieza con cuatro recursos:
/api/productos
/api/categorias
/api/movimientos
/api/usuarios
Primera etapa
CRUD de productos y categorías.
Segunda etapa
Validación, paginación y filtros.
Tercera etapa
Autenticación y permisos.
Cuarta etapa
Movimientos de inventario mediante transacciones.
Quinta etapa
Tests para endpoints, validación y autorización.
En qué orden aprender APIs REST con Laravel
HTTP
Métodos, headers y status.
Routes
Define recursos y endpoints.
Controllers
Coordina solicitudes.
Validation
Protege la entrada.
Eloquent
Consulta y persiste.
Resources
Diseña el JSON.
Auth
Identifica al cliente.
Authorization
Controla acciones.
Testing
Verifica el contrato.
Producción
Límites, logs y rendimiento.
Qué evitar al crear una API con Laravel
Una buena API Laravel es un contrato HTTP estable, no simplemente un controller que devuelve JSON
Diseña recursos y rutas coherentes.
Utiliza los métodos HTTP con intención.
Valida cada entrada.
Separa models de representación mediante Resources.
Pagina colecciones grandes.
Comprende Eloquent, MySQL y N+1.
Autentica al cliente y autoriza cada operación.
Utiliza status HTTP coherentes.
Añade transacciones cuando una operación modifica varios recursos.
Prueba tanto los éxitos como los errores.
Laravel puede reducir enormemente la infraestructura necesaria para construir APIs, pero el diseño del contrato, la seguridad, la autorización y la consistencia siguen dependiendo de tus decisiones como desarrollador.
Continúa profundizando en Laravel
Preguntas sobre crear APIs REST con Laravel
Sí. Laravel permite crear rutas, controllers, validación, acceso a bases de datos, autenticación y respuestas JSON, por lo que puede utilizarse como backend para aplicaciones web, móviles y otros sistemas.
Es una forma de registrar las rutas habituales de un resource controller orientado a API. Incluye operaciones como index, store, show, update y destroy, sin las rutas destinadas a formularios HTML como create y edit.
Son una capa de transformación entre tus models Eloquent y el JSON que devuelve la API. Permiten controlar explícitamente qué campos y relaciones forman parte de la respuesta.
Laravel Sanctum puede utilizarse para autenticación de SPAs, aplicaciones móviles y APIs sencillas basadas en tokens. La estrategia concreta depende del tipo de cliente que consumirá la API.
La autenticación identifica quién realiza la solicitud. La autorización determina si esa identidad puede ejecutar una acción concreta sobre un recurso. Un usuario autenticado no necesariamente tiene permiso para modificar todos los datos.
Puedes utilizar los mecanismos de paginación del Query Builder o Eloquent y después devolver el resultado mediante un API Resource. Esto evita recuperar colecciones enormes en una sola solicitud.
No necesariamente. Eloquent resulta muy conveniente para models y relaciones, pero Laravel también ofrece Query Builder y permite utilizar otros enfoques de persistencia cuando el proyecto lo requiere.
Puedes crear tests HTTP que envíen solicitudes JSON a los endpoints y verifiquen códigos de estado, estructuras JSON, validación, autenticación, autorización y cambios en la base de datos.
El siguiente nivel no es crear más endpoints: es controlar quién puede utilizarlos
Después de dominar rutas, controllers, validación, Resources, Eloquent y códigos HTTP, la siguiente pieza crítica es profundizar en autenticación y autorización para construir APIs multiusuario realmente seguras.
Volver al hub Laravel →