Índice de contenido
- Tipos de rutas y métodos HTTP disponibles
- Rutas con nombre
- Rutas dinámicas y parámetros
- Importancia de modularizar rutas en Laravel
- Problema con las rutas desorganizadas
- Solución: agrupar y reutilizar controladores
- Tip 1: reutilizar el mismo controlador para ID y slug
- Tip 2: aprovechar parámetros y autenticación
- Beneficios de modularizar
- Errores comunes y cómo solucionarlos
- CUIDADO CON LAS RUTAS
- Múltiples tipos de rutas
- Parámetros opcionales en las rutas de Laravel NO tienen sentido...
- ¿Y Si Fuerzo el Parámetro a ser Nulo?
- Como me gustaría que trabajase
- En Resumen
- Agrupar rutas en funciones
- Organiza rutas de Laravel en Archivos
- El problema del archivo único
- Estructura basada en módulos y APIs
- Evitando errores en Producción (Caso Real)
- Conclusión
- ¿Sigue siendo Laravel un framework MVC puro?
- Preguntas frecuentes (FAQs)
- Conclusión
Como presentamos antes sobre la estructura de archivos y carpetas en Laravel, las rutas forma parte de estas rutas.
Las rutas en Laravel son el punto de entrada de cualquier aplicación.
En mi experiencia, entenderlas bien es el primer paso para dominar el framework.
Las rutas en Laravel son un esquema flexible que vincula una URI con un proceso funcional, que puede ser una función, un controlador o incluso un componente Livewire. En palabras simples: cuando un usuario escribe una dirección en el navegador, Laravel busca una coincidencia en los archivos de rutas y ejecuta la acción correspondiente.
El archivo principal que gestiona este proceso es routes/web.php. Aquí definimos las rutas que responden a las solicitudes del navegador, mientras que api.php, console.php o channels.php se usan para otros contextos como APIs o comandos de consola.
Cuando Laravel recibe una solicitud, pasa por su Front Controller (generalmente public/index.php), interpreta la URI y la asocia con la ruta correspondiente.
Las rutas, son un esquema flexible que tenemos para vincular una URI a un proceso funcional; y este proceso funcional, puede ser:
- Un callback, que es una función local definida en las mismas rutas.
- Un controlador, que es una clase aparte.
- Un componente, que es como un controlador, pero más flexible.
Si revisamos en la carpeta de routes; veremos que existen 4 archivos:
- api: Para definir rutas de nuestras Apis Rest.
- channels: Para la comunicación fullduplex con los canales.
- console: Para crear comandos con artisan.
- web: Las rutas para la aplicación web.
Tipos de rutas y métodos HTTP disponibles
El que nos interesa en este capítulo es el de web.php; el cual permite definir las rutas de nuestra aplicación web (las que nuestro cliente consume desde el navegador).
Las rutas en Laravel son un elemento central que nos permiten enlazar controladores, como poder desencadenar nuestros propios procesos; es decir, las rutas no necesitan de los controladores para poder presentar un contenido; y por ende, es el primer enfoque que vamos a presentar.
Si te fijas, tenemos una ruta ya definía:
Route::get('/', function () {
return view('welcome');
});Que como puedes suponer es la que nosotros vemos por pantalla nada más al arrancar la aplicación como la figura 2-1.
Fíjate, que se emplea una clase llamada Route, que se importa de:
use Illuminate\Support\Facades\Route;
Que es interna a Laravel y se conocen como Facades.
Los Facades no son más que clases que nos permiten acceder a servicios propios del framework mediante clases estáticas.
Finalmente, con esta clase, usamos un método llamado get(); para las rutas tenemos distintos métodos, tantos métodos como tipo de peticiones tenemos:
- POST crear un recurso con el método post()
- GET leer un recurso o colección con el método get()
- PUT actualizar un recurso con el método put()
- PATCH actualizar un recurso con el método patch()
- DELETE eliminar un recurso con el método delete()
En este caso, empleamos una ruta de tipo get(), que conlleva a emplear a una petición de tipo GET.
El método get(), al igual que el resto de las funciones señaladas anteriormente, reciben dos parámetros:
Route::<FunctionResource>(URI, callback)- URI de la aplicación.
- El callback viene siendo la función controladora, que en este caso es una función, pero puede ser la referencia a la función de un controlador o un componente.
Y donde "FunctionResource" es la method get(), post(), put(), patch() o delete().
En el ejemplo anterior, el “/“ indica que es el root de la aplicación, que es:
http://larafirststeps.test/
O localhost si empleas MacOS o Linux mediante Docker.
En este caso, la parte funcional, viene siendo una función anónima; esta función, puede hacer cualquier cosa, devolver un JSON, un HTML, un documento, enviar un email y un largo etc.
En este ejemplo, devuelve una vista; para devolver vistas se emplea la función de ayuda (helper) llamada view(), la cual referencia las vistas que existen en la carpeta de:
resources/views/<Views and/or Folder>Por defecto, solamente existe un único archivo; el llamado welcome.blade.php, y si, es el que estamos reverenciando en la ruta anterior con:
return view('welcome');Fíjate, que no es necesario ni indicar la ruta, ni la extensión de blade o php.
Blade hace referencia al motor de plantillas que tiene Laravel que hablaremos sobre él un poco más adelante.
Si revisas la vista de welcome.blade.php:
Verás que todo el HTML de la misma:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
***Así que, si creamos unas rutas más:
Route::get('/writeme', function () {
return "Contact";
});
Route::get('/contact', function () {
return "Enjoy my web";
});Y vamos a cada una de estas páginas, respectivamente:
Y
Route::get('/custom', function () {
$msj2 = "Msj from server *-*";
$data = ['msj2' => $msj2, "age" => 15];
return view('custom', $data);
});views/cursom.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>{{ $msj2 }}</p>
<p>{{ $age }}</p>
</body>
</html>Rutas con nombre
Otra configuración que no puede faltar en tu aplicación, es el uso de rutas con nombre; como indica su nombre, le permite definir un nombre a una ruta.
***
Route::get('/contact', function () {
return "Enjoy my web";
})->name('welcome');Para eso se emplea una función llamada name() a la cual se le indica el nombre; para emplearla en la vista:
<a href="{{ name('welcome') }}">Welcome</a>Esto es particularmente útil, ya que, puedes cambiar la URI de la ruta, agruparla o aplicarle cualquier otra configuración, pero, mientras tengas el nombre definido en la ruta y uses este nombre para referenciarla, Laravel va a actualizarla automáticamente.
Rutas dinámicas y parámetros
Las rutas también pueden recibir parámetros dinámicos:
Route::get('/user/{id}', function ($id) {
return "Usuario con ID: $id";
});Puedes hacerlos opcionales:
Route::get('/user/{name?}', function ($name = 'Invitado') {
return "Hola, $name";
});Y, por supuesto, pasar datos a una vista:
Route::get('/custom', function () {
$msj2 = "Mensaje desde el servidor";
$data = ['msj2' => $msj2, 'age' => 15];
return view('custom', $data);
});En este ejemplo, la vista custom.blade.php recibe variables y las muestra con Blade.
Me encanta este enfoque porque simplifica la comunicación entre el servidor y la interfaz.
Importancia de modularizar rutas en Laravel
Quiero comentarte algo que considero muy importante: modularizar tus rutas y controladores. Por controladores me refiero a las funciones o métodos que procesan las solicitudes. Para ilustrarlo, voy a mostrarte mi versión anterior de desarrollo en Laravel, para que veas la comparación.
En la versión anterior, tenía un montón de rutas GET como estas:
Route::get('{tutorial:url_clean}', [\App\Http\Controllers\Api\TutorialController::class, 'show']);
Route::get('/coupon/check/{coupon}', [\App\Http\Controllers\Api\CouponController::class, 'active']);
Route::get('update-progress-by-user', [App\Http\Controllers\Api\TutorialController::class, 'update_progress_by_user']);
Route::get('/get-detail/{tutorial:url_clean}', [App\Http\Controllers\Api\TutorialController::class, 'getAllDetail']);
Route::get('/by-slug/{tutorial:url_clean}', [App\Http\Controllers\Api\TutorialController::class, 'by_slug']);
Route::get('/class/free/by-slug/{tutorial:url_clean}', [App\Http\Controllers\Api\TutorialController::class, 'class_free_by_slug']);
Route::get('{id}/class/resume', [App\Http\Controllers\Api\TutorialController::class, 'get_class']);
Route::group(['middleware' => 'auth:sanctum'], function () {
Route::post('inscribe/{tutorial:url_clean}', [App\Http\Controllers\Api\TutorialController::class, 'inscribe']);
Route::get('user/my-courses', [App\Http\Controllers\Api\TutorialController::class, 'my_courses']);
});Problema con las rutas desorganizadas
El motivo de tantas rutas es la anidación de cursos, secciones y clases, similar a cómo funciona Udemy:
- Curso principal (tutorial).
- Secciones del curso.
- Clases dentro de cada sección.
Además, dependiendo del usuario:
- Si compró el curso, el tutorial tiene más información.
- Si no lo compró, solo mostramos un detalle básico.
Esto llevaba a crear muchas rutas separadas para manejar diferentes vistas (detalle, full, simple) y diferentes filtros (ID o slug).
El problema es que estas rutas quedan poco comprensibles, con nombres largos y repetitivos, y se pierde modularidad.
Solución: agrupar y reutilizar controladores
Para simplificar, agrupamos las rutas y aprovechamos los mismos controladores:
Route::group(['prefix' => 'tutorial'], function () {
Route::get('', [App\Http\Controllers\Api\TutorialController::class, 'getAll']); // listado general
Route::get('simple/get-by-slug/{tutorial:url_clean}', [\App\Http\Controllers\Api\TutorialController::class, 'getSimple']);
Route::get('simple/get-by-id/{tutorial}', [\App\Http\Controllers\Api\TutorialController::class, 'getSimple']);
Route::get('full/get-by-slug/{tutorial:url_clean}', [\App\Http\Controllers\Api\TutorialController::class, 'getFull']);
Route::get('full/get-by-id/{tutorial}', [\App\Http\Controllers\Api\TutorialController::class, 'getFull']);
});Tip 1: reutilizar el mismo controlador para ID y slug
Si quieres traer un tutorial por ID o por slug, puedes usar el mismo controlador. Solo necesitas definir rutas adicionales:
Route::get('full/get-by-slug/{tutorial:url_clean}', [Controller::class, 'getFull']);
Route::get('full/get-by-id/{tutorial}', [Controller::class, 'getFull']);Esto reduce la mitad de las rutas y hace el código más limpio y mantenible.
Tip 2: aprovechar parámetros y autenticación
En lugar de crear rutas adicionales, puedes aprovechar parámetros y autenticación para devolver más o menos información:
public function getFull(Tutorial $tutorial)
{
$user = auth()->user() ?? auth('sanctum')->user();
if ($user) {
// Usuario autenticado: mostrar toda la información del tutorial
} else {
// Usuario no autenticado: mostrar solo información básica
}
}Para usuarios que no compraron el curso, se muestra solo la información básica.
Para usuarios que compraron el curso, se devuelve toda la información (full), incluyendo secciones y clases.
Se puede pasar un parámetro extra vía request para calcular precios adicionales, pero no se recomienda pasar información sensible directamente, ya que podría ser hackeada.
Beneficios de modularizar
Más fácil de mantener: no necesitas modificar múltiples rutas o controladores para agregar un nuevo parámetro.
Más legible: las rutas quedan agrupadas y con nombres claros.
Reutilización de controladores: puedes usar el mismo método para varias rutas y parámetros.
Seguridad: al usar auth o sanctum, los datos sensibles se manejan correctamente.
Errores comunes y cómo solucionarlos
| Problema | Causa | Solución |
|---|---|---|
| Ruta no encontrada | URI mal escrita o ruta duplicada | Verifica con php artisan route:list |
| Error 419 o CSRF | Falta el token en formularios POST | Añade @csrf en los formularios |
| Cache de rutas desactualizada | Cambios sin limpiar cache | Ejecuta php artisan route:clear |
CUIDADO CON LAS RUTAS
Quería hablar sobre un tema que considero muy interesante. Siempre me detengo un poco en estos puntos porque noto que casi nadie los menciona; me gusta conocer la opinión de los demás y aportar un toque de crítica. De lo "bueno" no hace falta conversar mucho, simplemente está ahí (y para eso ya tengo mis cursos y libros).
Actualmente estoy actualizando el proyecto del curso de Laravel a la versión 12, que es la última a la fecha. En este contexto, quiero hablar sobre el Middleware. Actualmente, podemos cargarlo de al menos tres formas:
Route::get(...)->middleware([]);
Route::group(['middleware' => [...]], function() { ... });
Route::middleware([...])->group(function() { ... });A mí me gustan las opciones; mientras más herramientas tengamos, mejor. Sin embargo, el punto importante es que, aunque ganamos en flexibilidad, a veces perdemos un poco de legibilidad o comprensión. La pieza con la que trabajamos se vuelve más compleja, por lo que no es un "win-win" total, sino que siempre hay un término gris.
Personalmente, prefiero tener estas opciones a que me limiten a usar solo rutas agrupadas. Al final, depende del desarrollador decidir qué prefiere; por eso, en mis cursos me gusta mostrar variantes para que seas tú quien elija su estilo.
Múltiples tipos de rutas
Hoy en día, Laravel nos permite definir rutas para distintos propósitos. Por ejemplo, rutas para controladores tradicionales:
Route::get('user/{id}', 'UserController@show');Rutas para componentes de Livewire:
Route::get('/', App\Livewire\Dashboard\Category\Index::class)->name("d-category-index");
Rutas de Volt (la nueva API funcional de Livewire):
Volt::route('volt/contact', 'volt.contact.general')->name('volt-contact');Y, por supuesto, rutas que retornan directamente vistas:
Route::get('/', function () {
return view('welcome');
})->name('home');En Livewire moderno, ahora tenemos:
Route::livewire('subscribe', 'pages::dashboard.subscribe.list')->name('subscribe-list');Parámetros opcionales en las rutas de Laravel NO tienen sentido...
Fíjate en algo muy curioso que me parece que tiene Laravel, y de lo cual realmente no me había dado cuenta hasta ahora. Estoy trabajando con una ruta que tiene un parámetro completamente opcional, y te explico lo que descubrí.
Tengo una ruta que apunta a /store/{productType?}, donde productType es un modelo (una categoría o clasificación). Este puede ser opcional, como lo indico en la definición.
Cuando se accede a /store sin ningún parámetro, el valor debería ser nulo o simplemente no estar definido.
Cuando se accede a /store/zapatos, entonces sí está definido.
Ahora viene lo curioso. Uno pensaría que, si este parámetro es opcional y no se pasa, el valor debería ser nulo, ¿cierto?
Como en Livewire este valor lo recibo en el mount(), asumí que simplemente podía hacer algo como:
function mount(?ProductType $productType)
{
$this->productType = $productType->id ? $productType : null;
}En el caso de /store/zapatos, todo funciona como se espera: $productType tiene un valor válido.
Pero en /store, en vez de recibir un valor null, Laravel me pasa una instancia vacía del modelo ProductType.
Este comportamiento me choca un poco. Es como si Laravel estuviera haciendo esto internamente:
$productType = new ProductType;Esto tiene sentido en ciertos contextos, como cuando trabajas con formularios para crear o editar modelos, donde quieres una instancia vacía para reutilizar código. Pero en este caso específico no tiene sentido.
Supongo que esto tiene que ver con el null safety, algo que se implementó más formalmente en versiones recientes de PHP. Pero me molesta porque:
- Te puede romper la lógica si asumes que estás trabajando con un objeto válido de base de datos.
- Puedes acceder al título ($productType->title) y te devuelve null, pero no sabes si eso es porque no existe en base de datos o porque Laravel te pasó una instancia vacía.
¿Y Si Fuerzo el Parámetro a ser Nulo?
Pensé en forzar el tipo de parámetro en el mount() como nullable:
function mount(?ProductType $productType = null)
{
***
}Pero si accede se accede a /store Laravel lanza un 404 directamente si no encuentra el modelo. Es decir, ni siquiera llega al componente. Esto me molesta más todavía, porque rompe la ruta sin razón aparente.
Como me gustaría que trabajase
Yo preferiría que Laravel me dejara trabajar con un null directamente. Es más limpio para las validaciones y para quien mantiene el código.
Por ejemplo, es mucho más sencillo validar:
if ($productType) Que:
if (is_null($productType->title)) ...Esto último genera confusión para cualquier desarrollador que venga después:
¿El título es nulo porque así lo definiste tú, o porque Laravel creó un objeto vacío?
En Resumen
- Laravel no devuelve null cuando el modelo es opcional y no se encuentra: devuelve una instancia vacía.
- Si defines el parámetro como nullable, Laravel retorna un 404 si el modelo no existe.
- Este comportamiento es curioso y un poco molesto, al menos para mí.
- No sé desde qué versión exactamente funciona así, pero intuyo que es reciente.
Agrupar rutas en funciones
Para el dashboard o panel de administración, tiene más sentido agrupar rutas debido a que existen múltiples funcionalidades:
function routesDashboard()
{
Route::get('/', function () {
return redirect()->route("post-list");
});
***
}Al agrupar rutas en funciones, obtenemos varias ventajas:
- Reutilización: podemos usar las mismas rutas en diferentes entornos sin duplicar código.
- Organización: podemos separar rutas por módulo (por ejemplo, web/dashboard, web/blog, web/academia).
- Flexibilidad: en producción podemos usar subdominios y en desarrollo un dominio local sin cambios complejos.
Ejemplo de uso en producción vs desarrollo
- Producción: usamos subdominios para separar los módulos.
- Desarrollo: todo se maneja desde un dominio de pruebas para simplificar el flujo de trabajo.
Organiza rutas de Laravel en Archivos
Anteriormente te comentaba que me gustaba mucho agrupar las rutas mediante funciones. En mi proyecto antiguo (el cual estoy migrando ahora), utilizaba este esquema para separar, por ejemplo, el Dashboard del Blog en mi aplicación de Desarrollo Libre.
Básicamente, creaba grupos funcionales y, dentro de cada uno, definía las rutas específicas. Sin embargo, es importante recordar que en este esquema las funciones deben ser invocadas explícitamente para que la aplicación las reconozca; no se cargan mágicamente.
El problema del archivo único
Aunque agrupar por funciones es un buen comienzo, tiene un inconveniente: todo sigue residiendo en el mismo archivo. Es como tener 50 métodos en un mismo controlador; al final, la lectura se vuelve pesada. Lo ideal es tener archivos independientes.
Esta idea me la dio la estructura por defecto de Livewire. Al crear un proyecto, noté que incluía un apartado de settings con varias rutas separadas. A partir de ahí, decidí replicar ese modelo para cada uno de mis módulos en este nuevo proyecto.
Estructura basada en módulos y APIs
Ahora, los archivos de rutas principales lucen mucho más limpio.
routes\web.php
<?php
require __DIR__.'/web/settings.php';
require __DIR__.'/web/blog.php';
require __DIR__.'/web/academy.php';
require __DIR__.'/web/dashboard.php';
require __DIR__.'/web/social.php';routes\web\blog
Route::group(['prefix' => 'anuncios'], function () {
Route::get('', [PostController::class, 'adverts'])->name('web-advert');
Route::get('{category_url_clean}', [PostController::class, 'show_advert'])->name('web-advert-show');
});Creamos una estructura de carpetas adicional y puedes hacer lo mismo con el archivo de api.php si tu aplicación utiliza una API Rest.
En definitiva, internamente, organizo cada grupo de rutas (Social, Blog, API, etc.) en archivos separados dentro de sus propias carpetas. Con esto gano dos cosas fundamentales:
- Legibilidad: Si necesito revisar las rutas de redes sociales, entro directamente al archivo social.php y veo solo lo que me interesa, sin liarme con cientos de líneas de código.
- Mantenimiento y seguridad: Como desarrollador, siempre estoy probando funcionalidades nuevas. Antes, todas esas pruebas convivían en el archivo web.php. Si algo se rompía, afectaba a todo el sistema de rutas.
Evitando errores en Producción (Caso Real)
Recientemente me ocurrió un caso curioso: estoy migrando mi academia a Laravel 13 y Google me rechazó la publicación de la app móvil porque las páginas de "Privacidad" y "Políticas" devolvían un error 404.
Gracias a este esquema modular, solo tuve que actualizar el archivo específico de esas rutas legales y subirlo al servidor de producción. Esto me da mucha tranquilidad, ya que no tuve que tocar el archivo web.php principal, donde quizás tengo pruebas pendientes o código a medio terminar. Al separar las rutas del blog de las rutas estáticas o de la API, el "dolor de cabeza" al actualizar es mucho menor.
Conclusión
Te recomiendo mucho este esquema modular, o incluso un mix entre funciones y archivos separados si tu aplicación es muy grande:
routes\web\blog
function routesBlog()
{
// ADS
Route::group(['prefix' => 'anuncios'], function () {
Route::get('', [PostController::class, 'adverts'])->name('web-advert');
Route::get('{category_url_clean}', [PostController::class, 'show_advert'])->name('web-advert-show');
});Al final, se trata de evitar errores accidentales al subir cambios a producción y de mantener el código lo más limpio posible.
¿Sigue siendo Laravel un framework MVC puro?
Recordemos que Laravel ya no es un framework MVC puro. Históricamente, desde Laravel 5 hasta la versión 6, las rutas estaban ligadas casi exclusivamente a controladores. Los componentes, tal como los conocemos, empezaron a tomar fuerza a partir de la versión 7.
En mi caso, cuando trabajo en un proyecto y decido usar Livewire, lo adopto por completo. Prácticamente no utilizo los controladores base de Laravel porque la librería me da muchísima flexibilidad. Como he dicho en otros videos, Livewire es mi scaffolding favorito para Laravel.
Sin embargo, hay que ser realistas: tener tantas definiciones de rutas para distintos tipos de componentes (Controladores, Livewire, Volt, Vistas) puede dificultar la lectura y el mantenimiento general de la aplicación si no se gestiona con cuidado.
Preguntas frecuentes (FAQs)
- ¿Dónde se definen las rutas en Laravel?
- En los archivos dentro de la carpeta routes/, principalmente en web.php.
- ¿Cómo pasar variables a una vista desde una ruta?
- Usa el helper view() y pasa un array asociativo con los datos.
- ¿Qué diferencia hay entre rutas web y api?
- Las rutas web cargan sesiones, cookies y vistas; las api son ligeras y sin estado.
- ¿Cómo listar todas las rutas disponibles?
- Con php artisan route:list.
- ¿Cuál es la mejor forma de agrupar rutas?
- Usar prefix(), middleware() o incluso Route::resource() para controladores REST.
Conclusión
Modularizar tus rutas y controladores es fundamental para mantener un proyecto ordenado, escalable y seguro. Esto evita el caos de rutas largas y confusas, y te permite aprovechar parámetros y autenticación para manejar diferentes escenarios con menos código.
Dominar su uso no solo te hace más eficiente, sino que te da control total sobre la estructura y rendimiento de tu aplicación.
La siguiente tarea, es conocer como emplear la poderosa línea de comandos de Laravel, Artisan.