Skip to main content
Laravel, shipping fast.

“We’ll deprecate v1 eventually” is a way of making sure v1 never leaves. Write the dates down where the code can read them:

// config/api.php
return [
    'versions' => [
        'v1' => [
            'deprecated' => '2027-03-01',
            'sunset' => '2027-09-01',
        ],
        'v2' => [],
    ],
];

A middleware turns those dates into the standard response headers, so that any consumer who checks finds out automatically:

// app/Http/Middleware/AnnounceDeprecation.php
public function handle(
    Request $request,
    Closure $next,
): Response {
    $response = $next($request);
    $version = $request->attributes->get('api_version');
    $dates = config("api.versions.{$version->value}", []);

    if (! isset($dates['deprecated'], $dates['sunset'])) {
        return $response;
    }

    $deprecated = Carbon::parse($dates['deprecated']);

    if ($deprecated->isPast()) {
        $response->headers->add([
            'Deprecation' => '@'.$deprecated->timestamp,
            'Sunset' => Carbon::parse($dates['sunset'])
                ->toRfc7231String(),
        ]);
    }

    return $response;
}

It goes in the api group after ResolveApiVersion, because it reads what that one set.

Two dates, not one. The first puts the version on notice: it still works, but it is no longer the recommendation. The second is the day it stops working. The gap between them is the migration window. It should be long enough for a consumer to schedule the work and short enough that somebody feels urgency. Ninety days is a reasonable floor. A partner tied to someone else’s release calendar may need more.

That is the last middleware this book adds, so here is the whole closure as it ends up. Inside each api() call, the order is the order the middleware runs:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(at: ['10.0.0.0/8']);
    $middleware->throttleApi();

    $middleware->api(prepend: [
        AssignTraceId::class,
        SetLocale::class,
    ]);
    $middleware->api(append: [
        LogApiRequest::class,
        'cache.headers:no_store',
        ResolveApiVersion::class,
        AnnounceDeprecation::class,
    ]);

    $middleware->alias([
        'ability' => CheckForAnyAbility::class,
        'idempotent' => Idempotent::class,
    ]);
    $middleware->prependToPriorityList(
        before: SubstituteBindings::class,
        prepend: CheckForAnyAbility::class,
    );
})

Telling Consumers Before They Find Out

Headers reach only the consumers who inspect headers, which in practice is almost nobody until the day their integration breaks. A response header is a courtesy for the careful. It isn’t a notification strategy.

I once watched a partner integration go down for four hours because a version’s sunset date passed and nobody on their side had been reading Sunset headers. The fix took ten minutes once someone noticed. Finding out took the whole morning. Ten minutes of fixing against four hours of not knowing is the argument for pushing the notice and not waiting for someone to pull it.

You already know who to tell. Chapter 6 records every request with its consumer. Add an api_version column to api_requests, and one line to LogApiRequest that fills it from the request attribute, and it records the version too. The consumers still calling v1 are a query away, and Laravel’s notifications deliver the message:

// app/Console/Commands/NotifyDeprecatedVersion.php
$stillOnV1 = ApiRequest::query()
    ->where('api_version', ApiVersion::V1)
    ->where('created_at', '>=', now()->subDays(30))
    ->distinct()
    ->pluck('consumer_id');

Notification::send(
    Consumer::query()->whereKey($stillOnV1)->get(),
    new VersionDeprecated(ApiVersion::V1),
);

Consumer needs the Notifiable trait and a routeNotificationForMail() method that returns its contact_email, and VersionDeprecated is an ordinary notification class from php artisan make:notification. Because it targets only the consumers who are affected, nobody learns to ignore it. (Laravel documentation: Notifications.)

Send it the day the deprecation date is set, again partway through the window, and once more a week before the sunset. Three notices, spaced out, beat one that nobody remembers reading. Silence reads as “nothing changed,” right up until something has.

Migration Guides People Can Follow

A deprecation notice says something is changing. It doesn’t say what to do about it. That is a separate document, and it answers four questions in order:

  1. What changed. The specific field or behavior, named exactly. Not “the response format was improved.”
  2. What it looked like before and after. A real pair of payloads, not a description of one.
  3. What action is required, if any. Sometimes the answer is “none, this is additive,” and saying so saves a consumer from unnecessary work.
  4. What the timeline is. The same two dates the headers carry, in plain language.
## License response: v1 -> v2

What changed
  `status` was added. Nothing was removed or renamed.

Before (v1)
  { "key": "lic_123", "name": "...", "domains": [...] }

After (v2)
  { "key": "lic_123", "name": "...", "domains": [...],
    "status": "active" }

Action required
  None, unless you want to read `status`.

Timeline
  v1 deprecated 1 March 2027. v1 stops 1 September 2027.

Write the guide before you write the deprecation notice. If you can’t fill in all four sections plainly, the change isn’t ready to ship.

Retiring a Version

Retiring a version is two decisions that look like one: deciding that it is safe, and deciding to do it. Don’t let the second happen before the first is true.

Safety comes from data, not from the calendar. Look at the request log before the sunset date arrives. If v1 traffic hasn’t dropped to zero, or to a short list of consumers you have already spoken to, move the date. Don’t ignore it.

Once the traffic is gone, retiring is deletion, not a flag left in place “just in case.” Remove the V1 case from the enum, remove the condition from the Resource so that status is always present, and delete its dates from the config.

Chapter 16 Summary

Before you version anything:

  • Know whether the change is additive or breaking before you write it.
  • Resolve the version once, at the edge, in a middleware.
  • Vary the Resource, never the code that fetches the data.

Before you deprecate:

  • Put a real deprecation date and a real sunset date on the version.
  • Notify the consumers who are actually affected, more than once. Headers alone are not notice.
  • Write the migration guide first.

Before you retire:

  • Confirm from the request log that the traffic is gone or accounted for.
  • Delete the old path completely.

The audio could not be loaded. Try again in a moment.