REST API Deprecation

Sunset headers, migration guides, and consumer communication for graceful API end-of-life

Last Updated:

Why Deprecation is a First-Class Concern

Breaking a production API breaks your users' systems. The cost of a surprise API removal includes emergency developer time, customer churn, and support load. A well-communicated deprecation process turns a potentially adversarial event into a collaborative migration.

This extends naturally from our API versioning guide — versioning creates the path, deprecation manages the sunset. Together they form your API lifecycle management strategy.

The Deprecation Lifecycle

PhaseDurationActions
ActiveIndefiniteNormal support, bug fixes, improvements
DeprecatedMin 3–6 monthsSunset headers, changelog notice, migration docs published
Sunset2–4 weeks410 Gone returned, redirect headers present, final warnings
RemovedEndpoint deleted, docs archived

HTTP Headers: Sunset & Deprecation

Two standard HTTP headers communicate deprecation status to API consumers:

Sunset Header (RFC 8594)

HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Deprecation: true
Link: <https://api.example.com/v2/users>; rel="successor-version",
      <https://docs.example.com/migration/v1-to-v2>; rel="deprecation"
Content-Type: application/json

Sunset gives the exact date/time the endpoint will stop responding. Deprecation signals that the endpoint is deprecated now (use true or an ISO 8601 date of when it was deprecated). The Link header with rel="successor-version" points to the replacement.

Node.js Deprecation Middleware

// Reusable deprecation middleware factory
function deprecate({ sunsetDate, successorUrl, docsUrl }) {
  const sunsetHeaderValue = new Date(sunsetDate).toUTCString();

  return (req, res, next) => {
    // RFC 8594: Sunset header
    res.set('Sunset', sunsetHeaderValue);

    // IETF draft: Deprecation header
    res.set('Deprecation', 'true');

    // Link to successor and migration docs
    const links = [];
    if (successorUrl) links.push(`<${successorUrl}>; rel="successor-version"`);
    if (docsUrl)      links.push(`<${docsUrl}>; rel="deprecation"`);
    if (links.length) res.set('Link', links.join(', '));

    next();
  };
}

// Apply to deprecated routes
app.use('/v1/users', deprecate({
  sunsetDate: '2026-12-31T23:59:59Z',
  successorUrl: 'https://api.example.com/v2/users',
  docsUrl: 'https://docs.example.com/migration/users-v1-to-v2'
}));

// After sunset date: return 410 Gone
function goneAfterSunset(sunsetDate) {
  return (req, res, next) => {
    if (new Date() > new Date(sunsetDate)) {
      return res.status(410).json({
        error: 'endpoint_removed',
        message: 'This endpoint was removed on ' + new Date(sunsetDate).toDateString(),
        migration_guide: 'https://docs.example.com/migration/users-v1-to-v2'
      });
    }
    next();
  };
}

app.use('/v1/users', goneAfterSunset('2026-12-31T23:59:59Z'), v1UserRoutes);

Versioning Sunset Strategy

API VersionTimelineNotes
v1 (current)ActiveFull support
v2 (new)Launch dateDeprecate v1 same day, start 6-month countdown
v1 sunset+6 months410 Gone, all consumers migrated

Minimum deprecation window by API type:

  • Internal APIs: 1–3 months
  • Partner APIs: 3–6 months
  • Public APIs: 6–12 months (Stripe gives 12+ months)

Monitoring Deprecated Endpoint Usage

// Track who is still calling deprecated endpoints
const deprecationUsage = new Map();

function trackDeprecationUsage(version) {
  return (req, res, next) => {
    const clientId = req.headers['x-client-id'] || req.ip;
    const key = `${version}:${clientId}`;

    // Increment counter in Redis (for production)
    // redis.zincrby('deprecated_usage', 1, key);

    // Log for monitoring
    logger.warn({
      event: 'deprecated_endpoint_called',
      version,
      client_id: clientId,
      path: req.path,
      user_agent: req.headers['user-agent']
    });

    next();
  };
}

app.use('/v1/', trackDeprecationUsage('v1'));

// Report: which clients still use v1?
// SELECT client_id, COUNT(*) FROM deprecated_usage
// WHERE version = 'v1' AND date > NOW() - INTERVAL '7 days'
// GROUP BY client_id ORDER BY count DESC;

Migration Guide for Consumers

A good migration guide includes:

  • Side-by-side diff — v1 request/response vs v2 side by side
  • Breaking changes list — renamed fields, removed fields, changed types
  • Code snippets — before/after in the top 2–3 languages your users use
  • Migration script — if you can provide one (e.g., for data format changes)
  • Staging environment — let consumers test v2 before cutover
  • Support channel — dedicated Slack/forum thread for migration questions

Case Studies: Stripe, GitHub, Twilio

CompanyApproachWindow
StripeDate-versioned (2024-01-01), each key locked to a version, automatic upgrade path12+ months
GitHubSunset + Deprecation headers on every deprecated endpoint, 3-month minimum3–12 months
TwilioEmail notifications to all API key holders + dashboard banner + headers6 months