Saltar al contenido
CodigoDev

Cómo construir una API REST con Laravel paso a paso

folder Backend Tutorial Intermedia
Ilustración de una API REST construida con Laravel
info Introducción a la guía

Aprende a crear una API REST completa y profesional en Laravel 13 y PHP 8.4. Guía paso a paso con migraciones, controladores, validaciones, recursos y buenas prácticas.

task_alt

Qué conseguirás

Al finalizar, tendrás una API REST funcional para gestionar productos, implementando operaciones CRUD, validaciones, recursos JSON y pruebas automatizadas.

groups Público objetivo

Desarrolladores web con conocimientos básicos de PHP y Laravel que desean aprender a crear APIs RESTful desde cero.

checklist Antes de empezar / Requisitos previos

PHP 8.4, Composer, entorno de desarrollo local configurado y conocimientos básicos de programación orientada a objetos en PHP.
Guía paso a paso verificada CodigoDev

1. Introducción

Las APIs REST (Representational State Transfer) son el estándar moderno para comunicar aplicaciones web, móviles y servicios backend. Proporcionan una forma estructurada y predecible de acceder y manipular datos utilizando los métodos estándar del protocolo HTTP.

En esta guía aprenderás a construir una API REST completa y profesional utilizando Laravel 13 y PHP 8.4. Crearás un sistema para gestionar un inventario, desde la configuración inicial hasta la validación, transformación de datos, manejo de errores y pruebas automatizadas. El resultado será una API robusta lista para ser consumida por cualquier frontend.

2. Qué construiremos

Desarrollaremos una API REST para gestionar un recurso llamado Product. La API proporcionará las siguientes operaciones CRUD:

  • Listar productos con paginación mediante GET.

  • Obtener un producto específico mediante GET.

  • Crear un producto mediante POST.

  • Reemplazar o actualizar parcialmente un producto mediante PUT o PATCH.

  • Eliminar un producto mediante DELETE.

La API validará los datos de entrada, protegerá contra asignación masiva, devolverá respuestas JSON consistentes e incluirá pruebas automatizadas.

3. Requisitos previos

Para seguir este tutorial necesitarás:

  • PHP 8.4 instalado.

  • Composer.

  • Laravel Installer, opcionalmente.

  • Una terminal.

  • Un editor como VS Code o PhpStorm.

  • Conocimientos básicos de PHP, Laravel, MVC, rutas y bases de datos.

4. Crear el proyecto Laravel

Comienza creando el proyecto con Composer:

composer create-project laravel/laravel api-productos
cd api-productos
php artisan install:api

composer create-project descarga la estructura base de Laravel y sus dependencias. El comando install:api genera y configura las rutas de API en las versiones modernas de Laravel.

5. Configurar la base de datos

Para el desarrollo local utilizaremos SQLite. Crea el archivo database/database.sqlite y configura el archivo .env:

# .env
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

SQLite guarda los datos en un archivo local y es suficiente para este ejemplo. Para utilizar MySQL, cambia DB_CONNECTION y completa las credenciales correspondientes.

6. Crear el modelo y la migración

Genera el modelo Product junto con su migración:

php artisan make:model Product -m

El modelo representa la tabla y la migración define su estructura.

7. Crear la tabla de productos

Edita la migración generada:

// database/migrations/YYYY_MM_DD_HHMMSS_create_products_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table): void {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->decimal('price', 10, 2);
            $table->unsignedInteger('stock')->default(0);
            $table->boolean('is_active')->default(true);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Protege el modelo contra asignación masiva:

// app/Models/Product.php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'description',
        'price',
        'stock',
        'is_active',
    ];

    protected function casts(): array
    {
        return [
            'price' => 'decimal:2',
            'stock' => 'integer',
            'is_active' => 'boolean',
        ];
    }
}

Ejecuta la migración:

php artisan migrate

8. Crear las validaciones con Form Requests

No confíes en los datos enviados por el cliente. Delega la validación a Form Requests:

