agentleFS
lgzarturolgzarturo/codeconductor/.agents/skills/api-versioning/SKILL.md

api-versioning

Skill0 starsChanged 25 days agoMITView on GitHub

Provides expert knowledge for designing, implementing, and managing REST API versioning strategies with deprecation workflows.

12 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use dependency-injection, openapi and 2 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • HamzaSheikh768Skill

    dependency-injection

    Leverages FastAPI's powerful Depends system for reusable authentication, database sessions, rate limiting, and business logic injection.

    Also in 3 projects that use this

  • ffsshhttiikkSkill

    openapi

    OpenAPI/Swagger specification and documentation

    Also in 3 projects that use this

  • diego-tobalinaSkill

    logging

    Logging best practices for Java (SLF4J/Logback) and Node.js (Winston/Pino). Use for structured logging, log levels, and avoiding sensitive data exposure.

    Also in 3 projects that use this

  • JohnJBorenSkill

    ci-cd

    CI/CD pipeline design, GitHub Actions, testing automation, and deployment strategies

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 10 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 4 edited versions we read keep these.

  • 10

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this when: you have breaking changes and need maximum visibility and cacheability.

    line 64
  2. Distinctive:

    Use this when: you need clean URLs and your clients are all programmatic (no browsers).

    line 81
  3. Distinctive:

    When a version or endpoint is being phased out, follow this process:

    line 119
  4. Distinctive:

    Keep at least two active major versions at all times.

    line 157
  5. Distinctive:

    Do not remove a version without a minimum 6-month notice period.

    line 169
  6. Very specific:

    Use $ref to share schemas across versions without duplication.

    line 192
  7. Distinctive:

    Put the breaking change in the endpoint description, not just in a changelog:

    line 232
  8. Distinctive:

    Create a new controller for each new version — do not modify the existing one

    line 286
  9. Distinctive:

    Never delete a versioned controller until after the sunset date

    line 290
  10. Distinctive:

    Rule: run contract tests in CI before any merge that touches an API surface.

    line 379
  11. Distinctive:

    Do not share test cases across versions.

    line 393

“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

API Versioning

Versioning Strategies

URL Path Versioning (recommended for breaking changes)

GET /api/v1/users
GET /api/v2/users

Tradeoffs:

  • Explicit and visible in logs, proxies, and browser history
  • Easy to cache at the CDN level — the URL uniquely identifies the resource version
  • Easy to route at the load balancer
  • Results in some duplication of controller code
  • Changing the URL violates REST HATEOAS principles, though in practice this is acceptable

Use this when: you have breaking changes and need maximum visibility and cacheability.

Header Versioning

GET /api/users
Accept: application/vnd.myapp+json;version=1

Tradeoffs:

  • Cleaner URLs
  • Harder to test manually — browsers and curl require extra flags
  • Cannot be bookmarked or linked directly
  • CDN caching requires Vary: Accept header, which reduces cache hit rates

Use this when: you need clean URLs and your clients are all programmatic (no browsers).

Query Parameter Versioning (avoid)

GET /api/users?version=1

This approach contaminates resource URLs with transport concerns. The version is not part of the resource identity. Do not use it. The only valid exception is temporary backward-compat support during a migration window.

When to Version

Version when the change is breaking. Not every change requires a version bump.

Breaking — requires new version:

  • Removing a field from a response
  • Renaming a field
  • Changing a field's type (e.g., string to object)
  • Changing the meaning of an existing field
  • Removing an endpoint
  • Changing required fields in a request
  • Changing status codes in a non-additive way

Not breaking — no version bump needed:

  • Adding an optional field to a response
  • Adding a new endpoint
  • Adding an optional request parameter
  • Deprecating a field (marking it, but still returning it)
  • Performance improvements
  • Bug fixes that restore documented behavior

Deprecation Process

When a version or endpoint is being phased out, follow this process:

Step 1: Mark in OpenAPI.

paths:
  /api/v1/users/{id}:
    get:
      deprecated: true
      description: |
        Deprecated since 2026-05-07. Use /api/v2/users/{id} instead.
        Sunset date: 2026-11-07.
      summary: Get user by ID (deprecated)

Step 2: Add deprecation headers to responses.

@GetMapping("/{id}")
fun getUserV1(@PathVariable id: UUID, response: HttpServletResponse): ResponseEntity<UserV1Response> {
    response.addHeader("Deprecation", "date=\"Wed, 07 May 2026 00:00:00 GMT\"")
    response.addHeader("Sunset", "Mon, 07 Nov 2026 00:00:00 GMT")
    response.addHeader("Link", "</api/v2/users/$id>; rel=\"successor-version\"")
    return ResponseEntity.ok(userService.getById(id).toV1Response())
}

Step 3: Document in CHANGELOG.

## Deprecated

- `GET /api/v1/users/{id}` — deprecated in favor of `GET /api/v2/users/{id}`.
  Sunset: 2026-11-07.

Step 4: Maintain dual support.

Keep at least two active major versions at all times. When v3 ships, v1 can be removed (v2 and v3 remain active).

Step 5: Communicate the sunset date.

Notify consumers before the sunset date through:

  • API changelog
  • Developer portal announcements
  • Deprecation headers (machine-readable)
  • Direct contact if you have consumer registration data

