Sistema CRUD API REST en Laravel 12

 

Sistema CRUD API REST en Laravel 12

Tutorial completo para crear una API REST con Laravel 12, basado en el tutorial original pero enfocado únicamente en el desarrollo de API.


📚 Índice

  1. Configuración inicial

  2. Creación de la migración

  3. Creación del modelo

  4. Creación del Factory y Seeder

  5. Creación del Controlador API

  6. Definición de Rutas API

  7. Pruebas de la API

  8. Manejo de errores y respuestas

  9. Comandos útiles


1️⃣ Configuración inicial

1.1 Requisitos previos

bash
# Verifica tu versión de PHP (mínimo 8.2 para Laravel 12)
php -v

# Verifica tu versión de Composer
composer -V

1.2 Crear el proyecto

bash
# Crea un nuevo proyecto Laravel
composer create-project laravel/laravel pasteles-api
cd pasteles-api

# Instalar dependencias adicionales (opcional pero recomendado para API)
composer require laravel/sanctum

1.3 Configurar la base de datos

Edita el archivo .env:

env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pasteles_api_db
DB_USERNAME=root
DB_PASSWORD=

1.4 Configurar Sanctum (para autenticación API)

bash
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

2️⃣ Creación de la migración

2.1 Generar la migración

bash
php artisan make:migration create_pasteles_table

2.2 Definir la estructura de la tabla

database/migrations/[timestamp]_create_pasteles_table.php

php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('pasteles', function (Blueprint $table) {
            $table->id();
            $table->string('nombre', 100);
            $table->enum('sabor', ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco']);
            $table->decimal('precio', 8, 2);
            $table->boolean('disponible')->default(true);
            $table->text('descripcion')->nullable();
            $table->timestamps();
            $table->softDeletes(); // Eliminación lógica
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('pasteles');
    }
};

2.3 Ejecutar la migración

bash
php artisan migrate

3️⃣ Creación del modelo

3.1 Generar el modelo

bash
php artisan make:model Pastel

3.2 Definir el modelo

app/Models/Pastel.php

php
<?php

namespace App\Models;

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

class Pastel extends Model
{
    use HasFactory, SoftDeletes;

    /**
     * La tabla asociada al modelo.
     */
    protected $table = 'pasteles';

    /**
     * Los atributos que son asignables masivamente.
     */
    protected $fillable = [
        'nombre',
        'sabor',
        'precio',
        'disponible',
        'descripcion'
    ];

    /**
     * Los atributos que deben ser convertidos a tipos nativos.
     */
    protected $casts = [
        'precio' => 'decimal:2',
        'disponible' => 'boolean',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
        'deleted_at' => 'datetime',
    ];

    /**
     * Los atributos que deben ser ocultados para arrays/JSON.
     */
    protected $hidden = [
        'deleted_at'
    ];

    /**
     * Los atributos que deben ser incluidos en arrays/JSON.
     */
    protected $visible = [
        'id',
        'nombre',
        'sabor',
        'precio',
        'disponible',
        'descripcion',
        'created_at',
        'updated_at'
    ];

    /**
     * Scope para obtener solo pasteles disponibles.
     */
    public function scopeDisponibles($query)
    {
        return $query->where('disponible', true);
    }

    /**
     * Scope para obtener pasteles por sabor.
     */
    public function scopeBySabor($query, $sabor)
    {
        return $query->where('sabor', $sabor);
    }

    /**
     * Scope para obtener pasteles por rango de precio.
     */
    public function scopePrecioEntre($query, $min, $max)
    {
        return $query->whereBetween('precio', [$min, $max]);
    }

    /**
     * Accesor para formatear el precio.
     */
    public function getPrecioFormateadoAttribute(): string
    {
        return '$' . number_format($this->precio, 2);
    }

    /**
     * Mutator para capitalizar el nombre.
     */
    public function setNombreAttribute($value): void
    {
        $this->attributes['nombre'] = ucwords(strtolower($value));
    }
}

4️⃣ Creación del Factory y Seeder

4.1 Generar el Factory

bash
php artisan make:factory PastelFactory

database/factories/PastelFactory.php

php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Pastel>
 */
class PastelFactory extends Factory
{
    /**
     * Define el estado predeterminado del modelo.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $sabores = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'];
        
        return [
            'nombre' => 'Pastel ' . $this->faker->unique()->firstName() . ' ' . $this->faker->lastName(),
            'sabor' => $this->faker->randomElement($sabores),
            'precio' => $this->faker->randomFloat(2, 50, 500),
            'disponible' => $this->faker->boolean(80), // 80% de probabilidad
            'descripcion' => $this->faker->sentence(10),
            'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
            'updated_at' => $this->faker->dateTimeBetween('-1 month', 'now'),
        ];
    }

    /**
     * Estado para pasteles no disponibles.
     */
    public function noDisponible(): static
    {
        return $this->state(fn (array $attributes) => [
            'disponible' => false,
        ]);
    }

    /**
     * Estado para pasteles de chocolate.
     */
    public function chocolate(): static
    {
        return $this->state(fn (array $attributes) => [
            'sabor' => 'chocolate',
        ]);
    }
}

4.2 Generar el Seeder

bash
php artisan make:seeder PastelSeeder

database/seeders/PastelSeeder.php

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Pastel;

class PastelSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // Crear 50 pasteles con el factory
        Pastel::factory(50)->create();

        // Crear 5 pasteles de chocolate no disponibles
        Pastel::factory(5)->chocolate()->noDisponible()->create();

        // Crear 10 pasteles de chocolate disponibles
        Pastel::factory(10)->chocolate()->create();

        $this->command->info('✅ Se crearon ' . Pastel::count() . ' pasteles en la base de datos.');
    }
}

4.3 Registrar el Seeder

database/seeders/DatabaseSeeder.php

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            PastelSeeder::class,
        ]);
    }
}

4.4 Ejecutar migraciones y seeders

bash
# Ejecutar migraciones y seeders juntos
php artisan migrate --seed

# O por separado
php artisan migrate
php artisan db:seed

5️⃣ Creación del Controlador API

5.1 Generar el controlador

bash
php artisan make:controller Api/PastelController

5.2 Definir el controlador API

app/Http/Controllers/Api/PastelController.php

php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Pastel;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class PastelController extends Controller
{
    /**
     * GET /api/pasteles
     * Lista todos los pasteles con opción de filtros.
     */
    public function index(Request $request)
    {
        try {
            $query = Pastel::query();

            // Filtro por disponibilidad
            if ($request->has('disponible')) {
                $query->where('disponible', filter_var($request->disponible, FILTER_VALIDATE_BOOLEAN));
            }

            // Filtro por sabor
            if ($request->has('sabor')) {
                $query->bySabor($request->sabor);
            }

            // Filtro por rango de precio
            if ($request->has('precio_min') && $request->has('precio_max')) {
                $query->precioEntre($request->precio_min, $request->precio_max);
            }

            // Búsqueda por nombre
            if ($request->has('search')) {
                $query->where('nombre', 'LIKE', '%' . $request->search . '%');
            }

            // Paginación
            $perPage = $request->input('per_page', 15);
            $pasteles = $query->paginate($perPage);

            return response()->json([
                'success' => true,
                'data' => $pasteles->items(),
                'meta' => [
                    'current_page' => $pasteles->currentPage(),
                    'per_page' => $pasteles->perPage(),
                    'total' => $pasteles->total(),
                    'last_page' => $pasteles->lastPage(),
                ],
                'message' => 'Pasteles obtenidos exitosamente'
            ], 200);

        } catch (\Exception $e) {
            return $this->errorResponse('Error al obtener los pasteles', 500);
        }
    }

    /**
     * POST /api/pasteles
     * Crea un nuevo pastel.
     */
    public function store(Request $request)
    {
        try {
            // Validación de datos
            $validated = $request->validate([
                'nombre' => 'required|string|max:100',
                'sabor' => ['required', Rule::in(['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'])],
                'precio' => 'required|numeric|min:0|max:999999.99',
                'disponible' => 'sometimes|boolean',
                'descripcion' => 'nullable|string|max:1000',
            ]);

            // Si no viene 'disponible', lo ponemos en true por defecto
            if (!isset($validated['disponible'])) {
                $validated['disponible'] = true;
            }

            $pastel = Pastel::create($validated);

            return response()->json([
                'success' => true,
                'data' => $pastel,
                'message' => 'Pastel creado exitosamente'
            ], 201);

        } catch (\Illuminate\Validation\ValidationException $e) {
            return $this->errorResponse('Error de validación', 422, $e->errors());
        } catch (\Exception $e) {
            return $this->errorResponse('Error al crear el pastel', 500);
        }
    }

    /**
     * GET /api/pasteles/{id}
     * Muestra un pastel específico.
     */
    public function show($id)
    {
        try {
            $pastel = Pastel::findOrFail($id);

            return response()->json([
                'success' => true,
                'data' => $pastel,
                'message' => 'Pastel encontrado'
            ], 200);

        } catch (ModelNotFoundException $e) {
            return $this->errorResponse('Pastel no encontrado', 404);
        } catch (\Exception $e) {
            return $this->errorResponse('Error al obtener el pastel', 500);
        }
    }

    /**
     * PUT /api/pasteles/{id}
     * Actualiza un pastel completo.
     */
    public function update(Request $request, $id)
    {
        try {
            $pastel = Pastel::findOrFail($id);

            $validated = $request->validate([
                'nombre' => 'sometimes|string|max:100',
                'sabor' => ['sometimes', Rule::in(['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'])],
                'precio' => 'sometimes|numeric|min:0|max:999999.99',
                'disponible' => 'sometimes|boolean',
                'descripcion' => 'nullable|string|max:1000',
            ]);

            $pastel->update($validated);

            return response()->json([
                'success' => true,
                'data' => $pastel->fresh(),
                'message' => 'Pastel actualizado exitosamente'
            ], 200);

        } catch (ModelNotFoundException $e) {
            return $this->errorResponse('Pastel no encontrado', 404);
        } catch (\Illuminate\Validation\ValidationException $e) {
            return $this->errorResponse('Error de validación', 422, $e->errors());
        } catch (\Exception $e) {
            return $this->errorResponse('Error al actualizar el pastel', 500);
        }
    }

    /**
     * DELETE /api/pasteles/{id}
     * Elimina un pastel (Eliminación lógica).
     */
    public function destroy($id)
    {
        try {
            $pastel = Pastel::findOrFail($id);
            $pastel->delete();

            return response()->json([
                'success' => true,
                'message' => 'Pastel eliminado exitosamente'
            ], 200);

        } catch (ModelNotFoundException $e) {
            return $this->errorResponse('Pastel no encontrado', 404);
        } catch (\Exception $e) {
            return $this->errorResponse('Error al eliminar el pastel', 500);
        }
    }

    /**
     * GET /api/pasteles/disponibles
     * Obtiene solo los pasteles disponibles.
     */
    public function disponibles()
    {
        try {
            $pasteles = Pastel::disponibles()->get();

            return response()->json([
                'success' => true,
                'data' => $pasteles,
                'message' => 'Pasteles disponibles obtenidos exitosamente'
            ], 200);

        } catch (\Exception $e) {
            return $this->errorResponse('Error al obtener los pasteles disponibles', 500);
        }
    }

    /**
     * GET /api/pasteles/sabor/{sabor}
     * Obtiene pasteles por sabor.
     */
    public function bySabor($sabor)
    {
        try {
            $saboresValidos = ['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'];
            
            if (!in_array($sabor, $saboresValidos)) {
                return $this->errorResponse('Sabor no válido', 400);
            }

            $pasteles = Pastel::bySabor($sabor)->get();

            if ($pasteles->isEmpty()) {
                return $this->errorResponse('No se encontraron pasteles del sabor ' . $sabor, 404);
            }

            return response()->json([
                'success' => true,
                'data' => $pasteles,
                'message' => 'Pasteles del sabor ' . $sabor . ' obtenidos exitosamente'
            ], 200);

        } catch (\Exception $e) {
            return $this->errorResponse('Error al obtener los pasteles por sabor', 500);
        }
    }

    /**
     * GET /api/pasteles/estadisticas
     * Obtiene estadísticas de los pasteles.
     */
    public function estadisticas()
    {
        try {
            $total = Pastel::count();
            $disponibles = Pastel::disponibles()->count();
            $noDisponibles = $total - $disponibles;
            $precioPromedio = Pastel::avg('precio');
            $precioMaximo = Pastel::max('precio');
            $precioMinimo = Pastel::min('precio');

            // Agrupar por sabor
            $porSabor = Pastel::selectRaw('sabor, count(*) as total, avg(precio) as promedio')
                ->groupBy('sabor')
                ->get();

            return response()->json([
                'success' => true,
                'data' => [
                    'total' => $total,
                    'disponibles' => $disponibles,
                    'no_disponibles' => $noDisponibles,
                    'precio_promedio' => round($precioPromedio, 2),
                    'precio_maximo' => round($precioMaximo, 2),
                    'precio_minimo' => round($precioMinimo, 2),
                    'distribucion_por_sabor' => $porSabor
                ],
                'message' => 'Estadísticas obtenidas exitosamente'
            ], 200);

        } catch (\Exception $e) {
            return $this->errorResponse('Error al obtener estadísticas', 500);
        }
    }

    /**
     * POST /api/pasteles/bulk
     * Crea múltiples pasteles en una sola petición.
     */
    public function bulkStore(Request $request)
    {
        try {
            $validated = $request->validate([
                'pasteles' => 'required|array|min:1|max:100',
                'pasteles.*.nombre' => 'required|string|max:100',
                'pasteles.*.sabor' => ['required', Rule::in(['chocolate', 'vainilla', 'cheesecake', 'fresa', 'coco'])],
                'pasteles.*.precio' => 'required|numeric|min:0',
                'pasteles.*.disponible' => 'sometimes|boolean',
                'pasteles.*.descripcion' => 'nullable|string|max:1000',
            ]);

            $creados = [];
            foreach ($validated['pasteles'] as $data) {
                if (!isset($data['disponible'])) {
                    $data['disponible'] = true;
                }
                $creados[] = Pastel::create($data);
            }

            return response()->json([
                'success' => true,
                'data' => $creados,
                'message' => count($creados) . ' pasteles creados exitosamente'
            ], 201);

        } catch (\Illuminate\Validation\ValidationException $e) {
            return $this->errorResponse('Error de validación', 422, $e->errors());
        } catch (\Exception $e) {
            return $this->errorResponse('Error al crear los pasteles', 500);
        }
    }

    /**
     * Método auxiliar para respuestas de error.
     */
    private function errorResponse($message, $code, $errors = null)
    {
        $response = [
            'success' => false,
            'message' => $message
        ];

        if ($errors) {
            $response['errors'] = $errors;
        }

        return response()->json($response, $code);
    }
}

