Routes, arguments, views and HTTP methods in Laravel

Video thumbnail

As we presented earlier regarding the file and folder structure in Laravel, paths are part of these paths.

Routes in Laravel are the entry point for any application.

In my experience, understanding them well is the first step to mastering the framework.

Routes in Laravel are a flexible scheme that links a URI to a functional process, which can be a function, a controller, or even a Livewire component. Simply put: when a user types an address into the browser, Laravel looks for a match in the routes files and executes the corresponding action.

The main file that manages this process is routes/web.php. Here, we define the routes that respond to browser requests, while api.php, console.php, or channels.php are used for other contexts such as APIs or console commands.

When Laravel receives a request, it passes through its Front Controller (usually public/index.php), interprets the URI, and associates it with the corresponding route.

Routes are a flexible scheme that we have to link a URI to a functional process; and this functional process can be: 

  1. A callback, which is a local function defined on the same routes.
  2. A controller, which is a separate class.
  3. A component, which is like a controller, but more flexible. 

If we check in the routes folder; we will see that there are 4 files:

  1. api: To define routes of our Apis Rest.
  2. channels: For fullduplex communication with channels.
  3. console: To create commands with artisan.
  4. web: The routes for the web application.

Available HTTP route types and methods

The one we are interested in in this chapter is the web.php; which allows us to define the routes of our web application (those that our client consumes from the browser).

The routes in Laravel are a central element that allow link controllers, and to be able to programmer our own processes; that is, the routes do not need the controllers to be able to present a content; and therefore, it is the first approach that we are going to present.

If you notice, we have a route already defined:

Route::get('/', function () {
    return view('welcome');
});

Which, as you can imagine, is what we see on the screen just when we start the application.

Note that a class called Route is used, which is imported from:

use Illuminate\Support\Facades\Route;

Which is internal to Laravel and are known as Facades.

The Facades are nothing more than classes that allow us to access the framework’s own services through static classes.

Finally, with this class, we use a method called get(); for routes we have different methods, as many methods as type of requests we have:

  • POST create a resource with the post() method.
  • GET read a resource or collection with the get() method.
  • PUT update a resource with the put() method.
  • PATCH update a resource with the patch() method.
  • DELETE delete a resource with the delete() method.

In this case, we use a route of type get(), which leads to using a request of type GET.

The get() method, like the rest of the methods noted above, takes two parameters: 

Route::<ResourceFunction>(URI, callback)
  1. Application URI.
  2. The callback is the controller function, which in this case is a function, but it can be the reference to the function of a controller or a component.

And where "ResourceFunction" is the get(), post(), put(), patch() o delete() method.

In the example above, the ”/” indicates that you are the root of the application, which is: 

http://larafirststeps.test/

Or localhost if you use MacOS or Linux using Docker.

In this case, the functional part is an anonymous function; this function can do anything, return a JSON, an HTML, a document, send an email and much more.

In this example, it returns a view; to return views, the helper function called view() is used, which references the views that exist in the folder: 

resources/views/<View and Foldes>

By default, there is only a single file; the one called welcome.blade.php, and yes, it’s the one we’re revering in the path above with: 

return view('welcome');

Note that it is not necessary to indicate the path, nor the blade or php extension.

Blade refers to Laravel’s template engine which we’ll talk about a bit later.

If you check the view of welcome.blade.php:

You’ll see all the HTML of it:

<!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>
***

So, if we create a few more routes:

Route::get('/writeme', function () {
    return "Contact";
});
Route::get('/contact', function () {
    return "Enjoy my web";
});

And we go to each of these pages, respectively:

Example route 1
Example route 1

And

Example route 2
Example route 2
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>
Example route 3
Example route 3

Named routes

Another configuration that cannot be missing in your application is the use of named routes; as its name indicates, it allows you to define a name for a route.

*
Route::get('/contact', function () {
    return "Enjoy my web";
})->name('welcome');

For that, a function called name() is used, to which the name is indicated; to use it in the view: 

<a href="{{ name('welcome') }}">Welcome</a>

This is particularly useful, since you can change the URI of the route, group it or apply any other configuration to it, but as long as you have the name defined in the route and use this name to reference it, Laravel will update it automatically.

Dynamic routes and parameters

Routes can also receive dynamic parameters:

Route::get('/user/{id}', function ($id) {
   return "Usuario con ID: $id";
});

You can make them optional:

Route::get('/user/{name?}', function ($name = 'Invitado') {
   return "Hola, $name";
});

And, of course, pass data to a view:

Route::get('/custom', function () {
   $msj2 = "Mensaje desde el servidor";
   $data = ['msj2' => $msj2, 'age' => 15];
   return view('custom', $data);
});

In this example, the custom.blade.php view receives variables and displays them using Blade.

I love this approach because it simplifies communication between the server and the interface.

Importance of Modularizing Routes in Laravel

Video thumbnail

I want to tell you about something I consider very important: modularizing your routes and controllers. By controllers, I mean the functions or methods that process requests. To illustrate this, I'm going to show you my previous version of Laravel development so you can see the comparison.

In the previous version, I had a bunch of GET routes like these:

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']);
});

Problem with Disorganized Routes

The reason for so many routes is the nesting of courses, sections, and classes, similar to how Udemy works:

  1. Main course (tutorial).
  2. Course sections.
  3. Classes within each section.

Additionally, depending on the user:

  • If they bought the course, the tutorial has more information.
  • If they haven't bought it, we only show a basic detail.

This led to creating many separate routes to handle different views (detail, full, simple) and different filters (ID or slug).

The problem is that these routes become hard to understand, with long and repetitive names, and modularity is lost.

Solution: Group and Reuse Controllers

To simplify, we group the routes and leverage the same controllers:

Route::group(['prefix' => 'tutorial'], function () {
  Route::get('', [App\Http\Controllers\Api\TutorialController::class, 'getAll']); // general list
 
  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: Reuse the Same Controller for ID and Slug

If you want to fetch a tutorial by ID or by slug, you can use the same controller. You just need to define additional routes:

Route::get('full/get-by-slug/{tutorial:url_clean}', [Controller::class, 'getFull']);
Route::get('full/get-by-id/{tutorial}', [Controller::class, 'getFull']);

This reduces routes by half and makes the code cleaner and more maintainable.

Tip 2: Leverage Parameters and Authentication

Instead of creating additional routes, you can leverage parameters and authentication to return more or less information:

public function getFull(Tutorial $tutorial)
{
   $user = auth()->user() ?? auth('sanctum')->user();
   if ($user) { // Authenticated user: show all tutorial information 
   } else { 
   	// Unauthenticated user: show only basic information } 
   }

For users who haven't bought the course, only basic information is shown.

For users who bought the course, all information (full) is returned, including sections and classes.

An extra parameter can be passed via request to calculate additional prices, but it is not recommended to pass sensitive information directly, as it could be hacked.

Benefits of Modularizing

Easier to maintain: You don't need to modify multiple routes or controllers to add a new parameter.

More readable: Routes are grouped and have clear names.

Controller reuse: You can use the same method for multiple routes and parameters.

Security: By using auth or sanctum, sensitive data is handled correctly.

Benefits of Modularization

Easier to maintain: You don't need to modify multiple routes or controllers to add a new parameter.

More readable: Routes are grouped and have clear names.

Controller reuse: You can use the same method for multiple routes and parameters.

Security: By using auth or sanctum, sensitive data is handled correctly.

Common Errors and How to Fix Them

ProblemCauseSolution
Route not foundMisspelled URI or duplicate routeVerify with php artisan route:list
Error 419 or CSRFMissing token in POST formsAdd @csrf in the forms
Outdated route cacheChanges made without clearing cacheRun php artisan route:clear

BE CAREFUL WITH ROUTES

Video thumbnail

I wanted to talk about a topic that I find very interesting. I always pause a bit on these points because I notice that almost nobody mentions them; I like to know others' opinions and provide a touch of criticism. There is no need to talk much about the "good" parts, they are simply there (and that's what my courses and books are for).

I am currently updating the Laravel course project to version 12, which is the latest to date. In this context, I want to talk about Middleware. Currently, we can load it in at least three ways:

Route::get(...)->middleware([]);
Route::group(['middleware' => [...]], function() { ... });
Route::middleware([...])->group(function() { ... });

I like options; the more tools we have, the better. However, the important point is that while we gain flexibility, we sometimes lose a bit of readability or understanding. The piece we are working with becomes more complex, so it is not a total "win-win," but rather there is always a grey area.

Personally, I prefer having these options over being limited to using only grouped routes. In the end, it is up to the developer to decide what they prefer; that's why in my courses I like to show variants so that you are the one who chooses your style.

Multiple route types

Nowadays, Laravel allows us to define routes for different purposes. For example, routes for traditional controllers:

Route::get('user/{id}', 'UserController@show');

Routes for Livewire components:

Route::get('/', App\Livewire\Dashboard\Category\Index::class)->name("d-category-index");
Volt routes (the new functional API of Livewire):

Volt::route('volt/contact', 'volt.contact.general')->name('volt-contact');

And, of course, routes that return views directly:

Route::get('/', function () {
   return view('welcome');
})->name('home');

In modern Livewire, we now have:

Route::livewire('subscribe', 'pages::dashboard.subscribe.list')->name('subscribe-list');

Is Laravel still a pure MVC framework?

Let's remember that Laravel is no longer a pure MVC framework. Historically, from Laravel 5 to version 6, routes were linked almost exclusively to controllers. Components, as we know them, began to gain traction starting from version 7.

In my case, when I work on a project and decide to use Livewire, I adopt it completely. I practically do not use Laravel's base controllers because the library gives me a lot of flexibility. As I have said in other videos, Livewire is my favorite scaffolding for Laravel.

However, we must be realistic: having so many route definitions for different types of components (Controllers, Livewire, Volt, Views) can make reading and general maintenance of the application difficult if not managed carefully.

Optional parameters in Laravel routes are meaningless

Video thumbnail

Look at something very curious about Laravel, which I hadn't really noticed until now. I'm working with a route that has a completely optional parameter, and I'll explain what I discovered.

I have a route that points to /store/{productType?}, where productType is a model (a category or classification). This can be optional, as I indicate in the definition.

When /store is accessed without any parameters, the value should be null or simply undefined.

When /store/zapatos is accessed, then it is defined.

Now comes the curious part. You'd think that if this parameter is optional and isn't passed, the value should be null, right?

Since in Livewire I receive this value in mount(), I assumed I could simply do something like:

function mount(?ProductType $productType)
{
   $this->productType = $productType->id ? $productType : null;
}

In the case of /store/zapatos, everything works as expected: $productType has a valid value.

But in /store, instead of receiving a null value, Laravel passes me an empty instance of the ProductType model.

This behavior is a bit shocking to me. It's as if Laravel is doing this internally:

$productType = new ProductType;

This makes sense in certain contexts, such as when working with forms to create or edit models, where you want an empty instance for code reuse. But in this specific case, it doesn't make sense.

I assume this has to do with null safety, something that was more formally implemented in recent versions of PHP. But it bothers me because:

It can break your logic if you assume you're working with a valid database object.

You can access the title ($productType->title) and it returns null, but you don't know if that's because it doesn't exist in the database or because Laravel passed you an empty instance.

What If I Force the Parameter to be Null?

I thought about forcing the parameter type in mount() to be nullable:

function mount(?ProductType $productType = null)
{
   ***
}

But if you access /store, Laravel throws a 404 directly if it doesn't find the model. That is, it doesn't even reach the component. This bothers me even more because it breaks the route for no apparent reason.

How I wish it would work

I would prefer if Laravel would let me work with null directly. It's cleaner for validations and for code maintainers.

For example, it's much easier to validate:

if ($productType) 

Instead:

if (is_null($productType->title)) ...

The latter creates confusion for any subsequent developers:

Is the title null because you defined it that way, or because Laravel created an empty object?

In summary

  • Laravel doesn't return null when the model is optional and cannot be found: it returns an empty instance.
  • If you define the parameter as nullable, Laravel returns a 404 if the model doesn't exist.
  • This behavior is odd and a bit annoying, at least to me.
  • I don't know the exact version that's been working this way, but I suspect it's a recent one.

Grouping routes into functions

For the dashboard or administration panel, grouping routes makes more sense because multiple functionalities exist:

function routesDashboard()
{
   Route::get('/', function () {
       return redirect()->route("post-list");
   });
***
}

By grouping routes into functions, we gain several advantages:

  • Reusability: We can use the same routes in different environments without duplicating code.
  • Organization: We can separate routes by module (e.g., web/dashboard, web/blog, web/academy).
  • Flexibility: In production, we can use subdomains, and in development, a local domain without complex changes.

Example of usage in production vs. development

  • Production: We use subdomains to separate the modules.
  • Development: Everything is managed from a test domain to simplify the workflow.

Organize Laravel Routes into Files

Video thumbnail

Previously, I mentioned that I really liked grouping routes using functions. In my old project (which I am migrating now), I used this scheme to separate, for example, the Blog Dashboard in my Desarrollo Libre application.

Basically, I created functional groups and, within each one, defined specific routes. However, it is important to remember that in this scheme, functions must be explicitly invoked for the application to recognize them; they don't load magically.

The Single File Problem

Although grouping by functions is a good start, it has a drawback: everything still resides in the same file. It's like having 50 methods in the same controller; in the end, reading it becomes heavy. The ideal approach is to have independent files.

This idea was given to me by the default Livewire structure. When creating a project, I noticed it included a settings section with several separate routes. From there, I decided to replicate that model for each of my modules in this new project.

Structure Based on Modules and APIs

Now, the main route files look much cleaner.

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');
    });