php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequest

Validación para crear productos

// app/Http/Requests/StoreProductRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'price' => ['required', 'numeric', 'min:0'],
            'stock' => ['required', 'integer', 'min:0'],
            'is_active' => ['sometimes', 'boolean'],
        ];
    }
}

Validación para PUT y PATCH

PUT representa un reemplazo completo y PATCH una actualización parcial. En el siguiente request, los campos se exigen para PUT y se vuelven opcionales para PATCH:

// app/Http/Requests/UpdateProductRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        $rules = [
            'name' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'price' => ['required', 'numeric', 'min:0'],
            'stock' => ['required', 'integer', 'min:0'],
            'is_active' => ['sometimes', 'boolean'],
        ];

        if ($this->isMethod('PATCH')) {
            foreach ($rules as $field => $validations) {
                $rules[$field] = array_values(array_diff($validations, ['required']));
                array_unshift($rules[$field], 'sometimes');
            }
        }

        return $rules;
    }
}

9. Crear el API Resource

Un API Resource evita exponer directamente la estructura interna del modelo y permite controlar la forma de las respuestas.

php artisan make:resource ProductResource
// app/Http/Resources/ProductResource.php
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'price' => (float) $this->price,
            'stock' => (int) $this->stock,
            'is_active' => (bool) $this->is_active,
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}

10. Crear el controlador API

Genera un controlador API:

php artisan make:controller Api/ProductController --api --model=Product
// app/Http/Controllers/Api/ProductController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;

class ProductController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        return ProductResource::collection(Product::query()->paginate(10));
    }

    public function store(StoreProductRequest $request): ProductResource
    {
        $product = Product::query()->create($request->validated());

        return (new ProductResource($product))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Product $product): ProductResource
    {
        return new ProductResource($product);
    }

    public function update(UpdateProductRequest $request, Product $product): ProductResource
    {
        $product->update($request->validated());

        return new ProductResource($product->refresh());
    }

    public function destroy(Product $product): Response
    {
        $product->delete();

        return response()->noContent();
    }
}

El route model binding resuelve automáticamente el producto y devuelve un 404 si no existe.

11. Definir las rutas REST

// routes/api.php
use App\Http\Controllers\Api\ProductController;
use Illuminate\Support\Facades\Route;

Route::apiResource('products', ProductController::class);

Comprueba las rutas:

php artisan route:list --path=api/products

12. Probar la API

Inicia el servidor:

php artisan serve

Envía siempre Accept: application/json para recibir errores en formato JSON.

Crear un producto

curl -X POST http://127.0.0.1:8000/api/products \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name":"Teclado Mecánico","description":"Teclado RGB switch rojo","price":120.50,"stock":15,"is_active":true}'

La respuesta debe utilizar el envoltorio data estándar de los API Resources:

{
  "data": {
    "id": 1,
    "name": "Teclado Mecánico",
    "description": "Teclado RGB switch rojo",
    "price": 120.5,
    "stock": 15,
    "is_active": true,
    "created_at": "2026-10-24T12:00:00+00:00",
    "updated_at": "2026-10-24T12:00:00+00:00"
  }
}

Listar productos

curl -X GET http://127.0.0.1:8000/api/products \
-H "Accept: application/json"

Actualizar parcialmente

curl -X PATCH http://127.0.0.1:8000/api/products/1 \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"stock":10}'

Eliminar un producto

curl -X DELETE http://127.0.0.1:8000/api/products/1 \
-H "Accept: application/json"

13. Manejar errores y códigos HTTP

404 Not Found

Si se solicita un producto inexistente, el route model binding devuelve una respuesta 404.

422 Unprocessable Entity

Los Form Requests interrumpen la solicitud cuando faltan datos obligatorios o los valores no cumplen las reglas:

{
  "message": "The name field is required.",
  "errors": {
    "name": ["The name field is required."],
    "price": ["The price field must be at least 0."]
  }
}
  • 200 OK: lecturas y actualizaciones exitosas.

  • 201 Created: creación de un producto.

  • 204 No Content: eliminación exitosa.

  • 404 Not Found: producto inexistente.

  • 422 Unprocessable Entity: validación fallida.

