Deprecations, Breaking Changes and API Evolution in Practice
AI generated
{ }
GET
REST API · Deprecation · Breaking Changes · Versioning · Evolution
Deprecations, Breaking Changes
and API Evolution in Practice

A REST API that never makes breaking changes is an API that never truly evolves. Teams that plan deprecations, sunset dates and migration paths from the start can evolve their APIs deliberately, without losing integrators or breaking them without warning.

20 min read Deprecation Header · Sunset · Versioning · Migration Guide REST · OpenAPI · Symfony

1. What breaking changes actually mean

A breaking change in a REST API is any change that causes an existing, correctly implemented client to stop working or behave incorrectly after an update, without the client code being changed. That sounds simple, but in practice it is subtle: renaming a JSON field is an obvious breaking change. Changing a field's data type from string to integer, removing an optional field that some clients evaluate, restricting the allowed values of an enum field, or altering pagination behavior are all breaking changes that are not always recognized as such.

The most common organizational mistake: the team evolving the API has no complete list of active integrators and no overview of which fields and endpoints are used by whom. In this situation, every change becomes a risk because you do not know who you will break. The solution is not to stop making changes but to establish a structured process: classification of changes, deprecation signaling, sunset dates and a migration path for every breaking change. Teams that have set up this process can actually evolve their APIs instead of being permanently stuck with old design decisions.

2. Breaking vs. non-breaking: classification in practice

Not every change is a breaking change. The distinction is crucial because it determines whether a change can be made without a version bump and without prior notice. Non-breaking changes are additive changes: adding a new optional field to the response, adding a new optional query parameter, adding a new endpoint, or documenting an existing error code in more detail. Correctly implemented clients ignore unknown fields and continue to function. RFC 7231 explicitly recommends that JSON APIs tolerate unknown fields.

Breaking changes include: removing or renaming fields, changing data types, removing enum values (adding new ones is non-breaking), adding required fields to requests, changing HTTP methods or paths, changing status codes for existing scenarios, and fundamentally different error behavior. A helpful test question: "Would a client correctly implemented against today's documentation continue to work correctly after this change, without any code changes?" If not, it is a breaking change. This classification should be recorded in a changelog for every API release, so integrators can see at a glance what a new version means for them.

# Example changelog for API v2.3.0, classification of every change
## API v2.3.0 (2026-05-10)

### Non-Breaking Changes (safe to adopt without code changes)
- GET /orders: Added optional field `tags` (array of strings) to response
- GET /orders: Added optional query parameter `?tags=` for filtering
- POST /orders: Field `notes` is now optional (was already optional, now documented)
- New endpoint: GET /orders/{id}/timeline, order history events

### Deprecations (still functional, removed in v3.0.0 on 2027-01-01)
- GET /orders: Field `customer_name` deprecated. Use `customer.fullName` instead.
  Deprecation: true; Sunset: Thu, 01 Jan 2027 00:00:00 GMT
- GET /legacy/orders: Entire endpoint deprecated. Use GET /orders instead.

### Breaking Changes (v3.0.0, announced 2025-11-01, effective 2027-01-01)
- REMOVED: Field `customer_name` (use `customer.fullName`)
- REMOVED: GET /legacy/orders endpoint
- CHANGED: `status` field now returns enum instead of string (new values: pending, confirmed, shipped, delivered, cancelled)

3. Setting Deprecation headers and Sunset dates correctly

HTTP defines two standard headers for deprecations: Deprecation and Sunset. The Deprecation header marks an endpoint or API version as outdated. It contains either true (deprecated immediately) or an RFC 7231 date (deprecated from that point on). The Sunset header communicates the planned shutdown date as an HTTP date. In addition, a Link header can point to the documentation of the deprecated resource or the migration guide. These three headers are standardized in RFC 8594 (Sunset HTTP Header) and the Deprecation HTTP header draft.

In Symfony, the simplest way to set these headers is via an event subscriber that appends response headers for marked routes. Alternatively, deprecation attributes can be defined on controller methods and read out by a central middleware layer. Important: deprecation headers must be set on every response of the deprecated endpoint, not just the first request. Integrators who do not review their logs daily should still eventually stumble upon the header. Monitoring tools like Datadog or Grafana can detect Deprecation headers and generate alerts when clients still make requests after the sunset date.

<?php
// Symfony EventSubscriber: sets deprecation headers for marked routes
declare(strict_types=1);

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Sets RFC 8594 Deprecation and Sunset headers on deprecated API routes.
 * Routes opt-in via _deprecation route attribute.
 */