Do not remove a version without a minimum 6-month notice period. 3 months is the absolute minimum if forced.

OpenAPI Conventions

One file per version (simple cases)

openapi-v1.yaml
openapi-v2.yaml

Each file is self-contained and independently valid.

Single file with version in info (evolving APIs)

openapi: '3.1.0'
info:
  title: Users API
  version: '2.0.0'

Use $ref to share schemas across versions without duplication.

Cross-version schema reuse

# schemas/user-base.yaml
UserBase:
  type: object
  properties:
    id:
      type: string
      format: uuid
    email:
      type: string

# openapi-v1.yaml
components:
  schemas:
    UserResponse:
      allOf:
        - $ref: './schemas/user-base.yaml#/UserBase'
        - properties:
            full_name:
              type: string

# openapi-v2.yaml — splits full_name into first_name + last_name
components:
  schemas:
    UserResponse:
      allOf:
        - $ref: './schemas/user-base.yaml#/UserBase'
        - properties:
            first_name:
              type: string
            last_name:
              type: string

Documenting breaking changes

Put the breaking change in the endpoint description, not just in a changelog:

/api/v2/users/{id}:
  get:
    description: |
      Returns user details.

      Breaking changes from v1:
      - `full_name` has been replaced by `first_name` and `last_name`

Spring Boot Implementation

URL path versioning

// V1 controller — never modify once published
@RestController
@RequestMapping("/api/v1/users")
class UserV1Controller(private val userService: UserService) {

    @GetMapping("/{id}")
    @Deprecated("Use /api/v2/users/{id}", ReplaceWith("UserV2Controller.getUser()"))
    fun getUser(
        @PathVariable id: UUID,
        response: HttpServletResponse
    ): ResponseEntity<UserV1Response> {
        response.addHeader("Deprecation", "date=\"Wed, 07 May 2026 00:00:00 GMT\"")
        response.addHeader("Sunset", "Mon, 07 Nov 2026 00:00:00 GMT")
        return when (val result = userService.getById(id)) {
            is UserResult.Found -> ResponseEntity.ok(result.user.toV1Response())
            is UserResult.NotFound -> ResponseEntity.notFound().build()
        }
    }
}

// V2 controller — new version, new controller, shared service
@RestController
@RequestMapping("/api/v2/users")
class UserV2Controller(private val userService: UserService) {

    @GetMapping("/{id}")
    fun getUser(@PathVariable id: UUID): ResponseEntity<UserV2Response> {
        return when (val result = userService.getById(id)) {
            is UserResult.Found -> ResponseEntity.ok(result.user.toV2Response())
            is UserResult.NotFound -> ResponseEntity.notFound().build()
        }
    }
}

Rules:

  • Create a new controller for each new version — do not modify the existing one
  • The service layer is shared across versions — only the controller and DTO change
  • DTO mapper functions are version-specific: toV1Response(), toV2Response()
  • Never delete a versioned controller until after the sunset date

DTO versioning

// V1 — original shape
data class UserV1Response(
    val id: UUID,
    val email: String,
    val full_name: String
)

// V2 — breaking change: split full_name
data class UserV2Response(
    val id: UUID,
    val email: String,
    val first_name: String,
    val last_name: String
)

// Extension functions for mapping
fun User.toV1Response(): UserV1Response = UserV1Response(
    id = id,
    email = email,
    full_name = "$firstName $lastName"
)

fun User.toV2Response(): UserV2Response = UserV2Response(
    id = id,
    email = email,
    first_name = firstName,
    last_name = lastName
)

Contract Testing

Contract tests verify that your API does not break existing consumers before changes reach production.

When to run: in CI, before merging any change that touches a controller, DTO, or OpenAPI spec.

Tool: Pact (consumer-driven contracts)

Consumer writes a pact:

// In the consumer service test
@ExtendWith(PactConsumerTestExt::class)
class UserServiceConsumerTest {

    @Pact(consumer = "order-service", provider = "user-service")
    fun getUserPact(builder: PactDslWithProvider): RequestResponsePact {
        return builder
            .given("user with id exists")
            .uponReceiving("a request for user by id")
            .path("/api/v1/users/123e4567-e89b-12d3-a456-426614174000")
            .method("GET")
            .willRespondWith()
            .status(200)
            .body(LambdaDsl.newJsonBody { body ->
                body.uuid("id")
                body.stringType("email")
                body.stringType("full_name")
            }.build())
            .toPact()
    }
}

Provider verifies the pact:

@Provider("user-service")
@PactFolder("pacts")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserServiceProviderTest {

    @TestTarget
    lateinit var target: HttpTestTarget

    @BeforeEach
    fun setUp(@LocalServerPort port: Int) {
        target = HttpTestTarget("localhost", port)
    }
}

Rule: run contract tests in CI before any merge that touches an API surface. A broken contract test means a consumer will break in production.

Test Structure Per Version

Each API version must have its own test class:

src/test/kotlin/{package}/user/
  controller/
    UserV1ControllerTest.kt   # tests for v1 endpoints
    UserV2ControllerTest.kt   # tests for v2 endpoints

Do not share test cases across versions. V1 behavior must be tested independently from V2 — they can diverge.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.