Building Web Apps with Laravel Module 2 – Build your first Laravel feature
Your first page: route, controller and view
Build a notice board page in Laravel 13 from a route, a controller and a Blade view, and watch Blade escape a dangerous title so it prints as text.
What you will learn
- Add a route that returns a Blade view, then move the logic into a controller
- Pass data from a controller to a view and print it safely with the double curly syntax
- Name a route and link to it with the route helper
- Read your page's route with php artisan route:list
Before you start
What you’ll learn · Before you start
What you will learn
- Add a route that returns a Blade view, then move the logic into a controller
- Pass data from a controller to a view and print it safely with the double curly syntax
- Name a route and link to it with the route helper
- Read your page's route with php artisan route:list
Before you start
On this page
Pathshala, a small coaching centre, wants one web page: a notice board that lists three notices. In Laravel that page is three short files working together, a route, a controller and a Blade view, and this lesson builds all three and serves them.
You will add a route for /notices, move its work into a NoticeController, pass a hard-coded list of notices to a view, and watch Blade turn a notice whose title is <script>alert(1)</script> into harmless text. Then you will name the route, link to it with the route() helper, and read it back with php artisan route:list.
A route maps a URL to code
A route: A line in Laravel that maps an HTTP method and a URL path to the code that answers it, such as Route::get('/notices', [NoticeController::class, 'index']). connects a URL to the code that answers it. Every page in Laravel starts here, with a line in routes/web.php that says: for this HTTP method and this path, run this code. The shortest form answers a request straight from a closure, for example Route::get('/notices', fn () => view('notices.index')).
A closure is fine for a page with no logic, but a real page gathers data, and once there is work to do you want it in a class you can name, test and reuse. Before writing any files, follow one request through Laravel so you can see where each file fits:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The request arrives. The browser asks for GET /notices. Nothing has decided yet what code will answer it.
- The router matches. Laravel looks in routes/web.php for a route whose method and path match, and passes the request through the web middleware group first.
- The action runs. The matched route names a controller action, so the router calls NoticeController@index. This is where your code runs.
- The action returns a view. index() builds the notices array and returns view('notices.index', ['notices' => ...]). It hands the data to the view and does no HTML itself.
- Blade renders the HTML. The view file notices/index.blade.php loops over the notices and prints each value with {{ }}, which escapes it. The result is a string of HTML.
- The response goes back. The finished HTML travels back to the browser as the response, and the page appears.
The figure shows the division of labour you are about to build: the router picks the code, the controller gathers the data, and the view turns that data into HTML. Because the route lives in routes/web.php, Laravel first sends the request through the A named stack of middleware Laravel applies to every route in routes/web.php: it starts the session, encrypts cookies and checks request-forgery tokens., the middleware that starts the session, encrypts cookies and checks request-forgery tokens. You write the route; that protection is applied for you. The first CRUD feature lesson relies on it when a form posts data.
Move the work into a controller
A controller: A class, by default in app/Http/Controllers, whose public methods (actions) answer requests; an action gathers the data a page needs and returns a view. is a class that holds the actions for one kind of page, each action a public method that answers a request. Create one with Artisan:
from session import run
# Generate an empty controller class, then show the file Artisan wrote.
run(
"""
php artisan make:controller NoticeController
cat app/Http/Controllers/NoticeController.php
""",
app=True,
) Output
$ php artisan make:controller NoticeController
INFO Controller [app/Http/Controllers/NoticeController.php] created successfully.
$ cat app/Http/Controllers/NoticeController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class NoticeController extends Controller
{
//
}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 make_controller_session.py
Artisan writes app/Http/Controllers/NoticeController.php, an empty class that extends the skeleton’s base Controller. Laravel does not require a controller to extend a base class; the generated one does so only to share helpers across controllers. The stub also imports Illuminate\Http\Request, which this first action will not need.
Now fill the action in. It builds the list of notices and returns a view, handing the data over as the second argument:
<?php
namespace App\Http\Controllers;
class NoticeController extends Controller
{
public function index()
{
$notices = [
[
'title' => 'Spoken English batch starts this week',
'body' => 'The new evening batch meets in Room 2, every weekday.',
],
[
'title' => 'Python workshop seats open',
'body' => 'Twenty seats are open for the weekend Python workshop.',
],
[
'title' => '<script>alert(1)</script>',
'body' => 'This title is deliberately unsafe, to show how Blade escapes it.',
],
];
return view('notices.index', ['notices' => $notices]);
}
} The call view('notices.index', ['notices' => $notices]) names a The template file that produces a page's HTML; in Laravel it lives in resources/views and is usually written in Blade. and passes it an array of data. The dotted name notices.index maps to the file resources/views/notices/index.blade.php: each dot is a folder. Each key of the data array becomes a variable of the same name in the view, so the view reads the list as $notices. The action does no HTML itself; it decides what data the page needs and leaves the markup to the view.
One line in routes/web.php points the URL at that action:
<?php
use App\Http\Controllers\NoticeController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/notices', [NoticeController::class, 'index'])->name('notices.index'); Route::get('/notices', [NoticeController::class, 'index']) answers GET /notices by calling index() on the controller. Keep it this way round: queries and logic belong in the controller and the models it calls, and resources/views holds HTML only. A Blade file that runs its own database queries is the usual cause of pages that are slow and hard to test.
Key idea
The route chooses the action, the action gathers the data and returns a view, and the view turns data into HTML. Each file has one job, which is what makes the page easy to change and to test.
Checkpoint
Check: A route maps a URL to code · The controller gathers data, the view renders it
Blade escapes what you print
Blade: Laravel's templating language: an HTML file in resources/views with directives such as @foreach and the {{ }} echo tags, compiled to plain PHP. is Laravel’s templating language: an HTML file with a few directives for looping and printing. The notice board loops over the notices it was given and prints each one:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Notices · Pathshala</title>
</head>
<body>
<h1>Notices</h1>
<ul>
@foreach ($notices as $notice)
<li>
<strong>{{ $notice['title'] }}</strong>
<p>{{ $notice['body'] }}</p>
</li>
@endforeach
</ul>
<p><a href="{{ route('notices.index') }}">Open the notice board</a></p>
</body>
</html> @foreach walks the array, and {{ $notice['title'] }} prints one value. Start the development server and fetch the page to see what it sends back:
from session import serve
# Start the development server, then fetch /notices and show the HTML it rendered.
serve(
"""
curl -s http://127.0.0.1:8123/notices
""",
app=True,
overlay=("notice-board",),
) Output
# Terminal 1: start the server
$ php artisan serve --host=127.0.0.1 --port=8123 --no-reload
# Terminal 2: send requests
$ curl -s http://127.0.0.1:8123/notices
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Notices · Pathshala</title>
</head>
<body>
<h1>Notices</h1>
<ul>
<li>
<strong>Spoken English batch starts this week</strong>
<p>The new evening batch meets in Room 2, every weekday.</p>
</li>
<li>
<strong>Python workshop seats open</strong>
<p>Twenty seats are open for the weekend Python workshop.</p>
</li>
<li>
<strong><script>alert(1)</script></strong>
<p>This title is deliberately unsafe, to show how Blade escapes it.</p>
</li>
</ul>
<p><a href="http://127.0.0.1:8123/notices">Open the notice board</a></p>
</body>
</html>
# Terminal 1 showed this, then Ctrl+C stopped the server:
INFO Server running on [http://127.0.0.1:8123].
Press Ctrl+C to stop the server
… /notices .......................................... ~ …
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 page_session.py
Look at the third notice. Its title in the controller is <script>alert(1)</script>, but the HTML that came back contains <script>alert(1)</script>: the angle brackets are now entities, so the browser shows the text and runs nothing. This is output escaping: Converting characters such as < and > into HTML entities before printing a value, so untrusted text shows as text; Blade's {{ }} does this with htmlspecialchars to prevent cross-site scripting.. Laravel’s own documentation says that {{ }} statements are “automatically sent through PHP’s htmlspecialchars function to prevent XSS attacks”, so any value a user could have typed is made safe before it reaches the page.
Printing user data without escaping
Blade’s {!! $value !!} prints a value without escaping it. On the dangerous title it would send <script>alert(1)</script> straight into the page, where the browser would execute it. Use {!! !!} only for HTML you built and trust; for anything that came from a form, a database or a URL, keep {{ }}. The Blade and security lessons go deeper into this.
Checkpoint
Check: Blade escapes output with the double curly tags
Name the route and link to it
Add ->name('notices.index') to the route and it becomes a named route: A route given a label with ->name('notices.index'), so its URL can be built anywhere with route('notices.index') instead of a hardcoded path.: a label you can turn into a URL anywhere in the app with route('notices.index'). The view already does this in its link, <a href="{{ route('notices.index') }}">, so the URL is built from the route rather than typed as a string. Read the route back with Artisan:
from session import run
# The notice board's route, with the middleware group it sits behind.
run(
"""
php artisan route:list --path=notices
php artisan route:list --path=notices -v
""",
app=True,
overlay=("notice-board",),
) Output
$ php artisan route:list --path=notices
GET|HEAD notices .............. notices.index › NoticeController@index
Showing [1] routes
$ php artisan route:list --path=notices -v
GET|HEAD notices .............. notices.index › NoticeController@index
⇂ web
Showing [1] routes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 route_list_session.py
The first command lists the route: the methods GET|HEAD, the path notices, the name notices.index and the action NoticeController@index. Adding -v prints a ⇂ web line under it, the web middleware group the route sits behind. --path=notices filters a long list down to the routes whose path contains that word.
Naming the route pays off the moment a URL changes. If /notices becomes /board/notices later, every route('notices.index') call still produces the right link, while any hardcoded /notices string would quietly break. A short feature test checks all three promises of the page at once:
from session import run
# Run the feature test for the notice board page (PHPUnit; the 13.x skeleton ships no Pest).
run(
"""
php artisan test --filter=NoticeTest
""",
app=True,
overlay=("notice-board",),
) Output
$ php artisan test --filter=NoticeTest PASS Tests\Feature\NoticeTest ✓ the notices page responds with 200 …s ✓ a dangerous title is escaped …s ✓ the page links to its named route …s Tests: 3 passed (4 assertions) Duration: …s
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_session.py
The three tests pass: the page responds with 200, the dangerous title is escaped, and the link is built from the named route. The Laravel 13 skeleton ships PHPUnit rather than Pest, so the test is a PHPUnit class, and php artisan test --filter=NoticeTest runs just that class.
Checkpoint
Check: Named routes and the route helper
Practise
Build the next page of the notice board yourself, on the Pathshala project. Laravel runs on your own machine, not in the browser here, so make these changes in your project and check them with php artisan test:
- Add an “About the centre” page. Create a route for
/aboutthat returns a viewabout, name itabout, and move its logic into a controller action if it grows beyond returning the view. - From the notice board view, add a link to the About page built with
route('about'), not a hardcoded/about. - Write a feature test that fetches
/about, asserts a200response, and asserts that the notice board page contains the About link built from its named route.
Then work through the self-check below.
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: how do Blade’s {{ }} and {!! !!} tags differ, and when do you use each?
Both print a value, but they treat it differently. {{ $value }} sends the value through PHP’s htmlspecialchars first, so characters such as <, > and & become HTML entities and the browser shows them as text; this is the default because it stops a value that someone typed from injecting a tag or a script into your page. {!! $value !!} prints the value exactly as it is, with no escaping, so any HTML in it becomes real markup. Use {{ }} for anything that came from a user, a form, a database or a URL, which is almost everything, and reach for {!! !!} only for a string of HTML that you built and trust, such as a snippet rendered from your own trusted source.
Two longer questions start from this page:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A teammate has put a whole page, the database query and the HTML, inside one closure in routes/web.php. Walk through how you would split it into a route, a controller and a view, and say what each part should and should not hold.
-
Fresher to mid
In a code review you find a Blade template that prints a visitor's comment with {!! $comment->body !!}. Explain the risk, how you would prove it on a running page, and how you would fix it while still showing the formatting that visitors expect.
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which three pieces make one Laravel page, and what does each do?
A route maps the URL to code, a controller action gathers the data, and a Blade view turns that data into HTML. Each file has one job, which keeps the page easy to change and to test.
-
What does a route in routes/web.php get without any setup of yours?
It runs behind the web middleware group, which starts the session, encrypts cookies and checks request-forgery tokens before your controller runs.
-
How does a controller hand data to resources/views/notices/index.blade.php?
It returns view('notices.index', ['notices' => $notices]): the dotted name points at the file under resources/views, and each array key becomes a variable in the template. Queries stay in the controller, not in the view.
-
How do Blade's double-brace and brace-bang echo tags differ?
Double braces send the value through htmlspecialchars first, so a title such as a script tag reaches the browser as harmless text. The brace-bang form prints the value raw, so use it only for HTML you built and trust.
-
Why give a route a name such as notices.index?
Links built with route('notices.index') follow the route wherever its path moves, so a changed URL never breaks them, and php artisan route:list shows the name beside the path and action.
Key takeaways
- A Laravel page is a route that maps a URL to code, a controller action that gathers the data, and a Blade view that turns that data into HTML, with each file doing one job.
- A route in
routes/web.phpsits behind the web middleware group, which starts the session, encrypts cookies and checks request-forgery tokens for you. view('notices.index', ['notices' => $notices])passes data to the fileresources/views/notices/index.blade.php, and keeping queries in the controller rather than the view keeps the page easy to test.- Blade’s
{{ }}escapes every value throughhtmlspecialchars, so a title of<script>alert(1)</script>prints as text;{!! !!}skips escaping and is only for HTML you trust. - Naming a route with
->name('notices.index')lets you build its URL withroute('notices.index')and read it back withphp artisan route:list, so a changed path never breaks your links.
Terms in this lesson
- Blade
- Laravel's templating language: an HTML file in resources/views with directives such as @foreach and the {{ }} echo tags, compiled to plain PHP.
- controller
- A class, by default in app/Http/Controllers, whose public methods (actions) answer requests; an action gathers the data a page needs and returns a view.
- named route
- A route given a label with ->name('notices.index'), so its URL can be built anywhere with route('notices.index') instead of a hardcoded path.
- output escaping
- Converting characters such as < and > into HTML entities before printing a value, so untrusted text shows as text; Blade's {{ }} does this with htmlspecialchars to prevent cross-site scripting.
- route
- A line in Laravel that maps an HTTP method and a URL path to the code that answers it, such as Route::get('/notices', [NoticeController::class, 'index']).
References
- Routing (Laravel (laravel.com))
- Controllers (Laravel (laravel.com))
- Views (Laravel (laravel.com))
- Blade Templates (Laravel (laravel.com))
- Middleware (Laravel (laravel.com))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress