Laravel tanto para web como para API al mismo tiempo.

 Te explico cómo crear un CRUD completo en Laravel tanto para web como para API al mismo tiempo.

1. Configuración Inicial

Primero, crea el modelo con migración y controlador:

bash
php artisan make:model Product -mcr

2. Migración

database/migrations/xxxx_create_products_table.php:

php
<?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) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->decimal('price', 10, 2);
            $table->integer('stock');
            $table->timestamps();
        });
    }

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

3. Modelo

app/Models/Product.php:

php
<?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'
    ];

    protected $casts = [
        'price' => 'decimal:2',
    ];
}

4. Controlador Unificado (Web + API)

app/Http/Controllers/ProductController.php:

php
<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\View\View;

class ProductController extends Controller
{
    // WEB: Mostrar lista de productos
    public function index(): View
    {
        $products = Product::latest()->paginate(10);
        return view('products.index', compact('products'));
    }

    // WEB: Mostrar formulario de creación
    public function create(): View
    {
        return view('products.create');
    }

    // WEB/API: Almacenar nuevo producto
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'price' => 'required|numeric|min:0',
            'stock' => 'required|integer|min:0'
        ]);

        $product = Product::create($validated);

        if ($request->wantsJson()) {
            return response()->json($product, 201);
        }

        return redirect()->route('products.index')
            ->with('success', 'Producto creado exitosamente.');
    }

    // WEB: Mostrar producto específico
    public function show(Product $product): View
    {
        return view('products.show', compact('product'));
    }

    // WEB: Mostrar formulario de edición
    public function edit(Product $product): View
    {
        return view('products.edit', compact('product'));
    }

    // WEB/API: Actualizar producto
    public function update(Request $request, Product $product)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'price' => 'required|numeric|min:0',
            'stock' => 'required|integer|min:0'
        ]);

        $product->update($validated);

        if ($request->wantsJson()) {
            return response()->json($product);
        }

        return redirect()->route('products.index')
            ->with('success', 'Producto actualizado exitosamente.');
    }

    // WEB/API: Eliminar producto
    public function destroy(Request $request, Product $product)
    {
        $product->delete();

        if ($request->wantsJson()) {
            return response()->json(null, 204);
        }

        return redirect()->route('products.index')
            ->with('success', 'Producto eliminado exitosamente.');
    }

    // API: Métodos específicos para API
    public function apiIndex(): JsonResponse
    {
        $products = Product::all();
        return response()->json($products);
    }

    public function apiShow(Product $product): JsonResponse
    {
        return response()->json($product);
    }
}

5. Rutas

routes/web.php:

php
<?php

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

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

*****************************************
Route::get('products', [ProductController::class, 'index'])->name('products.index');
Route::get('products/create', [ProductController::class, 'create'])->name('products.create');
Route::post('products', [ProductController::class, 'store'])->name('products.store');
Route::get('products/{product}', [ProductController::class, 'show'])->name('products.show');
Route::get('products/{product}/edit', [ProductController::class, 'edit'])->name('products.edit');
Route::put('products/{product}', [ProductController::class, 'update'])->name('products.update');
Route::patch('products/{product}', [ProductController::class, 'update']);
Route::delete('products/{product}', [ProductController::class, 'destroy'])->name('products.destroy');

routes/api.php:

php
<?php

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

Route::apiResource('products', ProductController::class);
Route::get('products', [ProductController::class, 'apiIndex']);
Route::get('products/{product}', [ProductController::class, 'apiShow']);

*********************
<?php

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

// Rutas normales para API
Route::get('products', [ProductController::class, 'apiIndex']); // Usando el método específico para API
Route::post('products', [ProductController::class, 'store']);
Route::get('products/{product}', [ProductController::class, 'apiShow']); // Usando el método específico para API
Route::put('products/{product}', [ProductController::class, 'update']);
Route::patch('products/{product}', [ProductController::class, 'update']);
Route::delete('products/{product}', [ProductController::class, 'destroy']);

// O si prefieres usar los métodos principales con validación de JSON:
Route::get('products', [ProductController::class, 'index']); // Este mostraría HTML para web, no recomendado para API pura
Route::get('products/{product}', [ProductController::class, 'show']); // Mismo caso que arriba

// Alternativa usando apiResource (si prefieres mantenerlo)
// Route::apiResource('products', ProductController::class);

Observaciones importantes:

  1. Problema de diseño: Tu controlador actual tiene un conflicto porque los métodos index() y show() devuelven vistas (View) para web, pero la API necesita JSON.

  2. Recomendación: Sería mejor separar los controladores como en tu primer ejemplo, o modificar los métodos para detectar el tipo de request:

php
// En el método index del controlador
public function index(Request $request)
{
    $products = Product::latest()->paginate(10);
    
    if ($request->wantsJson()) {
        return response()->json($products);
    }
    
    return view('products.index', compact('products'));
}
  1. Versión optimizada de rutas API:

php
// routes/api.php
Route::get('products', [ProductController::class, 'apiIndex']);
Route::post('products', [ProductController::class, 'store']);
Route::get('products/{product}', [ProductController::class, 'apiShow']);
Route::put('products/{product}', [ProductController::class, 'update']);
Route::patch('products/{product}', [ProductController::class, 'update']);
Route::delete('products/{product}', [ProductController::class, 'destroy']);

Si quieres un enfoque más limpio:

Te recomiendo mantener la separación de controladores que tenías inicialmente (Web/ProductController y Api/ProductController) ya que evita la lógica condicional y hace el código más mantenible.

6. Vistas (Web)

resources/views/products/index.blade.php:

blade
@extends('layouts.app')

@section('content')
<div class="container">
    <h1>Productos</h1>
    <a href="{{ route('products.create') }}" class="btn btn-primary mb-3">Nuevo Producto</a>
    
    @if(session('success'))
        <div class="alert alert-success">{{ session('success') }}</div>
    @endif

    <table class="table table-striped">
        <thead>
            <tr>
                <th>ID</th>
                <th>Nombre</th>
                <th>Precio</th>
                <th>Stock</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
            @foreach($products as $product)
            <tr>
                <td>{{ $product->id }}</td>
                <td>{{ $product->name }}</td>
                <td>${{ number_format($product->price, 2) }}</td>
                <td>{{ $product->stock }}</td>
                <td>
                    <a href="{{ route('products.show', $product) }}" class="btn btn-info btn-sm">Ver</a>
                    <a href="{{ route('products.edit', $product) }}" class="btn btn-warning btn-sm">Editar</a>
                    <form action="{{ route('products.destroy', $product) }}" method="POST" class="d-inline">
                        @csrf
                        @method('DELETE')
                        <button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('¿Eliminar?')">Eliminar</button>
                    </form>
                </td>
            </tr>
            @endforeach
        </tbody>
    </table>
    
    {{ $products->links() }}
</div>
@endsection

resources/views/products/create.blade.php:

blade
@extends('layouts.app')

@section('content')
<div class="container">
    <h1>Crear Producto</h1>
    <form action="{{ route('products.store') }}" method="POST">
        @csrf
        <div class="mb-3">
            <label for="name" class="form-label">Nombre</label>
            <input type="text" class="form-control" id="name" name="name" required>
        </div>
        <div class="mb-3">
            <label for="description" class="form-label">Descripción</label>
            <textarea class="form-control" id="description" name="description" rows="3"></textarea>
        </div>
        <div class="mb-3">
            <label for="price" class="form-label">Precio</label>
            <input type="number" step="0.01" class="form-control" id="price" name="price" required>
        </div>
        <div class="mb-3">
            <label for="stock" class="form-label">Stock</label>
            <input type="number" class="form-control" id="stock" name="stock" required>
        </div>
        <button type="submit" class="btn btn-primary">Crear</button>
        <a href="{{ route('products.index') }}" class="btn btn-secondary">Cancelar</a>
    </form>
</div>
@endsection

7. Ejecutar Migración

bash
php artisan migrate

8. Probar las Rutas

Web:

  • GET /products - Lista de productos

  • GET /products/create - Formulario de creación

  • POST /products - Crear producto

  • GET /products/{id} - Ver producto

  • GET /products/{id}/edit - Editar producto

  • PUT/PATCH /products/{id} - Actualizar producto

  • DELETE /products/{id} - Eliminar producto

API:

  • GET /api/products - Lista de productos (JSON)

  • POST /api/products - Crear producto (JSON)

  • GET /api/products/{id} - Ver producto (JSON)

  • PUT/PATCH /api/products/{id} - Actualizar producto (JSON)

  • DELETE /api/products/{id} - Eliminar producto

9. Ejemplo de Uso con API

bash
# Crear producto
curl -X POST http://localhost:8000/api/products \
  -H "Content-Type: application/json" \
  -d '{"name": "Laptop", "price": 999.99, "stock": 10, "description": "Gaming laptop"}'

# Listar productos
curl http://localhost:8000/api/products

# Actualizar producto
curl -X PUT http://localhost:8000/api/products/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Laptop Pro", "price": 1299.99, "stock": 5}'

# Eliminar producto
curl -X DELETE http://localhost:8000/api/products/1

Este enfoque te permite tener un CRUD completo tanto para interfaz web como para API RESTful usando el mismo controlador, optimizando el código y manteniendo la consistencia entre ambas interfaces

Comentarios

Entradas más populares de este blog

crear controladores separados para API y Web

Creación de una API RESTful con Laravel (sin autenticación)