Returning Eloquent models directly as JSON exposes implementation details and creates brittle APIs. Laravel provides transformers (pre-5.5) and API Resources (5.5+) to control exactly what data clients receive. At ZIRA Software, properly transformed APIs have eliminated versioning nightmares and enabled seamless mobile app updates.
Why Transform API Responses?
Problems with raw Eloquent:
Route::get('/users/{id}', function ($id) {
return User::find($id); // Bad!
});
Response exposes everything:
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"password": "$2y$10$...",
"remember_token": "abc123...",
"created_at": "2016-01-15 10:30:00",
"updated_at": "2016-06-15 14:20:00"
}
Issues:
- Exposes sensitive data
- Reveals database structure
- No control over format
- Can't add computed fields
- Hard to version
- Includes unwanted fields
Transformer benefits:
- Control exactly what's returned
- Hide sensitive data
- Format dates consistently
- Add computed fields
- Version APIs easily
- Transform relationships
- Consistent response structure
Fractal Transformer (Pre-5.5)
Install Fractal
composer require league/fractal
Create Transformer
app/Transformers/UserTransformer.php:
<?php namespace App\Transformers;
use App\User;
use League\Fractal\TransformerAbstract;
class UserTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'posts', 'comments'
];
protected $defaultIncludes = [];
public function transform(User $user)
{
return [
'id' => (int) $user->id,
'name' => $user->name,
'email' => $user->email,
'joined' => $user->created_at->toIso8601String(),
'links' => [
[
'rel' => 'self',
'uri' => '/users/' . $user->id,
]
]
];
}
public function includePosts(User $user)
{
$posts = $user->posts;
return $this->collection($posts, new PostTransformer());
}
public function includeComments(User $user)
{
$comments = $user->comments;
return $this->collection($comments, new CommentTransformer());
}
}
Use in Controller
<?php namespace App\Http\Controllers\Api;
use App\User;
use App\Transformers\UserTransformer;
use League\Fractal\Manager;
use League\Fractal\Resource\Item;
use League\Fractal\Resource\Collection;
class UserController extends Controller
{
protected $fractal;
public function __construct(Manager $fractal)
{
$this->fractal = $fractal;
}
public function index()
{
$users = User::all();
$resource = new Collection($users, new UserTransformer());
return response()->json(
$this->fractal->createData($resource)->toArray()
);
}
public function show($id)
{
$user = User::findOrFail($id);
$resource = new Item($user, new UserTransformer());
return response()->json(
$this->fractal->createData($resource)->toArray()
);
}
}
Include Relationships
GET /api/users/1?include=posts,comments
Response:
{
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"joined": "2016-01-15T10:30:00+00:00",
"posts": {
"data": [
{
"id": 1,
"title": "My First Post",
"published": "2016-02-01T10:00:00+00:00"
}
]
},
"comments": {
"data": []
}
}
}
Laravel API Resources (5.5+)
Create Resource
php artisan make:resource UserResource
app/Http/Resources/UserResource.php:
<?php namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'post_count' => $this->posts->count(),
'joined' => $this->created_at->toIso8601String(),
'links' => [
'self' => url('/users/' . $this->id),
],
];
}
}
Use Resource in Controller
<?php namespace App\Http\Controllers\Api;
use App\User;
use App\Http\Resources\UserResource;
class UserController extends Controller
{
public function index()
{
return UserResource::collection(User::all());
}
public function show($id)
{
return new UserResource(User::findOrFail($id));
}
}
Response:
{
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"post_count": 5,
"joined": "2016-01-15T10:30:00+00:00",
"links": {
"self": "http://api.example.com/users/1"
}
}
}
Conditional Attributes
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->when($this->isAdmin(), 'admin'),
'secret' => $this->when($request->user()->isAdmin(), $this->secret),
];
}
Conditional Relationships
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'posts' => PostResource::collection($this->whenLoaded('posts')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
];
}
Merging Conditional Attributes
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
$this->mergeWhen($request->user()->isAdmin(), [
'password_updated' => $this->password_updated_at,
'last_login' => $this->last_login_at,
]),
];
}
Resource Collections
Custom Collection
php artisan make:resource UserCollection
app/Http/Resources/UserCollection.php:
<?php namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*/
public function toArray($request)
{
return [
'data' => $this->collection,
'links' => [
'self' => url('/users'),
],
'meta' => [
'total' => $this->collection->count(),
'generated_at' => now()->toIso8601String(),
],
];
}
}
Use:
return new UserCollection(User::all());
Response:
{
"data": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com"
}
],
"links": {
"self": "http://api.example.com/users"
},
"meta": {
"total": 2,
"generated_at": "2016-06-15T14:30:00+00:00"
}
}
Pagination
Controller:
public function index()
{
return UserResource::collection(
User::paginate(15)
);
}
Response:
{
"data": [
{
"id": 1,
"name": "John Doe"
}
],
"links": {
"first": "http://api.example.com/users?page=1",
"last": "http://api.example.com/users?page=10",
"prev": null,
"next": "http://api.example.com/users?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 10,
"path": "http://api.example.com/users",
"per_page": 15,
"to": 15,
"total": 150
}
}
Custom Pagination
public function toArray($request)
{
return [
'data' => $this->collection,
'pagination' => [
'total' => $this->total(),
'count' => $this->count(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'total_pages' => $this->lastPage()
],
];
}
Nested Resources
PostResource.php:
<?php namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'published_at' => $this->published_at->toIso8601String(),
'author' => new UserResource($this->whenLoaded('author')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
];
}
}
Eager load relationships:
public function index()
{
$posts = Post::with('author', 'comments', 'tags')->get();
return PostResource::collection($posts);
}
Wrapping Response
Disable data wrapper:
<?php namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Resources\Json\Resource;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Resource::withoutWrapping();
}
}
Now returns:
{
"id": 1,
"name": "John Doe"
}
Instead of:
{
"data": {
"id": 1,
"name": "John Doe"
}
}
Additional Metadata
With method:
return (new UserResource($user))
->additional(['meta' => [
'version' => '1.0',
'generated_at' => now()->toIso8601String(),
]]);
Response:
{
"data": {
"id": 1,
"name": "John Doe"
},
"meta": {
"version": "1.0",
"generated_at": "2016-06-15T14:30:00+00:00"
}
}
Response Codes
return (new UserResource($user))
->response()
->setStatusCode(201);
Or:
return response()->json(
new UserResource($user),
201
);
API Versioning
v1/UserResource.php:
<?php namespace App\Http\Resources\V1;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}
v2/UserResource.php:
<?php namespace App\Http\Resources\V2;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'full_name' => $this->name, // Changed key
'email_address' => $this->email, // Changed key
'avatar_url' => $this->avatar, // Added field
];
}
}
Route by version:
Route::prefix('v1')->group(function () {
Route::get('/users/{id}', function ($id) {
return new App\Http\Resources\V1\UserResource(
User::findOrFail($id)
);
});
});
Route::prefix('v2')->group(function () {
Route::get('/users/{id}', function ($id) {
return new App\Http\Resources\V2\UserResource(
User::findOrFail($id)
);
});
});
Error Responses
Consistent error format:
<?php namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
public function render($request, Exception $exception)
{
if ($request->expectsJson()) {
return response()->json([
'error' => [
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
'type' => class_basename($exception),
]
], $this->getStatusCode($exception));
}
return parent::render($request, $exception);
}
protected function getStatusCode(Exception $exception)
{
if (method_exists($exception, 'getStatusCode')) {
return $exception->getStatusCode();
}
return 500;
}
}
Error response:
{
"error": {
"message": "Resource not found",
"code": 0,
"type": "ModelNotFoundException"
}
}
Validation Errors
{
"message": "The given data was invalid.",
"errors": {
"email": [
"The email field is required."
],
"password": [
"The password must be at least 6 characters."
]
}
}
Performance Considerations
Eager load relationships:
// Bad - N+1 queries
$users = User::all();
return UserResource::collection($users);
// Good - 2 queries
$users = User::with('posts', 'comments')->all();
return UserResource::collection($users);
Conditionally load:
public function index(Request $request)
{
$users = User::query();
if ($request->has('include')) {
$includes = explode(',', $request->include);
$users->with($includes);
}
return UserResource::collection($users->get());
}
Usage:
GET /api/users?include=posts,comments
Testing Transformed Responses
<?php
class UserApiTest extends TestCase
{
/** @test */
public function it_transforms_user_correctly()
{
$user = factory(User::class)->create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
$response = $this->json('GET', "/api/users/{$user->id}");
$response->assertStatus(200)
->assertJson([
'data' => [
'id' => $user->id,
'name' => 'John Doe',
'email' => 'john@example.com',
]
])
->assertJsonMissing(['password', 'remember_token']);
}
/** @test */
public function it_includes_relationships_when_requested()
{
$user = factory(User::class)->create();
$user->posts()->save(factory(Post::class)->make());
$response = $this->json('GET', "/api/users/{$user->id}?include=posts");
$response->assertStatus(200)
->assertJsonStructure([
'data' => [
'id',
'name',
'posts' => [
'data' => [
'*' => ['id', 'title']
]
]
]
]);
}
}
Best Practices
- Hide sensitive data - Never expose passwords, tokens
- Use ISO 8601 dates - Consistent timezone handling
- Include relationships conditionally - Avoid N+1 queries
- Version your API - Don't break existing clients
- Document responses - Use API Blueprint or Swagger
- Consistent error format - Standard error structure
- Use pagination - Don't return unlimited data
- Add HATEOAS links - Self-documenting API
Conclusion
API Resources give precise control over JSON responses, hiding implementation details and enabling API evolution. Whether using Fractal or Laravel's built-in resources, transformed responses make APIs cleaner and more maintainable. At ZIRA Software, proper API transformations have eliminated countless version compatibility issues.
Start by transforming sensitive endpoints. Add relationships conditionally. Build consistent, documented APIs your clients will love.
Building robust APIs for mobile or third-party integrations? Contact ZIRA Software to discuss API design, versioning strategies, and scalable backend architectures.