API Transformer Pattern for Backward Compatibility in Laravel

📖 3 minutes read

You’ve refactored your database schema, rewritten your business logic, and modernized your API internals. Great! Now you just need to make sure every client that’s been hitting your API for the past 3 years doesn’t break.

This is where transformer classes save you.

The Problem

Your new data structure looks like this:

{
  "product_id": 123,
  "pricing": {
    "base_price": 100,
    "currency": "USD",
    "variants": [
      {"type": "adult", "price": 100},
      {"type": "child", "price": 50}
    ]
  },
  "availability": {
    "slots": [...],
    "max_capacity": 20
  }
}

But your old API promised this:

{
  "id": 123,
  "price": 100,
  "adult_price": 100,
  "child_price": 50,
  "currency": "USD",
  "time_slots": [...],
  "capacity": 20
}

You can’t just change the API response. Mobile apps from 2022 are still hitting your endpoints. Breaking them isn’t an option.

The Solution: Transformer Pattern

Create a dedicated transformer class that maps your new structure to the legacy format:

namespace App\Transformers;

class LegacyProductTransformer
{
    public function transform(Product $product): array
    {
        $base = [
            'id' => $product->id,
            'currency' => $product->pricing->currency,
        ];

        // Feature flag: new pricing structure
        if ($product->type->hasVariantPricing()) {
            $base['adult_price'] = $product->pricing->variants->firstWhere('type', 'adult')?->price;
            $base['child_price'] = $product->pricing->variants->firstWhere('type', 'child')?->price;
            $base['price'] = $product->pricing->base_price;
        } else {
            // Fallback to old structure
            $base['price'] = $product->price;
            $base['adult_price'] = $product->price;
            $base['child_price'] = $product->price * 0.5;
        }

        // Feature flag: new availability structure
        if ($product->type->hasSlotBasedAvailability()) {
            $base['time_slots'] = $product->availability->slots->map(fn($slot) => [
                'start' => $slot->start_time,
                'end' => $slot->end_time,
                'available' => $slot->remaining_capacity > 0
            ])->toArray();
            $base['capacity'] = $product->availability->max_capacity;
        } else {
            // Fallback: no slots
            $base['time_slots'] = [];
            $base['capacity'] = $product->stock_quantity ?? 0;
        }

        return $base;
    }
}

Then use it in your controller:

namespace App\Http\Controllers\Api\V1;

use App\Transformers\LegacyProductTransformer;

class ProductController extends Controller
{
    public function show(Product $product)
    {
        $transformer = new LegacyProductTransformer();
        return response()->json($transformer->transform($product));
    }
}

Why This Works

Backward compatibility without technical debt. Your internal models use the new structure. The transformer handles the messy mapping logic in one place.

Feature flags control rollout. Check flags on the model (hasVariantPricing(), hasSlotBasedAvailability()) to gradually migrate products to the new structure without breaking old ones.

No dual data storage. You’re not maintaining two database schemas. The old format is generated on-the-fly from the new data.

Easy testing. Transformer is a plain PHP class. Write unit tests that assert old API format from new models.

When NOT to Use This

If you only have a handful of API consumers and you can coordinate with them, just version your API (/api/v2) and deprecate v1. Transformers add complexity.

But if you have hundreds of clients, mobile apps in the wild, or partners integrated years ago – transformers let you evolve your system without breaking the world.

Bonus: Reverse Transformers

You can also build reverse transformers for POST/PUT requests that accept the old format and convert it to new models:

class LegacyProductReverseTransformer
{
    public function fromArray(array $legacy): Product
    {
        $product = new Product();
        $product->pricing = new Pricing([
            'base_price' => $legacy['price'],
            'currency' => $legacy['currency'],
            'variants' => [
                ['type' => 'adult', 'price' => $legacy['adult_price']],
                ['type' => 'child', 'price' => $legacy['child_price']],
            ]
        ]);
        return $product;
    }
}

Now your API accepts both old and new formats. Clients migrate at their own pace.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *