本文へ移動
cccskills
無料GitHub で公開

api-versioning

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

インストール方法を見る

含まれるファイル(1)

  • SKILL.md9.7 KB

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.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when adding monitoring, metrics, logging, or tracing to Spring AI or LLM integration code. Covers token tracking, latency measurement, cost estimation, and prompt/response logging. Use when user mentions AI monitoring, token costs, or LLM observability.

日本語の概要は準備中です。原文の説明を表示しています。

lgzarturo/springboot-course32026年10月12日 更新

android

無料

Provides expert knowledge for building modern native Android apps using Kotlin, Jetpack Compose, Material Design 3, MVI architecture, Hilt dependency injection, and Media3/ExoPlayer.

日本語の概要は準備中です。原文の説明を表示しています。

lgzarturo/springboot-course32026年10月12日 更新

Design, implement, review, and audit Android Jetpack Compose UI/UX for phone interfaces. Use when a request concerns concrete Compose layout, components, interaction, accessibility, or visual behavior. Do not activate for framework or dependency presence alone; non-UI Android work is excluded.

日本語の概要は準備中です。原文の説明を表示しています。

lgzarturo/springboot-course32026年10月12日 更新

astro

無料

Provides expert knowledge for building Astro 5+ sites with Islands Architecture, Content Collections, TypeScript, and performance-first rendering strategies.

日本語の概要は準備中です。原文の説明を表示しています。

lgzarturo/springboot-course32026年10月12日 更新

Audits token storage mechanisms to prevent XSS-based JWT theft.

日本語の概要は準備中です。原文の説明を表示しています。

lgzarturo/springboot-course32026年10月12日 更新

Provides patterns to configure AWS RDS (Aurora, MySQL, PostgreSQL) with Spring Boot applications. Configures HikariCP connection pools, implements read/write splitting, sets up IAM database authentication, enables SSL connections, and integrates with AWS Secrets Manager. Use when setting up RDS connections in Spring Boot, configuring connection pooling, or managing database credentials securely.

日本語の概要は準備中です。原文の説明を表示しています。

lgzarturo/springboot-course32026年10月12日 更新

lgzarturo のスキルをすべて見る

このスキルの問題を報告する