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?
answer
- cookies, session, errors, forgery check, bindings
- api group: SubstituteBindings only
- EncryptCookies before StartSession
- PreventRequestForgery, formerly VerifyCsrfToken
- statefulApi() and throttleApi() are opt-in
basics
~10 sThe 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 sIn 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
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
Recall the six web-group entries in order and that the api group holds only SubstituteBindings by default.
Explain why each web entry depends on the previous one, and what is missing on an API route: session, cookie decryption and CSRF checks.
Judge when an endpoint belongs in web or api, and recognise that moving a cookie-authenticated form to api.php silently drops CSRF protection.
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