Laravel for Beginners — Routes, Views, & Controllers
This tutorial teaches core Laravel building blocks by building one small feature step by step: a guestbook that greets a visitor. Each step…
Laravel for Beginners — Routes, Views, & Controllers

This tutorial teaches core Laravel building blocks by building one small feature step by step: a guestbook that greets a visitor. Each step adds one new idea on top of the last, so by the end you will have touched routing, views, Blade templating, parameter passing, and controllers — plus a few bonus concepts that naturally follow.
Prerequisites
- PHP 8.1+ and Composer installed
- A Laravel project already created (e.g. via composer create-project laravel/laravel guestbook-app)
- The dev server running: php artisan serve
- A code editor (VS Code recommended) and basic command-line comfort
What you’ll learn
- Routing — defining URL endpoints in routes/web.php
- Views — returning Blade templates from a route
- Blade syntax — echoing variables and basic directives
- Route parameters — capturing dynamic segments of a URL
- Passing data from a route/controller into a view
- Controllers — moving logic out of routes/web.php
- Artisan CLI — generating controllers and inspecting routes
- Blade layouts — reusing a common page structure
- Named routes — referring to routes without hardcoding URLs
- Optional parameters & default values
- Handling form input, the Request object, and basic validation
- Session storage and looping over data with @foreach
Part 1 — The Core Four Steps
These four steps build the guestbook feature from nothing, in the exact order requested. Work through them in order — each one only makes sense once the previous one works.
Step 1: Route that returns “hello, guest”
Every Laravel request starts at a route. Open routes/web.php and add a new route for the /guestbook endpoint that returns plain text.
routes/web.php
use Illuminate\Support\Facades\Route;
Route::get('/guestbook', function () {
return 'hello, guest';
});
Visit http://127.0.0.1:8000/guestbook in your browser. You should see the plain text “hello, guest”.
Note: Route::get() matches HTTP GET requests. The first argument is the URL path (the endpoint); the second is a closure, i.e. an anonymous function, that runs when that path is requested and whose return value becomes the response body.
Step 2: Call a view to display “hello, guest”
Returning raw strings from routes.php works, but real pages need HTML. Laravel’s view layer (Blade) handles that. Create a Blade view file:
resources/views/guestbook/index.blade.php
<!DOCTYPE html>
<html>
<head><title>Guestbook</title></head>
<body>
<h1>hello, guest</h1>
</body>
</html>
Now update the route to call view() instead of returning a string directly:
routes/web.php
Route::get('/guestbook', function () {
return view('guestbook.index');
});
Note: The dot notation guestbook.index maps to the file resources/views/guestbook/index.blade.php. Laravel looks inside resources/views by convention, and the dot stands in for a folder separator.
Step 3: Route parameter — /guestbook/{name}
Static pages are limited. Add a second route that captures a name from the URL itself and passes it into the same view.
routes/web.php
Route::get('/guestbook/{name}', function ($name) {
return view('guestbook.index', ['name' => $name]);
});
Update the view to print the passed-in variable using Blade’s echo syntax, {{ }}:
resources/views/guestbook/index.blade.php
<!DOCTYPE html>
<html>
<head><title>Guestbook</title></head>
<body>
<h1>hello, {{ $name }}</h1>
</body>
</html>
Visiting /guestbook/Alice now renders “hello, Alice”. The {name} segment in the route is called a route parameter, and Laravel automatically passes it into the closure as an argument with the same name.
Note: {{ $name }} is Blade’s escaped-output syntax i.e. it HTML-encodes the value automatically, which protects your page from malicious input. Never use raw PHP echo tags for user-supplied data.
Step 4: Move the logic into a Controller
Closures in routes/web.phpare fine for tiny examples, but real applications extract that logic into a Controller so routes stay short and logic stays testable. Generate one with Artisan:
php artisan make:controller GuestbookController
This creates app/Http/Controllers/GuestbookController.php. Add a method that does what the closure did before:
app/Http/Controllers/GuestbookController.php
<?php
namespace App\Http\Controllers;
class GuestbookController extends Controller
{
public function index(string $name)
{
return view('guestbook.index', ['name' => $name]);
}
}
Now point the route at the controller method instead of a closure:
routes/web.php
use App\Http\Controllers\GuestbookController;
Route::get('/guestbook/{name}', [GuestbookController::class, 'index']);
Visiting /guestbook/Alice behaves exactly as before, but the logic now lives in a dedicated class. This is the same pattern you’ll use for every real feature: route → controller method → view.
Note: [GuestbookController::class, 'index'] is PHP’s array callable syntax. It tells Laravel which class to instantiate and which method to call, and Laravel automatically passes the {name} route parameter into that method’s $name argument. This is called route-model-binding-style parameter injection.
Part 2 — Bonus Concepts Worth Learning Next
The four steps above cover the essentials, but a few closely related ideas come up almost immediately in real Laravel projects. These are optional extensions to the same guestbook feature.
5. Blade layouts (@extends, @section, @yield)
Instead of repeating <html>, <head>, and <body> in every view, Laravel lets you define one master layout and extend it. Create a layout file:
resources/views/layouts/guestbookapp.blade.php
<!DOCTYPE html>
<html>
<head><title>@yield('title', 'Guestbook')</title></head>
<body>
@yield('content')
</body>
</html>
Then simplify the guestbook view to extend it:
resources/views/guestbook/index.blade.php
@extends('layouts.guestbookapp')
@section('content')
<h1>hello, {{ $name }}</h1>
@endsection
Note: @yield defines a placeholder in the layout; @section fills it in from a child view. This is how nearly every multi-page Laravel app avoids repeating boilerplate HTML.
6. Named routes
Hardcoding URLs like /guestbook/Alice all over your app becomes fragile. Give the route a name instead:
routes/web.php
Route::get('/guestbook/{name}', [GuestbookController::class, 'index'])
->name('guestbook.show');
Then generate the URL anywhere, i.e. in a view, a controller, or a redirect , using the name instead of the raw path:
route('guestbook.show', ['name' => 'Alice']) // => /guestbook/Alice
Note: If you ever change the URL structure, every route(‘guestbook.show’, …) call updates automatically; nothing else in the app needs to change.
7. Optional route parameters
What if /guestbook (no name) should still work, defaulting to “guest”?
Add a ? and a default value:
routes/web.php
Route::get('/guestbook/{name?}', [GuestbookController::class, 'index'])
->name('guestbook.show');
app/Http/Controllers/GuestbookController.php
public function index(string $name = 'guest')
{
return view('guestbook.index', ['name' => $name]);
}
This single route can now replace the two separate routes from Steps 1–3 entirely, since /guestbook and /guestbook/{name} are handled by the same line.
8. Handling a form with Request and validation
A guestbook is more useful if visitors can submit their own name through a form rather than typing it into the URL. Add a form to the view and a POST route:
resources/views/guestbook/index.blade.php
<form method="POST" action="{{ route('guestbook.store') }}">
@csrf
<input type="text" name="name" placeholder="Your name">
<button type="submit">Sign guestbook</button>
</form>
routes/web.php
Route::post('/guestbook', [GuestbookController::class, 'store'])
->name('guestbook.store');
app/Http/Controllers/GuestbookController.php
public function store(\Illuminate\Http\Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
]);
return redirect()->route('guestbook.show', ['name' => $validated['name']]);
}
Note: @csrf outputs a hidden token that protects the form from cross-site request forgery. Laravel requires it on every POST/PUT/DELETE form by default. The Request object represents the incoming HTTP request, and validate() both checks the rules and stops execution with an error response if they fail.
9. Remembering every visitor with the session and @foreach
So far only the latest name is shown. Storing a running list in the session lets the guestbook display everyone who has signed it:
app/Http/Controllers/GuestbookController.php
public function store(\Illuminate\Http\Request $request)
{
$validated = $request->validate(['name' => 'required|string|max:50']);
$entries = session('entries', []);
$entries[] = $validated['name'];
session(['entries' => $entries]);
return redirect()->route('guestbook.show', ['name' => $validated['name']]);
}
public function index(string $name = 'guest')
{
return view('guestbook.index', [
'name' => $name,
'entries' => session('entries', []),
]);
}
resources/views/guestbook/index.blade.php (inside @section(‘content’))
<h1>hello, {{ $name }}</h1>
<ul>
@foreach ($entries as $entry)
<li>{{ $entry }}</li>
@endforeach
</ul>
Note: session() reads and writes data that persists across requests for the same visitor. @foreach is Blade’s shorthand for a PHP foreach loop. It must always be paired with @endforeach.
Where to go next
- Eloquent models & migrations — replace the session array with a real guestbook_entries database table
- Form Request classes — move validation rules out of the controller into their own class
- Middleware — run code before/after every request (e.g. authentication checks)
- Resource controllers — the conventional index/show/store/update/destroy method set
- Route caching (php artisan route:cache) and php artisan route:list for debugging routes
메타데이터
- post_id
- f512c3d80841
- slug
- laravel-for-beginners-routes-views-controllers-f512c3d80841
- url
- https://towardsdev.com/laravel-for-beginners-routes-views-controllers-f512c3d80841
- canonical_url
- https://towardsdev.com/laravel-for-beginners-routes-views-controllers-f512c3d80841
- author_url
- https://medium.com/@mohamad.razzi.my
- status
- ok
- fetched_at
- 2026-07-17 22:01:38