skip to content

Views, JSON & Downloads

A route can return a string, array, model or view, or build a response with response()->json(), download() or file() plus headers and cookies. Interviewers probe what is converted and how.

on this pageshow

explore

questions

6

In a Laravel route or controller, what response does the framework build when you return a string, an array, an Eloquent model or a view?

level: juniorimportance: must knowfreq 62%

answer

  1. the router wraps whatever you return
  2. arrays and Arrayable become JsonResponse
  3. a freshly created model gets 201
  4. strings and views: text/html, status 200
  5. Router::toResponse, then prepare()

basics

~20 s

Laravel's router wraps the return value: a string or view becomes an HTML response with status 200, while arrays, Eloquent models and collections become a JsonResponse. A model created during the request returns 201 instead of 200.

solid answer

~40 s

Anything a route or controller returns goes through `Router::toResponse()`. A string becomes an `Illuminate\Http\Response` with `Content-Type: text/html` and status 200. A view is also wrapped in a `Response`, and its `render()` runs as the response is built. Arrays and anything `Arrayable`, `Jsonable` or `JsonSerializable` (Eloquent models, collections, paginators) become a `JsonResponse` with `application/json`. The model is serialized through its `toArray()`, so `$hidden` columns stay out. One special case: a model whose `wasRecentlyCreated` flag is true, such as the result of `Ticket::create()`, is returned with status **201**. An object that implements `Responsable` builds its own response, and an existing Symfony response passes through unchanged. You build the response yourself only when you need a different status, extra headers, cookies or a file.

code

php · 16 lines
php
<?php

use App\Models\Concert;
use App\Models\Ticket;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/status', fn () => ['venue' => 'open', 'capacity' => 1800]);   // JSON, 200

Route::get('/concerts', fn () => Concert::orderBy('starts_at')->get());      // JSON array, 200

Route::get('/concerts/{concert}', fn (Concert $concert) =>
    view('concerts.show', ['concert' => $concert]));                        // HTML, 200

Route::post('/tickets', fn (Request $request) =>
    Ticket::create($request->only('concert_id', 'seat')));                  // JSON, 201

go deeper

for a junior

Recall the three outcomes: strings and views become HTML, arrays, models and collections become JSON, and a model created in the request returns 201.

for a middle

Explain that Router::toResponse() does the conversion, that model JSON comes from toArray() and honours $hidden, and that views render inside Response::setContent().

for a senior

Show when you stop relying on conversion: explicit statuses and headers, Responsable objects for reusable response logic, and knowing that wrapping a new model in an array silently drops the 201.

for a principal

Frame it as an API contract decision: implicit conversion is fine for internal pages, but a public API usually wants explicit statuses and a dedicated serialization layer rather than raw models.