final class ApiDeprecationSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::RESPONSE => 'onKernelResponse'];
    }

    public function onKernelResponse(ResponseEvent $event): void
    {
        $request  = $event->getRequest();
        $response = $event->getResponse();

        /** @var array{sunset?: string, link?: string}|null $deprecation */
        $deprecation = $request->attributes->get('_deprecation');

        if ($deprecation === null) {
            return;
        }

        // RFC 8594: Deprecation header, value "true" or HTTP-date
        $response->headers->set('Deprecation', 'true');

        if (isset($deprecation['sunset'])) {
            // Sunset header: RFC 7231 HTTP-date format
            $response->headers->set(
                'Sunset',
                (new \DateTimeImmutable($deprecation['sunset']))->format(\DateTimeInterface::RFC7231)
            );
        }

        if (isset($deprecation['link'])) {
            $response->headers->set(
                'Link',
                sprintf('<%s>; rel="deprecation"', $deprecation['link'])
            );
        }
    }
}

4. Versioning strategies: URL, header, query parameter

The three common versioning strategies for REST APIs are URL versioning (/api/v1/orders), header versioning (Accept: application/vnd.mironsoft.v2+json) and query parameter versioning (/api/orders?version=2). Each has different consequences for caching, routing, client complexity and documentation.

URL versioning is the most pragmatic and widely used method. It is immediately visible in logs and proxies, easy to route, and browser-friendly. The downside: a completely new route hierarchy must be built for every new major version, and both versions must run in parallel until the old version is shut down. Header versioning keeps URLs stable and is more HTTP-compliant, but requires a Vary: Accept header for correct caching and is harder for some integrators to implement. Query parameter versioning is the most flexible but also the worst to cache, and is suitable only for APIs without caching requirements. The recommendation for most public and semi-public APIs: URL versioning with a clear sunset policy.

5. Writing migration guides integrators actually use

A migration guide that is nothing more than a list of changes will not be used. Integrators need concrete answers to three questions: What do I need to change in my code? How do I test whether my migration is correct? By when do I need to have migrated at the latest? A good migration guide starts with a one-sentence summary of the breaking changes per change, then gives a concrete "before/after" code example for every change, explains edge cases, and describes how both versions can be used in parallel during the migration.

Especially helpful: a compatibility layer for the transition period. When v1 and v2 run in parallel, clients can migrate step by step. An endpoint that accepts v1 requests and internally maps them to v2 logic reduces the migration pressure on integrators who cannot rewrite everything immediately. This layer should be communicated with a clear end date and must not remain permanently in place; it is a migration aid, not a permanent part of the API.

# Migration Guide v1 to v2: concrete before/after examples

## Breaking Change: customer_name to customer.fullName

### v1 Response (deprecated, removed 2027-01-01)
GET /api/v1/orders/123
{
  "id": "123",
  "customer_name": "Max Mustermann",
  "total": 149.99
}

### v2 Response (current)
GET /api/v2/orders/123
{
  "id": "123",
  "customer": {
    "id": "CUST-42",
    "fullName": "Max Mustermann",
    "email": "max@example.com"
  },
  "total": 149.99
}

## Migration Path
1. Switch to the v2 endpoint: change URL prefix /v1/ to /v2/
2. Replace customer_name with customer.fullName
3. Use the new fields customer.id and customer.email (optional, non-breaking)
4. Test with the staging system before switching production
5. Migrate by 2026-10-01 at the latest (3 months before sunset)

6. Sunset monitoring and usage analysis before shutdown

Shutting down an endpoint without knowing whether anyone still uses it is a reliable way to cause an integration failure in production. Sunset monitoring means tracking the actual usage of a deprecated endpoint until the shutdown date. The data source for this is the API access log, from which you can extract how many requests the deprecated endpoint still receives per day, from which client IPs or API keys, and whether the numbers decline after the deprecation announcement.

If requests do not drop to zero shortly before the sunset date, action is required: actively contact the integrators still active, postpone the sunset date, or send one last reminder through all available channels. Shutting down an endpoint that a dozen clients still use daily is not a technical failure, it is a communication failure. Automated alerts that notify the team when a deprecated endpoint still receives requests on the sunset date are an effective safety net. In Symfony, request metrics can be analyzed via the Profiler, Prometheus, or directly in Nginx logs.

7. Documenting deprecations in OpenAPI

OpenAPI 3.x supports the deprecated: true attribute at the endpoint, parameter and schema level. That is enough for the technical marker, but not sufficient for integrators, because OpenAPI does not include a sunset date or migration path. The solution: extend the description property of the deprecated endpoint with a structured deprecation block that contains the date, reason and a link to the migration guide. With the vendor extension mechanism (x-sunset, x-deprecation-link), tools like Redoc or Stoplight can highlight this information.

A separate document (CHANGELOG.md or an API history page in the portal), maintained independently of the OpenAPI schema, is well suited for the API changelog and lists all versions with their breaking changes, deprecations and sunset dates. Integrators who want to adopt a new version should be able to read the changelog without comparing the entire OpenAPI file. A well-maintained changelog is often the first document external teams read before deciding whether and when to upgrade.

8. Versioning strategies compared

The choice of versioning strategy affects caching, routing, client complexity and visibility in logs over the entire lifecycle of the API.

Strategy Example Advantages Disadvantages
URL versioning /api/v2/orders Visible in logs, easy to route, cacheable URL proliferation with many versions
Header versioning Accept: vnd.v2+json Stable URLs, HTTP-compliant Needs Vary header for caching, higher test effort
Query parameter /orders?v=2 Simple for clients without header control Poorly cacheable, uncommon in public APIs
No versioning Only non-breaking changes allowed Simplest client integration Hinders evolution, no escape hatch

In practice, many teams combine URL versioning for major versions with field-level deprecation markers for smaller changes. This gives integrators the best of both worlds: stability (URL versions change only for truly large breaking changes) and transparency (deprecated fields are recognizable in the schema and in response headers). The most important rule remains: any strategy is better than none, as long as sunset dates are communicated and honored.

9. Summary

API evolution without breaking-change management means every change is a risk to all integrators, and that leads either to an API that never truly improves, or to an API that breaks integrators without warning. Deprecation headers and sunset dates are the HTTP standard for structured obsolescence: integrators learn in every response what is changing and by when. A clear classification of breaking and non-breaking changes makes it possible to roll out additive changes without a version bump and to announce breaking changes transparently.

URL versioning is the most pragmatic strategy for most REST APIs. Migration guides with concrete before/after examples significantly increase the adoption rate. Sunset monitoring via API logs prevents endpoints that are still actively used from being shut down. Teams that establish this process can actually evolve their APIs, and keep the trust of their integrators, because breaking changes are planned, communicated and tested before they go into production.

API Evolution and Breaking Changes: The Essentials at a Glance

Deprecation Header

Set Deprecation: true and Sunset: {date} on every response of the deprecated endpoint. Add a Link header to the migration guide. RFC 8594.

Breaking vs. Non-Breaking

Additive changes (new fields, new endpoints) are safe. Anything that breaks existing clients is a breaking change and needs an announcement plus a migration path.

Sunset Monitoring

Track actual usage of deprecated endpoints via access logs. Alert if requests still arrive on the sunset date. Never shut down blindly.

Migration Guide

Concrete before/after examples. Testing instructions for migration. Communicate the deadline clearly. Offer a compatibility layer for the transition period.

10. FAQ: Deprecations, Breaking Changes and API Evolution

1What is a breaking change in a REST API?
Any change that breaks a correctly implemented client without a code change: removing fields, renaming them, changing types, adding required fields, changing status codes for existing scenarios.
2What are non-breaking changes?
Additive changes: new optional fields, new optional parameters, new endpoints, new enum values (provided clients tolerate unknown ones). Correctly implemented clients continue to work.
3What does the Deprecation header do?
Marks an endpoint as outdated (RFC 8594). Combined with the Sunset header, the shutdown date is communicated. Monitoring tools can detect both headers and generate alerts.
4Which versioning strategy is recommended?
URL versioning (/api/v2/) for most public APIs: visible in logs, easy to route, cacheable. Header versioning is more HTTP-compliant but more effort.
5How long should the sunset period be?
Public APIs: at least 12 months. Internal APIs or a small integrator base: 3 to 6 months. Long enough that all active integrators can migrate.
6What is sunset monitoring?
Tracking actual usage of deprecated endpoints until the shutdown date. Check via access logs whether requests drop to zero. If not: communicate actively or postpone the sunset.
7Modeling deprecations in OpenAPI?
deprecated: true at the endpoint or field level. Sunset date and migration link in the description or as a vendor extension (x-sunset). Redoc and Stoplight highlight deprecated markers.
8What is a compatibility layer?
A temporary layer that maps v1 requests onto v2 logic. Gives integrators more time without forcing immediate code changes. Only with a clear end date, no permanent part of the API.
9Shut down a deprecated endpoint without warning?
No, if it is still actively used. Sunset monitoring shows whether requests are still coming in. If a positive finding: contact integrators, postpone the sunset, or send a final reminder.
10Structure of a good migration guide?
1. Changes summarized per sentence. 2. Before/after examples. 3. Testing instructions. 4. Clear deadline date. 5. Contact person for follow-up questions.