Sunday, March 24, 2019

Versioning in REST API


Hello Friends, Welcome to my blog. Today our topic for discussion is versioning in REST API. We will be using the spring-boot application to demonstrate versioning whereas REST API Versioning is a Theoretical Concept or I would say a kind of design pattern which solves the industry problem. It is a technology independent and you can apply this pattern with any other REST API development frameworks. Let's get started then !!!

A bit history of a problem to understand the need for versioning
Before digging into the concept, let's understand why versioning. Suppose you have a product, and you are providing services to your clients on software as a service basis. your clients had integrated your REST APIs to their software & mobile application. Now after years you have come across a situation, where one of your client "Mr. X" requesting some enhancement of API Contract in the existing feature, but at the same time, the changes would break the already in place integration for the customer "Mr. A".

How would you handle this situation? The answer is versioning. You need 2 different version of the same REST API. one is old which is already used by "Mr. A" & another V2 is for "Mr. X" where they are requesting new features.

Ways of versioning
There are many ways by which you can archive versioning in your REST Apis. Out which we will be discussing 2 of them.

  • Custom Content Type
  • Using Version number in URI

Above 2 approaches are widely adopted in the industry. there are good benefits of using those pattern. versioning comes with the cost of code complexity. Your approach should be such that you should be able to maintain both version of the code with ease, minimum effort, and less mess. Let's have a close look at both of them.

Versioning through Custom Content Type
In this technique of versioning, we will be creating a new media type for each representation of the version of an entity. It will be passed through the header "content-type" & "Accept" depending up the operation type GET, POST, PUT, HEAD. 

Example for old customer representation media-type will be "application/vnd.javahotfix.customer.v1+json" and for the newly updated contract for the customer entity could be represented by media-type "application/vnd.javahotfix.customer.v2+json". 

With this change, old clients could call our REST API through v1 media-type and the new client can call our REST API through v2 media-type. We will be covering this in our example below.

Versioning using URI
In this technique, we will be using a URI such way that it includes the version number in it. once we need changes in the contract, we will keep the old as it is and will create new REST API and assigning a new version number in the URI. 

For example, to get the old representation of the customer entity we will give a REST call to the API in like "GET http://something.xyz/v1/customer/1234". To get the new representation of the customer entity one can call "GET http://something.xyz/v2/customer/1234".

Let's take a look at the below code snippet. We are storing the customer entity in our repository. This was our initial version of the code.

Existing system design
// Customer Representation object which will be the end user contract
public class CustomerRep {
 private int id;
 private String name;
 private String city;
 private String status;
 
 //getter and setters for above fields
}

// Customer DTO Object which will be stored in Database
public class CustomerDto {
 private int id;
 private String name;
 private String city;
 private String status;
 
 //getter and setters for above fields
}

// Controller which will take care of GET and POST Methods
@Controller
public class CustomerController {

 @Autowired
 private CustomerRepository repo;
 
 @RequestMapping(value="/customer",method=RequestMethod.POST,
   consumes={"application/json"})
 public ResponseEntity create(@RequestBody CustomerRep rep) {
  CustomerDto dto = CustomerConverter.convertToDto(rep);
  repo.save(dto);
  
  CustomerRep rt = CustomerConverter.convertToRep(dto);
  return new ResponseEntity(rt, HttpStatus.CREATED);
 }
 
 @RequestMapping(value="/customer/{id}",method=RequestMethod.GET, 
   produces={"application/json"})
 public ResponseEntity getCustomerRepV1(@PathVariable(name="id") int id) {
  
  CustomerDto dto = repo.get(id);
  CustomerRep rep =  CustomerConverter.convertToRep(dto);
  return new ResponseEntity(rep, HttpStatus.OK);
 }
} 


Based on the above coding snippets, now we want to enhance the customer entity.

  • Support 2 new fields email address which is a mandatory filed and contact number. 
  • Make sure that our old endpoint remains working as it is as it was earlier because they might be integrated with many other systems and changing to API will break many things.
  • Create new API which will support this new representation of customer entity.

So let's have a look at the first approach of custom media-type. from below snippet, you can get to know how this technique will help us for archiving above requirement/versioning.


Example for Versioning Using Custom Media Type
// Customer Representation object which will be the end user contract
public class CustomerRep {
 private int id;
 private String name;
 private String city;
 private String status;
 
 //getter and setters for above fields
}

// Customer Representation object for V2 Version 
public class CustomerV2Rep extends CustomerRep {
 private String emailAddress;
 private String contactNumer;
 
 // getter and setters
}

// Customer DTO Object which will be stored in Database
public class CustomerDto {
 private int id;
 private String name;
 private String city;
 private String status;
 private String emailAddress; // new filed added in database
 private String contactNumber; // new filed added in database
 
 //getter and setters for above fields
}