## The rule: the router converts, you rarely have to A Laravel route closure or controller action may return almost anything. Before the response travels back out through the middleware, the router hands the value to the static method `Illuminate\Routing\Router::toResponse($request, $response)`. That method decides which HTTP response class to build, and finally calls `prepare($request)` on it so that headers such as `Content-Type` and the protocol version match the request. In a concert-venue app, all of these are valid return values: - `return 'Doors open at 19:00';` for a quick status line - `return ['venue' => 'open', 'capacity' => 1800];` for a health endpoint - `return Concert::upcoming()->get();` for a list of concerts - `return view('concerts.show', ['concert' => $concert]);` for the event page ## What each value becomes `toResponse()` checks the value in a fixed order: 1. An object implementing `Illuminate\Contracts\Support\Responsable` is asked to build its own response through `toResponse($request)`. 2. A PSR-7 response is converted into a Symfony response. 3. An Eloquent **model** whose `wasRecentlyCreated` property is `true` becomes `new JsonResponse($model, 201)`. 4. An `Illuminate\Support\Stringable` (the object `Str::of()` returns) becomes an HTML response. 5. An **array**, or an object that is `Arrayable`, `Jsonable`, `ArrayObject`, `JsonSerializable` or `stdClass`, becomes a `JsonResponse` with status 200. 6. Anything else that is not already a Symfony response, which includes plain strings and **views**, becomes `new Response($value, 200, ['Content-Type' => 'text/html'])`. | You return | Laravel builds | Status | Content-Type | |---|---|---|---| | `'Doors open'` | `Illuminate\Http\Response` | 200 | `text/html` | | `['venue' => 'open']` | `Illuminate\Http\JsonResponse` | 200 | `application/json` | | `Concert::find(1)` | `JsonResponse` | 200 | `application/json` | | `Ticket::create([...])` | `JsonResponse` | **201** | `application/json` | | `Concert::all()` (a collection) | `JsonResponse` | 200 | `application/json` | | `view('concerts.show')` | `Response` with rendered HTML | 200 | `text/html` | | `response()->json(...)`, `redirect(...)` | returned unchanged | as built | as built | ## Models and collections An Eloquent model is `Arrayable` and `Jsonable`, so the JSON body comes from the model's own serialization: its `toArray()` output, which leaves out anything in `$hidden` and includes anything in `$appends`. Those serialization rules belong to the model, not to the response. An Eloquent collection is serialized item by item the same way, which is why `return Concert::all();` gives a JSON array of concert objects. The **201** case is easy to miss. `Model::create()` and a first `save()` set `wasRecentlyCreated` to `true` on that instance, and the router honours it only when you return the model itself. Wrap it in an array or pass it to `response()->json()` and you are back to 200 unless you set the status yourself. ## Views `view('concerts.show', $data)` returns an `Illuminate\View\View` object, not a string. It is not `Arrayable`, so it lands in the last branch and is wrapped in an `Illuminate\Http\Response`. That class's `setContent()` sees a `Renderable` and calls `render()` immediately, so Blade compiles and runs while the response is being built, still inside the route's middleware pipeline and the exception handler's reach. When you need a status other than 200 or extra headers with a view, use `response()->view('concerts.sold-out', $data, 410)`. ## When to build the response yourself The automatic conversion always picks status 200 (or 201 for a new model) and adds no custom headers. Reach for the response factory when you need: - a different status, such as `response()->json(['queued' => true], 202)` - extra headers through `header()` or `withHeaders()` - a cookie through `cookie()` or `withCookie()` - a file, through `download()` or `file()` ## Less common return values The same method handles a few values people rarely think about: - **`stdClass` and `ArrayObject`** are in the JSON list, so `return (object) ['open' => true];` sends JSON. - **`Illuminate\Support\Stringable`**, the fluent string from `Str::of()`, is unwrapped with `__toString()` and sent as `text/html`. - **A paginator** is `Arrayable` and `Jsonable`, so returning `Concert::paginate()` also gives JSON; what goes into that JSON is the paginator's business. - **A PSR-7 response** from a third-party library is converted into a Symfony response through the PSR bridge. - **Status 304** on whatever response comes out triggers `setNotModified()`, which strips the body and content headers. Because the conversion runs inside the route's middleware pipeline, after-middleware always receives a real response object, never your raw array or model. ## What interviewers listen for A good answer names the conversion (strings and views to HTML, arrays and models to JSON), says that model JSON respects `$hidden`, and knows the 201 special case. A strong one mentions `Responsable` as the hook that lets any object decide its own response, which is exactly how API resource classes plug into the same mechanism.

  • Why would `return ['ticket' => Ticket::create($data)];` answer with 200 while `return Ticket::create($data);` answers with 201?
    The 201 rule in `Router::toResponse()` checks whether the returned value itself is a model with `wasRecentlyCreated` set. An array holding that model is just an array, so it takes the generic JSON branch with status 200. If the endpoint must return 201 with a wrapper, build it explicitly with `response()->json([...], 201)`.
  • What happens if a controller returns an object that implements `Responsable`?
    The router calls its `toResponse($request)` method first and uses whatever Symfony response it returns, before any of the other conversion rules run. That lets a class encapsulate its own status, headers and body. API resource classes use this hook, and you can write your own, for example a `TicketPdf` object that returns a download response.

saying these in an interview costs you the question

  • Returning an array prints 'Array' unless you json_encode it first.
  • A returned model is serialized with every column, ignoring $hidden.
  • Every returned value gets status 200, including a model just created with create().
  • Plain strings returned from a route are sent as text/plain.
  • A route must return a Response object or Laravel throws an exception.
open as a page

In Laravel, how do you set a cookie with response()->cookie() or withCookie() versus Cookie::queue(), and when do you need each?

level: middleimportance: should knowfreq 42%

basics

~20 s

Chain ->cookie() or ->withCookie() when you hold the response object. Use Cookie::queue() when the code setting the cookie runs before the response exists; the web group's AddQueuedCookiesToResponse middleware adds queued cookies to whatever response comes back.

open as a page

In Laravel, how do response()->download() and response()->file() differ when a concert-venue app serves a PDF ticket?

level: middleimportance: should knowfreq 40%

basics

~20 s

Both return a Symfony BinaryFileResponse for a file on the server's disk. download() adds Content-Disposition: attachment with a chosen filename, so the browser saves it; file() adds no disposition, so the browser displays the PDF inline.

open as a page

In Laravel, when do you use response()->json() instead of returning an array, and what do its status, headers and options arguments control?

level: middleimportance: should knowfreq 45%

basics

~10 s

Returning an array gives JSON with status 200 and nothing else. response()->json($data, $status, $headers, $options) adds a chosen status, extra headers and json_encode flags, and returns an Illuminate\Http\JsonResponse you can keep chaining.

open as a page

In Laravel, how do Response::macro() and a class implementing Responsable each let you reuse one response shape across controllers?

level: middleimportance: nice to knowfreq 16%

basics

~10 s

Response::macro('name', fn) adds a method to the response factory, so response()->name(...) builds the shared shape anywhere. A Responsable class implements toResponse($request), and the router calls it when a controller returns that object.

open as a page