skip to content

In Laravel 13, which middleware does the web group run that the api group does not, and why does an API route have no session?

level: juniorimportance: must knowfreq 64%

answer

  1. cookies, session, errors, forgery check, bindings
  2. api group: SubstituteBindings only
  3. EncryptCookies before StartSession
  4. PreventRequestForgery, formerly VerifyCsrfToken
  5. statefulApi() and throttleApi() are opt-in

basics

~10 s

The web group adds EncryptCookies, AddQueuedCookiesToResponse, StartSession, ShareErrorsFromSession and PreventRequestForgery before SubstituteBindings; the api group has only SubstituteBindings. API routes therefore never start a session, decrypt cookies or check CSRF tokens.

solid answer

~40 s

In Laravel 13 the `web` group, applied to `routes/web.php`, runs `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession`, `ShareErrorsFromSession`, `PreventRequestForgery` (the CSRF middleware, renamed from `VerifyCsrfToken` in 13) and `SubstituteBindings`. The `api` group, applied to `routes/api.php` with the `/api` prefix, contains only `SubstituteBindings` unless you opt into Sanctum's stateful middleware with `statefulApi()` or rate limiting with `throttleApi()`. So an API route has no session because `StartSession` never runs for it: `$request->session()` throws, and cookies arrive still encrypted. That is deliberate, since API clients authenticate with tokens rather than cookies. Both groups sit inside the same global stack. The order in `web` matters: cookies are decrypted before `StartSession` reads the session ID cookie, and the session exists before errors are shared or the CSRF token is checked.

code

php · 12 lines
php
<?php

use App\Http\Controllers\BallotController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// routes/api.php - wrapped in the api group: no session, no cookie decryption, no CSRF
Route::post('/polls/{poll}/votes', [BallotController::class, 'store']);

Route::get('/debug-session', function (Request $request) {
    return $request->hasSession() ? 'session' : 'no session';   // returns 'no session'
});

go deeper

for a junior

Recall the six web-group entries in order and that the api group holds only SubstituteBindings by default.

for a middle

Explain why each web entry depends on the previous one, and what is missing on an API route: session, cookie decryption and CSRF checks.

for a senior

Judge when an endpoint belongs in web or api, and recognise that moving a cookie-authenticated form to api.php silently drops CSRF protection.

for a principal

Decide the authentication model per client type, tokens for API consumers and cookie sessions for first-party pages, and keep route files aligned with it.

## Two groups, one global stack Every request to a Laravel 13 application passes through the **global middleware stack** first: path-encoding validation, deferred callbacks, proxy trust, CORS, maintenance mode, post-size check, string trimming and empty-string conversion. After the router matches a route, the route's **middleware group** runs. `withRouting()` in `bootstrap/app.php` attaches: - the `web` group to `routes/web.php`, - the `api` group to `routes/api.php`, together with the `/api` URI prefix, once you have run `php artisan install:api`. ## What the web group contains In order, as defined in `Illuminate\Foundation\Configuration\Middleware`: | # | Middleware | Job | |---|---|---| | 1 | `Illuminate\Cookie\Middleware\EncryptCookies` | Decrypts incoming cookies, encrypts outgoing ones | | 2 | `Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse` | Attaches cookies queued with `Cookie::queue()` to the response | | 3 | `Illuminate\Session\Middleware\StartSession` | Loads the session from its store, saves it after the response | | 4 | `Illuminate\View\Middleware\ShareErrorsFromSession` | Shares the validation error bag with every view | | 5 | `Illuminate\Foundation\Http\Middleware\PreventRequestForgery` | CSRF protection for state-changing requests | | 6 | `Illuminate\Routing\Middleware\SubstituteBindings` | Resolves route parameters into models or enums | If you call `$middleware->authenticateSessions()`, `auth.session` is appended as a seventh entry. ## What the api group contains By default, only `SubstituteBindings`. Two configurators add optional entries at the front: - `$middleware->statefulApi()` prepends Sanctum's `EnsureFrontendRequestsAreStateful`, which lets a first-party SPA use cookie sessions on API routes. - `$middleware->throttleApi()` adds `throttle:api`. ## Why the order inside web matters Each entry depends on the one before it: 1. `StartSession` finds the session by reading the session ID from a cookie. That cookie is encrypted, so `EncryptCookies` must run first to decrypt it. 2. `AddQueuedCookiesToResponse` sits inside `EncryptCookies`, so on the way out the queued cookies are added before the outer layer encrypts them. 3. `ShareErrorsFromSession` and `PreventRequestForgery` both read from the session, so they come after `StartSession`. 4. `SubstituteBindings` comes last, so the checks above run before database queries for bound models. ## Why an API route has no session Because `StartSession` is not in the `api` group, nothing loads a session for `routes/api.php`. Concretely, on an API route in a voting app: - `$request->session()` throws a `RuntimeException` because no session store is set on the request. - `session('poll_draft')` cannot see anything written by a web page. - Cookies are not decrypted, so `$request->cookie('laravel_session')` holds ciphertext. - No CSRF token is checked, which is correct for token-authenticated clients and dangerous if you move a cookie-authenticated form endpoint into `api.php` by mistake. This is a design choice, not an omission: API clients such as mobile apps authenticate per request with a token, so a server-side session adds cost without benefit. A first-party SPA that wants cookie sessions on API routes uses Sanctum's `statefulApi()` rather than adding `StartSession` by hand. ## Seeing it for a real route `php artisan route:list -v` prints each route's route-level middleware, with groups expanded. For a poll page from `routes/web.php` you will see the six web entries; for `POST api/polls/{poll}/votes` only `SubstituteBindings`. Global middleware does not appear there, because it runs before routing. ## Common mistakes - Adding `StartSession` to one API route to "fix" a session error, without `EncryptCookies`, so the session ID cookie is never decrypted and each request gets a fresh session. - Calling `session()->flash()` in an API controller and expecting a later web page to show it. - Assuming `SubstituteBindings` is web-only; both groups include it, so route model binding works on API routes too. ## Version notes that trip candidates - **Laravel 13 renamed the CSRF middleware** from `VerifyCsrfToken` to `PreventRequestForgery`. The old names survive only as deprecated aliases; code that excludes CSRF should name the new class. - Since Laravel 11 there is no `app/Http/Kernel.php` listing `$middlewareGroups`; the defaults live in the framework's `Configuration\Middleware` class and are adjusted from `bootstrap/app.php`. - Since Laravel 11, `routes/api.php` does not exist until you run `php artisan install:api`.

  • Why must EncryptCookies run before StartSession in the Laravel web group?
    `StartSession` looks up the session by the ID stored in the session cookie. Laravel encrypts that cookie, so until `EncryptCookies` has decrypted incoming cookies the ID is ciphertext and no session would be found. Reversing them would start a fresh, empty session on every request.
  • A Laravel 13 test disables CSRF with withoutMiddleware(VerifyCsrfToken::class); why should it be updated?
    In Laravel 13 the web group uses `PreventRequestForgery`. `VerifyCsrfToken` remains only as a deprecated alias, and the upgrade guide tells you to reference `PreventRequestForgery` directly, especially when excluding the middleware in tests or route definitions.

saying these in an interview costs you the question

  • Expects $request->session() to work on routes/api.php by default
  • Names VerifyCsrfToken as the current CSRF class in Laravel 13
  • Believes the api group throttles requests without any configuration
  • Thinks the web and api groups replace the global stack
  • Moves a cookie-authenticated form endpoint into api.php to avoid CSRF errors