public class CustomerConverter {
 public static CustomerDto convertToDto(CustomerRep rep1) {
  CustomerDto dto = new CustomerDto();
  dto.setId(rep1.getId());
  dto.setCity(rep1.getCity());
  dto.setName(rep1.getName());
  dto.setStatus(rep1.getStatus());
  
  // Setting not applicable defaults value to new Parameter 
  // added as part of V2 of customer
  dto.setEmailAddress("NA");
  dto.setContactNumber("NA");
  return dto;
 }
 
 public static CustomerDto convertToDto(CustomerV2Rep rep1) {
  CustomerDto dto = new CustomerDto();
  dto.setId(rep1.getId());
  dto.setCity(rep1.getCity());
  dto.setName(rep1.getName());
  dto.setStatus(rep1.getStatus());
  dto.setEmailAddress(rep1.getEmailAddress());
  dto.setContactNumber(rep1.getContactNumer());
  return dto;
 }
 
 public static CustomerRep convertToRep(CustomerDto dto) {
  CustomerRep rep = new CustomerRep();
  rep.setId(dto.getId());
  rep.setCity(dto.getCity());
  rep.setName(dto.getName());
  rep.setStatus(dto.getStatus());
  return rep;
 }
 
 public static CustomerV2Rep convertToRepV2(CustomerDto dto) {
  CustomerV2Rep rep = new CustomerV2Rep();
  rep.setId(dto.getId());
  rep.setCity(dto.getCity());
  rep.setName(dto.getName());
  rep.setStatus(dto.getStatus());
  rep.setEmailAddress(dto.getEmailAddress());
  rep.setContactNumer(dto.getContactNumber());
  return rep;
 }
 
}

// Controller which will take care of GET and POST Methods
@Controller
public class CustomerController {

 @Autowired
 private CustomerRepository repo;
 
 // Create customer object with V1 or default version of the entity
 @RequestMapping(value="/customer",method=RequestMethod.POST,
   consumes={"application/vnd.customer.api.v1+json",
   "application/json"})
 public ResponseEntity create(@RequestBody CustomerRep rep) {
  CustomerDto dto = CustomerConverter.convertToDto(rep);
  repo.save(dto);
  
  CustomerRep rt = CustomerConverter.convertToRep(dto);
  return new ResponseEntity(rt, HttpStatus.CREATED);
 }
 
 
 // Create customer object with V2 version of the customer entity
 @RequestMapping(value="/customer",method=RequestMethod.POST, 
   consumes={"application/vnd.customer.api.v2+json"})
 public ResponseEntity createV2(@RequestBody CustomerV2Rep rep) {
  
  // validate new attribute
  if(rep.getEmailAddress() == null) {
   return new ResponseEntity (
    new Exception(
     "Email address is madanatory for customer"),
     HttpStatus.BAD_REQUEST );
  }
  
  CustomerDto dto = CustomerConverter.convertToDto(rep);
  repo.save(dto);
  
  CustomerV2Rep rt = CustomerConverter.convertToRepV2(dto);
  return new ResponseEntity(rt, HttpStatus.CREATED);
 }
 

 // Get the V1 or default Version of the Customer entity
 @RequestMapping(value="/customer/{id}",method=RequestMethod.GET, 
   produces={"application/vnd.customer.api.v1+json",
    "application/json"})
 public ResponseEntity getCustomerRepV1(@PathVariable(name="id") int id) {
  
  CustomerDto dto = repo.get(id);
  CustomerRep rep =  CustomerConverter.convertToRep(dto);
  return new ResponseEntity(rep, HttpStatus.OK);
 } 
 
 
 // Get the V2 version of the customer entity
 @RequestMapping(value="/customer/{id}",method=RequestMethod.GET,
   produces={"application/vnd.customer.api.v2+json"})
 public ResponseEntity getCustomerRepV2(@PathVariable(name="id") int id) {
  CustomerDto dto = repo.get(id);
  CustomerV2Rep rep =  CustomerConverter.convertToRepV2(dto);
  return new ResponseEntity(rep, HttpStatus.OK);
 }
 
} 

Here is the summery for the above code snippet.

  • CustomerRep is the primary customer entity representation.
  • We have created new Class extending above class named CustomerV2Rep
  • CustomerV2Rep represent the customer with new attributes
  • CustomerDto is updated for newly required fields
  • We have written DTO to REP Converter for Customer having all static methods. this allows us to convert the DTO to both versions of REPS.
  • In controller class, we have create() & createV2() methods which create the customer entity in the database.
  • create() method is default one which can consume content type of application/json and application/vnd.customer.api.v1+json
  • createV2() is the new method which is consume newer version of customer ie. application/vnd.customer.api.v2+json
  • similar for GET methods, we have 2 version each of them produces the  old and new version of entity i.e. application/vnd.customer.api.v1+json, application/vnd.customer.api.v2+json


I have executed a few flows for above and below is the result
$ curl -s --request POST \
 --header "Content-type:application/json" \
 --data '{ "id" : 1, "name" : "Anil", "city" : "pune", "status" : "active" }' \
 http://localhost:8080/customer
{"id":1,"name":"Anil","city":"pune","status":"active"}

