Aikido

How to avoid breaking public API contracts: maintaining backward compatibility

Maintainability

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, TypeScript

Introduction

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.

FAQs

Got Questions?

What changes are considered breaking?

Removing fields from responses, renaming fields, changing field types (string to number), making optional parameters required, changing HTTP status codes for existing conditions, altering authentication requirements, and modifying error response formats. Adding new required request parameters or removing endpoints entirely are also breaking changes.

How do I add new required fields without breaking clients?

Make the new field optional initially with a sensible default. Document the change and give clients time to adopt it. After sufficient time (6-12 months for public APIs), make the field required in a new API version. Never make existing optional fields required without versioning.

What's the difference between API versioning and deprecation?

Versioning creates a new endpoint (/v2/users) alongside the old one, allowing both to coexist. Deprecation marks an old endpoint or field as obsolete while keeping it functional, with a timeline for eventual removal. Use versioning for major changes, deprecation for gradual phase-outs of minor features.

How long should I maintain deprecated API versions?

For public APIs, maintain deprecated versions for at least 12-18 months. For internal APIs, coordinate with client teams for a migration timeline. Always provide advance notice (3-6 months minimum) before removing deprecated endpoints. Monitor usage metrics to ensure clients have migrated before shutdown.

Can I change response field order?

Yes, JSON object field order is not part of the API contract. Well-written clients parse JSON by field name, not position. However, test thoroughly as some poorly written clients might depend on field order. For arrays, order usually matters and shouldn't change unless documented.

How do I version APIs without URL path changes?

Use HTTP headers: Accept: application/vnd.myapi.v2+json or custom headers like API-Version: 2. Query parameters work too: /api/users?version=2. Content negotiation via headers is cleaner but harder to test in browsers. Pick one strategy and use it consistently.

Get secure for free

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.