Deprecation and Sunset headers, write a migration guide, and set a removal date. But without tracking which consumers still use the deprecated endpoint, you are flying blind, you will either remove it too early (breaking someone) or too late (carrying dead code for years).DeprecationWhat is deprecation?Marking a feature or API version as outdated and scheduled for removal, giving users time to switch to the replacement. is a promiseWhat is promise?An object that represents a value you don't have yet but will get in the future, letting your code keep running while it waits.: "this still works, but it will stop working on a specific date, and here is what to use instead." Done well, it gives consumers time to migrate gracefully. Done badly, it breaks production systems or leaves zombie endpoints running forever because nobody tracked who was still using them.
The deprecationWhat is deprecation?Marking a feature or API version as outdated and scheduled for removal, giving users time to switch to the replacement. lifecycle
Every deprecation follows four phases. Skipping any of them creates problems.
Phase 1: announce (months before sunset)
Communicate the deprecation before consumers feel any effect. Use every channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue). available: changelog, email, dashboard notification, and response headers.
// Add deprecation headers to the response
app.get('/v1/users', (req, res) => {
// RFC 8594: Deprecation header
res.setHeader('Deprecation', 'true');
// RFC 8594: Sunset header with exact date
res.setHeader('Sunset', 'Sat, 01 Nov 2025 00:00:00 GMT');
// Custom header pointing to migration docs
res.setHeader('Link', '</docs/migration/v1-to-v2>; rel="deprecation"');
// Still return normal response
const users = await getUsers();
res.json(users);
});The Deprecation header is defined in RFC 8594. It tells automated tools and monitoring systems that this endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. is marked for removal. The Sunset header gives the exact date. Together, they provide machine-readable deprecation metadata that consumers can build alerts around.
Phase 2: warn (weeks before sunset)
Increase the urgency. Add warning headers and start logging which consumers still use the deprecated endpoint.
// Middleware that tracks deprecated endpoint usage
function deprecationWarning(config) {
return (req, res, next) => {
const clientId = req.headers['x-client-id'] || 'anonymous';
const daysUntilSunset = Math.ceil(
(new Date(config.sunsetDate) - new Date()) / (1000 * 60 * 60 * 24)
);
// Set standard headers
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', new Date(config.sunsetDate).toUTCString());
// Add a Warning header (RFC 7234)
res.setHeader(
'Warning',
`299 - "Deprecated: use ${config.replacement} instead. ` +
`Sunset in ${daysUntilSunset} days."`
);
// Log usage for tracking
logger.warn('Deprecated endpoint accessed', {
endpoint: req.path,
method: req.method,
clientId,
daysUntilSunset,
replacement: config.replacement
});
// Track in metrics
metrics.increment('api.deprecated.request', {
endpoint: req.path,
client: clientId
});
next();
};
}
// Apply to deprecated routes
app.get('/v1/users',
deprecationWarning({
sunsetDate: '2025-11-01',
replacement: 'GET /v2/users'
}),
v1GetUsers
);Phase 3: sunset (the removal date)
On the sunset date, stop serving the deprecated endpoint. Return a clear error that points consumers to the replacement.
function sunsetResponse(config) {
return (req, res) => {
res.status(410).json({
error: 'Gone',
message: `This endpoint was deprecated and removed on ${config.sunsetDate}.`,
migration: config.replacement,
documentation: config.docsUrl
});
};
}
// Replace the old handler with a sunset response
app.get('/v1/users',
sunsetResponse({
sunsetDate: '2025-11-01',
replacement: 'GET /v2/users',
docsUrl: 'https://api.example.com/docs/migration/v1-to-v2'
})
);HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. 410 Gone is the correct status codeWhat is status code?A three-digit number in an HTTP response that tells the client what happened: 200 means success, 404 means not found, 500 means the server broke. here, it tells clients (and search engines) that the resource existed but has been intentionally removed. It is different from 404, which means "never heard of it."
Phase 4: cleanup (after sunset)
Remove the old code, database columns, and infrastructure that supported the deprecated version. This is the step most teams skip, and then they carry dead codeWhat is dead code?Code that exists in the project but is never executed or referenced, adding confusion without serving a purpose. for years.
// Before cleanup: dead code still in the codebase
app.get('/v1/users', sunsetResponse({ /* ... */ }));
app.get('/v2/users', v2GetUsers);
// After cleanup: only the current version remains
app.get('/v2/users', v2GetUsers);
// v1 handler, v1 serializers, v1 tests - all deletedDeprecationWhat is deprecation?Marking a feature or API version as outdated and scheduled for removal, giving users time to switch to the replacement. best practices
| Practice | Description | Why it matters |
|---|---|---|
| Set a concrete sunset date | Always include a date, never "soon" or "eventually" | Consumers cannot plan without a deadline |
| Minimum 6 months for public APIs | Give external teams time to schedule migration work | Enterprise consumers have quarterly release cycles |
| 1-3 months for internal APIs | Shorter timeline, since you control both sides | Still need time for testing and rollout |
| Provide migration docs | Step-by-step guide mapping old to new | Without docs, deprecation is just abandonment |
| Monitor usage metrics | Track how many requests hit deprecated endpoints daily | Know when it is safe to remove |
| Notify known consumers directly | Email or message teams using the deprecated feature | Headers alone are not enough |
| Keep deprecated endpoints functional until sunset | Never break early, even if usage drops to zero | Trust matters, breaking your own timeline erodes confidence |
| Return 410 Gone after sunset | Not 404, 410 communicates intentional removal | Helps consumers distinguish "removed" from "wrong URL" |
| Version your deprecation announcements | Include them in your API changelog | Consumers can search the changelog for relevant changes |
Writing effective migrationWhat is migration?A versioned script that changes your database structure (add a column, create a table) so every developer and server stays in sync. guides
A migration guide must answer three questions: what changed, why, and exactly how to update.
# Migration Guide: /v1/users -> /v2/users
## What changed
- `email` field renamed to `contactEmail`
- `name` field split into `firstName` and `lastName`
- Response now includes `profile` object (new)
- `role` field values changed: "admin" -> "administrator"
## Why
The v1 user model combined data that belongs in separate domains.
v2 separates user identity from user profile, enabling independent updates.
## Step-by-step migration
### 1. Update the endpoint URL// Before
GET /v1/users/123
// After
GET /v2/users/123
### 2. Update field references// Before
const email = user.email;
const name = user.name;
// After
const email = user.contactEmail;
const name = ${user.firstName} ${user.lastName};
### 3. Handle new response shape// Before
const { id, name, email, role } = await fetchUser(123);
// After
const { id, firstName, lastName, contactEmail, profile, role } = await fetchUser(123);
// Note: role is now "administrator" instead of "admin"
The migration guide should be specific enough that a developer can follow it mechanically without guessing.
Monitoring deprecated endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. usage
Tracking usage is essential to know when you can safely remove a deprecated endpoint. Build a dashboard that shows:
// Express middleware to collect deprecation metrics
function collectDeprecationMetrics(req, res, next) {
const isDeprecated = res.getHeader('Deprecation') === 'true';
if (isDeprecated) {
const clientId = req.headers['x-client-id'] || 'unknown';
// Store in your metrics system (Datadog, Prometheus, etc.)
metrics.gauge('api.deprecated.daily_requests', 1, {
endpoint: `${req.method} ${req.route.path}`,
client: clientId,
version: req.baseUrl // /v1, /v2, etc.
});
// Track unique consumers per day
metrics.set('api.deprecated.unique_clients', clientId, {
endpoint: `${req.method} ${req.route.path}`
});
}
next();
}
app.use(collectDeprecationMetrics);What to track on your dashboard:
| Metric | What it tells you |
|---|---|
| Daily request count to deprecated endpoints | Overall usage trend (should be declining) |
| Unique consumers per day | How many teams still need to migrate |
| Request count by consumer ID | Which specific consumers are lagging |
| Days until sunset | Urgency, are consumers migrating fast enough? |
| Error rate on new endpoint | Whether migration is causing issues on the new version |
| Consumer migration completion rate | Percentage of consumers fully migrated |
When the daily request count hits zero for a sustained period (at least a week after sunset), you can confidently clean up the deprecated code.
Timeline template
Here is a practical timeline for deprecating a public APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users.:
Day 0: Announce deprecation in changelog + response headers
Day 0: Publish migration guide
Week 1: Email known consumers directly
Month 1: Review usage metrics - are consumers migrating?
Month 3: Send reminder notification to remaining consumers
Month 5: Final warning - increase Warning header urgency
Month 6: Sunset - return 410 Gone
Month 7: Remove deprecated code from codebaseFor internal APIs, compress this to weeks instead of months. The key is that every step is planned, communicated, and tracked.