Cómo construir una API REST con Laravel paso a paso
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.
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
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
PUToPATCH.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:apicomposer 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 -mEl 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 migrate8. 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 UpdateProductRequestValidació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/products12. Probar la API
Inicia el servidor:
php artisan serveEnví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 test15. 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=falseen 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.
Herramientas y su función en este proceso
No es una lista genérica: cada recurso tiene un papel concreto dentro del proceso.
Framework principal
Laravel
Laravel se utiliza para construir la API REST, definir las rutas, gestionar los modelos, validar las peticiones y devolver respuestas JSON.
Continúa con una guía relacionada
Sigue mejorando tus proyectos con otros flujos prácticos.