Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SpringBoot/Kotlin and Versioning through Content Negotiation: correct approach?

I have been experimenting with Content Negotiation as backend versioning for my SpringBoot/Kotlin application. I have the following:

 @GetMapping("/user", produces = [MediaType.APPLICATION_JSON_VALUE])
      fun getUsers() {
        //some code here
      }

I have found this project combining accept" header and a "Accept-Version" custom header. I wonder whether this is the correct way of implementing a content negotiation approach and if not how can I fix it?

@GetMapping("/user", produces = [MediaType.APPLICATION_JSON_VALUE], headers = ["Accept-Version=$CUSTOM_ACCEPT_HEADER"])
          fun getUsers() {
            //some code here
          }

object VersioningUtility {
  const val CUSTOM_ACCEPT_HEADER = "vnd.sample.com-v1+json"
  //here more constants as each controller can be versioned independently
}

Thank you

like image 580
paranzana Avatar asked Jan 25 '23 05:01

paranzana


1 Answers

Yes, you can implement API versioning using content negotiation by having a custom header and header value as you have specified. However, since that is not a standard header, there are other scenarios which you might have to handle by yourself, such as:

  • default representation when the header is not present
  • exception scenarios when invalid media type values are passed as part of the header.

In case you are working with only json responses, the JSON API standard for content negotiation is to send the Accept header with the value application/vnd.api+json. Since Accept is a standard request header, using that is preferred. In case you need to handle other types of responses, you can still go ahead with the custom header.

You can implement content negotiation as below:

@RestController
class UserController {

    @GetMapping("/users", headers = ["Accept=${VersioningUtility.VERSION_1_HEADER}"])
    fun getUser(): ResponseEntity<Any> {
        return ResponseEntity(listOf(User("Abraham Lincoln")), HttpStatus.OK)
    }

    @GetMapping("/users", headers = ["Accept=${VersioningUtility.VERSION_2_HEADER}"])
    fun getNewUser(): ResponseEntity<Any> {
        return ResponseEntity(listOf(NewUser(Name("Abraham", "Lincoln"))), HttpStatus.OK)
    }
}

data class User(val name: String)
data class NewUser(val name: Name)
data class Name(val firstName: String, val lastName: String)

object VersioningUtility {
    const val VERSION_1_HEADER = "application/vnd.v1+json"
    const val VERSION_2_HEADER = "application/vnd.v2+json"
}

The above with enable you to have 2 versions of the GET /users endpoint with the Accept header.

When the curl request is made with v1 of the header value, the response would be according to the version v1

curl -L -X GET 'http://localhost:8080/users' \
-H 'Accept: application/vnd.v1+json'

[
    {
        "name": "Abraham Lincoln"
    }
]

When the curl request is made with v2 of the header value, the response would be according to the version v2

curl -L -X GET 'http://localhost:8080/users' \
-H 'Accept: application/vnd.v2+json'

[
    {
        "name": {
            "firstName": "Abraham",
            "lastName": "Lincoln"
        }
    }
]

When an invalid header value is sent, it would respond with a 406 Not Acceptable

curl -L -X GET 'http://localhost:8080/users' \
-H 'Accept: application/vnd.abc+json'

{
    "timestamp": "2020-04-01T18:33:16.393+0000",
    "status": 406,
    "error": "Not Acceptable",
    "message": "Could not find acceptable representation",
    "path": "/users"
}

When no Accept header is sent, it would respond with the default version, ie v1 here

curl -L -X GET 'http://localhost:8080/users'

[
    {
        "name": "Abraham Lincoln"
    }
]

Even GitHub has implemented versioning with content negotiation in a similar way and you can have a look at that in their documentation.

like image 97
Madhu Bhat Avatar answered May 17 '23 08:05

Madhu Bhat