Welcome back. You have just made the market-price integration configurable through validated options, so the API can fail safely at startup when its operational contract is incomplete. This lesson shifts from configuration contracts to HTTP API contracts: how they evolve when clients cannot all update at once.
For the portfolio tracker, that matters as soon as an Angular client, a partner integration, or a future mobile client relies on a URL, request shape, response JSON, status code, or error format. By the end of this lesson, you will be able to classify a client-visible change, decide whether versioning is actually needed, and defend a practical migration strategy for a realistic portfolio API change.
An API contract is larger than its endpoint URL
An API is a promise made by the service to its consumers. For an HTTP API, that promise includes more than a route such as GET /api/portfolios/42. It includes:
- The HTTP method, route, query parameters, and headers.
- Request JSON fields, validation rules, and defaults.
- Response JSON fields, types, nullability, and semantics.
- Status codes and
ProblemDetailserror shape. - Authentication and authorization requirements.
- Behavioral expectations such as pagination, ordering, idempotency, and rate limits.
A harmless-looking change can therefore be breaking. For example, changing:
{
"totalValue": 1250.43
}
to:
{
"valuation": {
"amount": 1250.43,
"currency": "USD"
}
}
is not merely a property rename. Existing clients looking for totalValue fail to deserialize or display the result. More importantly, the meaning changes: the original amount had an implicit currency, while the new structure makes currency explicit.
Creating, evolving, and versioning microservice APIs and contracts - .NET | Microsoft Learn
Read the opening discussion and the later section on incompatible changes from Microsoft Learn. It establishes why independent service delivery depends on preserving client contracts, and it distinguishes small compatible changes from changes that require parallel support.
In the opening paragraphs of “Creating, evolving, and versioning microservice APIs and contracts,” read from the contract explanation. Then continue to the discussion beginning “When the API changes are small,” focusing on its distinction between additive changes and incompatible changes, plus its two implementation choices: supporting versions inside one service or deploying them separately.
A useful distinction is:
- Backward compatible means a newer server continues to work for older clients. This is the compatibility that protects already deployed consumers during a service upgrade.
- Forward compatible means an older server can safely handle requests sent by a newer client. It matters during staged deployments and rollback, but it is not always desirable. An old server should not blindly accept fields that could alter authorization, money movements, or persistence behavior.
In practical API design, be conservative about what you send and deliberate about what you accept. Explicit request DTOs and server-owned validation remain essential; forward compatibility must not become accidental over-posting.
First decision: is the proposed change actually breaking?
Versioning is a cost. Every supported version adds code paths, test cases, documentation, telemetry dimensions, security review surface, and a retirement obligation. Before creating /v2, try to make the change compatible.
| Proposed change | Usually backward compatible? | Why |
|---|---|---|
| Add a new endpoint | Yes | Existing clients keep calling existing endpoints. |
| Add an optional request field with a safe default | Yes | Older clients omit it and retain old behavior. |
| Add a response field | Usually | Older clients often ignore unknown JSON properties, but verify generated clients and strict deserializers. |
| Add a new optional query parameter | Yes | Existing requests remain valid. |
| Rename, remove, or change a response field’s type | No | Existing clients may not deserialize or find the expected field. |
| Make a previously optional request field required | No | Existing clients send requests that are now invalid. |
Change a status code or ProblemDetails format | Often no | Consumers may branch on status codes or parse errors. |
| Change a value’s unit, currency, precision, or business meaning | No | The JSON may still parse while producing incorrect business behavior. |
| Add an authorization requirement to an existing operation | Usually no | Previously valid client calls are rejected. |
The word usually is important. Whether an added response field is safe depends on real consumers, not just REST theory. A TypeScript interface does not enforce runtime JSON shape, but a generated C# client, strict JSON schema validator, or UI that assumes an exhaustive set of values might react differently.
For private APIs where you own every consumer, an expand-and-contract approach often avoids a version bump:
- Expand: Add the new representation while retaining the old one.
- Release compatible server code and migrate clients.
- Measure whether any clients still use the old contract.
- Contract: Remove the deprecated representation only after the agreed migration window and evidence of zero remaining use.
For example, a temporary compatible response might be:
{
"totalValue": 1250.43,
"valuation": {
"amount": 1250.43,
"currency": "USD",
"asOfUtc": "2026-04-08T14:00:00Z"
}
}
This is appropriate only if retaining totalValue remains truthful. If it now has unclear currency semantics or may encourage clients to calculate with the wrong assumption, preserving it indefinitely is not responsible compatibility.
API Design Backward and Forward Compatible APIs
Watch “API Design Backward and Forward Compatible APIs” from Grow with Anto for a concrete demonstration of additive changes, cautious forward compatibility, and expand-and-contract migration.
Watch an additive change to see why a new optional response field can leave an older client functioning. Then watch forward compatibility, noting the warning that accepting unknown input is context-dependent. Finish with migration choices, which contrasts expand-and-contract with a new major API version.
The main HTTP versioning choices
When an incompatible contract is unavoidable, the client needs an unambiguous way to request the contract it understands. HTTP does not impose one universal convention. The important thing is to select one deliberately, document it, and use it consistently.
Web API Design Best Practices - Azure Architecture Center
Read the “Implement versioning” section of Microsoft’s Azure Architecture Center guidance. It compares the principal versioning mechanisms and, crucially, their consequences for discoverability, routing, caching, and client complexity.
In “Implement versioning,” read the subsections “No versioning,” “URI versioning,” “Query string versioning,” “Header versioning,” and “Media type versioning.” Start at the versioning discussion. As you read, focus on what the client must send, whether the requested contract is visible in the URL, and the caching trade-offs.
Here is the practical comparison.
| Strategy | Example | Strengths | Costs and cautions |
|---|---|---|---|
| No explicit version | /api/portfolios/42 | Cleanest when changes are additive and clients are controlled. | Cannot safely represent major incompatible contracts at one URL. |
| URI versioning | /api/v2/portfolios/42 | Highly visible, easy to test in Postman, easy to route, document, monitor, and cache. | Version becomes part of every route and link. |
| Query-string versioning | /api/portfolios/42?api-version=2 | Keeps the logical resource path stable; easy to try manually. | Easy for clients to omit; may complicate caching and tooling conventions. |
| Custom header versioning | api-version: 2 | Keeps URLs clean and can default omitted versions. | Less discoverable in a browser or copied URL; clients must reliably set headers. |
| Media-type versioning | Accept: application/vnd.portfolio.v2+json | Uses HTTP content negotiation and can fit hypermedia-oriented APIs. | Harder for many teams to reason about, test, cache, and support. |
For the capstone and for many business APIs, URI versioning is the most interview-defensible default for a major breaking change. It makes the contract visible to a partner reading documentation, to a support engineer examining logs, and to a test suite calling a concrete endpoint. It is not the only correct answer; it is a trade-off that fits a modest team and multiple independently deployed clients.
Do not confuse an API contract version with an application build number. Your container image might be tagged 1.8.3, while it serves both stable API contracts v1 and v2. A patch release can fix a defect in v1 without creating API v1.1.
A stated capstone scenario and the recommended decision
Assume the portfolio tracker currently exposes this endpoint:
GET /api/portfolios/{portfolioId}/summary
Version 1 response:
{
"portfolioId": 42,
"totalValue": 1250.43,
"lastUpdatedUtc": "2026-04-08T14:00:00Z"
}
The Angular application is maintained by your team, but three external financial-reporting partners also consume the endpoint. A new requirement introduces portfolios in multiple currencies. Product wants the response to contain a valuation amount, ISO currency code, valuation timestamp, and source. Partners have a 90-day migration commitment, and their releases cannot be coordinated with yours.
The new desired representation is:
{
"portfolioId": 42,
"valuation": {
"amount": 1250.43,
"currency": "USD",
"asOfUtc": "2026-04-08T14:00:00Z",
"source": "market-price-provider"
}
}
Decision
Use URI-based major versioning and support v1 and v2 concurrently for the migration period:
GET /api/v1/portfolios/{portfolioId}/summary
GET /api/v2/portfolios/{portfolioId}/summary
The decision is justified by the scenario:
- The new currency-aware valuation is a semantic and structural breaking change.
- External consumers cannot be upgraded atomically.
- A clear URL is easier for partner documentation, support, contract tests, and usage telemetry.
- The API has only two active contracts, so URI versioning’s route duplication is an acceptable operational cost.
- A 90-day, measurable retirement policy prevents v1 becoming permanent accidental baggage.
The version selection is not “version every release.” It is: use no new version for safe additive evolution; use a major contract version when you cannot preserve old behavior honestly.
Support two contracts without duplicating the whole system
Versioned contracts do not require two copies of all business logic. Keep the domain and application behavior shared where it genuinely is the same, and place differences at the HTTP boundary.
For this scenario:
- The API loads one canonical internal valuation model with amount, currency, timestamp, and source.
- The v1 endpoint maps it to the legacy
totalValueresponse, with the documented legacy behavior. - The v2 endpoint maps it to the explicit
valuationobject. - Each endpoint has separate contract tests that assert its JSON shape, status codes, and error responses.
Conceptually:
[ApiController]
[Route("api/v1/portfolios")]
public sealed class PortfolioSummaryV1Controller : ControllerBase
{
[HttpGet("{portfolioId:int}/summary")]
public async Task<ActionResult<PortfolioSummaryV1Response>> GetSummary(
int portfolioId,
CancellationToken cancellationToken)
{
// Call shared application service.
// Map canonical valuation to legacy v1 response.
throw new NotImplementedException();
}
}
[ApiController]
[Route("api/v2/portfolios")]
public sealed class PortfolioSummaryV2Controller : ControllerBase
{
[HttpGet("{portfolioId:int}/summary")]
public async Task<ActionResult<PortfolioSummaryV2Response>> GetSummary(
int portfolioId,
CancellationToken cancellationToken)
{
// Call the same shared application service.
// Map canonical valuation to explicit v2 response.
throw new NotImplementedException();
}
}
The controllers above demonstrate the boundary, not a recommendation to duplicate calculations. A shared PortfolioValuationService should own the calculation and domain rules. Version-specific DTOs and mapping should own the contract difference.
There are two valid deployment models:

One service supports both versions
Start here for the capstone. A single deployed API serves both route groups and shares the same domain logic and database. It has lower infrastructure overhead, simpler data consistency, and one deployment pipeline.
Its downside is coupling: every deployment can affect both contracts. Thorough v1 regression tests become non-negotiable.
Side-by-side deployments
Use separate deployable versions when v2 is substantially different, has a distinct runtime or scaling profile, requires an isolated release cadence, or needs stronger blast-radius separation. A gateway or ingress layer routes requests by path or header to the relevant deployment.
This costs more operationally. You must manage two builds, security patching, telemetry, deployment health, and potentially compatibility with a shared database. Side-by-side deployment does not eliminate the need for a data migration plan.
For a database-backed API, use the same expand-and-contract thinking internally:
- Add new tables, columns, or nullable fields before code depends on them.
- Deploy code that can coexist with the old data shape.
- Backfill or dual-write only when necessary and deliberately.
- Do not drop legacy schema support until v1 traffic has ended and rollback risk has passed.
Make version retirement an engineering plan, not a calendar hope
Publishing v2 without a retirement policy yields an API that supports obsolete contracts forever. Your deprecation plan should be explicit before v2 is released.
| Phase | What to do |
|---|---|
| Define | Publish the v2 contract, migration guide, supported-version policy, and v1 sunset date. |
| Implement | Preserve v1 response and error behavior with contract tests; build v2 independently. |
| Observe | Record an API-version dimension in request logs and metrics; identify clients still using v1. |
| Communicate | Notify known partners, provide a test environment if available, and issue reminders before the sunset date. |
| Enforce | After the agreed date and confirmed migration, return a documented retirement response for v1 or remove it according to policy. |
| Clean up | Remove v1 code, tests, documentation, routes, and obsolete database compatibility code in a planned change. |
A version label belongs in operational data. At minimum, capture:
- Requested API version.
- Endpoint and response status.
- Client identifier where authentication provides one.
- Latency and error rate by version.
- Deprecation notices issued.
Do not use raw IP addresses or unauthenticated User-Agent strings as the only evidence that a client has migrated. They can be unreliable. Where possible, identify registered partner clients through their credentials or API subscriptions.
A concise decision record you can use in review or interviews
When asked, “How would you version this API?”, avoid starting with a framework package or route syntax. State the contract analysis and operational choice first.
For the stated scenario, an effective answer is:
The move from an implicit numeric
totalValueto a currency-aware valuation object is a breaking contract and semantic change. Because external partners cannot upgrade with our Angular client, I would introduce URI-basedv2endpoints and retainv1for a published 90-day window. I would keep the valuation calculation in a shared application service, map separate v1 and v2 DTOs at the HTTP boundary, and add contract tests that protect the old route, status codes, and Problem Details behavior. Initially, one service can host both versions to keep operations simple. I would emit API-version telemetry and retire v1 only after partner migration is confirmed. If the change had merely added an optional field with stable semantics, I would prefer an additive expand-and-contract change rather than versioning.
That answer covers the decision, client constraints, implementation shape, testing, observability, and retirement cost.
A focused capstone implementation pass
Make a small design artifact in the repository, such as docs/api-versioning-decision.md, containing:
- The current v1 summary contract and its consumers.
- The proposed currency-aware v2 contract.
- Why the change is breaking.
- The choice of URI versioning and one-service dual support initially.
- A 90-day deprecation and measurement plan.
- Explicit non-goals, such as supporting both contracts indefinitely.
Then implement only the skeleton necessary to prove the decision:
- Define distinct
PortfolioSummaryV1ResponseandPortfolioSummaryV2ResponseDTOs. - Add separate v1 and v2 routes.
- Reuse a shared application service rather than duplicating calculations.
- Add an integration test for each URL and assert the expected JSON field names.
- Add a test ensuring the v1 route remains unchanged after introducing v2.
- Add a structured log property such as
ApiContractVersionat the request boundary.
For a compact implementation-oriented view of version readers and endpoint mapping in ASP.NET Core minimal APIs, this optional video segment is useful. The principle carries over to controller-based APIs even though the endpoint syntax differs.
How to Implement API Versioning for Minimal APIs | ASP.NET Core 8
Watch Milan Jovanović’s “How to Implement API Versioning for Minimal APIs | ASP.NET Core 8” to connect the strategy decision to concrete ASP.NET Core endpoint organization and version-aware test changes.
Watch the motivation for a concise definition of breaking changes. Then watch version readers to compare query, header, and URL approaches. For route-based organization, watch versioned endpoints; finally, watch test updates to see why functional tests must call the intended version explicitly.
Key takeaways
- An API contract includes route, JSON, validation, status codes, errors, security requirements, and business semantics.
- Prefer compatible additive change or expand-and-contract when it preserves truthful behavior.
- Renaming/removing fields, changing types or meaning, requiring new input, and altering established status behavior are breaking changes.
- For the stated external-partner scenario, URI-based
v1andv2endpoints with a defined migration window are a practical choice. - Host both contracts in one service initially when shared logic and operational simplicity outweigh isolation needs; use side-by-side deployments when isolation or independent evolution justifies their cost.
- Version support must include contract tests, version-level telemetry, client communication, and a measured retirement plan.
Next, you will use an AI assistant for a constrained code change, then independently verify its correctness, security implications, tests, and documentation—the discipline that turns AI assistance into dependable engineering practice.
Can't find a good explanation? Sign up and we'll make it for you
Sign up