Backend Development with .NET
Session 06
API Versioning
Eng. Seif Mansour  ·  Andalusia Academy
Week 3  ·  2 hours
Session Goals

By the end of this session, you will be able to:

  • Explain why API versioning is necessary and when to introduce it
  • Compare the three main versioning strategies and choose the right one
  • Implement URL-based versioning in .NET using Asp.Versioning
  • Mark endpoints as deprecated and communicate that to consumers
Session Agenda
Time Segment Type Duration
0:00The problem — why versioning existsDiscussion15 min
0:15The three versioning strategiesTheory25 min
0:40Live demo — Asp.Versioning setupDemo25 min
1:05Break10 min
1:15Lab — evolve a UserController v1 to v2Lab30 min
1:45Deprecation strategies & real-world conventionsTheory15 min
Why Versioning Exists
APIs are contracts. Once a client is consuming your API, you cannot change the contract without breaking that client. Versioning lets you evolve without leaving existing users behind.
In this section
  • The breaking-change problem
  • Why clients cannot update instantly
  • How versioning solves it
The Breaking Change Problem

Imagine a mobile app consuming GET /api/users/{id} that returns:

Current response (v1)

{ "name": "Ahmed" }

A new client requests you split name into firstName and lastName.

The trap
If you rename the field, every existing mobile app that reads name breaks — and you cannot force all users to update.

With versioning (v2)

{
  "firstName": "Ahmed",
  "lastName": "Hassan"
}
The solution
Run both endpoints simultaneously. Old clients keep calling v1. New clients call v2 and get the improved shape.
Why Clients Cannot Update Instantly

The backend can be deployed in minutes. Clients operate on entirely different timelines.

  • Mobile apps go through app store review cycles — iOS App Store review can take days, and users may never update at all
  • Enterprise clients have quarterly or annual release cycles and run formal regression testing before updating third-party dependencies
  • Partner integrations may be maintained by entirely separate companies with their own roadmaps
  • Older SDKs or scripts are often never updated — they simply keep running as long as the API behaves consistently
Key insight
The backend can be updated instantly. Clients cannot. This asymmetry is the core reason API versioning exists.
The Three Versioning Strategies
URL path, query parameter, and header versioning each make different trade-offs between REST purity, developer experience, and tooling compatibility.
In this section
  • URL path versioning — pros and cons
  • Query parameter versioning — pros and cons
  • Header versioning — pros and cons
  • Which to choose for this course
Three Strategies at a Glance
Query Parameter
GET /api/users?api-version=1.0
URL stays clean; version is optional and can default to latest. Often stripped by caches and proxies, and less discoverable for new consumers.
Header
Api-Version: 1.0
Cleanest resource URIs; favored by REST purists. Invisible — cannot be tested in a browser bar and harder to explain in documentation.
Real-world usage
URL path: GitHub (older API), Twitter. Query parameter: Microsoft Azure REST APIs. Header: GitHub (newer API via Accept: application/vnd.github.v3+json), Stripe.
URL Path Versioning — Deep Dive

Advantages

  • Immediately visible in the URL — no guessing which version is being called
  • Easy to test directly in a browser or with curl
  • Works reliably with reverse proxies, load balancers, and API gateways
  • Most widely understood and expected by developers

Trade-offs

  • Technically violates strict REST — the resource URI should be stable regardless of representation
  • URL structure changes between versions
Example requests
GET /api/v1/users/42 HTTP/1.1

GET /api/v2/users/42 HTTP/1.1
Course convention
Use URL path versioning for all sessions in this course. It is the most practical choice and what most employers will expect.
Asp.Versioning Setup
.NET provides first-class versioning support via the Asp.Versioning.Mvc NuGet package. Installation, configuration, and two controllers co-existing side by side.
In this section
  • Package installation
  • Configuring Program.cs
  • v1 controller
  • v2 controller — the breaking change
  • Testing in Postman
Installing Asp.Versioning

Two NuGet packages are required:

  • Asp.Versioning.Mvc — core versioning attributes and route constraints
  • Asp.Versioning.Mvc.ApiExplorer — integrates versioning with Swagger / OpenAPI discovery (needed for Session 08)
Note
These packages replaced the older Microsoft.AspNetCore.Mvc.Versioning package. If you see tutorials referencing the old name, they are outdated.
Terminal
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
Configuring Versioning in Program.cs
Program.cs
builder.Services.AddControllers();

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
}).AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});
  • DefaultApiVersion — falls back to v1.0 when no version is specified
  • AssumeDefaultVersionWhenUnspecified — prevents a 400 error for clients that omit the version
  • ReportApiVersions — adds api-supported-versions header to every response
UsersV1Controller
Controllers/UsersV1Controller.cs
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        Ok(new { id, name = "Ahmed Hassan" });
}
  • [ApiVersion("1.0")] declares which version this controller handles
  • v{version:apiVersion} is a route constraint that Asp.Versioning resolves automatically
  • The response shape exposes a single name field — this is the contract v1 clients depend on
UsersV2Controller — The Breaking Change
Controllers/UsersV2Controller.cs
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV2Controller : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        Ok(new
        {
            id,
            firstName = "Ahmed",
            lastName  = "Hassan",
            createdAt = DateTime.UtcNow
        });
}

v1 and v2 coexist in the same project. Asp.Versioning routes each request to the correct controller based on the URL segment.

Testing Both Versions in Postman

Send these two requests and compare the responses:

GET /api/v1/users/1

GET /api/v2/users/1

Also inspect the response headers — with ReportApiVersions: true, every response includes:

api-supported-versions: 1.0, 2.0

v1 response

{ "id": 1, "name": "Ahmed Hassan" }

v2 response

{
  "id": 1,
  "firstName": "Ahmed",
  "lastName": "Hassan",
  "createdAt": "2026-01-15T10:00:00Z"
}
Lab — Evolve a UserController from v1 to v2

Starting from a v1 UserController that returns { "id": 1, "name": "Ahmed Hassan" }, add versioning so v1 and v2 coexist:

1 Install Asp.Versioning.Mvc and Asp.Versioning.Mvc.ApiExplorer via dotnet add package
2 Add AddApiVersioning config to Program.cs with default version 1.0
3 Create UsersV1Controller and UsersV2Controller with correct [ApiVersion] attributes
4 v2 response must include: id, firstName, lastName, email, createdAt
5 Verify in Postman: GET /api/v1/users/1 still returns the old shape; GET /api/v2/users/1 returns the new shape
6 Stretch goal: add Deprecated = true to [ApiVersion("1.0")] and verify the api-deprecated-versions header appears
Design Principle
"Never make breaking changes to an existing version.
Create a new version instead."
Adding a new optional field is non-breaking. Removing a field, renaming a field, or adding a required field are all breaking changes that require a new version.
Deprecation & Real-World Conventions
When you release v2, v1 does not vanish — it enters a deprecation period. How long to keep it, how to signal its sunset date, and how leading APIs handle versioning in production.
In this section
  • Marking a version deprecated in code
  • RFC 8594 Sunset header
  • How long to maintain old versions
  • Versioning numbering conventions
  • Stripe's date-based approach
Marking a Version Deprecated

Set Deprecated = true on the [ApiVersion] attribute. Asp.Versioning automatically adds the deprecation header to all responses from that controller.

  • The endpoint still works — deprecation is a warning, not a removal
  • Clients who inspect headers can detect it and prompt their developers to migrate
  • Combine with a Sunset header (RFC 8594) to communicate the removal date
Important
Never remove a version without a deprecation period and a concrete sunset date announced in advance.
Controllers/UsersV1Controller.cs
[ApiController]
[ApiVersion("1.0", Deprecated = true)]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase
{
    // ...
}
Response headers
api-supported-versions: 2.0
api-deprecated-versions: 1.0
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Deprecation: true
How Long to Maintain Old Versions

There is no universal rule — the right duration depends on who your clients are and what their update cycle looks like.

Audience Recommended minimum Rationale
Startups / internal APIs3–6 monthsSmall teams, fast iteration, direct communication
Public APIs with mobile clients12–18 monthsApp store cycles and user update lag
Enterprise / B2B2+ yearsAnnual release cycles and formal validation processes
Stripe's approach
Stripe versions by date (e.g., 2024-01-01) and pins each API key to the version active when the key was created. They maintain every past version indefinitely — eliminating accidental breakage at the cost of significant maintenance complexity.
Versioning Numbering Conventions
Convention Example Used by
Integerv1, v2, v3GitHub, Twitter, most public REST APIs
Semantic (major.minor)v1.0, v2.1Microsoft Azure REST APIs
Date-based2024-01-01Stripe
Course convention
Use integer URL path versioning: v1, v2, and so on. This is simple, explicit, and what most employers will expect when they ask about API versioning experience.
  • Only increment the major version on breaking changes — not for every new endpoint or optional field
  • Semantic versioning minor bumps (v1.1) are useful for signalling additions without a breaking change
Summary
  • APIs are contracts — once consumed, they cannot change without breaking clients who cannot update instantly
  • Three strategies — URL path, query parameter, and header versioning; URL path is the most practical and widely expected
  • Asp.Versioning makes it easy — [ApiVersion] attributes and route constraints handle all the routing automatically
  • Breaking vs. non-breaking — adding optional fields is safe; removing, renaming, or adding required fields demands a new version
  • Deprecate before you remove — set Deprecated = true, announce a sunset date, and give clients time to migrate
  • Integer versioning is the default — simple, clear, and universally understood
What's Next

Session 07 — Pagination, Filtering & Sorting

  • Your API now supports multiple coexisting versions — Session 07 focuses on making those endpoints scalable for large data sets
  • You will implement cursor-based and page-based pagination so GET /api/v1/tasks does not return ten thousand records at once
  • Filtering and sorting are added as query parameters — tied directly to the concepts of non-breaking changes from today's session
  • These techniques apply equally to every version of a versioned API
Before next session
Complete today's assignment. Your Task API should have working v1 and v2 endpoints, with v1 marked as deprecated and the api-deprecated-versions header confirmed in Postman.
Assignment

Add URL-based versioning to your Task API and introduce a v2 with a breaking response change.

What to build:

  • Install Asp.Versioning.Mvc and configure it in Program.cs with default version 1.0
  • Create TasksV1Controller — returns tasks with id, title, isCompleted
  • Create TasksV2Controller — returns tasks with id, title, status (string: "pending" / "in-progress" / "completed"), dueDate, createdAt
  • Mark v1 as deprecated using Deprecated = true

Acceptance criteria:

  • GET /api/v1/tasks returns the old shape and includes api-deprecated-versions: 1.0 in the response headers
  • GET /api/v2/tasks returns the new shape with the status field
  • Both versions work simultaneously — neither breaks the other
  • Response headers include api-supported-versions: 1.0, 2.0 on every response
Bonus
Add a custom middleware that injects a Sunset header (set to one year from today) whenever the requested version is flagged as deprecated.
Questions?
Session 06 — API Versioning