Laravel Core

Laravel Routing Essentials: A Beginner's Guide

Friday, October 25, 2024

// 6 min read

Laravel 11 Routing
Laravel Routing

Learn about the fundamentals of routing in Laravel in this blog post.

What is a Route?

A primary function of the route is to direct requests to the relevant section of your application. Routes are created in the routes folder of a Laravel application.

In particular, the web.php file includes website URLs, while the api.php file contains API URLs. You can map these URLs to specific controllers or closures.

Laravel automatically loads these files based on the withRouting configuration in your application's bootstrap/app.php.

In this post, we will only focus on the web routes.

Larave route folder structure

Route definitions

The simplest way to define a route is by using a closure:

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

When you visit the /about URL of the application, the router matches the appropriate method in the web.php file. It first matches the line above and then executes the related action, which could be a callback, a view or a controller method. This callback function calls the template file located at resources/views/about.blade.php. And the view method generates the HTML output that the visitor sees in the browser.

Laravel route flow

View Routes

If you only want to display a static HTML page at your selected URL, you can use a View route like this:

Route::view('/about', 'about');

This achieves the same result as the previous version, which uses a closure.

Using Controllers for Route handling

What if you need to perform an action or extract data from a database before displaying your HTML output? Instead of using a closure, it is better to utilize a controller where you can define a specific action for the route.

And now instead of a closure, you can use an array. The first element of the array is the controller class, and the second element is the name of the method within that controller. For example:

Route::get('/posts', [PostController::class, 'index'])->name('posts.index');

Here is the simple index method that retrieves post records from the database and passes them to the view.

<?php

namespace App\Http\Controllers;

use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::all();

        return view('post.index', [
            'posts' => $posts,
        ]);
    }
}

And a simple blade template to list the posts:

@foreach ($posts as $post)
    <article class="py-6 border-b border-grey-lighter ">
        <span class="inline-block px-2 py-1 mb-4 text-sm rounded-full bg-green-light font-body text-green">
            {{ $post->category }}
        </span>
        <a href="/posts/{{ $post->slug }}">
            <img src="{{ $post->featured_image }}" alt="{{ $post->title }}"
                class="object-cover w-full h-56 rounded-t-md" />
        </a>
        <a href="/posts/{{ $post->slug }}"
            class="block mt-4 text-lg font-semibold transition-colors font-body text-primary hover:text-green">
            {{ $post->title }}
        </a>
        <p class="text-gray-800">
            <a href="/posts/{{ $post->slug }}">
                {{ $post->excerpt }}
            </a>
        </p>
    </article>
@endforeach

Route parameters

Sometimes, you need to fetch a record from the database using a route parameter. You may define a parameter in your route definition like this:

Route::get('/posts/{id}', [PostController::class, 'show']);

In this example, I want to retrieve a post using its ID. In my controller method, I can search for the post model by the given ID:

public function show(int $id)
{
    $post = Post::find($id);

    if (!$post) {
        abort(404);
    }

    return view('post.show', [
        'post' => $post,
    ]);
}

Laravel can automatically inject model instances directly into your routes. Instead of using {id}, you can replace it with the {post} model. This way, Laravel automatically searches for the Post model by ID and retrieves it if it exists.

Route::get('/posts/{post}', [PostController::class, 'show']);

And the method can be simplified by passing the Post model as a parameter. I can also skip the extra condition to check if the post exists, since Laravel handles that automatically.

public function show(Post $post)
{
    return view('post.show', [
        'post' => $post,
    ]);
}

However, this approach may not create a user-friendly URL. Instead of writing a URL like /posts/2, I would prefer something like /posts/laravel-routing-basics that uses the post's slug instead of its ID.

You can customize the route to specify that it should search by the slug.

Route::get('/posts/{post:slug}', [PostController::class, 'show']);

Now, the URL https://localhost:8000/posts/laravel-routing-basics will load the specified post by its slug.

You can also change the default model binding in your model so all of your links will use the slug instead of the ID.

In my Post model, I added the getRouteKeyName method to achieve this:

/**
 * Get the route key for the model.
 */
public function getRouteKeyName(): string
{
    return 'slug';
}

After this change, there is no longer a need to specify the column name in the route definition, allowing us to revert to the original definition:

Route::get('/posts/{post}', [PostController::class, 'show']);

Scope binding

You can include as many route parameters as needed. For example, if you want to fetch a specific User's Post, you can write:

Route::get('/users/{user}/posts/{post}', [UserPostController::class, 'show']);

And your controller method parameters should look like this:

public function show(User $user, Post $post)
{
    return view('user.post.show', [
        'post' => $post,
        'user' => $user,
    ]);
}

With this approach, any post can be displayed under any user, even if the post doesn't belong to that user. This is why it is recommended to use scope binding, which ensures that the child model (Post) is linked to the parent model (User), preventing access to unauthorized data. In this case, we want to verify that the selected Post belongs to the User. If it does not, the application should return a 404 page.

First, ensure that your User model has a posts relationship.

public function posts(): \Illuminate\Database\Eloquent\Relations\HasMany
{
    return $this->hasMany(Post::class);
}

In the Post model, establish a relationship with the User as well.

public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
    return $this->belongsTo(User::class);
}

After this, you can invoke the scopeBindings method in your route definition.

Route::get('/users/{user}/posts/{post}', [UserPostController::class, 'show'])->scopeBindings();

Router methods

Of course, in addition to the GET method, you can register routes that respond to any HTTP method.

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

For instance, to update a specific Post model, send a PATCH request to the appropriate route:

Route::patch('/posts/{post}', [PostController::class, 'update']);

Resource Route

Another useful route type is the resource route. This allows you to generate multiple route definitions with a single line of code:

Route::resource('posts', PostController::class);

This line generates the following route definitions:

Route::get('/posts', [PostController::class, 'index']);
Route::post('/posts', [PostController::class, 'store']);
Route::get('/posts/create', [PostController::class, 'create']);
Route::get('/posts/{post}', [PostController::class, 'show']);
Route::get('/posts/{post}/edit', [PostController::class, 'edit']);
Route::patch('/posts/{post}', [PostController::class, 'update']);
Route::delete('/posts/{post}', [PostController::class, 'destroy']);

If you don’t need all the routes, you can specify which ones to generate by using the only method. For example:

Route::resource('posts', PostController::class)->only(['index', 'show']);

The line above will generate only the index and show route definitions.

Alternatively, you can use the except method to list the routes you want to exclude from generation:

Route::resource('posts', PostController::class)->except(['delete']);

This will generate all routes except the delete route.

Listing Routes

You can list all the routes used by your application in the console by using the following Artisan command:

php artisan route:list

It displays the available URLs, their HTTP methods, route names, and whether they use a controller.

Laravel route list

Named routes

Currently, the about URL does not have a name. Assigning a name to your routes is useful because it allows you to generate URLs throughout your code using the route function. To name the route, you can chain the name method onto the route definition:

Route::view('/about', 'about')->name('about');

About route with name

For instance, if you want to link to the About page in your navigation bar, you can use the route('about') method.

<a href="{{ route('about') }}" class="block px-2 mb-3 text-lg font-medium text-white font-body">
    About
</a>

Additionally, if you decide to change the URL later, as shown here where I've changed it to /about-me:

Route::view('/about-me', 'about')->name('about');

You won’t have to update any references in your code, as they will automatically point to the new URL.

Route groups

You can group routes to share the same attributes.

Controller

For example, let's look at the previous examples for the PostController without using a resource route. Instead of listing each route individually, you can modify them in the following way using the controller method:

Route::controller(PostController::class)->group(function () {
    Route::get('/posts/{id}', 'show');
    Route::post('/posts', 'store');
    ....
});

Middleware

Another useful way to group routes is by using middleware. Middleware are helpful for filtering HTTP requests or for performing tasks before or after each request. A common middleware is the auth middleware, which checks if a user is authenticated. This allows us to create a middleware group for routes intended specifically for authenticated users.

Route::middleware('auth')->group(function () {
    Route::put('password', [PasswordController::class, 'update'])->name('password.update');
    Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout');
});

It is possible to use multiple middlewares in a group. In the following example, the user dashboard is accessible only to authenticated users who have also verified their emails.

Route::middleware(['auth', 'verified'])->group(function () {
    Route::view('/dashboard', 'dashboard');
});

You can read more about middlewares HERE.

Prefix

Using the prefix method, you can easily add a prefix to a route group. In this example, I am using the blog prefix, which means I don't need to prepend each route definition with /blog/. Instead, I can simply create my routes like this:

Route::prefix('blog')->group(function () {
    Route::get('/', Blog::class)->name('blog'); // -> /blog/
    Route::get('/posts', [PostController::class, 'index'])->name('posts.index'); // -> /blog/posts
    Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show'); // -> /blog/posts/:post
});

This way, all routes in this group will automatically have the /blog/ prefix.

Name prefix

We can also use prefixing for route names using the name method. Let's refactor the post routes from the previous example:

Route::prefix('posts')->name('posts.')->group(function () {
    Route::get('/', [PostController::class, 'index'])->name('index');
    Route::get('/{post}', [PostController::class, 'show'])->name('show');
});

Here you have the basics of routing in Laravel. There are many additional methods and options available, such as route redirects, fallback routes, and rate limiting, which I plan to discuss in future blog posts. It's helpful to take a look at the official documentation for more detailed information.