Rule
Avoid breaking public API contracts.
Changes to public API endpoints that would break
existing client requests are breaking changes.
Think of the API contract as a promise — changing
it after clients depend on it breaks their code.
Supported languages: PHP, Java, C#, Python, JavaScript, TypeScriptIntroduction
Public APIs are contracts between your service and its consumers. Once clients depend on an endpoint's request format, response structure, or behavior, changing it breaks their code. Breaking changes force all clients to update simultaneously, which is often impossible when you don't control the clients. Mobile apps can't be force-updated, third-party integrations need migration time, and legacy systems may never update.
Why it matters
Client disruption and trust: Breaking API changes cause immediate failures in production client applications. Users experience errors, data loss, or complete service outages. This damages trust between API providers and consumers, and violates the implicit contract that stable APIs remain stable.
Coordination costs: Coordinating breaking changes across multiple client teams is expensive and slow. Each team needs time to update code, test changes, and deploy. For public APIs with unknown clients (mobile apps, third-party integrations), coordination is impossible.
Version proliferation: Poorly managed breaking changes lead to maintaining multiple API versions simultaneously. Each version requires separate code paths, tests, documentation, and bug fixes, multiplying maintenance burden exponentially.
Code examples
❌ Non-compliant:
// Version 1: Original API
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json({ id: user.id, name: user.name });
});
// Version 2: Breaking change - renamed field
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json({
id: user.id,
fullName: user.name // Breaking: 'name' renamed to 'fullName'
});
});
Why it's wrong: Renaming name to fullName breaks all existing clients expecting the name field. Client code accessing response.name will receive undefined, causing errors. This change forces all clients to update simultaneously or fail.
✅ Compliant:
// Version 2: Additive change - keeps old field, adds new
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json({
id: user.id,
name: user.name, // Keep for backward compatibility
fullName: user.name // Add new field (deprecated 'name')
});
});
// Or use API versioning
app.get('/api/v2/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json({ id: user.id, fullName: user.name });
});
Why this matters: Keeping the old name field maintains backward compatibility while adding fullName for new clients. Alternatively, creating a new versioned endpoint (/api/v2/) allows breaking changes without affecting existing clients still using /api/v1/.
Conclusion
Evolve APIs through additive changes: add new fields, add new endpoints, add optional parameters. When breaking changes are unavoidable, use API versioning to run old and new versions concurrently. Deprecate old fields with clear timelines and migration guides before removing them.
.avif)
