Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 10 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Interactive · stage by stage One GET /notices, from browser to HTML Step through one GET /notices, from the browser to the HTML that comes back.
How a request becomes the notices page, step 6 of 6. Showing Browser, Router (matched, web middleware), Controller (returns view() + data), View, Response. In focus: Response.rendered pageBrowserGET /noticesRoutermatched, web middlewareControllerreturns view() + dataViewnotices.index (Blade)ResponseHTML for the browser

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The request arrives. The browser asks for GET /notices. Nothing has decided yet what code will answer it.
  2. 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.
  3. The action runs. The matched route names a controller action, so the router calls NoticeController@index. This is where your code runs.
  4. 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.
  5. 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.
  6. 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:

php artisan make:controller NoticeController Python · make_controller_session.py
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:

app/Http/Controllers/NoticeController.php PHP · notice-board/app/Http/Controllers/NoticeController.php
<?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:

routes/web.php PHP · notice-board/routes/web.php
<?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

  1. Question 1 of 2 Route::get('/notices', [NoticeController::class, 'index']) is in routes/web.php. Which request does it answer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A GET request for the path /notices

    Route::get(path, action) binds one HTTP method and one path to the code that answers it. Here GET /notices calls index() on NoticeController; the action's name has no effect on the URL.

  2. Question 2 of 2 The action returns view('notices.index', ['notices' => $notices]). Which file renders the page?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: resources/views/notices/index.blade.php

    The first argument to view() is a dotted name resolved under resources/views, where a dot means a subfolder. So notices.index loads resources/views/notices/index.blade.php, and the array after it becomes the data the file can print.

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:

resources/views/notices/index.blade.php PHP · notice-board/resources/views/notices/index.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Notices &middot; 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:

Serving /notices and fetching its HTML Python · page_session.py
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 &middot; 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>&lt;script&gt;alert(1)&lt;/script&gt;</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 &lt;script&gt;alert(1)&lt;/script&gt;: 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

  1. One question A notice title is the text <script>alert(1)</script> and the view prints it with {{ $notice['title'] }}. What does the page send to the browser?

    Choose one answer.

    Show the answer: One question

    Answer: The characters escaped as &lt;script&gt;alert(1)&lt;/script&gt;, shown as text

    Blade sends every {{ }} value through PHP's htmlspecialchars, so <, > and & become entities. The dangerous title appears on the page as text, and the browser executes none of 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:

php artisan route:list --path=notices Python · route_list_session.py
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:

A feature test for the notice board page Python · test_session.py
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

  1. Question 1 of 2 What does adding ->name('notices.index') to a route let you do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Build the route's URL anywhere with route('notices.index')

    Naming a route attaches a label to it. You then generate its URL with route('notices.index') instead of writing the path by hand, so a later change to the path does not break your links.

  2. Question 2 of 2 php artisan route:list --path=notices prints the line: GET|HEAD notices notices.index › NoticeController@index. What is notices.index here?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The route's name

    route:list shows, per route, the methods, the path, the name and the action. notices.index is the name column, the same label you pass to route() to rebuild the URL.

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:

  1. Add an “About the centre” page. Create a route for /about that returns a view about, name it about, and move its logic into a controller action if it grows beyond returning the view.
  2. From the notice board view, add a link to the About page built with route('about'), not a hardcoded /about.
  3. Write a feature test that fetches /about, asserts a 200 response, 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.

  1. Question 1 of 8 In routes/web.php, what is the shortest way to answer GET /notices by rendering a view, without a controller?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: Route::get('/notices', fn () => view('notices.index'))

    A route can answer straight from a closure, so Route::get('/notices', fn () => view('notices.index')) serves the page. You move to a controller once the action has real work to do.

  2. Question 2 of 8 A page needs the ten most recent notices from the database. Where should the query that loads them live?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: In the controller action (or a model it calls), which then passes the result to the view

    Keep data loading in the controller and the models it calls, and keep the view for markup. The action builds the data and passes it to view(), so the template only prints what it is given.

  3. Question 3 of 8 Why does Blade escape {{ }} output by default instead of leaving it to the developer to remember?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: So a value a user typed cannot inject a tag or script into the page

    Most values a page prints come from users or the database and cannot be trusted. Escaping by default means the safe choice is the one you get without thinking, so a forgotten escape cannot open a cross-site scripting hole.

  4. Question 4 of 8 You store a notice body as a short string of HTML that your own editor produced and sanitised, and you want its bold and links to render. Which approach is sound?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: Print it with {!! $notice['body'] !!}, since the HTML is yours and already sanitised

    {!! !!} exists for HTML you produced and trust. Here the body is your own sanitised markup, so unescaping just that field renders it, while the title and other untrusted values stay on {{ }}.

  5. Question 5 of 8 The notices route changes from /notices to /board/notices, but keeps its name notices.index. What happens to a link written as <a href="{{ route('notices.index') }}">?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: It now points at /board/notices, with no edit to the link

    Because the link is built from the route name, route('notices.index') produces whatever path the route has now. A hardcoded /notices would have broken; the named route did not.

  6. Question 6 of 8 A teammate asks why your views build URLs with route('...') rather than writing the paths as strings. Which reasons are sound? (Choose all that apply.)

    Choose every answer that is right.

    Show the answer: Question 6 of 8

    Answer:

    • A path can change in one place, the route, without hunting for every link
    • route() fills route parameters in the correct positions for you

    Named routes keep the path in one place and insert parameters correctly, so links survive path changes and avoid malformed URLs. They do not change request speed or enforce access.

  7. Question 7 of 8 routes/web.php gains Route::get('/about', fn () => view('about')). Which file must exist for GET /about to render a page?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: resources/views/about.blade.php

    The closure returns view('about'), and Laravel turns the name into a path under resources/views: about.blade.php here, notices/index.blade.php for notices.index. Without that file there is no page to render.

  8. Question 8 of 8 NoticeController's index action returns view('notices.index', ['notices' => $notices]). How does the Blade file reach the list?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: As the variable $notices

    The second argument of view() is the page's data. Its key notices becomes $notices inside resources/views/notices/index.blade.php, and the view only prints what it was given.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.php sits 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 file resources/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 through htmlspecialchars, 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 with route('notices.index') and read it back with php 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.