Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel API Resources vs Fractal Transformers

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.

Response Layer ArchitectureControllerreturns resourceAPI ResourcetoArray()JSON Responsenative LaravelControllerbuilds fractalTransformertransform()Fractal Manager+ SerializerJSON Responsevia packageAPI Resources pathFractal path
Laravel API Resources vs Fractal Transformers — two paths from controller to JSON response

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.

CriteriaLaravel API ResourcesFractal Transformers (League / Spatie)
Package dependencyNone — built into Laravel 12/13league/fractal + often spatie/laravel-fractal
Class locationapp/Http/Resources/app/Transformers/ (convention)
Nested relationswhenLoaded(), nested Resource classesincludeAuthor() methods + fractal includes
PaginationResource::collection($paginator) nativeManual or Spatie helper wrapping paginator
JSON:API specCustom; no official JSON:API resourceJsonApiSerializer built in
Conditional fieldsmergeWhen(), when()Logic inside transform()
Learning curveLow if you know LaravelMedium — manager, scopes, serializers
Long-term maintenanceTracked with framework upgradesDepends 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.

API Resource Request FlowRouteGET /ordersControllerauthorizeEloquentwith()ResourcetoArray()JSON200 OKCommon gotcha: N+1 without whenLoaded()Bad: embed relation directly — lazy-loads per rowGood: whenLoaded() + controller eager load
Standard Laravel API Resource flow and the N+1 query trap to avoid

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:

  1. 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.
  2. Strict JSON:API output. Fractal’s JsonApiSerializer produces spec-shaped payloads. API Resources can mimic JSON:API, but you will write more boilerplate.
  3. 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.

Include Strategy ComparisonAPI Resources1. Parse ?include in controller2. Whitelist allowed relations3. Eloquent with() eager load4. whenLoaded() in resource5. You own security rulesExplicit, flexible,more app codeFractal1. parseIncludes() on fractal()2. availableIncludes whitelist3. includeX() methods fire4. Serializer shapes output5. JSON:API built inConvention-heavy,less controller code
How each tool handles optional relationship includes in Laravel APIs

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:

  1. Inventory transformers. List models, nested includes, and serializers. Use your test suite or contract tests—API contract testing with Pact catches silent field renames.
  2. 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.
  3. Version the break. Ship Resources under /api/v2 while v1 keeps Fractal. Document sunset dates per API deprecation best practices.
  4. Replace controller returns one domain at a time—orders, then customers, then admin-only routes.
  5. Remove Fractal only when no route imports transformers and Composer can drop spatie/laravel-fractal.

Map Fractal patterns to Resource equivalents:

  • transform()toArray()
  • includeComments() → nested CommentResource::collection($this->whenLoaded('comments'))
  • defaultIncludes → always call ->load() in controller or use $this->when(true, …) sparingly
  • Fractal pagination → Resource::collection($paginator)
Pick Resources or Fractal?New Laravel 13 API?YesUse API Resourcesdefault choiceNoLegacy Fractalcodebase?Keep Fractal on v1migrate graduallyNeed JSON:API?Fractal serializer
Decision tree for Laravel API Resources vs Fractal Transformers in 2026 projects

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

Both separate presentation from controllers: models load in the controller, a dedicated class shapes JSON. API Resources extend Illuminate\Http\Resources\Json\JsonResource and return through Laravel’s response pipeline with native pagination and wrapping. Fractal Transformers implement League Fractal’s contract and pass output through a manager, serializer, and optional fractal scope. Resources feel native in Laravel 12 and 13; Fractal sits beside Laravel with pluggable serializers and explicit include declarations. Either can hide internal IDs and normalise dates without exposing database columns.

Default to API Resources for any new Laravel 13 project on PHP 8.3 or higher. They ship with the framework, need no extra Composer package, and align with thin controllers, Form Request validation, and policy checks before returning new OrderResource($order). New team members recognise the pattern from Laravel docs. Use whenLoaded() for nested relations, mergeWhen() or when() for conditional fields, and Resource::collection() for paginated lists. On Nepal Gift Card, Resources kept mobile and web clients on one contract while admin fields stayed server-side.

Fractal remains defensible in three cases: brownfield apps with hundreds of existing transformers, tests, and client SDKs keyed to Fractal include strings; teams needing strict JSON:API output via JsonApiSerializer without writing boilerplate; and systems reusing the same transformer output for HTTP, exports, webhooks, or queue payloads. Adventure Third Pole Trek reused transformer output for supplier webhooks. The trade-off is maintaining league/fractal and spatie/laravel-fractal compatibility across Laravel major upgrades instead of riding the framework release train.

Conceptually yes—both turn Eloquent models into JSON arrays. “Transformer” usually means Fractal’s TransformerAbstract; Laravel calls its equivalents API Resources. Resources integrate directly with Laravel’s HTTP layer; Fractal requires a manager and serializer step between transform() and the response.

Yes, and many brownfield apps do during migration. Run Fractal on /api/v1 routes and Resources on /api/v2 while matching JSON shape with contract tests or side-by-side diffs before clients switch. Avoid calling both on the same endpoint—it duplicates maintenance and produces inconsistent response shapes. Remove spatie/laravel-fractal from Composer only when no route imports transformers. Incremental API versioning lets v1 stay on Fractal while v2 ships Resources one domain at a time.

API Resources, for most teams. Mobile clients need stable JSON contracts and predictable pagination meta, not a specific PHP serializer. Resources are easier to predict from Laravel documentation and pair cleanly with Sanctum token auth on protected routes.

The Spatie package wraps League Fractal and generally works on current Laravel releases if Composer resolves dependencies, but it is community-maintained, not first-party. Check composer.json constraints before every major Laravel upgrade; API Resources avoid that compatibility check entirely.

Fractal parses ?include= strings natively via $availableIncludes and $defaultIncludes on each transformer, with the manager resolving nested includes when the whitelist stays tight. Laravel does not ship a global include parser—you implement it explicitly, often via middleware or a base controller trait that reads query params, intersects against an allowed list, and calls load() or loadMissing() before Resources guard output with whenLoaded(). For public APIs, unbounded includes become a denial-of-wallet vector, so explicit whitelisting with Resources is auditable. Field limiting maps more naturally to Fractal’s JSON:API serializers.

Full rewrite rarely pays off. Inventory transformers, models, nested includes, and serializers first. Create Resources that emit byte-identical JSON for critical endpoints and compare with diffs or contract tests such as Pact. Ship Resources under /api/v2 while v1 keeps Fractal, document sunset dates, then replace controller returns one domain at a time. Map transform() to toArray(), includeComments() to CommentResource::collection($this->whenLoaded('comments')), defaultIncludes to controller load() calls, and Fractal pagination to Resource::collection($paginator). Drop Fractal only when no route imports transformers.

With API Resources, paginated endpoints are one line: return OrderResource::collection(Order::query()->with(['customer'])->latest()->paginate(25)). Laravel automatically merges pagination meta alongside data. Custom wrapping uses $wrap on the resource or JsonResource::withoutWrapping() globally in AppServiceProvider. Fractal requires manual paginator wrapping or Spatie’s fractal() helper to attach meta. For public APIs documented with Scribe or OpenAPI, predictable wrapping from native Resource collections reduces documentation drift.

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. Always pair Resources with whenLoaded() and explicit controller eager loads. Whether you choose Resources or Fractal, query optimisation and indexing matter far more than serializer overhead under load.

Never expose every Eloquent relation just because nesting is easy. In the controller, eager load only allowed relationships: $order->load(['items', 'customer']) before returning new OrderResource($order). In toArray(), wrap nested output with whenLoaded() so unloaded relations never trigger lazy queries. For index endpoints, parse include query params against a whitelist and pass the result to with() on the query builder before Resource::collection(). Unbounded client-driven includes on rate-limited public APIs can explode query count and hosting cost if you skip this step.

Resource unit tests are straightforward: instantiate the resource, call ->response()->getData(true), and assert field presence with assertArrayHasKey or assertArrayNotHasKey. Example: verify admin_notes is absent for guests when internal_notes exist on the model. Feature tests use assertJson on HTTP responses. Fractal tests often snapshot full fractal output, which grows brittle when serializers change. Either approach belongs inside a broader QA process—response contract tests before load testing catch silent field renames that break mobile clients on the next deploy.

API Resources read $request->user() directly inside toArray(), which keeps conditional fields clean: admin_notes exposed only when $request->user()?->can('viewInternalNotes', $this->resource) passes, using mergeWhen() or when(). Authentication layers like Passport or Sanctum sit upstream of both tools, but Fractal transformers often need auth context passed in manually rather than pulled from the request object. For public SDK design, stable field names and documented enums matter more than serializer choice—document date formats and pagination meta once in OpenAPI.

Fractal’s JsonApiSerializer produces spec-shaped payloads with built-in support for JSON:API envelopes, includes, and field-set semantics that plain array serializers ignore unless you add custom logic. Laravel API Resources use a custom output shape with no official JSON:API resource—you can mimic JSON:API, but expect more boilerplate for links, relationships, and sparse fieldsets. Choose Fractal when a team standardised on JSON:API early and serializer investment already exists. For most REST APIs that are not JSON:API-compliant, explicit include parsing with Resources paired with whenLoaded() is simpler to audit and optimise.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: