12-CRUD en Laravel para Web y API con Controladores Separados

 

Tutorial Actualizado: CRUD en Laravel para Web y API con Controladores Separados

Esta guía te mostrará cómo crear un sistema de gestión de tareas con soporte simultáneo para Web (interfaz HTML) y API (RESTful JSON) usando controladores separados y sin autenticación.

Estructura del Proyecto

text
gestor-tareas/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Api/
│   │   │   │   └── TareaController.php (API)
│   │   │   └── Web/
│   │   │       └── TareaController.php (Web)
│   │   └── Models/
│   │       └── Tarea.php
├── routes/
│   ├── api.php (rutas API)
│   └── web.php (rutas Web)
└── resources/views/
    └── tareas/ (vistas Blade)

Comandos Paso a Paso

1. Crear el Proyecto Laravel

bash
composer create-project laravel/laravel gestor-tareas
cd gestor-tareas

2. Configurar Base de Datos

Editar .env:

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

3. Crear Modelo y Migración

bash
php artisan make:model Tarea -m

4. Definir la Migración

Editar database/migrations/xxxx_create_tareas_table.php:

php
public function up()
{
    Schema::create('tareas', function (Blueprint $table) {
        $table->id();
        $table->string('titulo');
        $table->text('descripcion')->nullable();
        $table->boolean('completada')->default(false);
        $table->timestamps();
    });
}

5. Ejecutar Migraciones

bash
php artisan migrate

6. Crear Controladores Separados

Controlador para Web:

bash
php artisan make:controller Web/TareaController --resource --model=Tarea

Controlador para API:

bash
php artisan make:controller Api/TareaController --api --model=Tarea

7. Configurar Rutas

Rutas Web (routes/web.php):

php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Web\TareaController;

Route::resource('tareas', TareaController::class);

Rutas API (routes/api.php):

php
<?php

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

Route::apiResource('tareas', TareaController::class);

8. Implementar Controlador Web

Editar app/Http/Controllers/Web/TareaController.php:

php
<?php

namespace App\Http\Controllers\Web;

use App\Http\Controllers\Controller;
use App\Models\Tarea;
use Illuminate\Http\Request;

class TareaController extends Controller
{
    public function index()
    {
        $tareas = Tarea::latest()->get();
        return view('tareas.index', compact('tareas'));
    }

    public function create()
    {
        return view('tareas.create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'titulo' => 'required|max:255',
            'descripcion' => 'nullable|string',
        ]);

        Tarea::create([
            'titulo' => $request->titulo,
            'descripcion' => $request->descripcion,
            'completada' => $request->has('completada')
        ]);

        return redirect()->route('tareas.index')
            ->with('success', 'Tarea creada exitosamente');
    }

    public function show(Tarea $tarea)
    {
        return view('tareas.show', compact('tarea'));
    }

    public function edit(Tarea $tarea)
    {
        return view('tareas.edit', compact('tarea'));
    }

    public function update(Request $request, Tarea $tarea)
    {
        $request->validate([
            'titulo' => 'required|max:255',
            'descripcion' => 'nullable|string',
        ]);

        $tarea->update([
            'titulo' => $request->titulo,
            'descripcion' => $request->descripcion,
            'completada' => $request->has('completada')
        ]);

        return redirect()->route('tareas.index')
            ->with('success', 'Tarea actualizada exitosamente');
    }

    public function destroy(Tarea $tarea)
    {
        $tarea->delete();
        return redirect()->route('tareas.index')
            ->with('success', 'Tarea eliminada exitosamente');
    }
}

9. Implementar Controlador API

Editar app/Http/Controllers/Api/TareaController.php:

php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Tarea;
use Illuminate\Http\Request;

class TareaController extends Controller
{
    public function index()
    {
        $tareas = Tarea::latest()->get();
        return response()->json([
            'success' => true,
            'data' => $tareas
        ]);
    }

    public function store(Request $request)
    {
        $request->validate([
            'titulo' => 'required|max:255',
            'descripcion' => 'nullable|string',
            'completada' => 'boolean'
        ]);

        $tarea = Tarea::create([
            'titulo' => $request->titulo,
            'descripcion' => $request->descripcion,
            'completada' => $request->completada ?? false
        ]);

        return response()->json([
            'success' => true,
            'data' => $tarea,
            'message' => 'Tarea creada exitosamente'
        ], 201);
    }

    public function show(Tarea $tarea)
    {
        return response()->json([
            'success' => true,
            'data' => $tarea
        ]);
    }

    public function update(Request $request, Tarea $tarea)
    {
        $request->validate([
            'titulo' => 'sometimes|required|max:255',
            'descripcion' => 'nullable|string',
            'completada' => 'boolean'
        ]);

        $tarea->update($request->only(['titulo', 'descripcion', 'completada']));

        return response()->json([
            'success' => true,
            'data' => $tarea,
            'message' => 'Tarea actualizada exitosamente'
        ]);
    }

    public function destroy(Tarea $tarea)
    {
        $tarea->delete();
        return response()->json([
            'success' => true,
            'message' => 'Tarea eliminada exitosamente'
        ]);
    }
}

10. Crear Vistas Web

Layout Base (resources/views/layouts/app.blade.php):

html
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'Gestor de Tareas')</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{{ route('tareas.index') }}">
                <i class="bi bi-check2-square"></i> Gestor de Tareas
            </a>
        </div>
    </nav>

    <div class="container mt-4">
        @if(session('success'))
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                {{ session('success') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        @endif

        @yield('content')
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    @stack('scripts')
</body>
</html>

Lista de Tareas (resources/views/tareas/index.blade.php):

html
@extends('layouts.app')

@section('title', 'Lista de Tareas')

@section('content')
<div class="row mb-4">
    <div class="col">
        <h1>
            <i class="bi bi-list-task"></i> Lista de Tareas
        </h1>
    </div>
    <div class="col text-end">
        <a href="{{ route('tareas.create') }}" class="btn btn-primary">
            <i class="bi bi-plus-circle"></i> Nueva Tarea
        </a>
    </div>
</div>

<div class="card">
    <div class="card-body">
        @if($tareas->isEmpty())
            <div class="alert alert-info">
                No hay tareas registradas. ¡Crea tu primera tarea!
            </div>
        @else
            <div class="table-responsive">
                <table class="table table-hover">
                    <thead>
                        <tr>
                            <th width="50px">ID</th>
                            <th>Título</th>
                            <th>Descripción</th>
                            <th>Estado</th>
                            <th>Creado</th>
                            <th width="200px">Acciones</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach($tareas as $tarea)
                        <tr>
                            <td>{{ $tarea->id }}</td>
                            <td>{{ $tarea->titulo }}</td>
                            <td>{{ Str::limit($tarea->descripcion, 50) }}</td>
                            <td>
                                @if($tarea->completada)
                                    <span class="badge bg-success">Completada</span>
                                @else
                                    <span class="badge bg-warning">Pendiente</span>
                                @endif
                            </td>
                            <td>{{ $tarea->created_at->format('d/m/Y H:i') }}</td>
                            <td>
                                <div class="btn-group" role="group">
                                    <a href="{{ route('tareas.show', $tarea) }}" 
                                       class="btn btn-sm btn-info">
                                        <i class="bi bi-eye"></i>
                                    </a>
                                    <a href="{{ route('tareas.edit', $tarea) }}" 
                                       class="btn btn-sm btn-warning">
                                        <i class="bi bi-pencil"></i>
                                    </a>
                                    <form action="{{ route('tareas.destroy', $tarea) }}" 
                                          method="POST" 
                                          class="d-inline"
                                          onsubmit="return confirm('¿Eliminar esta tarea?')">
                                        @csrf
                                        @method('DELETE')
                                        <button type="submit" class="btn btn-sm btn-danger">
                                            <i class="bi bi-trash"></i>
                                        </button>
                                    </form>
                                </div>
                            </td>
                        </tr>
                        @endforeach
                    </tbody>
                </table>
            </div>
        @endif
    </div>
</div>

<div class="mt-4">
    <div class="card">
        <div class="card-header">
            <i class="bi bi-terminal"></i> Endpoints API Disponibles
        </div>
        <div class="card-body">
            <p>Esta aplicación también expone una API RESTful en <code>/api/tareas</code></p>
            <ul>
                <li><strong>GET</strong> /api/tareas - Listar todas las tareas</li>
                <li><strong>POST</strong> /api/tareas - Crear nueva tarea</li>
                <li><strong>GET</strong> /api/tareas/{id} - Mostrar una tarea</li>
                <li><strong>PUT/PATCH</strong> /api/tareas/{id} - Actualizar tarea</li>
                <li><strong>DELETE</strong> /api/tareas/{id} - Eliminar tarea</li>
            </ul>
        </div>
    </div>
</div>
@endsection

Formulario de Creación/Edición (resources/views/tareas/create.blade.php):

html
@extends('layouts.app')

@section('title', 'Nueva Tarea')

@section('content')
<div class="row">
    <div class="col-md-8 offset-md-2">
        <div class="card">
            <div class="card-header">
                <h3>
                    <i class="bi bi-plus-circle"></i> Crear Nueva Tarea
                </h3>
            </div>
            <div class="card-body">
                <form action="{{ route('tareas.store') }}" method="POST">
                    @csrf
                    <div class="mb-3">
                        <label for="titulo" class="form-label">Título *</label>
                        <input type="text" 
                               class="form-control @error('titulo') is-invalid @enderror" 
                               id="titulo" 
                               name="titulo" 
                               value="{{ old('titulo') }}" 
                               required>
                        @error('titulo')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>
                    
                    <div class="mb-3">
                        <label for="descripcion" class="form-label">Descripción</label>
                        <textarea class="form-control @error('descripcion') is-invalid @enderror" 
                                  id="descripcion" 
                                  name="descripcion" 
                                  rows="3">{{ old('descripcion') }}</textarea>
                        @error('descripcion')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>
                    
                    <div class="mb-3 form-check">
                        <input type="checkbox" 
                               class="form-check-input" 
                               id="completada" 
                               name="completada" 
                               value="1"
                               {{ old('completada') ? 'checked' : '' }}>
                        <label class="form-check-label" for="completada">¿Completada?</label>
                    </div>
                    
                    <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                        <a href="{{ route('tareas.index') }}" class="btn btn-secondary me-md-2">
                            <i class="bi bi-arrow-left"></i> Cancelar
                        </a>
                        <button type="submit" class="btn btn-primary">
                            <i class="bi bi-save"></i> Guardar Tarea
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
@endsection

Detalles de Tarea (resources/views/tareas/show.blade.php):

html
@extends('layouts.app')

@section('title', 'Detalles de Tarea')

@section('content')
<div class="row">
    <div class="col-md-8 offset-md-2">
        <div class="card">
            <div class="card-header">
                <div class="d-flex justify-content-between align-items-center">
                    <h3>
                        <i class="bi bi-eye"></i> Detalles de Tarea
                    </h3>
                    <a href="{{ route('tareas.index') }}" class="btn btn-outline-secondary">
                        <i class="bi bi-arrow-left"></i> Volver
                    </a>
                </div>
            </div>
            <div class="card-body">
                <div class="row">
                    <div class="col-md-6">
                        <h5>ID</h5>
                        <p>{{ $tarea->id }}</p>
                    </div>
                    <div class="col-md-6">
                        <h5>Estado</h5>
                        <p>
                            @if($tarea->completada)
                                <span class="badge bg-success">Completada</span>
                            @else
                                <span class="badge bg-warning">Pendiente</span>
                            @endif
                        </p>
                    </div>
                </div>
                
                <div class="mb-3">
                    <h5>Título</h5>
                    <p class="fs-5">{{ $tarea->titulo }}</p>
                </div>
                
                <div class="mb-3">
                    <h5>Descripción</h5>
                    <p>{{ $tarea->descripcion ?: 'Sin descripción' }}</p>
                </div>
                
                <div class="row">
                    <div class="col-md-6">
                        <h5>Creado</h5>
                        <p>{{ $tarea->created_at->format('d/m/Y H:i') }}</p>
                    </div>
                    <div class="col-md-6">
                        <h5>Actualizado</h5>
                        <p>{{ $tarea->updated_at->format('d/m/Y H:i') }}</p>
                    </div>
                </div>
            </div>
            <div class="card-footer">
                <div class="d-flex justify-content-between">
                    <div>
                        <a href="{{ route('tareas.edit', $tarea) }}" class="btn btn-warning">
                            <i class="bi bi-pencil"></i> Editar
                        </a>
                    </div>
                    <div>
                        <form action="{{ route('tareas.destroy', $tarea) }}" method="POST" 
                              onsubmit="return confirm('¿Eliminar esta tarea?')">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="btn btn-danger">
                                <i class="bi bi-trash"></i> Eliminar
                            </button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
        
        <div class="card mt-4">
            <div class="card-header">
                <i class="bi bi-code-slash"></i> Datos en Formato JSON (API)
            </div>
            <div class="card-body">
                <pre><code class="language-json">{{ json_encode($tarea, JSON_PRETTY_PRINT) }}</code></pre>
            </div>
        </div>
    </div>
</div>
@endsection

@push('styles')
<style>
    pre {
        background-color: #f8f9fa;
        padding: 1rem;
        border-radius: 0.375rem;
        max-height: 300px;
        overflow-y: auto;
    }
</style>
@endpush

Formulario de Edición (resources/views/tareas/edit.blade.php):

html
@extends('layouts.app')

@section('title', 'Editar Tarea')

@section('content')
<div class="row">
    <div class="col-md-8 offset-md-2">
        <div class="card">
            <div class="card-header">
                <div class="d-flex justify-content-between align-items-center">
                    <h3>
                        <i class="bi bi-pencil"></i> Editar Tarea #{{ $tarea->id }}
                    </h3>
                    <a href="{{ route('tareas.index') }}" class="btn btn-outline-secondary">
                        <i class="bi bi-arrow-left"></i> Volver
                    </a>
                </div>
            </div>
            <div class="card-body">
                <form action="{{ route('tareas.update', $tarea) }}" method="POST">
                    @csrf
                    @method('PUT')
                    
                    <div class="mb-3">
                        <label for="titulo" class="form-label">Título *</label>
                        <input type="text" 
                               class="form-control @error('titulo') is-invalid @enderror" 
                               id="titulo" 
                               name="titulo" 
                               value="{{ old('titulo', $tarea->titulo) }}" 
                               required>
                        @error('titulo')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>
                    
                    <div class="mb-3">
                        <label for="descripcion" class="form-label">Descripción</label>
                        <textarea class="form-control @error('descripcion') is-invalid @enderror" 
                                  id="descripcion" 
                                  name="descripcion" 
                                  rows="3">{{ old('descripcion', $tarea->descripcion) }}</textarea>
                        @error('descripcion')
                            <div class="invalid-feedback">{{ $message }}</div>
                        @enderror
                    </div>
                    
                    <div class="mb-3 form-check">
                        <input type="checkbox" 
                               class="form-check-input" 
                               id="completada" 
                               name="completada" 
                               value="1"
                               {{ $tarea->completada ? 'checked' : '' }}>
                        <label class="form-check-label" for="completada">¿Completada?</label>
                    </div>
                    
                    <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                        <button type="submit" class="btn btn-primary">
                            <i class="bi bi-save"></i> Actualizar Tarea
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
@endsection

11. Crear Directorios de Vistas

bash
mkdir -p resources/views/tareas

12. Iniciar el Servidor

bash
php artisan serve

Acceso a la Aplicación

Interfaz Web:

API REST:

Comandos Rápidos de Referencia

bash
# Crear proyecto
composer create-project laravel/laravel nombre-proyecto

# Crear modelo con migración
php artisan make:model Modelo -m

# Crear controlador para Web (con métodos resource)
php artisan make:controller Web/TareaController --resource --model=Tarea

# Crear controlador para API (con métodos API)
php artisan make:controller Api/TareaController --api --model=Tarea

# Ejecutar migraciones
php artisan migrate

# Iniciar servidor
php artisan serve

# Ver rutas disponibles
php artisan route:list

Endpoints API Ejemplo

bash
# Listar todas las tareas
curl -X GET http://localhost:8000/api/tareas

# Crear nueva tarea
curl -X POST http://localhost:8000/api/tareas \
  -H "Content-Type: application/json" \
  -d '{"titulo":"Mi tarea","descripcion":"Descripción de prueba","completada":false}'

# Ver una tarea específica
curl -X GET http://localhost:8000/api/tareas/1

# Actualizar tarea
curl -X PUT http://localhost:8000/api/tareas/1 \
  -H "Content-Type: application/json" \
  -d '{"titulo":"Título actualizado","completada":true}'

# Eliminar tarea
curl -X DELETE http://localhost:8000/api/tareas/1

Características Implementadas

  1. Separación Clara: Controladores separados para Web y API

  2. Rutas Dedicadas: web.php para vistas, api.php para endpoints JSON

  3. Mismo Modelo: Ambos controladores usan el mismo modelo Tarea

  4. Respuestas Adecuadas:

    • Web: Redirecciones y vistas Blade

    • API: Respuestas JSON estructuradas

  5. Sin Autenticación: Acceso público para primera versión

  6. Interfaz Moderna: Bootstrap 5 con íconos

  7. Documentación Incluida: Endpoints API visibles en la interfaz web

Este sistema proporciona una base sólida para expandir con autenticación, autorización, paginación, búsqueda y otras funcionalidades según sea necesario.

Comentarios

Entradas más populares de este blog

0-Sistema de Tareas

13-CRUD en Laravel para Web y API con Rutas Normales (No Resource)

10-Introducción a Blade en Laravel con un Ejemplo Práctico