6️⃣ Definición de Rutas API

6.1 Configurar rutas

routes/api.php

php
<?php

use App\Http\Controllers\Api\PastelController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
*/

// Rutas públicas (sin autenticación)
Route::prefix('pasteles')->group(function () {
    // CRUD básico
    Route::get('/', [PastelController::class, 'index']);           // GET    /api/pasteles
    Route::post('/', [PastelController::class, 'store']);          // POST   /api/pasteles
    Route::get('/{id}', [PastelController::class, 'show']);        // GET    /api/pasteles/1
    Route::put('/{id}', [PastelController::class, 'update']);      // PUT    /api/pasteles/1
    Route::delete('/{id}', [PastelController::class, 'destroy']);  // DELETE /api/pasteles/1
    
    // Rutas adicionales
    Route::get('/disponibles', [PastelController::class, 'disponibles']);     // GET /api/pasteles/disponibles
    Route::get('/sabor/{sabor}', [PastelController::class, 'bySabor']);      // GET /api/pasteles/sabor/chocolate
    Route::get('/estadisticas', [PastelController::class, 'estadisticas']);  // GET /api/pasteles/estadisticas
    
    // Bulk operations
    Route::post('/bulk', [PastelController::class, 'bulkStore']);             // POST /api/pasteles/bulk
});

6.2 Ver las rutas

bash
php artisan route:list

7️⃣ Pruebas de la API

7.1 Usando cURL (desde la terminal)

bash
# GET - Obtener todos los pasteles
curl -X GET http://localhost:8000/api/pasteles

# GET - Con paginación
curl -X GET "http://localhost:8000/api/pasteles?per_page=5&page=2"

# GET - Con filtros
curl -X GET "http://localhost:8000/api/pasteles?disponible=true&sabor=chocolate"

# GET - Búsqueda por nombre
curl -X GET "http://localhost:8000/api/pasteles?search=Pastel"

# GET - Obtener un pastel específico
curl -X GET http://localhost:8000/api/pasteles/1

# POST - Crear un pastel
curl -X POST http://localhost:8000/api/pasteles \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "Pastel de Chocolate Premium",
    "sabor": "chocolate",
    "precio": 350.00,
    "disponible": true,
    "descripcion": "El mejor pastel de chocolate de toda la ciudad"
  }'

# PUT - Actualizar un pastel
curl -X PUT http://localhost:8000/api/pasteles/1 \
  -H "Content-Type: application/json" \
  -d '{
    "precio": 400.00,
    "disponible": false
  }'

# DELETE - Eliminar un pastel
curl -X DELETE http://localhost:8000/api/pasteles/1

# GET - Pasteles disponibles
curl -X GET http://localhost:8000/api/pasteles/disponibles

# GET - Pasteles por sabor
curl -X GET http://localhost:8000/api/pasteles/sabor/chocolate

# GET - Estadísticas
curl -X GET http://localhost:8000/api/pasteles/estadisticas

# POST - Crear múltiples pasteles
curl -X POST http://localhost:8000/api/pasteles/bulk \
  -H "Content-Type: application/json" \
  -d '{
    "pasteles": [
      {
        "nombre": "Pastel de Fresa",
        "sabor": "fresa",
        "precio": 280.00,
        "disponible": true,
        "descripcion": "Delicioso pastel de fresa"
      },
      {
        "nombre": "Pastel de Vainilla",
        "sabor": "vainilla",
        "precio": 250.00,
        "disponible": true,
        "descripcion": "Clásico pastel de vainilla"
      }
    ]
  }'

7.2 Usando Postman

  1. Importar colección: Crea una colección llamada "Pasteles API"

  2. Variables de entorno: Configura {{base_url}} = http://localhost:8000

Endpoints para Postman:

MétodoURLDescripción
GET{{base_url}}/api/pastelesListar pasteles
GET{{base_url}}/api/pasteles?disponible=trueFiltrar disponibles
GET{{base_url}}/api/pasteles?search=chocolateBuscar por nombre
POST{{base_url}}/api/pastelesCrear pastel
GET{{base_url}}/api/pasteles/1Ver pastel
PUT{{base_url}}/api/pasteles/1Actualizar pastel
DELETE{{base_url}}/api/pasteles/1Eliminar pastel
GET{{base_url}}/api/pasteles/disponiblesPasteles disponibles
GET{{base_url}}/api/pasteles/sabor/chocolatePasteles por sabor
GET{{base_url}}/api/pasteles/estadisticasEstadísticas
POST{{base_url}}/api/pasteles/bulkCrear múltiples

7.3 Usando PHPUnit (Pruebas automáticas)

bash
# Crear un test para la API
php artisan make:test PastelApiTest

tests/Feature/PastelApiTest.php

php
<?php

namespace Tests\Feature;

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

class PastelApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_get_all_pasteles()
    {
        Pastel::factory(3)->create();

        $response = $this->getJson('/api/pasteles');

        $response->assertStatus(200)
            ->assertJsonStructure([
                'success',
                'data',
                'meta',
                'message'
            ]);
    }

    public function test_can_create_pastel()
    {
        $data = [
            'nombre' => 'Pastel de Prueba',
            'sabor' => 'chocolate',
            'precio' => 299.99,
            'disponible' => true,
            'descripcion' => 'Descripción de prueba'
        ];

        $response = $this->postJson('/api/pasteles', $data);

        $response->assertStatus(201)
            ->assertJson([
                'success' => true,
                'message' => 'Pastel creado exitosamente'
            ]);

        $this->assertDatabaseHas('pasteles', [
            'nombre' => 'Pastel de Prueba'
        ]);
    }

    public function test_can_show_pastel()
    {
        $pastel = Pastel::factory()->create();

        $response = $this->getJson("/api/pasteles/{$pastel->id}");

        $response->assertStatus(200)
            ->assertJson([
                'success' => true,
                'data' => [
                    'id' => $pastel->id
                ]
            ]);
    }

    public function test_can_update_pastel()
    {
        $pastel = Pastel::factory()->create();

        $response = $this->putJson("/api/pasteles/{$pastel->id}", [
            'precio' => 500.00,
            'disponible' => false
        ]);

        $response->assertStatus(200)
            ->assertJson([
                'success' => true,
                'message' => 'Pastel actualizado exitosamente'
            ]);

        $this->assertDatabaseHas('pasteles', [
            'id' => $pastel->id,
            'precio' => 500.00,
            'disponible' => false
        ]);
    }

    public function test_can_delete_pastel()
    {
        $pastel = Pastel::factory()->create();

        $response = $this->deleteJson("/api/pasteles/{$pastel->id}");

        $response->assertStatus(200)
            ->assertJson([
                'success' => true,
                'message' => 'Pastel eliminado exitosamente'
            ]);

        $this->assertSoftDeleted('pasteles', [
            'id' => $pastel->id
        ]);
    }

    public function test_can_get_disponibles()
    {
        Pastel::factory(3)->create(['disponible' => true]);
        Pastel::factory(2)->create(['disponible' => false]);

        $response = $this->getJson('/api/pasteles/disponibles');

        $response->assertStatus(200)
            ->assertJsonCount(3, 'data');
    }

    public function test_can_get_by_sabor()
    {
        Pastel::factory(2)->create(['sabor' => 'chocolate']);
        Pastel::factory(3)->create(['sabor' => 'vainilla']);

        $response = $this->getJson('/api/pasteles/sabor/chocolate');

        $response->assertStatus(200)
            ->assertJsonCount(2, 'data');
    }

    public function test_returns_404_for_nonexistent_pastel()
    {
        $response = $this->getJson('/api/pasteles/999');

        $response->assertStatus(404)
            ->assertJson([
                'success' => false,
                'message' => 'Pastel no encontrado'
            ]);
    }

    public function test_validation_fails_for_invalid_data()
    {
        $response = $this->postJson('/api/pasteles', [
            'nombre' => '', // Nombre vacío
            'sabor' => 'invalido',
            'precio' => -100
        ]);

        $response->assertStatus(422)
            ->assertJson([
                'success' => false,
                'message' => 'Error de validación'
            ]);
    }
}

7.4 Ejecutar los tests

bash
php artisan test --filter=PastelApiTest

8️⃣ Manejo de errores y respuestas

8.1 Estructura de respuestas exitosas

json
{
    "success": true,
    "data": {
        // Datos del recurso
    },
    "message": "Mensaje descriptivo"
}

8.2 Estructura de respuestas con paginación

json
{
    "success": true,
    "data": [
        // Array de recursos
    ],
    "meta": {
        "current_page": 1,
        "per_page": 15,
        "total": 50,
        "last_page": 4
    },
    "message": "Pasteles obtenidos exitosamente"
}

8.3 Estructura de respuestas de error

json
{
    "success": false,
    "message": "Mensaje de error",
    "errors": {
        // Errores de validación (opcional)
    }
}

8.4 Códigos de estado HTTP utilizados

CódigoSignificadoUso
200OKOperación exitosa
201CreatedRecurso creado exitosamente
400Bad RequestSolicitud incorrecta
401UnauthorizedNo autenticado
403ForbiddenNo autorizado
404Not FoundRecurso no encontrado
422Unprocessable EntityError de validación
500Internal Server ErrorError del servidor

9️⃣ Comandos útiles

9.1 Comandos de migración

bash
# Ejecutar migraciones
php artisan migrate

# Reiniciar y ejecutar migraciones con seeders
php artisan migrate:fresh --seed

# Ver estado de migraciones
php artisan migrate:status

# Revertir última migración
php artisan migrate:rollback

9.2 Comandos de limpieza

bash
# Limpiar caché
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear

# Optimizar para producción
php artisan optimize

# Ver todas las rutas
php artisan route:list

9.3 Comandos de desarrollo

bash
# Iniciar servidor de desarrollo
php artisan serve

# Generar clave de aplicación
php artisan key:generate

# Crear un controlador
php artisan make:controller Api/NombreController

# Crear un modelo con migración y factory
php artisan make:model Nombre -m -f

# Crear un seeder
php artisan make:seeder NombreSeeder

📌 Resumen de endpoints

MétodoURLDescripción
GET/api/pastelesListar pasteles (con filtros)
POST/api/pastelesCrear pastel
GET/api/pasteles/{id}Obtener pastel
PUT/api/pasteles/{id}Actualizar pastel
DELETE/api/pasteles/{id}Eliminar pastel
GET/api/pasteles/disponiblesPasteles disponibles
GET/api/pasteles/sabor/{sabor}Pasteles por sabor
GET/api/pasteles/estadisticasEstadísticas
POST/api/pasteles/bulkCrear múltiples pasteles

🔒 Autenticación (Opcional)

Si quieres proteger tu API con autenticación:

bash
# Instalar Sanctum
composer require laravel/sanctum

# Publicar configuración
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

# Migrar
php artisan migrate

En el modelo User:

php
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

En routes/api.php:

php
Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('pasteles', PastelController::class);
});

Para obtener token:

bash
curl -X POST http://localhost:8000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"password"}'

Usar token:

bash
curl -X GET http://localhost:8000/api/pasteles \
  -H "Authorization: Bearer TOKEN_AQUI"

¡Y eso es todo! Ahora tienes una API REST completa en Laravel 12 con todas las operaciones CRUD, filtros, paginación, validaciones y manejo de errores. 🚀

La API está lista para ser consumida por aplicaciones frontend (React, Vue, Angular), aplicaciones móviles o cualquier otro cliente que soporte JSON

Comentarios

Entradas más populares de este blog

crear controladores separados para API y Web

Laravel tanto para web como para API al mismo tiempo.

Creación de una API RESTful con Laravel