REST API Deprecation
Sunset headers, migration guides, and consumer communication for graceful API end-of-life
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
| Phase | Duration | Actions |
|---|---|---|
| Active | Indefinite | Normal support, bug fixes, improvements |
| Deprecated | Min 3–6 months | Sunset headers, changelog notice, migration docs published |
| Sunset | 2–4 weeks | 410 Gone returned, redirect headers present, final warnings |
| Removed | — | Endpoint 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 Version | Timeline | Notes |
|---|---|---|
| v1 (current) | Active | Full support |
| v2 (new) | Launch date | Deprecate v1 same day, start 6-month countdown |
| v1 sunset | +6 months | 410 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
| Company | Approach | Window |
|---|---|---|
| Stripe | Date-versioned (2024-01-01), each key locked to a version, automatic upgrade path | 12+ months |
| GitHub | Sunset + Deprecation headers on every deprecated endpoint, 3-month minimum | 3–12 months |
| Twilio | Email notifications to all API key holders + dashboard banner + headers | 6 months |