Laravel Basics: Routes, Controllers and Views

24 Jul 2026 | Basile Aelterman

Laravel Basics: Routes, Controllers and Views

Every request that hits a Laravel app follows the same journey: a route decides where it should go, a controller handles the request and decides what should happen, and a view decides what the end user actually sees. In this blogpost we'll break down that whole journey — from everything you can do with routes, what belongs in a controller, what exactly a view is and how blade components keep your templates from turning into a pile of spaghetti code.

Routes

The basics

To create a route, you first need to decide what type of request you want to catch. In this case, we want to show the user a simple welcome page.

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

If we now serve this project locally and visit the route /welcome, we will see our welcome page.

  1. We use the method GET from Route to catch get requests that point to this endpoint. It won't catch anything else such as POSTDELETE, etc.
  2. The first variable this method requires is the actual endpoint, everything after your domain name. In this case it's /welcome.
  3. The second variable we need is a callable function/method. Here we used a function to return a view called welcome. This looks for a file called welcome.blade.php in the directory resources/views/.

Most real routes point to a controller instead of a closure since that keeps your logic nicely organized:

Route::get('/posts', [PostController::class, 'index']);

Here instead of using an inline function, we call a method called index from a class called PostController. Inside this method we would then fetch all the posts, and return a view, much like what you would do inside an inline function.

But lets say we only want to see a singular post, how does our controller ever know what post we are talking about? We pass through a parameter straight from the URL:

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

Now in our show method, we can add a parameter with the exact same name as we put between the brackets. In this case we would add int $id. And based on this id, we can fetch the post that has this id and return it to the user.

But lets also say that you want your frontend to link to a certain route but you don't always want to enter the full route. Laravel has got you covered on that too. You can also name your routes. You can do this by simply using the name method after your request type:

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

Then later in your app, instead having to type out the full route, you'd use something like this:

<a href="{{ route('posts.show', ['id' => 1]) }}">View post 1</a>

This is the absolute foundation. Everything else builds on top of this.

Route groups and prefixes

Lets say that we want our /posts routes to only be accessible when a user is logged in, and keep /welcome accessible to anyone. To achieve this, we let our requests first run through a middleware and then put inside a group, which we can do like this:

// Our welcome page stays outside of the middleware
Route::get('/welcome', function () {
    return view('welcome');
});

// Everything else gets wrapped in a group
Route::middleware(['auth'])->group(function () {
    Route::get('/posts', [PostController::class, 'index'])
        ->name('posts.index');
    Route::get('/posts/{id}', [PostController::class, 'show'])
        ->name('posts.show');
});

If a user visits this route without being logged in, Laravel will redirect them straight to the login page instead of showing them the posts.

In order to make it more clear what routes are for example behind a loginwall, we can also prefix those routes by using the prefix method:

// ...

Route::prefix('auth')
    ->middleware(['auth'])
    ->group(function () {
        Route::get('/posts', [PostController::class, 'index'])
            ->name('posts.index');
        Route::get('/posts/{id}', [PostController::class, 'show'])
            ->name('posts.show');
        Route::post('/posts', [PostController::class, 'store'])
            ->name('posts.store');
        Route::get('/posts/{id}/edit', [PostController::class, 'edit'])
            ->name('posts.edit');
        Route::put('/posts/{id}', [PostController::class, 'update'])
            ->name('posts.update');
        Route::delete('/posts/{id}', [PostController::class, 'delete'])
            ->name('posts.delete');
    });

Now you can use your routes like this:

<a href="{{ route('auth.posts.show', ['id' => 1]) }}">View post 1</a>

As you might have noticed already, while this works, it can quickly become quite chaotic and unreadable. Luckily Laravel also has a fix for that: resource routes.

Instead of manually writing each route, you can simply create a resource route like this:

Route::prefix('auth')
    ->middleware(['auth'])
    ->group(function () {
        Route::resource('posts', PostController::class);
    });

Just like that! It does the exact same like we just did, only more readable and clean.

Route model binding

So far, whenever we needed a specific post, we've been passing through an id and then manually looking it up ourselves inside the controller, something like this:

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

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

That's not necessarily wrong, but it's also something you'd end up writing over and over again for basically every route that deals with a single record. Laravel has a neat trick for this called route model binding. Instead of typing our parameter as int $id, we type-hint it as the model itself:

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

As long as the name of your route parameter ({post}) matches the name of your method parameter ($post), Laravel will automatically fetch the matching Post model for you before your method even runs. If no post is found with that id, Laravel throws a 404 for you, so you don't have to worry about that either.

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

By default this looks the post up by its id, but you're not stuck with that. Lets say you'd rather have your URLs look like /posts/my-first-blog-post instead of /posts/1. You can tell your model to use a different column by adding this method to it:

public function getRouteKeyName(): string
{
    return 'slug';
}

Now Laravel will look up your post by its slug column instead of its id, without you having to change anything in your controller or your routes.

Resource routes vs. manual routes

We already saw resource routes earlier when we cleaned up our auth group, but lets dig a bit deeper into when you'd actually want to use one over writing your routes manually.

If you're building a standard CRUD feature (create, read, update, delete), you'll notice the same seven actions keep coming back: indexcreatestoreshoweditupdate and destroy. That's exactly the pattern Route::resource() generates for you:

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

That single line is the equivalent of writing out all seven routes by hand, with the correct HTTP verbs, URIs and names already in place (posts.indexposts.showposts.store, etc.).

So when do you reach for a resource route, and when do you write things manually?

  1. Use a resource route when your controller actually follows this standard CRUD pattern. It's less boilerplate, the naming stays consistent, and anyone else who knows Laravel immediately understands what routes exist without having to go look.
  2. Use manual routes when you only need one or two of those actions, or when what you're building doesn't fit the CRUD mold at all. Think something like /posts/{post}/publish or /posts/{post}/report.

Nothing stops you from combining the two either. You can define a resource route and simply add your extra manual routes right next to it for the actions that fall outside the standard set:

Route::resource('posts', PostController::class);
Route::get('/posts/{post}/report', [PostController::class, 'report'])
    ->name('posts.report');

Controllers and Views

So far we've only talked about deciding where a request should go. That's only half the story though. Once a route points somewhere, something needs to actually do the work and hand back a response. That's where controllers and views come in.

What are controllers?

A controller is simply a class that groups related request-handling logic together. Instead of cramming all our logic directly into routes/web.php as closures like we did at the start of this post, we move it into a dedicated class instead:

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

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

And our route simply points to it, just like we've been doing this whole time:

Route::get('/posts', [PostController::class, 'index']);

The main reason controllers exist is organization. Once your app has more than a handful of routes, closures scattered across your routes file quickly become unmanageable. Controllers give each resource in your app (posts, users, orders, whatever it is you're building) a natural home, with one method per action.

What do we put in a controller?

A controller's job is to sit between the incoming request and the rest of your app. In practice, that usually comes down to:

  1. Reading input from the request, whether that's query parameters, form data, or route parameters like the ones we bound earlier.
  2. Talking to your models and services to fetch or persist data.
  3. Deciding what to return, whether that's a view, a redirect, or a JSON response.

What you generally don't want inside a controller is business logic that doesn't really belong there. Things like heavy calculations, complex validation rules, or anything you'd want to reuse outside of a web request. That kind of logic tends to move to your models, form requests, or dedicated service classes as your app grows. 

A good rule of thumb is that a controller method should read like a short summary of what happens, not a wall of logic.

class PostController extends Controller
{
    public function store(StorePostRequest $request)
    {
        $post = Post::create($request->validated());

        return redirect()
            ->route('posts.show', $post)
            ->with('success', __('Post created'));
    }
}

Here the validation lives inside StorePostRequest, the actual creation logic lives in the Post model, and our controller just coordinates between the two.

What is a view?

A view is the part of your app responsible for what the user actually sees. In Laravel, views are usually written in Blade, Laravel's templating engine, and stored as .blade.php files inside resources/views, exactly like the welcome.blade.php file we returned all the way back at the start of this post.

A controller hands data over to a view, and the view decides how to display it:

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


{{-- resources/views/posts/index.blade.php --}}
<h1>All posts</h1>

<ul>
    @foreach ($posts as $post)
        <li>
            <a href="{{ route('auth.posts.show', $post) }}">
                {{ $post->title }}
            </a>
        </li>
    @endforeach
</ul>

Blade gives you plain HTML plus a handful of directives, such as @foreach and @if, and {{  }} for safely printing out variables. Views keep your display logic separate from your controllers, so a controller never has to know or care what the final HTML actually looks like. It just passes the data to it.

Components, includes and slots

As soon as you add more pages and your views start repeating the same bits of markup (think a card, a button, a navbar, etc.), copy-pasting the same Blade code across multiple files gets messy really fast. Blade gives you a couple of tools to avoid exactly that.

Includes are the simplest option, they just pull one Blade file into another:

@include('partials.alert')

These are great for static, self-contained chunks of markup that don't need much configuration, like a footer or a simple alert banner.

Components are a step up from that. They're reusable pieces of UI that accept their own data, a bit like a small template with parameters:

{{-- resources/views/components/alert.blade.php --}}
<div class="bg-red-500 w-100 h-32 absolute bottom-4 left-auto right-auto alert-{{ $type }}">
    {{ $message }}
</div>


<x-alert type="error" message="Something went wrong." />

Slots let a component accept a whole chunk of HTML from wherever it's used, instead of just simple attributes:

{{-- resources/views/components/button.blade.php --}}
<button class="bg-{{ $variant }}-500 text-white rounded-lg p-{{ $size }}">
    {{ $slot }}
</button>


<x-button variant="primary" size="normal">
    Hello, world!
</x-button>

Anything you put between <x-button></x-button> becomes available inside the component as $slot. You can also define multiple named slots when a component needs more than one flexible area, like a header and a footer:

<x-card>
    <x-slot:header>
        Latest post
    </x-slot:header>

    <p>Main content goes here.</p>

    <x-slot:footer>
        Posted 2 hours ago
    </x-slot:footer>
</x-card>

Then in your markup you can access those named slots like this:

{{-- resources/views/components/card.blade.php --}}

<div class="shadow-md rounded-xl p-4 space-y-4">
    <div>
        {{ $header }}
    </div>

    <div>
        {{ $slot }}
    </div>

    <div>
        {{ $footer }}
    </div>
</div>

So, when should you use what?

  1. Use an include for a static chunk of markup that you just want to reuse as-is.
  2. Use a component when you need reusable UI that takes different data or content every time it's used.
  3. Use slots when your component needs to wrap arbitrary, flexible content rather than just accept simple values through attributes.

Wrapping up

That's the full loop you need for a simple Laravel application: a route decides where a request should go, a controller decides what should happen with the provided data, and a view decides what the user actually gets to see. Once your views start repeating themselves, includes, components and slots let you build that UI once and reuse it everywhere, instead of copy-pasting the same markup across your application. Get comfortable with these basics, and you'll be totally fine the further you go down the Laravel rabbit hole.

Basile Aelterman

Basile Aelterman

Auteur bij Dennenboom.