We create an additional folder structure and you can do the same with the api.php file if your application uses a REST API.

In short, internally, I organize each group of routes (Social, Blog, API, etc.) into separate files within their own folders. With this, I gain two fundamental things:

  • Readability: If I need to review the social media routes, I go directly to the social.php file and see only what interests me, without getting tangled in hundreds of lines of code.
  • Maintenance and Security: As a developer, I am always testing new features. Before, all those tests lived together in the web.php file. If something broke, it affected the entire route system.

Avoiding Production Errors (Real Case)

Recently, a curious thing happened to me: I am migrating my academy to Laravel 13 and Google rejected the publication of the mobile app because the "Privacy" and "Policies" pages returned a 404 error.

Thanks to this modular scheme, I only had to update the specific file for those legal routes and upload it to the production server. This gives me a lot of peace of mind, as I didn't have to touch the main web.php file, where I might have pending tests or half-finished code. By separating blog routes from static or API routes, the "headache" when updating is much smaller.

Conclusion

I highly recommend this modular scheme, or even a mix between functions and separate files if your application is very large:

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');
    });

Ultimately, it's about avoiding accidental errors when pushing changes to production and keeping the code as clean as possible.

Frequently Asked Questions (FAQs)

  • Where are routes defined in Laravel? 
    • In the files within the routes/ folder, primarily in web.php.
  • How do I pass variables to a view from a route? 
    • Use the view() helper and pass an associative array with the data.
  • What is the difference between web routes and API routes? 
    • Web routes load sessions, cookies, and views; APIs are lightweight and stateless.
  • How do I list all available routes? With php artisan route:list.
  • What's the best way to group routes? 
    • Use prefix(), middleware(), or even Route::resource() for REST controllers.

Conclusion

Modularizing your routes and controllers is fundamental to keeping a project organized, scalable, and secure. This avoids the chaos of long and confusing routes and allows you to leverage parameters and authentication to handle different scenarios with less code.

Mastering its use not only makes you more efficient, but also gives you total control over the structure and performance of your application.

The next task is to learn how to use Laravel's powerful command line, Artisan.

We will take the first steps with the routes and the views, to start seeing screens through the browser; we’ll also cover using controllers with views; redirects, directives and blade as template engine.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español