Lección 32 de 45 18 min de lectura

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:

json
{
    "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:

bash
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
<?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
<?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:

bash
# 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
<?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
<?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
<?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.

bash
php artisan make:resource PostResource
php
<?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
<?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á:

json
{
    "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
<?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:

json
{
    "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
<?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
<?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
<?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
<?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:

json
{
    "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:

bash
# 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:api para habilitar rutas API en Laravel 11+
  • Usa --api al crear controladores para omitir métodos de formularios
  • Usa apiResource para registrar rutas RESTful
  • Los API Resources transforman modelos en JSON de forma controlada
  • Usa whenLoaded para 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'])
    );
}

¿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