APIs y Recursos
Las APIs REST permiten que tu aplicación Laravel se comunique con aplicaciones móviles, frontends JavaScript y otros servicios. En esta lección aprenderás a crear APIs usando rutas API, controladores de recursos y API Resources para transformar tus modelos en respuestas JSON.
¿Qué es una API REST?
Una API (Application Programming Interface) REST es una forma de exponer datos y funcionalidades de tu aplicación a través de URLs que devuelven JSON en lugar de HTML.
Mientras que las rutas web devuelven vistas Blade, las rutas API devuelven datos estructurados:
{
"data": {
"id": 1,
"name": "Juan García",
"email": "juan@example.com",
"created_at": "2024-01-15T10:30:00Z"
}
}
El archivo de rutas API
En Laravel 11+, el archivo routes/api.php
no existe por defecto. Para habilitarlo, ejecuta:
php artisan install:api
Este comando hace varias cosas:
- Crea el archivo
routes/api.php - Instala Laravel Sanctum para autenticación de APIs
- Registra las rutas API en
bootstrap/app.php - Configura automáticamente el prefijo
/api
En bootstrap/app.php verás el registro:
<?php
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php', // Añadido por install:api
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
Sobre Sanctum: Laravel Sanctum permite autenticar APIs usando tokens. En esta lección nos centramos en crear endpoints; la autenticación de APIs es un tema más amplio que cubrimos en nuestro curso de API REST.
<?php
// routes/api.php
use Illuminate\Support\Facades\Route;
// Esta ruta será accesible en /api/status
Route::get('/status', function () {
return response()->json([
'status' => 'ok',
'version' => '1.0.0',
]);
});
Controladores API
Los controladores API son similares a los controladores web, pero devuelven JSON en lugar de vistas. Puedes crear un controlador de recursos con:
# Controlador API con métodos de recurso (sin create ni edit)
php artisan make:controller Api/PostController --api
La bandera --api genera un controlador
con los métodos index, store,
show, update y destroy,
omitiendo create y edit que
solo sirven para mostrar formularios HTML.
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(): JsonResponse
{
$posts = Post::with('author')->latest()->paginate(15);
return response()->json($posts);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
]);
$post = $request->user()->posts()->create($validated);
return response()->json($post, 201);
}
public function show(Post $post): JsonResponse
{
return response()->json($post->load('author'));
}
public function update(Request $request, Post $post): JsonResponse
{
$validated = $request->validate([
'title' => ['sometimes', 'string', 'max:255'],
'content' => ['sometimes', 'string'],
]);
$post->update($validated);
return response()->json($post);
}
public function destroy(Post $post): JsonResponse
{
$post->delete();
return response()->json(null, 204);
}
}
Rutas de recurso API
Registra todas las rutas del controlador con
apiResource:
<?php
// routes/api.php
use App\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;
Route::apiResource('posts', PostController::class);
Esto genera las siguientes rutas:
| Método | URI | Acción |
|---|---|---|
| GET | /api/posts | index |
| POST | /api/posts | store |
| GET | /api/posts/{post} | show |
| PUT/PATCH | /api/posts/{post} | update |
| DELETE | /api/posts/{post} | destroy |
Códigos de estado HTTP
Las APIs usan códigos de estado para indicar el resultado de cada petición:
- 200 OK: Petición exitosa
- 201 Created: Recurso creado
- 204 No Content: Eliminado correctamente
- 400 Bad Request: Error en la petición
- 401 Unauthorized: No autenticado
- 403 Forbidden: Sin permiso
- 404 Not Found: Recurso no encontrado
- 422 Unprocessable Entity: Error de validación
<?php
// Recurso creado exitosamente
return response()->json($post, 201);
// Eliminado sin contenido que devolver
return response()->json(null, 204);
// Error personalizado
return response()->json([
'message' => 'El post no existe.',
], 404);
API Resources
Los API Resources son clases que transforman modelos Eloquent en JSON de forma controlada. Permiten definir exactamente qué campos exponer y cómo formatearlos.
php artisan make:resource PostResource
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => str($this->content)->limit(150),
'content' => $this->content,
'author' => [
'id' => $this->author->id,
'name' => $this->author->name,
],
'published_at' => $this->published_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
Usa el Resource en tu controlador:
<?php
use App\Http\Resources\PostResource;
use App\Models\Post;
class PostController extends Controller
{
public function show(Post $post): PostResource
{
return new PostResource($post->load('author'));
}
}
El resultado JSON será:
{
"data": {
"id": 1,
"title": "Mi primer post",
"slug": "mi-primer-post",
"excerpt": "Este es el contenido del post...",
"content": "Este es el contenido completo del post...",
"author": {
"id": 1,
"name": "Juan García"
},
"published_at": "2024-01-15T10:30:00+00:00",
"created_at": "2024-01-14T08:00:00+00:00"
}
}
Colecciones de recursos
Para devolver múltiples modelos, usa el método
collection:
<?php
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class PostController extends Controller
{
public function index(): AnonymousResourceCollection
{
$posts = Post::with('author')->latest()->paginate(15);
return PostResource::collection($posts);
}
}
Laravel incluye automáticamente los enlaces de paginación:
{
"data": [
{ "id": 1, "title": "Post 1", ... },
{ "id": 2, "title": "Post 2", ... }
],
"links": {
"first": "http://app.test/api/posts?page=1",
"last": "http://app.test/api/posts?page=5",
"prev": null,
"next": "http://app.test/api/posts?page=2"
},
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 15,
"total": 67
}
}
Campos condicionales
Puedes incluir campos solo cuando se cumpla una
condición usando when o
whenLoaded:
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
// Solo incluir si la relación está cargada
'author' => new UserResource($this->whenLoaded('author')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
// Solo incluir para administradores
'views_count' => $this->when(
$request->user()?->is_admin,
$this->views_count
),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
Recursos anidados
Puedes usar otros Resources dentro de un Resource para relaciones:
<?php
// app/Http/Resources/UserResource.php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'avatar_url' => $this->avatar_url,
];
}
}
<?php
// app/Http/Resources/PostResource.php
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'author' => new UserResource($this->whenLoaded('author')),
'created_at' => $this->created_at->toIso8601String(),
];
}
Validación en APIs
La validación funciona igual que en controladores web. Laravel devuelve automáticamente errores en formato JSON cuando la petición espera JSON:
<?php
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
'category_id' => ['required', 'exists:categories,id'],
]);
$post = $request->user()->posts()->create($validated);
return response()->json(new PostResource($post), 201);
}
Si la validación falla, Laravel devuelve un error 422:
{
"message": "The title field is required.",
"errors": {
"title": ["The title field is required."],
"content": ["The content field is required."]
}
}
Probar la API
Puedes probar tu API con herramientas como
Postman, Insomnia
o directamente con curl:
# Listar posts
curl http://localhost:8000/api/posts
# Ver un post específico
curl http://localhost:8000/api/posts/1
# Crear un post (requiere autenticación)
curl -X POST http://localhost:8000/api/posts \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"title": "Nuevo post", "content": "Contenido..."}'
Importante: Siempre incluye el
header Accept: application/json en
las peticiones API para que Laravel devuelva
errores en formato JSON.
Resumen
- Ejecuta
php artisan install:apipara habilitar rutas API en Laravel 11+ - Usa
--apial crear controladores para omitir métodos de formularios - Usa
apiResourcepara registrar rutas RESTful - Los API Resources transforman modelos en JSON de forma controlada
- Usa
whenLoadedpara incluir relaciones solo cuando estén cargadas - Laravel devuelve errores de validación en JSON automáticamente
- Siempre usa códigos de estado HTTP apropiados (201 para crear, 204 para eliminar)
¿Quieres dominar APIs en Laravel? Esta lección cubre los fundamentos, pero crear APIs profesionales requiere más: autenticación con tokens, versionado, rate limiting, documentación y testing. Tenemos un curso completo de API REST con Laravel donde construirás una API real paso a paso.
Ejercicios
Ejercicio 1: API Resource básico
Crea un UserResource que exponga solo
los campos id, name,
email y created_at (formateado
como ISO 8601). No debe exponer el campo
password.
Ver solución
php artisan make:resource UserResource
<?php
// app/Http/Resources/UserResource.php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toIso8601String(),
];
}
}
<?php
// Uso en controlador
use App\Http\Resources\UserResource;
use App\Models\User;
public function show(User $user): UserResource
{
return new UserResource($user);
}
Ejercicio 2: Controlador API completo
Crea un controlador API para el modelo
Category con los métodos index
(listar todas), show (ver una) y
store (crear). Usa validación para el
campo name (requerido, máximo 100
caracteres) y devuelve códigos de estado apropiados.
Ver solución
php artisan make:controller Api/CategoryController --api
php artisan make:resource CategoryResource
<?php
// app/Http/Resources/CategoryResource.php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class CategoryResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'posts_count' => $this->when(
$this->posts_count !== null,
$this->posts_count
),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
<?php
// app/Http/Controllers/Api/CategoryController.php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\CategoryResource;
use App\Models\Category;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class CategoryController extends Controller
{
public function index(): AnonymousResourceCollection
{
$categories = Category::withCount('posts')->get();
return CategoryResource::collection($categories);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:100'],
]);
$validated['slug'] = str($validated['name'])->slug();
$category = Category::create($validated);
return response()->json(
new CategoryResource($category),
201
);
}
public function show(Category $category): CategoryResource
{
return new CategoryResource(
$category->loadCount('posts')
);
}
}
<?php
// routes/api.php
use App\Http\Controllers\Api\CategoryController;
use Illuminate\Support\Facades\Route;
Route::apiResource('categories', CategoryController::class)
->only(['index', 'show', 'store']);
Ejercicio 3: Campos condicionales
Modifica el PostResource para que:
el campo content solo se incluya en
la vista individual (no en listados), y el campo
comments solo aparezca si la relación
está cargada.
Ver solución
<?php
// app/Http/Resources/PostResource.php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => str($this->content)->limit(150),
// Solo incluir content en vista individual (show)
'content' => $this->when(
$request->routeIs('api.posts.show'),
$this->content
),
'author' => new UserResource($this->whenLoaded('author')),
// Solo incluir si la relación comments está cargada
'comments' => CommentResource::collection(
$this->whenLoaded('comments')
),
'comments_count' => $this->when(
$this->comments_count !== null,
$this->comments_count
),
'published_at' => $this->published_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
<?php
// Uso en controlador
// index: sin content, sin comments
public function index(): AnonymousResourceCollection
{
$posts = Post::with('author')
->withCount('comments')
->latest()
->paginate(15);
return PostResource::collection($posts);
}
// show: con content y comments
public function show(Post $post): PostResource
{
return new PostResource(
$post->load(['author', 'comments.author'])
);
}
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Quieres crear APIs profesionales?
Aprende a construir APIs REST completas con autenticación, versionado y buenas prácticas en nuestro curso especializado.
Ver curso de API REST