Skip to content

10. API REST y JSON en Laravel

Laravel proporciona herramientas poderosas para crear APIs RESTful de forma rápida y sencilla. En esta guía, exploraremos cómo configurar rutas API, crear controladores específicos, formatear respuestas JSON y autenticar usuarios mediante tokens.

Rutas API y controladores tipo apiResource

Section titled “Rutas API y controladores tipo apiResource”

Laravel facilita la creación de rutas API mediante el prefijo api y el método apiResource que genera automáticamente las rutas necesarias para un recurso RESTful.

Las rutas API se definen en el archivo routes/api.php y automáticamente se les aplica el prefijo /api y el grupo de middleware api.

<?php
use AppHttpControllersAPIProductController;
use IlluminateSupportFacadesRoute;
// Ruta API básica
Route::get('/products', [ProductController::class, 'index']);
// Grupo de rutas con prefijo y middleware adicional
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
Route::get('/user', function () {
return auth()->user();
});
});

El método apiResource crea automáticamente las rutas necesarias para un controlador de recursos API, excluyendo las rutas create y edit que son específicas para aplicaciones web con formularios HTML.

// Definir un recurso API completo
Route::apiResource('products', ProductController::class);
// Definir múltiples recursos API
Route::apiResources([
'products' => ProductController::class,
'categories' => CategoryController::class,
]);
// Recurso API con rutas anidadas
Route::apiResource('products.reviews', ProductReviewController::class);

Esto genera las siguientes rutas:

Terminal window
+--------+-----------+----------------------------+---------------+----------------------------------------------+
| Method | URI | Name | Action | Controller |
+--------+-----------+----------------------------+---------------+----------------------------------------------+
| GET | /products | products.index | index | AppHttpControllersAPIProductController |
| POST | /products | products.store | store | AppHttpControllersAPIProductController |
| GET | /products/{product} | products.show | show | AppHttpControllersAPIProductController |
| PUT | /products/{product} | products.update | update | AppHttpControllersAPIProductController |
| DELETE | /products/{product} | products.destroy| destroy | AppHttpControllersAPIProductController |
+--------+-----------+----------------------------+---------------+----------------------------------------------+

Puedes generar un controlador específico para API con el comando Artisan:

Terminal window
php artisan make:controller API/ProductController --api

Esto creará un controlador con los métodos necesarios para un recurso API:

<?php
namespace AppHttpControllersAPI;
use AppHttpControllersController;
use AppModelsProduct;
use IlluminateHttpRequest;
class ProductController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$products = Product::all();
return response()->json($products);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric',
'description' => 'nullable|string',
]);
$product = Product::create($validated);
return response()->json($product, 201);
}
/**
* Display the specified resource.
*/
public function show(Product $product)
{
return response()->json($product);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Product $product)
{
$validated = $request->validate([
'name' => 'sometimes|string|max:255',
'price' => 'sometimes|numeric',
'description' => 'nullable|string',
]);
$product->update($validated);
return response()->json($product);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Product $product)
{
$product->delete();
return response()->json(null, 204);
}
}
// Limitar acciones disponibles
Route::apiResource('products', ProductController::class)->only([
'index', 'show'
]);
// Excluir acciones específicas
Route::apiResource('products', ProductController::class)->except([
'destroy'
]);
// Registrar rutas adicionales para un recurso
Route::apiResource('products', ProductController::class);
Route::post('products/{product}/restore', [ProductController::class, 'restore'])->name('products.restore');

Laravel proporciona varias formas de devolver respuestas JSON desde tus controladores API. Estas respuestas incluyen los encabezados HTTP adecuados (Content-Type: application/json) y convierten automáticamente los datos PHP en JSON.

// Convertir un array o modelo a JSON
return response()->json(['name' => 'John', 'age' => 30]);
// Especificar un código de estado HTTP
return response()->json(['error' => 'No autorizado'], 401);
// Incluir encabezados personalizados
return response()->json($data, 200, [
'X-API-Version' => '1.0',
'X-Custom-Header' => 'Value'
]);
// Permitir JSONP
return response()->json($data)->withCallback($request->input('callback'));

Los modelos Eloquent y sus colecciones se convierten automáticamente a JSON cuando se devuelven desde rutas o controladores:

// Devolver un modelo como JSON
public function show(Product $product)
{
return $product; // Automáticamente convertido a JSON
}
// Devolver una colección como JSON
public function index()
{
return Product::all(); // Automáticamente convertido a JSON
}
// Devolver una colección paginada
public function index()
{
return Product::paginate(15); // Incluye metadatos de paginación
}

Personalización de la serialización JSON

Section titled “Personalización de la serialización JSON”

Puedes personalizar cómo se serializan tus modelos a JSON utilizando propiedades y métodos en tus modelos Eloquent:

<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* Los atributos que deben estar ocultos en la serialización.
*
* @var array
*/
protected $hidden = ['created_at', 'updated_at', 'deleted_at'];
/**
* Los atributos que deben ser visibles en la serialización.
*
* @var array
*/
protected $visible = ['id', 'name', 'price', 'description', 'category_id'];
/**
* Los atributos que deben ser convertidos a tipos nativos.
*
* @var array
*/
protected $casts = [
'price' => 'float',
'active' => 'boolean',
'options' => 'array',
];
/**
* Los atributos que deben ser añadidos a la serialización.
*
* @var array
*/
protected $appends = ['formatted_price', 'is_available'];
/**
* Obtener el precio formateado.
*
* @return string
*/
public function getFormattedPriceAttribute()
{
return '$' . number_format($this->price, 2);
}
/**
* Determinar si el producto está disponible.
*
* @return bool
*/
public function getIsAvailableAttribute()
{
return $this->stock > 0 && $this->active;
}
}

Las clases Resource proporcionan una capa de transformación entre tus modelos Eloquent y las respuestas JSON. Son especialmente útiles para APIs complejas donde necesitas un control preciso sobre la estructura de tus respuestas.

Terminal window
php artisan make:resource ProductResource
<?php
namespace AppHttpResources;
use IlluminateHttpRequest;
use IlluminateHttpResourcesJsonJsonResource;
class ProductResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'formatted_price' => '$' . number_format($this->price, 2),
'description' => $this->description,
'category' => [
'id' => $this->category->id,
'name' => $this->category->name,
],
'created_at' => $this->created_at->format('Y-m-d H:i:s'),
'url' => route('api.products.show', $this->id),
];
}
/**
* Get additional data that should be returned with the resource array.
*
* @return array<string, mixed>
*/
public function with(Request $request): array
{
return [
'meta' => [
'version' => '1.0',
'api_status' => 'stable',
],
];
}
}
<?php
namespace AppHttpControllersAPI;
use AppHttpControllersController;
use AppHttpResourcesProductResource;
use AppHttpResourcesProductCollection;
use AppModelsProduct;
class ProductController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
// Usar una colección de recursos
return ProductResource::collection(Product::paginate(15));
}
/**
* Display the specified resource.
*/
public function show(Product $product)
{
// Usar un recurso individual
return new ProductResource($product);
}
}

Para personalizar la estructura de las colecciones, puedes crear una clase ResourceCollection:

Terminal window
php artisan make:resource ProductCollection --collection
<?php
namespace AppHttpResources;
use IlluminateHttpRequest;
use IlluminateHttpResourcesJsonResourceCollection;
class ProductCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => route('api.products.index'),
],
];
}
/**
* Get additional data that should be returned with the resource array.
*
* @return array<string, mixed>
*/
public function with(Request $request): array
{
return [
'meta' => [
'products_count' => $this->collection->count(),
'version' => '1.0',
],
];
}
}

Para APIs, es importante devolver respuestas de error consistentes y bien estructuradas:

// Error 404 - No encontrado
return response()->json([
'message' => 'Producto no encontrado',
'error' => 'not_found'
], 404);
// Error 422 - Validación fallida
return response()->json([
'message' => 'Los datos proporcionados no son válidos',
'errors' => [
'name' => ['El nombre es obligatorio'],
'price' => ['El precio debe ser un número positivo']
]
], 422);
// Error 403 - Prohibido
return response()->json([
'message' => 'No tienes permiso para realizar esta acción',
'error' => 'forbidden'
], 403);
// Error 500 - Error del servidor
return response()->json([
'message' => 'Ha ocurrido un error inesperado',
'error' => 'server_error'
], 500);

Los middleware en Laravel permiten filtrar las solicitudes HTTP que ingresan a tu aplicación. El grupo de middleware api se aplica automáticamente a todas las rutas definidas en routes/api.php.

El grupo de middleware api está definido en app/Http/Kernel.php y generalmente incluye:

protected $middlewareGroups = [
'web' => [
// Middleware web...
],
'api' => [
// LaravelSanctumHttpMiddlewareEnsureFrontendRequestsAreStateful::class,
IlluminateRoutingMiddlewareThrottleRequests::class.':api',
IlluminateHttpMiddlewareHandleCors::class,
IlluminateRoutingMiddlewareSubstituteBindings::class,
],
];

Este grupo incluye middleware esencial para APIs:

  • ThrottleRequests: Limita la tasa de solicitudes para prevenir abusos
  • HandleCors: Maneja las solicitudes de origen cruzado (CORS)
  • SubstituteBindings: Resuelve las dependencias de inyección de ruta

Puedes crear middleware personalizado para tus APIs:

Terminal window
php artisan make:middleware EnsureApiVersion
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateHttpRequest;
class EnsureApiVersion
{
/**
* Handle an incoming request.
*
* @param IlluminateHttpRequest $request
* @param Closure $next
* @param string $version
* @return mixed
*/
public function handle(Request $request, Closure $next, $version = '1.0')
{
// Verificar la versión de la API en el encabezado
if ($request->header('X-API-Version') !== $version) {
return response()->json([
'message' => 'Esta versión de la API no es compatible',
'required_version' => $version
], 400);
}
return $next($request);
}
}

Registra tu middleware en app/Http/Kernel.php:

protected $middlewareAliases = [
// Otros middleware...
'api.version' => AppHttpMiddlewareEnsureApiVersion::class,
'api.key' => AppHttpMiddlewareVerifyApiKey::class,
];
// Aplicar a una ruta individual
Route::get('/products', [ProductController::class, 'index'])
->middleware('api.version:2.0');
// Aplicar a un grupo de rutas
Route::middleware(['auth:sanctum', 'api.version:2.0'])
->prefix('v2')
->group(function () {
Route::apiResource('products', ProductControllerV2::class);
});
// Aplicar a todos los endpoints de un controlador
Route::apiResource('products', ProductController::class)
->middleware('api.key');

Limita el número de solicitudes que un cliente puede hacer en un período de tiempo:

// Limitar a 60 solicitudes por minuto
Route::middleware('throttle:60,1')
->group(function () {
Route::apiResource('products', ProductController::class);
});
// Diferentes límites para rutas específicas
Route::get('/status', [ApiController::class, 'status'])
->middleware('throttle:120,1'); // 120 solicitudes por minuto
Route::post('/webhooks', [WebhookController::class, 'handle'])
->middleware('throttle:1000,1'); // 1000 solicitudes por minuto

Configura CORS en config/cors.php:

return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];

Para una configuración más restrictiva y segura:

return [
'paths' => ['api/*'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
'allowed_origins' => ['https://myapp.com', 'https://admin.myapp.com'],
'allowed_origins_patterns' => ['/^https://.*.myapp.com$/'],
'allowed_headers' => ['X-API-KEY', 'Origin', 'Content-Type', 'X-Auth-Token', 'Authorization'],
'exposed_headers' => ['X-API-Version', 'X-Request-Id'],
'max_age' => 60 * 60 * 24, // 24 horas
'supports_credentials' => true,
];

Middleware para registrar todas las solicitudes API:

<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateHttpRequest;
use IlluminateSupportFacadesLog;
class LogApiRequests
{
/**
* Handle an incoming request.
*
* @param IlluminateHttpRequest $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// Registrar la solicitud entrante
$startTime = microtime(true);
// Procesar la solicitud
$response = $next($request);
// Calcular el tiempo de respuesta
$duration = microtime(true) - $startTime;
// Registrar detalles de la solicitud y respuesta
Log::channel('api')->info('API Request', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'user' => $request->user() ? $request->user()->id : 'guest',
'ip' => $request->ip(),
'duration' => round($duration * 1000, 2) . 'ms',
'status' => $response->getStatusCode(),
'content_length' => $response->headers->get('Content-Length') ?? 'unknown',
]);
return $response;
}
}

Laravel ofrece dos paquetes oficiales para la autenticación de APIs: Sanctum y Passport. Cada uno tiene sus propios casos de uso y ventajas.

Laravel Sanctum es una solución ligera para la autenticación de APIs y SPAs (Single Page Applications). Es ideal para aplicaciones que necesitan tokens API simples o autenticación basada en cookies para SPAs.

Terminal window
composer require laravel/sanctum
php artisan vendor:publish --provider="LaravelSanctumSanctumServiceProvider"
php artisan migrate

Agrega el middleware de Sanctum en app/Http/Kernel.php:

'api' => [
LaravelSanctumHttpMiddlewareEnsureFrontendRequestsAreStateful::class,
IlluminateRoutingMiddlewareThrottleRequests::class.':api',
IlluminateHttpMiddlewareHandleCors::class,
IlluminateRoutingMiddlewareSubstituteBindings::class,
],

Asegúrate de que tu modelo User use el trait HasApiTokens:

<?php
namespace AppModels;
use IlluminateFoundationAuthUser as Authenticatable;
use LaravelSanctumHasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
// ...
}
  • Necesitas una solución simple y ligera
  • Estás construyendo una SPA que se comunica con una API de Laravel
  • Necesitas tokens API para servicios de primera parte
  • Quieres autenticación basada en tokens sin la complejidad de OAuth
  • Necesitas autenticación para WebSockets con Laravel Echo
<?php
namespace AppHttpControllersAPI;
use AppModelsUser;
use IlluminateHttpRequest;
use IlluminateSupportFacadesHash;
use IlluminateValidationValidationException;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['Las credenciales proporcionadas son incorrectas.'],
]);
}
// Crear token con habilidades/permisos
$token = $user->createToken($request->device_name, ['products:read'])->plainTextToken;
return response()->json([
'token' => $token,
'user' => $user,
]);
}
public function logout(Request $request)
{
// Revocar el token actual
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Sesión cerrada correctamente']);
}
public function user(Request $request)
{
return response()->json($request->user());
}
}
routes/api.php
use AppHttpControllersAPIAuthController;
use AppHttpControllersAPIProductController;
// Rutas públicas
Route::post('/login', [AuthController::class, 'login']);
// Rutas protegidas
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', [AuthController::class, 'user']);
Route::post('/logout', [AuthController::class, 'logout']);
// Verificar habilidades/permisos específicos
Route::get('/products', [ProductController::class, 'index'])
->middleware('ability:products:read');
Route::post('/products', [ProductController::class, 'store'])
->middleware('ability:products:create');
});

Puedes crear clientes OAuth mediante comandos Artisan:

Terminal window
// Cliente para Authorization Code Grant
php artisan passport:client
// Cliente para Client Credentials Grant
php artisan passport:client --client
// Cliente para Password Grant
php artisan passport:client --password

Ejemplo de autenticación con Password Grant

Section titled “Ejemplo de autenticación con Password Grant”
<?php
namespace AppHttpControllersAPI;
use IlluminateHttpRequest;
use IlluminateSupportFacadesHttp;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$response = Http::post(config('app.url') . '/oauth/token', [
'grant_type' => 'password',
'client_id' => config('passport.password_grant_client.id'),
'client_secret' => config('passport.password_grant_client.secret'),
'username' => $request->email,
'password' => $request->password,
'scope' => '',
]);
return $response->json();
}
public function logout(Request $request)
{
$request->user()->token()->revoke();
return response()->json(['message' => 'Sesión cerrada correctamente']);
}
public function user(Request $request)
{
return response()->json($request->user());
}
}
routes/api.php
use AppHttpControllersAPIAuthController;
use AppHttpControllersAPIProductController;
// Rutas públicas
Route::post('/login', [AuthController::class, 'login']);
// Rutas protegidas
Route::middleware('auth:api')->group(function () {
Route::get('/user', [AuthController::class, 'user']);
Route::post('/logout', [AuthController::class, 'logout']);
// Verificar scopes específicos
Route::get('/products', [ProductController::class, 'index'])
->middleware('scope:read-products');
Route::post('/products', [ProductController::class, 'store'])
->middleware('scope:create-products');
});

Laravel proporciona un conjunto completo de herramientas para crear APIs RESTful robustas y seguras. Desde la definición de rutas y controladores hasta la serialización de respuestas JSON y la autenticación mediante tokens, el framework facilita el desarrollo de APIs modernas que siguen las mejores prácticas.

Al combinar estas características con otras funcionalidades de Laravel como validación, middleware, y manejo de excepciones, puedes construir APIs escalables y mantenibles que satisfagan las necesidades de tus clientes y aplicaciones frontend.

🐝