Baeldung Pro – Kotlin – NPI EA (cat = Baeldung on Kotlin)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Overview

In this tutorial, we’ll learn how to implement API versioning in a Spring Boot application using Kotlin. API versioning is a common practice in RESTful APIs to manage changes in the API over time. It lets clients specify the API version they want to use and helps maintain backward compatibility.

Multiple methods exist to version an API, such as using different paths, query parameters, custom headers, or the Accept header. We’ll explore and implement them in a Spring Boot application using Kotlin.

2. Code Example

To demonstrate API versioning, we’ll create a simple Spring Boot application with a RESTful API that returns a greeting message. We’ll then implement versioning strategies to access different versions of the API.

2.1. Dependencies

First, let’s set up a new Spring Boot project. We’ll use the Spring Boot Web Starter to create a web application. We’ll also need the Spring Boot Starter Test dependency for testing the application:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    <dependency>
    </dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2. Creating the API

Next, let’s create a simple REST controller that returns a greeting message:

@RestController
class GreetingController {

    @GetMapping("/greeting")
    fun greeting(): String {
        return "Hello, welcome to the API!"
    }
}

We use the @RestController annotation to mark the class as a controller and the @GetMapping annotation to map the method to the /greeting endpoint. When we access this endpoint, the method returns the greeting message.

In the coming sections, we’ll introduce different implementations of this controller that support API versioning.

3. Versioning Using Different Paths

One common way to version an API is to use different paths for each version. To differentiate between API versions, we can include the version number in the URL path.

3.1. Changing Path Mapping

Let’s change our controller to support versioning using different paths:

@GetMapping("/v1/greeting")
fun greetingV1(): String {
    return "Hello, welcome to the API v1!"
}

@GetMapping("/v2/greeting")
fun greetingV2(): String {
    return "Hello, welcome to the API v2!"
}

In this implementation, we define two different versions of the greeting endpoint: /v1/greeting and /v2/greeting. Clients can access an exact version by specifying the version number in the URL path. This is easy to implement and understand but can clutter the URL path with version numbers.

3.2. Testing Path-Based Versioning

Let’s test the path-based versioning by accessing the different versions of the API:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PathBasedGreetingControllerIntegrationTest {
    @Autowired
    lateinit var testRestTemplate: TestRestTemplate

    @LocalServerPort
    var port: Int = 0

    @Test
    fun `given version 1 in path when greeting then return v1 message`() {
        val response = testRestTemplate
          .getForEntity("http://localhost:$port/v1/greeting", String::class.java)
        assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
        assertThat(response.body).isEqualTo("Hello, welcome to the API v1!")
    }

    @Test
    fun `given version 2 in path when greeting then return v2 message`() {
        val response = testRestTemplate
          .getForEntity("http://localhost:$port/v2/greeting", String::class.java)
        assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
        assertThat(response.body).isEqualTo("Hello, welcome to the API v2!")
    }
}

In this test, we use the TestRestTemplate to access the different versions of the API and validate the response. We check that the response status is OK and the response body matches the expected message for each version.

4. Versioning Using Query Param

Another way to version an API is to use query parameters. We can include the version number as a query parameter in the URL to specify the API version.

4.1. Adding Query Parameter Check

Let’s modify our controller to support versioning using query parameters:

@GetMapping("/greeting", params = ["version=1"])
fun greetingV1(): String {
    return "Hello, welcome to the API v1!"
}

@GetMapping("/greeting", params = ["version=2"])
fun greetingV2(): String {
    return "Hello, welcome to the API v2!"
}

Here, we add the params = [“version=1”] check to the @GetMapping annotation to map the method to the /greeting endpoint only when the version query parameter is 1. We create separate methods for each API version.

This approach keeps the URL path clean and lets clients specify the version number dynamically.

4.2. Testing Query-Based Versioning

Let’s test the query-based versioning by accessing the different versions of the API using query parameters:

@Test
fun `given version 1 in query param when greeting then return v1 message`() {
    val response = testRestTemplate
      .getForEntity("http://localhost:$port/greeting?version=1", String::class.java)
    assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(response.body).isEqualTo("Hello, welcome to the API v1!")
}

@Test
fun `given version 2 in query param when greeting then return v2 message`() {
    val response = testRestTemplate
      .getForEntity("http://localhost:$port/greeting?version=2", String::class.java)
    assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(response.body).isEqualTo("Hello, welcome to the API v2!")
}

Here we add the version number as a query parameter in the URL when accessing the different versions of the API. We validate the response status and body for each version.

5. Versioning Using Custom Header

We can also use custom headers to specify the API version. Clients can include a custom header in the HTTP request to indicate the version they want to use.

5.1. Adding Custom Header Check

Let’s update our controller to support versioning using custom headers:

@RestController
class HeaderBasedGreetingController {
    @GetMapping("/greeting", headers = ["X-API-Version=1"])
    fun greetingV1(): String {
        return "Hello, welcome to the API v1!"
    }
    
    @GetMapping("/greeting", headers = ["X-API-Version=2"])
    fun greetingV2(): String {
        return "Hello, welcome to the API v2!"
    }
}

Similar to the query parameter versioning, we can use the headers attribute in the @GetMapping annotation to map the method to the /greeting endpoint based on the custom header value. Clients can include a custom header X-API-Version with the version number in the request to access different versions of the API.

5.2. Testing Header-Based Versioning

Let’s test the header-based versioning by including a custom header in the HTTP request to access the different versions of the API:

@Test
fun `given version 1 in header when greeting then return v1 message`() {
    val headers = HttpHeaders()
    headers.set("X-API-Version", "1")
    val entity = HttpEntity<String>(headers)
    val response = testRestTemplate
      .exchange("http://localhost:$port/greeting", HttpMethod.GET, entity, String::class.java)
    assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(response.body).isEqualTo("Hello, welcome to the API v1!")
}

@Test
fun `given version 2 in header when greeting then return v2 message`() {
    val headers = HttpHeaders()
    headers.set("X-API-Version", "2")
    val entity = HttpEntity<String>(headers)
    val response = testRestTemplate
      .exchange("http://localhost:$port/greeting", HttpMethod.GET, entity, String::class.java)
    assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(response.body).isEqualTo("Hello, welcome to the API v2!")
}

Here we include a custom header X-API-Version with the version number in the HTTP request when accessing the different API versions. We validate the response status and body for each version.

6. Versioning Using Content Negotiation

Another way to version an API is to use content negotiation. Clients can specify the API version they want to use in the Accept header of the HTTP request. This is useful when clients want to receive different representations of the same resource based on the version.

For example, if the API response structure changes between versions, clients can specify the version they want to use in the Accept header to receive the appropriate response.

6.1. Adding Content Negotiation Check

Let’s update our controller to support versioning using content negotiation. Let’s say the API v2 response includes additional information compared to the API v1 response:

@GetMapping("/greeting", produces = ["application/vnd.api.v1+json"])
fun greetingV1(): Map<String, String> {
    return mapOf("message" to "Hello, welcome to the API v1!")
}

@GetMapping("/greeting", produces = ["application/vnd.api.v2+json"])
fun greetingV2(): GreetingV2 {
    val messageMap = HashMap<String, String>()
    messageMap["message"] = "Hello, welcome to the API v2!"
    messageMap["additionalInfo"] = "This is additional information for API v2"
    return messageMap
}

In this implementation, we use the produces attribute in the @GetMapping annotation to specify the media type for the response. Clients can include the Accept header with the appropriate media type to access different API versions.

As our API v2 response includes additional information compared to the API v1 response, we return a Map with additional information for API v2. This demonstrates how versioning is useful when the response structure changes between versions.

6.2. Testing the Content Negotiation Versioning

Let’s test the content negotiation versioning by including the Accept header in the HTTP request to access the different versions of the API:

@Test
fun `given version 1 in Accepts header when greeting then return v1 message`() {
    val headers = HttpHeaders()
    headers.set("Accept", "application/vnd.api.v1+json")
    val entity = HttpEntity<String>(headers)
    val response = testRestTemplate.exchange("http://localhost:$port/greeting",
      HttpMethod.GET, entity, object : ParameterizedTypeReference<Map<String, String>>() {})
    assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(response.body).containsEntry("message", "Hello, welcome to the API v1!")
}

@Test
fun `given version 2 in Accepts header when greeting then return v2 message`() {
    val headers = HttpHeaders()
    headers.set("Accept", "application/vnd.api.v2+json")
    val entity = HttpEntity<String>(headers)
    val response = testRestTemplate.exchange("http://localhost:$port/greeting",
      HttpMethod.GET, entity, object : ParameterizedTypeReference<Map<String, String>>() {})
    assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(response.body).containsEntry("message", "Hello, welcome to the API v2!")
    assertThat(response.body).containsEntry("additionalInfo", "This is additional information for API v2")
}

Here we include the Accept header with the appropriate media type in the HTTP request when accessing the different versions of the API. We validate the response status and body for each version.

7. Conclusion

In this article, we learned how to implement API versioning in a Spring Boot application using Kotlin. We explored different versioning strategies like using different paths, query parameters, custom headers, and content negotiation.

Each strategy has its pros and cons, and the choice of versioning strategy depends on the requirements of the API and the clients using it.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.