Reader Stacks

How to Customize Laravel's JSON Response Structure

API Resources are the standard, framework-idiomatic way to control exactly what an endpoint's JSON looks like — a consistent envelope, renamed fields, and hidden internal columns.

Returning an Eloquent model directly from a route auto-converts it to JSON, but that default output mirrors your database columns exactly — API Resources are Laravel's standard, framework-idiomatic tool for controlling exactly what shape a JSON response actually takes.

The default, unshaped response

Route::get('/users/{user}', fn (User $user) => $user);
{"id": 1, "name": "Alex", "email": "alex@example.com", "password": "$2y$10$...", "created_at": "...", "updated_at": "..."}

This default output includes every column, including ones (like password, even if hashed) that should genuinely never be exposed in an API response — a real security concern, not just a cosmetic one.

Creating an API Resource

php artisan make:resource UserResource
class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'member_since' => $this->created_at->format('Y-m-d'),
        ];
    }
}
Route::get('/users/{user}', fn (User $user) => new UserResource($user));
{"data": {"id": 1, "name": "Alex", "email": "alex@example.com", "member_since": "2022-03-15"}}

The resource explicitly controls exactly which fields appear, renames created_at to a friendlier member_since, and formats the date — while the sensitive password field is simply never mentioned, so it never appears at all.

Why the response is wrapped in a "data" key

Laravel wraps a single resource's output in a top-level data key by default — a convention that leaves room to add metadata (pagination info, for instance) alongside the actual resource data without a naming conflict, and matches a common API response convention many client libraries expect.

Resource collections, for a list of many models

Route::get('/users', fn () => UserResource::collection(User::all()));
{"data": [{"id": 1, "name": "Alex", ...}, {"id": 2, "name": "Sam", ...}]}

Adding conditional fields to the response

public function toArray($request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->when($request->user()?->is_admin, $this->email),
    ];
}

$this->when() includes a field in the output only when the given condition is true — here, only an admin viewing the response sees the email address at all, which is a genuinely different (and stronger) approach than including the field and relying on the front end to simply not display it.

Adding extra top-level metadata alongside the data

public function with($request): array
{
    return [
        'meta' => [
            'api_version' => '1.0',
        ],
    ];
}
{"data": {...}, "meta": {"api_version": "1.0"}}

Wrapping every response in a consistent envelope globally

// AppServiceProvider::boot()
JsonResource::withoutWrapping(); // to REMOVE the default "data" wrapper, if a different convention is preferred

For a project wanting a different response envelope convention entirely, withoutWrapping() removes Laravel's default data key wrapping globally, letting each resource's toArray() define the complete top-level response structure itself instead.