
September 07, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Every Laravel API eventually faces the same design choice: how do you turn Eloquent models into stable JSON without leaking database columns or breaking mobile clients on the next deploy? Laravel API Resources vs Fractal Transformers is the comparison most teams run into when upgrading legacy code or starting a greenfield REST API in Laravel. API Resources ship with the framework; Fractal (often via Spatie’s wrapper) predates them and still appears in older codebases. This guide walks through how each tool works, where they differ in daily use, and which one I reach for on production projects in 2026.
What is the difference between Laravel API Resources and Fractal Transformers?
Both tools solve one problem: presentation layer separation. Your controller loads models; a dedicated class shapes the HTTP response. The difference is ownership and ergonomics. API Resources extend Illuminate\Http\Resources\Json\JsonResource and return through Laravel’s response pipeline. Fractal Transformers implement League Fractal’s transformer contract and pass output through a manager, serializer, and optional fractal scope for nested includes.
On a legal-tech portal I built, client-facing JSON had to hide internal IDs, normalise dates, and expose document metadata without exposing storage paths. Either tool can enforce that boundary. API Resources feel native in Laravel 12 and 13; Fractal feels like a small framework sitting beside Laravel—powerful, but another abstraction to maintain.
Think of API Resources as Blade for JSON: conditional fields, relationships, and wrapping are idiomatic Laravel. Fractal treats transformation as a standalone library with pluggable serializers (DataArraySerializer, JsonApiSerializer, etc.) and explicit availableIncludes / defaultIncludes on each transformer class.
| Criteria | Laravel API Resources | Fractal Transformers (League / Spatie) |
|---|---|---|
| Package dependency | None — built into Laravel 12/13 | league/fractal + often spatie/laravel-fractal |
| Class location | app/Http/Resources/ | app/Transformers/ (convention) |
| Nested relations | whenLoaded(), nested Resource classes | includeAuthor() methods + fractal includes |
| Pagination | Resource::collection($paginator) native | Manual or Spatie helper wrapping paginator |
| JSON:API spec | Custom; no official JSON:API resource | JsonApiSerializer built in |
| Conditional fields | mergeWhen(), when() | Logic inside transform() |
| Learning curve | Low if you know Laravel | Medium — manager, scopes, serializers |
| Long-term maintenance | Tracked with framework upgrades | Depends on package + League Fractal activity |
When should you use Laravel API Resources for JSON responses?
Default to API Resources for any new Laravel 13 project on PHP 8.3 or higher. They align with Laravel API best practices: thin controllers, Form Request validation, policy checks, then return new OrderResource($order). You avoid Composer conflicts, and new team members already reading the Laravel docs will recognise the pattern immediately.
Creating a resource
Generate a resource with Artisan:
php artisan make:resource OrderResource
php artisan make:resource OrderCollection A typical resource for an eCommerce order might look like this:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->uuid,
'status' => $this->status,
'total' => $this->total_amount,
'currency' => $this->currency,
'placed_at' => $this->created_at->toIso8601String(),
'items' => OrderItemResource::collection(
$this->whenLoaded('items')
),
'customer' => new CustomerResource(
$this->whenLoaded('customer')
),
'admin_notes' => $this->when(
$request->user()?->can('viewInternalNotes', $this->resource),
$this->internal_notes
),
];
}
} Controller usage stays clean:
public function show(Order $order): OrderResource
{
$this->authorize('view', $order);
$order->load(['items', 'customer']);
return new OrderResource($order);
} That pattern scales. On Nepal Gift Card, a Laravel + MySQL platform, exposing gift-card balances and redemption history through Resources kept mobile and web clients on a single contract while internal admin fields stayed server-side only.
Collections, pagination, and wrapping
Paginated endpoints are one line:
return OrderResource::collection(
Order::query()
->with(['customer'])
->latest()
->paginate(25)
); Laravel automatically merges pagination meta alongside data. If you need a custom wrapper, set $wrap on the resource or disable it globally in AppServiceProvider via JsonResource::withoutWrapping(). For public APIs documented with Scribe or OpenAPI, predictable wrapping matters—see API documentation with Scribe for Laravel for keeping examples in sync.
When does Spatie Laravel Fractal still make sense?
Fractal is not dead code in 2026. It still appears in brownfield apps started on Laravel 5.x–8.x, and in teams that standardised on JSON:API early. League Fractal’s serializer layer lets you swap output shape—plain arrays, JSON:API envelopes, custom keys—without rewriting every transformer. Spatie’s spatie/laravel-fractal package wraps the manager so you can write:
return fractal($bookings, new BookingTransformer())
->parseIncludes(['customer', 'payments'])
->respond(); A Fractal transformer for the same booking domain:
<?php
namespace App\Transformers;
use App\Models\Booking;
use League\Fractal\TransformerAbstract;
class BookingTransformer extends TransformerAbstract
{
protected array $availableIncludes = ['customer', 'payments'];
protected array $defaultIncludes = [];
public function transform(Booking $booking): array
{
return [
'id' => $booking->uuid,
'status' => $booking->status,
'starts_at' => $booking->starts_at->toIso8601String(),
];
}
public function includeCustomer(Booking $booking)
{
return $this->item(
$booking->customer,
new CustomerTransformer()
);
}
} Three scenarios where Fractal remains defensible:
- Existing Fractal investment. Hundreds of transformers, tests, and client SDKs keyed to Fractal include strings—migrating has a real cost. Incremental API versioning (see Laravel API versioning strategy) may expose v2 via Resources while v1 stays on Fractal.
- Strict JSON:API output. Fractal’s
JsonApiSerializerproduces spec-shaped payloads. API Resources can mimic JSON:API, but you will write more boilerplate. - Shared transformation outside HTTP. Fractal managers can serialise data for exports, webhooks, or queue payloads using the same transformer—a pattern I have seen on booking systems like Adventure Third Pole Trek where supplier webhooks reused transformer output.
The trade-off is dependency surface. You are maintaining League Fractal compatibility across Laravel major upgrades, whereas Resources ride the framework release train documented at Laravel Eloquent Resources documentation.
How do Laravel API Resources and Fractal compare on includes and sparse fieldsets?
Client-driven includes are where the two libraries feel most different in daily work.
API Resources approach
Laravel does not ship a global ?include= parser. You implement it explicitly—often middleware or a base controller trait that reads query params and calls load() or loadMissing() on allowed relationships:
protected function allowedIncludes(Request $request): array
{
$includes = explode(',', $request->query('include', ''));
return array_intersect($includes, ['customer', 'items', 'payments']);
}
public function index(Request $request)
{
$query = Order::query();
$query->with($this->allowedIncludes($request));
return OrderResource::collection($query->paginate(25));
} Resources then guard output with whenLoaded(). That is explicit and auditable—important for rate-limited public APIs where unbounded includes become a denial-of-wallet vector.
Fractal approach
Fractal parses include strings natively. Transformers declare $availableIncludes; the manager resolves nested includes and prevents arbitrary relationship loading if you keep the whitelist tight. Field limiting (?fields[orders]=id,status) maps more naturally to JSON:API serializers, though plain array serializers ignore it unless you add custom logic.
For most REST APIs that are not JSON:API-compliant, I prefer explicit include parsing with Resources. You see the eager-load list in one place, which pairs well with query optimisation work described in PostgreSQL for Laravel developers when APIs move off MySQL.
How do you migrate from Fractal Transformers to Laravel API Resources?
Full rewrite rarely pays off. A staged migration reduces client breakage:
- Inventory transformers. List models, nested includes, and serializers. Use your test suite or contract tests—API contract testing with Pact catches silent field renames.
- Match JSON shape first. Create Resources that emit byte-identical JSON for critical endpoints. Compare responses with a diff tool or the site’s JSON formatter during QA.
- Version the break. Ship Resources under
/api/v2while v1 keeps Fractal. Document sunset dates per API deprecation best practices. - Replace controller returns one domain at a time—orders, then customers, then admin-only routes.
- Remove Fractal only when no route imports transformers and Composer can drop
spatie/laravel-fractal.
Map Fractal patterns to Resource equivalents:
transform()→toArray()includeComments()→ nestedCommentResource::collection($this->whenLoaded('comments'))defaultIncludes→ always call->load()in controller or use$this->when(true, …)sparingly- Fractal pagination →
Resource::collection($paginator)
What about performance, testing, and API auth integration?
Performance differences are usually negligible compared to database work. Both layers run in PHP on each request; the expensive part remains N+1 queries and missing indexes. Profile with Laravel Debugbar locally and slow-query logs in production—Resources do not magically fix lazy loading.
Testing API Resources is straightforward with assertJson and resource unit tests:
public function test_order_resource_hides_internal_notes_for_guests(): void
{
$order = Order::factory()->create(['internal_notes' => 'secret']);
$json = (new OrderResource($order))
->response()
->getData(true);
$this->assertArrayNotHasKey('admin_notes', $json['data']);
} Fractal tests often snapshot full fractal output, which can be brittle when serializers change. Either approach belongs inside a broader QA process—testing and optimization services often start with response contract tests before load testing.
Authentication layers—Passport vs Sanctum—sit upstream of both tools. Resources read $request->user() inside toArray() for field-level authorization, which is cleaner than passing auth context into Fractal transformers manually.
For public SDK design, stable field names matter more than which serializer you pick. Document enums, date formats, and pagination meta once in OpenAPI and generate client stubs—patterns covered in SDK design for your public API.
Key Takeaways
- Choose Laravel API Resources for new Laravel 13 APIs on PHP 8.3+—zero extra packages and native pagination support.
- Keep Fractal Transformers when a legacy codebase or JSON:API serializer investment already exists; migrate via API versioning, not a big-bang rewrite.
- Always pair Resources with
whenLoaded()and explicit controller eager loads to prevent N+1 queries under load. - Implement your own include whitelist for public endpoints; never expose every Eloquent relation because Fractal or Resources make nesting easy.
- Match JSON output before switching tools—contract tests and side-by-side diffs save mobile clients from silent breakage.
- Need hands-on help architecting a production API? See API development in Nepal for end-to-end design, auth, and deployment.
People Also Ask
Are Laravel API Resources the same as API transformers?
Conceptually yes—they both transform models into JSON arrays. “Transformer” usually refers to Fractal’s TransformerAbstract classes, while Laravel calls its equivalents API Resources. Resources integrate directly with Laravel’s HTTP layer; Fractal transformers require a manager and serializer step in between.
Can you use Fractal and API Resources together?
Yes, and many brownfield apps do during migration. Run Fractal on /api/v1 routes and Resources on /api/v2. Avoid calling both on the same endpoint—it duplicates maintenance and confuses response shapes.
Does Laravel 13 still support Spatie Laravel Fractal?
The Spatie package wraps League Fractal and generally works on current Laravel releases if Composer resolves dependencies, but it is community-maintained rather than first-party. Check your composer.json constraints before upgrading Laravel major versions; Resources avoid that compatibility check entirely.
Which is better for mobile app backends?
API Resources, for most teams. Mobile clients care about stable JSON contracts and predictable pagination meta, not which PHP library built them. Resources are easier for mobile developers to predict from Laravel documentation, and they pair cleanly with Sanctum token auth on Sanctum-protected routes.
Ship APIs with the Right Serialization Layer
The Laravel API Resources vs Fractal Transformers decision is really about project age and spec requirements, not which tool is “better” in abstract. Greenfield Laravel 13 work should start with API Resources, strict eager-loading discipline, and versioned endpoints documented for clients. Fractal earns its keep when JSON:API serializers and existing transformer libraries are already paying rent—or until you migrate them deliberately.
If you are planning a booking platform, eCommerce API, or client portal and want the response layer designed before controllers multiply, review how to build a REST API in Laravel the right way and modern Laravel architecture patterns. For payment-ready APIs with local gateways, see Laravel payment integrations. When you need someone who has shipped production APIs—not just tutorial demos—contact us or browse the portfolio for Laravel systems already running in the wild.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