$ curl -s --request POST \
 --header "Content-type:application/vnd.customer.api.v1+json" \
 --data '{ "id" : 2, "name" : "Scott", "city" : "Mumbai", "status" : "active" }' \
 http://localhost:8080/customer
{"id":2,"name":"Scott","city":"Mumbai","status":"active"}

$ curl -s --request POST \
 --header "Content-type:application/vnd.customer.api.v2+json" \
 --data '{ "id" : 3, "name" : "tiger", "city" : "Mumbai", "status" : "active" }' \
 http://localhost:8080/customer
{....removed..unnecessary..content.., "localizedMessage":"Email address is madenatory for customer", "message":"Email address is madanatory for customer","suppressed":[]}
 
$ curl -s --request POST \
 --header "Content-type:application/vnd.customer.api.v2+json" \
 --data '{ "id" : 3, "name" : "tiger", "city" : "Mumbai", "status" : "active", "emailAddress" : "tiger@gmail.com" , "contactNumer" : "+91-9876543210" }' \
 http://localhost:8080/customer
{"id":3,"name":"tiger","city":"Mumbai","status":"active","emailAddress":"tiger@gmail.com","contactNumer":"+91-9876543210"}

$ curl -s --request GET \
 --header "Accept:application/vnd.customer.api.v2+json" \
 http://localhost:8080/customer/1
{"id":1,"name":"Anil","city":"pune","status":"active","emailAddress":"NA","contactNumer":"NA"}

$ curl -s --request GET \
 --header "Accept:application/vnd.customer.api.v2+json" \
 http://localhost:8080/customer/3
{"id":3,"name":"tiger","city":"Mumbai","status":"active","emailAddress":"tiger@gmail.com","contactNumer":"+91-9876543210"}

$ curl -s --request GET \
 --header "Accept:application/vnd.customer.api.v1+json" \
 http://localhost:8080/customer/1
{"id":1,"name":"Anil","city":"pune","status":"active"}

$ curl -s --request GET \
 --header "Accept:application/vnd.customer.api.v1+json" \
 http://localhost:8080/customer/3
{"id":3,"name":"tiger","city":"Mumbai","status":"active"}

$ curl -s --request GET  http://localhost:8080/customer/3
{"id":3,"name":"tiger","city":"Mumbai","status":"active"}


Example of Versioning through URI
So in this approach, everything remains the same as above only our controller class will change. We will be using one class perversion strategy. Have a look at below controller classes & you will come to know how we can implement this.
@Controller
public class CustomerControllerURI {

 @Autowired
 private CustomerRepository repo;

 // Create V1 or default version of the customer entity
 @RequestMapping(value={"customer", "v1/customer"}, method=RequestMethod.POST)
 public ResponseEntity create(@RequestBody CustomerRep rep) {
  CustomerDto dto = CustomerConverter.convertToDto(rep);
  repo.save(dto);

  CustomerRep rt = CustomerConverter.convertToRep(dto);
  return new ResponseEntity(rt, HttpStatus.CREATED);
 }

 // Get the V1 or default Version of the Customer entityentity
 @RequestMapping(value={"customer/{id}", "v1/customer/{id}"},method=RequestMethod.GET)
 public ResponseEntity getCustomerRepV1(@PathVariable(name="id") int id) {
  CustomerDto dto = repo.get(id);
  CustomerRep rep =  CustomerConverter.convertToRep(dto);
  return new ResponseEntity(rep, HttpStatus.OK);
 } 

}

@Controller
public class CustomerControllerURIV2 {
 @Autowired
 private CustomerRepository repo;
 
 // Create V2 version of the customer entity
 @RequestMapping(value="v2/customer",method=RequestMethod.POST)
 public ResponseEntity createV2(@RequestBody CustomerV2Rep rep) {

  // validate new attribute
  if(rep.getEmailAddress() == null) {
   return new ResponseEntity (
   new Exception("Email address is madanatory for customer"),
   HttpStatus.BAD_REQUEST );
  }

  CustomerDto dto = CustomerConverter.convertToDto(rep);
  repo.save(dto);

  CustomerV2Rep rt = CustomerConverter.convertToRepV2(dto);
  return new ResponseEntity(rt, HttpStatus.CREATED);
 }

 // Get the V2 version of the customer entity
 @RequestMapping(value="v2/customer/{id}",method=RequestMethod.GET)
 public ResponseEntity getCustomerRepV2(@PathVariable(name="id") int id) {
  CustomerDto dto = repo.get(id);
  CustomerV2Rep rep =  CustomerConverter.convertToRepV2(dto);
  return new ResponseEntity(rep, HttpStatus.OK);
 }
}


I hope you have understood both the patterns well. please share your feedback, question in the comment section below. I will definitely try to address your queries.

Thanks
Happy Coding and have a wonderful day !!!

3 comments: