Blog Laravel

CRUD con Laravel puro, sin paquetes

Muchos tutoriales arrancan con Livewire, Filament o Inertia. Este no. Aquí construimos el CRUD completo con lo que Laravel trae de fábrica: Eloquent, Blade, validaciones y redirecciones. Sin dependencias extras, sin magia.

01

Crear el proyecto

Arrancamos con un proyecto Laravel limpio. Si ya tienes Composer instalado, corre esto en tu terminal:

composer create-project laravel/laravel micrud
cd micrud
php artisan serve

El servidor queda en http://localhost:8000. Dejalo corriendo mientras trabajamos.

02

Configurar la base de datos

Abre el archivo .env en la raíz del proyecto y ajusta los datos de tu base de datos local:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=micrud
DB_USERNAME=root
DB_PASSWORD=
Crea la base de datos micrud en tu gestor (phpMyAdmin, TablePlus, DBeaver) antes de continuar. Laravel no la crea automáticamente.
03

Crear la migración

Generamos la migración para la tabla de productos:

php artisan make:migration create_products_table

Laravel crea el archivo en database/migrations/. Ábrelo y define las columnas:

public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('description')->nullable();
        $table->decimal('price', 8, 2);
        $table->timestamps();
    });
}

Ejecuta la migración para crear la tabla en la base de datos:

php artisan migrate
04

El Modelo

Crea el modelo con Artisan:

php artisan make:model Product

Abre app/Models/Product.php y declara los campos asignables en masa con $fillable:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = [
        'name',
        'description',
        'price',
    ];
}
Sin $fillable, Laravel bloquea la asignación masiva y el método Product::create() no funcionará. Siempre declara los campos que el usuario puede llenar.
05

El Controlador resource

Generamos el controlador con los 7 métodos RESTful ya estructurados:

php artisan make:controller ProductController --resource

Abre app/Http/Controllers/ProductController.php y completa cada método:

<?php

namespace App\Http\Controllers;

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

class ProductController extends Controller
{
    // Listar todos los productos
    public function index()
    {
        $products = Product::all();
        return view('products.index', compact('products'));
    }

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

    // Guardar nuevo producto
    public function store(Request $request)
    {
        $request->validate([
            'name'  => 'required',
            'price' => 'required|numeric',
        ]);

        Product::create($request->all());

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

    // Mostrar un producto
    public function show(Product $product)
    {
        return view('products.show', compact('product'));
    }

    // Formulario de edición
    public function edit(Product $product)
    {
        return view('products.edit', compact('product'));
    }

    // Actualizar producto
    public function update(Request $request, Product $product)
    {
        $request->validate([
            'name'  => 'required',
            'price' => 'required|numeric',
        ]);

        $product->update($request->all());

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

    // Eliminar producto
    public function destroy(Product $product)
    {
        $product->delete();

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

Las rutas

Una sola línea en routes/web.php genera las 7 rutas RESTful automáticamente:

<?php

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

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

Verifica que las rutas quedaron bien con:

php artisan route:list --path=products

Deberías ver esto:

GET    /products              products.index
GET    /products/create       products.create
POST   /products              products.store
GET    /products/{product}    products.show
GET    /products/{product}/edit  products.edit
PUT    /products/{product}    products.update
DELETE /products/{product}    products.destroy
07

Las vistas Blade

Crea la carpeta resources/views/products/ y los siguientes archivos:

index.blade.php — listado con botón eliminar y mensajes flash:

@if(session('success'))
    <div class="alert">{{ session('success') }}</div>
@endif

<a href="{{ route('products.create') }}">Nuevo producto</a>

<table>
    <thead>
        <tr>
            <th>Nombre</th>
            <th>Precio</th>
            <th>Acciones</th>
        </tr>
    </thead>
    <tbody>
        @foreach($products as $product)
        <tr>
            <td>{{ $product->name }}</td>
            <td>{{ $product->price }}</td>
            <td>
                <a href="{{ route('products.edit', $product) }}">Editar</a>
                <form action="{{ route('products.destroy', $product) }}" method="POST">
                    @csrf
                    @method('DELETE')
                    <button type="submit">Eliminar</button>
                </form>
            </td>
        </tr>
        @endforeach
    </tbody>
</table>

create.blade.php — formulario de creación con errores de validación:

<form action="{{ route('products.store') }}" method="POST">
    @csrf

    <label>Nombre</label>
    <input type="text" name="name" value="{{ old('name') }}">
    @error('name') <span>{{ $message }}</span> @enderror

    <label>Descripción</label>
    <textarea name="description">{{ old('description') }}</textarea>

    <label>Precio</label>
    <input type="number" step="0.01" name="price" value="{{ old('price') }}">
    @error('price') <span>{{ $message }}</span> @enderror

    <button type="submit">Guardar</button>
</form>

edit.blade.php — igual que create pero con los valores actuales y método PUT:

<form action="{{ route('products.update', $product) }}" method="POST">
    @csrf
    @method('PUT')

    <label>Nombre</label>
    <input type="text" name="name" value="{{ old('name', $product->name) }}">
    @error('name') <span>{{ $message }}</span> @enderror

    <label>Descripción</label>
    <textarea name="description">{{ old('description', $product->description) }}</textarea>

    <label>Precio</label>
    <input type="number" step="0.01" name="price" value="{{ old('price', $product->price) }}">
    @error('price') <span>{{ $message }}</span> @enderror

    <button type="submit">Actualizar</button>
</form>
old('name', $product->name) es el patrón correcto en el formulario de edición: usa el valor del request previo si hubo error de validación, y si no usa el valor actual del modelo.
08

Probar el flujo completo

Con el servidor corriendo en php artisan serve, entra a:

http://localhost:8000/products

El flujo completo que deberías poder hacer sin errores:

  1. Crear un producto nuevo desde /products/create
  2. Ver el listado con el producto recién creado
  3. Editar el producto y guardar los cambios
  4. Eliminar el producto y confirmar que desaparece del listado
  5. Intentar crear un producto sin nombre — el formulario debe mostrar el error de validación
jamesmosq / micrud
Código completo de este tutorial — dos recursos (productos y empleados) con todo el stack Laravel puro.
Ver en GitHub
Volver al blog