# Adding an API version

> Introduce a new API version (v2) without breaking existing clients.

## How versioning is wired

Every route in Slicekit lives under `/api/{version}`. The version segment is not a string scattered
across endpoints: it comes from a single registry, and each version owns a folder of endpoint
classes that are discovered at compile time. Adding `v2` means registering one more version, pointing
the source generator at a new folder, and writing the endpoints. The `v1` surface keeps working
untouched, because nothing about it changes.

The pieces involved, all in `api/src/Slicekit.Api`:

```
Configuration/Versioning.cs        // the list of known versions
Endpoints/RegisterEndpoints.cs     // scans folders, maps each version group
Endpoints/Groups.cs                // route group helpers: Users(), Auth(), ...
Endpoints/v1/                      // the existing version's endpoint classes
```

Endpoints themselves are thin HTTP adapters. The command, handler and validation behind them live in
`Slicekit.Core` and are shared across versions, so a new version is a new shape over the same business
logic. See [Adding a vertical slice](/docs/vertical-slices) for that split, and
[Architecture](/docs/architecture) for the wider picture.

## 1. Register the version

`ApiVersions` is the single source of truth. Add the new constant and put it in `All`, in
`api/src/Slicekit.Api/Configuration/Versioning.cs`:

```csharp
namespace Slicekit.Api.Configuration;

internal static class ApiVersions
{
    public const string V1 = "v1";
    public const string V2 = "v2";

    public static readonly string[] All = [V1, V2];
}
```

`All` drives OpenAPI document generation, so a Swagger document for `v2` appears automatically.
Endpoint registration is still explicit (step 2): listing the version is not the same as mapping its
routes.

## 2. Scan and map the new folder

Endpoints are not registered by hand. `ServiceScan.SourceGenerator` finds every `IEndpoint` whose
type name matches a filter and emits the `Map...` method at compile time. Add a `[ScanForTypes]`
attribute plus a matching partial method to `api/src/Slicekit.Api/Endpoints/RegisterEndpoints.cs`,
mirroring the `v1` block:

```csharp
[ScanForTypes(AssignableTo = typeof(IEndpoint), Handler = "Map",
    TypeNameFilter = "Slicekit.Api.Endpoints.V2.*")]
private static partial IEndpointRouteBuilder MapV2Endpoints(this IEndpointRouteBuilder routes);
```

Then mount it under a version group in `MapApiEndpoints`, next to the existing `v1` wiring:

```csharp
var v2 = app.VersionGroup(ApiVersions.V2);
v2.MapV2Endpoints();
```

`VersionGroup` is the private helper that prefixes `/api/{version}`, requires authorization, applies
the default rate limiter and the TOTP-setup filter, and advertises a 401 response. Every version
inherits that baseline, so a `v2` route is secured the same way a `v1` route is.

## 3. Create the endpoint classes

Add `api/src/Slicekit.Api/Endpoints/v2/` and place one class per endpoint, grouped into subfolders by
domain (`v2/Me/`, `v2/Admin/`, and so on) exactly as `v1` is laid out. The namespace must start with
`Slicekit.Api.Endpoints.V2` so the scan filter from step 2 picks it up.

Each class implements `IEndpoint`, declares its route group with an extension from `Groups.cs`, and
dispatches to a `Slicekit.Core` handler over Wolverine's message bus:

```csharp
using Slicekit.Core.Features.Users.GetMe;
using Slicekit.Core.Permissions;

namespace Slicekit.Api.Endpoints.V2.Me;

internal sealed class GetMeEndpoint : IEndpoint
{
    public static void Map(IEndpointRouteBuilder routes) =>
        routes.Users().MapGet("/me", HandleAsync)
            .WithName("V2_Users_GetMe")
            .WithSummary("Get current user")
            .WithTags("Users")
            .RequirePermission(Allow.UserGetMe)
            .AllowWithoutTotpSetup()
            .Produces()
            .ProducesProblem(403);

    private static async Task<Results<Ok, ProblemHttpResult>> HandleAsync(
        ClaimsPrincipal principal,
        IMessageBus bus,
        CancellationToken ct)
    {
        var userId = principal.TryGetUserId()
            ?? throw new UnauthorizedAccessException("User ID claim not found or invalid.");
        var result = await bus.InvokeAsync<Result>(
            new GetMeQuery(userId, principal.TryGetActingUserId()), ct);
        if (!result.IsSuccess) return result.Error.ToProblem();

        var r = result.Value;
        return TypedResults.Ok(new Response(r.Id, r.Email, r.DisplayName));
    }

    public sealed record Response(Guid Id, string Email, string? DisplayName);
}
```

Two conventions matter here:

- **Give the endpoint a version-scoped name.** `WithName` must be unique across the whole app, so
  prefix it with the version (`V2_Users_GetMe`). The `v1` class keeps its own `Users_GetMe`.
- **Reuse the handler, change only the shape.** The example above returns a leaner `Response` than
  `v1` does, but it sends the same `GetMeQuery`. New versions are where you reshape requests and
  responses; the logic in `Slicekit.Core` stays shared. If a version needs genuinely new behavior, add
  a new command and handler as a [vertical slice](/docs/vertical-slices) rather than branching on the
  version inside a handler.

## 4. Add a route group if needed

The route group helpers live in `api/src/Slicekit.Api/Endpoints/Groups.cs` as extension methods on
`IEndpointRouteBuilder`:

```csharp
internal static class ApiGroups
{
    extension(IEndpointRouteBuilder routes)
    {
        public RouteGroupBuilder Auth() => routes.MapGroup("/auth");
        public RouteGroupBuilder Users() => routes.MapGroup("/users");
        public RouteGroupBuilder ApiKeys() => routes.MapGroup("/api-keys");
        public RouteGroupBuilder Admin() => routes.MapGroup("/admin");
    }
}
```

These are version-agnostic: `routes.Users()` resolves to `/api/v1/users` or `/api/v2/users`
depending on the version group the endpoint is mapped under. Reuse the existing groups. Only add a new
one when `v2` introduces a domain that does not exist yet, for example:

```csharp
public RouteGroupBuilder Payments() => routes.MapGroup("/payments");
```

## Keeping v1 working

Nothing in steps 1 through 4 edits the `v1` folder, the `v1` scan attribute, or `Users_GetMe`. That is
the point: `v1` clients keep hitting `/api/v1/...` against unchanged endpoints while `v2` is built
alongside. A few rules keep it that way:

- **Never reshape a `v1` response in place.** Changing the JSON a `v1` endpoint returns is a breaking
  change even though the route is identical. Reshape in `v2` instead.
- **Share handlers, fork endpoints.** Edits to a `Slicekit.Core` handler reach every version. If a
  change would alter what `v1` returns, branch it into a new slice rather than mutating the shared
  handler.
- **The frontend pins its version.** The SPA hard-codes the version in each `apiFetch('/api/v1/...')`
  call. Shipping `v2` does not move the frontend automatically. Migrate it slice by slice in
  `frontend/src/features/<slice>/api.ts` when each `v2` endpoint is ready. See the
  [API client](/docs/api-client) for how those calls are structured.

## Checklist

- [ ] Added the version constant and entry in `Configuration/Versioning.cs` (`ApiVersions.All`).
- [ ] Added a `[ScanForTypes]` attribute and partial `MapV2Endpoints` method in `RegisterEndpoints.cs`.
- [ ] Mounted the version group with `app.VersionGroup(ApiVersions.V2).MapV2Endpoints()`.
- [ ] Created `Endpoints/v2/` with endpoint classes under the `Slicekit.Api.Endpoints.V2` namespace.
- [ ] Gave every endpoint a version-prefixed `WithName` (`V2_...`).
- [ ] Reused `Slicekit.Core` handlers; reshaped only the request and response records.
- [ ] Added new route groups in `Groups.cs` only for genuinely new domains.
- [ ] Left the `v1` folder, scan filters, and response shapes untouched.
- [ ] Migrated frontend calls in `frontend/src/features/<slice>/api.ts` as each `v2` endpoint lands.