14. Crear pruebas automatizadas

Las pruebas evitan que futuras modificaciones rompan la API. Genera una prueba PHPUnit y una factory:

php artisan make:factory ProductFactory --model=Product
php artisan make:test ProductApiTest --phpunit
// database/factories/ProductFactory.php
namespace Database\Factories;

use App\Models\Product;
use Illuminate\Database\Eloquent\Factories\Factory;

/** @extends Factory<Product> */
class ProductFactory extends Factory
{
    protected $model = Product::class;

    public function definition(): array
    {
        return [
            'name' => fake()->productName(),
            'description' => fake()->sentence(),
            'price' => fake()->randomFloat(2, 1, 1000),
            'stock' => fake()->numberBetween(0, 100),
            'is_active' => true,
        ];
    }
}
// tests/Feature/ProductApiTest.php
namespace Tests\Feature;

use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ProductApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_it_can_list_paginated_products(): void
    {
        Product::factory()->count(15)->create();

        $this->getJson('/api/products')
            ->assertOk()
            ->assertJsonStructure(['data', 'links', 'meta']);
    }

    public function test_it_can_create_a_product(): void
    {
        $this->postJson('/api/products', [
            'name' => 'Mouse Gamer',
            'price' => 50.99,
            'stock' => 30,
            'is_active' => true,
        ])->assertCreated()
            ->assertJsonPath('data.name', 'Mouse Gamer');

        $this->assertDatabaseHas('products', ['name' => 'Mouse Gamer']);
    }

    public function test_it_validates_required_fields(): void
    {
        $this->postJson('/api/products', [])
            ->assertUnprocessable()
            ->assertJsonValidationErrors(['name', 'price', 'stock']);
    }
}

Ejecuta las pruebas con:

php artisan test

15. Buenas prácticas para APIs REST en Laravel

  • Utiliza API Resources para controlar el formato JSON y evitar exponer campos internos.

  • Usa route model binding para resolver modelos y respuestas 404.

  • Protege los modelos con $fillable.

  • Mantén APP_DEBUG=false en producción.

  • Devuelve códigos HTTP semánticos.

  • Valida siempre los datos enviados por el cliente.

  • Utiliza paginación para evitar respuestas demasiado grandes.

16. Errores frecuentes

Olvidar el encabezado Accept

Si no envías Accept: application/json, Laravel puede responder con HTML en lugar de JSON cuando ocurre un error.

Confundir PUT y PATCH

PUT representa una actualización completa y PATCH una actualización parcial. El Form Request utiliza reglas diferentes para cada método.

Devolver directamente el modelo

Usa un API Resource para controlar los datos expuestos y mantener una respuesta estable aunque cambie la base de datos.

17. Conclusión

Has construido una API REST profesional en Laravel utilizando controladores dedicados, Form Requests, API Resources, route model binding, respuestas HTTP semánticas y pruebas funcionales. Esta estructura permite escalar la aplicación sin perder claridad ni mantenibilidad.

18. Próximos pasos

Una vez dominado el CRUD básico, puedes implementar autenticación con Laravel Sanctum, filtros dinámicos mediante scopes, rate limiting, paginación avanzada, documentación OpenAPI y una base de datos como MySQL o PostgreSQL.

C
Publicado por Equipo CodigoDev
calendar_today
Stack de la guía

Herramientas y su función en este proceso

No es una lista genérica: cada recurso tiene un papel concreto dentro del proceso.

Logo de Laravel, framework backend para aplicaciones PHP

Framework principal

Laravel

Principal

Laravel se utiliza para construir la API REST, definir las rutas, gestionar los modelos, validar las peticiones y devolver respuestas JSON.

Sigue mejorando tus proyectos con otros flujos prácticos.

Ver todas las guías arrow_forward