Renovating a building's only entrance while it's still the only way in strands everyone who was already relying on it that morning. Building the new entrance alongside the old one, and only closing the old one once everyone's had time to switch, lets both kinds of visitors keep arriving safely during the transition. API versioning applies the same idea to a response shape: v1 keeps serving its existing callers exactly as before, while v2 exists in parallel for whoever's ready to move.
Across 10+ Infracommerce clients sharing the same base Magento repository, I regularly modified modules both at the client-specific level and in that shared base — and a change to the base couldn't just assume every client customization built on top of it would keep working unchanged. Keeping an older client integration functioning while a newer one adopted a changed module is the same discipline versioned API routes rely on: old and new consumers coexisting without one breaking the other.
Trade-off: two versions in parallel means a bug found in shared logic often has to be fixed in both controllers, and someone has to decide when — or whether — the old version ever actually gets retired.
Before
Route::get('/api/orders/{id}', [OrderController::class, 'show']);
class OrderController
{
public function show(Order $order): JsonResponse
{
// changing this shape breaks every existing client at once
return response()->json([
'id' => $order->id,
'total' => $order->total,
]);
}
}After
Route::get('/api/v1/orders/{id}', [OrderV1Controller::class, 'show']);
Route::get('/api/v2/orders/{id}', [OrderV2Controller::class, 'show']);
class OrderV2Controller
{
public function show(Order $order): JsonResponse
{
// v1 keeps its old shape, untouched, in its own controller
return response()->json([
'id' => $order->id,
'total' => ['amount' => $order->total, 'currency' => 'USD'],
]);
}
}