12-CRUD en Laravel para Web y API con Controladores Separados
- Obtener vínculo
- X
- Correo electrónico
- Otras apps
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
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
composer create-project laravel/laravel gestor-tareas
cd gestor-tareas2. Configurar Base de Datos
Editar .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
php artisan make:model Tarea -m4. Definir la Migración
Editar database/migrations/xxxx_create_tareas_table.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
php artisan migrate6. Crear Controladores Separados
Controlador para Web:
php artisan make:controller Web/TareaController --resource --model=TareaControlador para API:
php artisan make:controller Api/TareaController --api --model=Tarea7. Configurar Rutas
Rutas Web (routes/web.php):
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Web\TareaController;
Route::resource('tareas', TareaController::class);Rutas API (routes/api.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
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
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):
<!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):
@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>
@endsectionFormulario de Creación/Edición (resources/views/tareas/create.blade.php):
@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>
@endsectionDetalles de Tarea (resources/views/tareas/show.blade.php):
@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>
@endpushFormulario de Edición (resources/views/tareas/edit.blade.php):
@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>
@endsection11. Crear Directorios de Vistas
mkdir -p resources/views/tareas12. Iniciar el Servidor
php artisan serveAcceso a la Aplicación
Interfaz Web:
Métodos: GET, POST, PUT, DELETE (a través de formularios)
API REST:
URL Base: http://localhost:8000/api/tareas
Métodos: GET, POST, GET/{id}, PUT/PATCH/{id}, DELETE/{id}
Formato: JSON
Comandos Rápidos de Referencia
# 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:listEndpoints API Ejemplo
# 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/1Características Implementadas
Separación Clara: Controladores separados para Web y API
Rutas Dedicadas:
web.phppara vistas,api.phppara endpoints JSONMismo Modelo: Ambos controladores usan el mismo modelo
TareaRespuestas Adecuadas:
Web: Redirecciones y vistas Blade
API: Respuestas JSON estructuradas
Sin Autenticación: Acceso público para primera versión
Interfaz Moderna: Bootstrap 5 con íconos
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.
- Obtener vínculo
- X
- Correo electrónico
- Otras apps
Comentarios
Publicar un comentario