0

API Resources — why transform Eloquent models before responding

Intermediate5 min read·lv-24-001
interview

Concept

API Resources transform Eloquent models (and collections) into JSON responses with explicit, versioned control over the output format. They're the recommended way to build API responses in Laravel — preferred over toArray() or returning models directly.

Why transform before responding:

  • Security: User::find($id) serialized directly includes password, remember_token, internal fields. Resources explicitly allowlist what to expose.
  • Decoupling: API response shape and database schema are separate concerns. You can rename DB columns without breaking the API.
  • Consistency: Every API endpoint uses the same transformation layer — consistent field names, types, and envelope structure.
  • Versioning: V1\UserResource and V2\UserResource can coexist — different shapes for different API versions.
  • Computed fields: Resources can add derived fields (is_online, avatar_url) not in the DB.

Creating: php artisan make:resource UserResource. Generates app/Http/Resources/UserResource.php.

JsonResource vs ResourceCollection:

  • JsonResource: Transforms a single model.
  • ResourceCollection: Wraps a collection or paginator. Use when you need custom collection-level metadata.

Returning from controllers: return new UserResource($user) — the Resource implements Responsable and automatically returns the correct JSON response with Content-Type: application/json.

Code Example

php
<?php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Request;

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'         => $this->id,
            'name'       => $this->name,
            'email'      => $this->email,
            'role'       => $this->role,
            'created_at' => $this->created_at->toISOString(),
            // 'password' NOT included — security by exclusion
        ];
    }
}

// Controller usage
class UserController extends Controller
{
    public function show(User $user): UserResource
    {
        return new UserResource($user);
    }

    public function index(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection
    {
        return UserResource::collection(User::paginate(15));
        // Automatically wraps pagination metadata
    }
}

// Response envelope
// GET /users/1
// {
//   "data": {
//     "id": 1,
//     "name": "Alice",
//     "email": "alice@example.com",
//     "role": "admin",
//     "created_at": "2024-01-15T10:00:00.000Z"
//   }
// }

// Collection response with pagination
// GET /users
// {
//   "data": [...],
//   "links": {"first": "...", "last": "...", "prev": null, "next": "..."},
//   "meta": {"current_page": 1, "from": 1, "last_page": 5, "total": 75}
// }

// Wrapping: JsonResource adds "data" key. Disable with:
// UserResource::withoutWrapping();
// Or per-resource: public static $wrap = null;