Booting a REST API with swagger

Generating an easy-to-use documented REST API, and run it on-the-fly

Publicado en CodingMarkers el 28 de marzo del 2018

Booting a REST API with swagger

One of the most usual tasks requested for back-end developers is generating a microservices oriented API. When we talk about microservices, we refer to small sets of endpoints, each one of them resolving a different problem, which can be deployed individually in a fast and simple way, so they are easy to escalate in case we need to take care of high amount of requests in a certain area. This is a great advantage compared to the classic monolithic architecture, which requires the deployment of all the services at once, taking more time and resources.

REST services are more efficient than SOAP services, and also easier to securize, so it’s no surprise they are becoming so popular, but deploying and testing the services can be troublesome. Fortunately there are some frameworks which provide us some easy-to-use tools.

Let’s split this issue in 3 steps:

  1. Generating the REST service using JSON.
  2. Booting the service with Spring boot.
  3. Document it and get a user-friendly interface for tests with Swagger.

In our example code, we are going to simulate a small system to handle a videogame database via REST. The code packages will be defined as follows:

  • The API package will contain all the REST API, dividing it by layers (services, data access and data transfer objects), and providing the controller with the endpoints.
  • To set up the swagger system we will need a configuration file, plus the API package classes should get new annotations to document its content.
  • Finally we will add the generic Spring boot runner, which may be used for any package and requires very little tuning.
The hierarchical packages structure
The hierarchical packages structure

 

1. Designing a dummy RESTful web service

The first step is creating a basic structure for the REST service. We’ll split it up in different layers:

  • The controller would be VideogameController, and would contain all the endpoints, we’ll leave this for later as, we’ll talk about setting it up with swagger.
  • The interface IVideogameService will define the service layer, which would have the “normal” endpoints.
  • The IVideogameDAO gives an idea of the methods available through the Data Access Layer. In order to void much otrouble setting up a database connection or a real repository, we will mock it.

A good practice is to split the design and the implementation by using interfaces. It’s not really necessary, but it will make the code more reusable.

The UML diagram with the layers
The UML diagram with the layers

Therefore the Service would simulate commonly known as CRUD operations: get (find), set (update), add (save) and remove (delete).

import org.thalion.snippet.swagger.api.dto.Videogame;

public interface IVideogameService {
  Collection<Videogame> findAll();
  Videogame findOne(String name);
  String save(Videogame videogame);
  void update(Videogame setName);
  void delete(String name);
}

The service won’t have much to do today, it will only connect to the data access layer and let it handle all the database work.

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thalion.snippet.swagger.api.dao.IVideogameDAO;
import org.thalion.snippet.swagger.api.dto.Videogame;

@Service
public class VideogameService implements IVideogameService {

  @Autowired
  private IVideogameDAO dao;

  @Override
  public Collection<Videogame> findAll() {
    return dao.findAll();
  }

  @Override
  public Videogame findOne(String name) {
    return dao.findOne(name);
  }

  @Override
  public String save(Videogame videogame) {
    return dao.save(videogame);
  }

  @Override
  public void update(Videogame videogame) {
    update(videogame);
  }

  @Override
  public void delete(String name) {
    dao.delete(name);
  }
}

The Data Access Object or DAO will actually do the hard work, it’s the one which will connect to the database.

import java.util.Collection;
import org.thalion.snippet.swagger.api.dto.Videogame;

public interface IVideogameDAO {
  Collection<Videogame> findAll();
  Videogame findOne(String name);
  String save(Videogame videogame);
  void update(Videogame videogame);
  void delete(String name);
}

But in this case we want to keep it simple since we are focusing on the services layer., so instead of configuring a database connection and writting some queries, we are going to mock it with a simple Map structure.

import java.util.Collection;
import java.util.Map;

import org.springframework.stereotype.Repository;
import org.thalion.snippet.swagger.api.dto.Videogame;

@Repository
public class VideogameDAOMocker implements IVideogameDAO {

  private Map<String, Videogame> storage;

  @Override
  public Collection<Videogame> findAll() {
    return storage.values();
  }

@Override
public Videogame findOne(String name) {
  return storage.get(name);
}

  @Override
  public String save(Videogame videogame) {
    storage.put(videogame.getName(), videogame);
    return videogame.getName();
  }

  @Override
  public void update(Videogame videogame) {
    storage.put(videogame.getName(), videogame);
  }

  @Override
  public void delete(String name) {
    storage.remove(name);
  }
}

The Data Transfer Object or DTO isn’t really interesting right now, as it would be just as simple as a class with a couple of Strings to define its attributes. So we will skip its content for now.

Finally, we will refer to the endpoint as the Controller, and since we would be using a REST service with JSON, we will need to add its maven dependency.

<dependency>
  <groupId>com.sun.jersey</groupId>
  <artifactId>jersey-json</artifactId>
</dependency>

The Controller for Spring MVC would be an extra layer where most of the interesting things we are studying today will happen. First of all, we are going to connect it with the service, and set up the REST via spring JAX-RS annotations:

  • RequestMapping configures the path.
  • RequestMapping states the method expects an HTTP request.
  • RequestBody gets the information from the HTTP body.
  • PathVariable gets a value from the URL.
  • ResponseStatus will store the status code to send back.
  • ResponseBody gets the information from the HTTP body
import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thalion.snippet.swagger.api.dto.Videogame;
import org.thalion.snippet.swagger.api.service.IVideogameService;

@RestController
@RequestMapping("/api/videogames")
public class VideogameControllerBeta {

  @Autowired
  private IVideogameService service;

  @ResponseStatus(HttpStatus.OK)
  @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  public Collection<Videogame> getAllVideogames() {
    return service.findAll();
  }

  @ResponseStatus(HttpStatus.OK)
  @RequestMapping(method = RequestMethod.GET, value = "{name}", produces = MediaType.APPLICATION_JSON_VALUE)
  public Videogame getVideogameByName(@PathVariable String name) {
    return service.findOne(name);
  }

  @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<String> createVideogame(@RequestBody Videogame videogame) {
    String videogameCreatedId = service.save(videogame);
    return new ResponseEntity<String>(videogameCreatedId, HttpStatus.CREATED);
  }

  @ResponseStatus(HttpStatus.NO_CONTENT)
  @RequestMapping(method = RequestMethod.PUT, value = "{name}")
  public void updateVideogame(@PathVariable String name, @RequestBody Videogame videogame) {
    videogame.setName(name);
    service.update(videogame);
  }

  @ResponseStatus(HttpStatus.NO_CONTENT)
  @RequestMapping(method = RequestMethod.DELETE, value = "{name}")
  public void deleteInfo( @PathVariable String name) {
    service.delete(name);
  }
}

With this we are all set-up, so let’s find somewhere to run this code.

2. Let’s boot it!

Spring boot is a tool to generate a “just run” application, which can be set up with only a few lines of code an annotations, without any xml configuration. This makes it really interesting in order to run and test Java microservices really fast.

Let’s start by adding its maven dependencies and setting up its just run configuration:

<dependencies>
  <!-- spring boot setup ability -->
  <dependency>
    <groupId>org.springframework.boot>/groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
</dependencies>

<!-- Just run configuration -->
<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

Then, we need to set it up in the Java code itself. The longest way to configure it would be using the following annotations:

  • RestController, it’s an Spring-MVC, which sets up this class as controller for a REST service
  • EnableAutoConfiguration, this configures the boot service according to dependencies we have included. Since we used TesController, the system will consider to add Spring MVC and Tomcat.
  • ComponentScan, scans the components of the package and its children packages.

But all this 3 annotations can be reduced to a single one: ‘SpringBootApplication (scanBasePackages = { “replace_with_main_package” })’, which is the one used in the example.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Generic SpringBoot, only configured by setting the scanBasePackages restricts the scanned packages
*/
@SpringBootApplication(scanBasePackages = { "org.thalion.snippet.swagger" })
public class SpringbootRunner {

  public static void main(String[] args) {
SpringApplication.run(SpringbootRunner.class, args);
  }
}

So just by running this class, a local server will be deployed, and we are ready to test the service… but that’s not intuitive. Let’s make it better.

3. Making it pretty and easy to test, plus avoiding setting up the client side

Documenting an API to make it easy to understand for other developers, and make it friendly for the testers is no easy task. Swagger is a framework which, using a few extra annotations, is able to generate a simple web user interface with REST calls to the API documented, using the metadata available. We are going to use the springfox version, as it comes already bundled with the correct annotations.

❕This was the stable version when the post was originally written.

<dependencies>
  <!-- Swagger -->
  <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.3.0</version>
  </dependency>
  <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.3.0</version>
  </dependency>
</dependencies>

The most usual annotations are:

  • Api: for controller classes, it sets the API endpoint documentation.
  • Apimodel: for Data Transfer Objects class names.
  • ApiModelProperty: for Data Transfer Objects attributes.
  • ApiOperation: they go above the methods or services available on an endpoint.
  • ApiParam: for input parameter on a method.
  • ApiResponse: for output parameters. You can have more than one with a set of ApiResponses (e.g. also add the 404 error, etc).

Then, in order to fully set this up, there are 2 important points to recheck: the DTO and the controller.

Let’s start with the DTO, as it’s the most straight-forward part: we only need to document the information about the class definition itself and the attributes.

package org.thalion.snippet.swagger.api.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel(value = "Videogame entity", description = "Complete data of an entity videogame")
public class Videogame {

  @ApiModelProperty(value = "The name of the videogame", required = true)
  private String name;
  @ApiModelProperty(value = "The developer of the videogame", required = false)
  private String developer;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getDeveloper() {
    return developer;
  }

  public void setDeveloper(String developer) {
    this.developer = developer;
  }
}

The controller is a bit tougher, as we need to document every single endpoint.

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thalion.snippet.swagger.api.dto.Videogame;
import org.thalion.snippet.swagger.api.service.IVideogameService;

import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

@RestController
@RequestMapping("/api/videogames")
public class VideogameController {

  @Autowired
  private IVideogameService service;

  @ResponseStatus(HttpStatus.OK)
  @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ApiOperation(value = "Get Videogames", notes = "Returns all the videogame data")
  @ApiResponses({ @ApiResponse(code = 200, message = "Returns this information") })
  public Collection<Videogame> getAllVideogames() {
    return service.findAll();
  }

  @ResponseStatus(HttpStatus.OK)
  @RequestMapping(method = RequestMethod.GET, value = "{name}", produces = MediaType.APPLICATION_JSON_VALUE)
  @ApiOperation(value = "Get the info for one videogame", notes = "Returns the info from one videogame")
  @ApiResponses({ @ApiResponse(code = 200, message = "Exists this information") })
  public Videogame getVideogameByName(
@ApiParam(defaultValue = "default", value = "The name of the videogame to return") @PathVariable String name) {
    return service.findOne(name);
  }

  @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
  @ApiOperation(value = "Create videogame information", notes = "Create a videogame entry")
  @ApiResponses({ @ApiResponse(code = 201, message = "The videgame entry was created successfully") })
  public ResponseEntity<String> createVideogame(@RequestBody Videogame videogame) {
    String videogameCreatedId = service.save(videogame);
    return new ResponseEntity<String>(videogameCreatedId, HttpStatus.CREATED);
  }

  @ResponseStatus(HttpStatus.NO_CONTENT)
  @RequestMapping(method = RequestMethod.PUT, value = "{name}")
  @ApiOperation(value = "Update videogame information", notes = "Update a videogame information entry")
  @ApiResponses({ @ApiResponse(code = 204, message = "The videgame entry was updated successfully") })
  public void updateVideogame(
@ApiParam(defaultValue = "Default", value = "The name of the videogame to update") @PathVariable String name,
@RequestBody Videogame videogame) {
    videogame.setName(name);
    service.update(videogame);
  }

  @ResponseStatus(HttpStatus.NO_CONTENT)
  @RequestMapping(method = RequestMethod.DELETE, value = "{name}")
  @ApiOperation(value = "Delete videogame", notes = "Deletes a videogame entry")
  @ApiResponses({ @ApiResponse(code = 204, message = "The videgame entry was deleted successfully") })
  public void deleteInfo(
@ApiParam(defaultValue = "Default", value = "The name of the videogame to delete") @PathVariable String name) {
    service.delete(name);
  }
}

Once all the documentation annotations have been written down, we just need to configure the API page with the help of a Docket builder, which provides the primary graphic interface for our swagger implementation.

Since we are working with an API, we are only going to worry about the URLs under the “/api/” pattern, which we can select with the help of regular expressions.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

  @Bean
  public Docket newsApi() {
    return new Docket(DocumentationType.SWAGGER_2).groupName("api-videogames").apiInfo(apiVideogames()).select()
.paths(PathSelectors.regex("/api.*")).build();
  }

  private ApiInfo apiVideogames() {
    return new ApiInfoBuilder().title("Videogames REST api POC")
.description("PoC of a REST api, to test both Springboot and Swagger")
.termsOfServiceUrl("https://creativecommons.org/licenses/by/4.0/").contact("abcd@mail.com")
.license("GNU General Public License v3.0").licenseUrl("https://www.gnu.org/licenses/gpl-3.0.en.html")
.version("3.0").build();
  }
}

And we finally get this beautiful webpage with all the endpoints, which we can easily interact with on “localhost:8080/swagger-ui.html”. We can interact with the different methods and set up the arguments to do quick tests, avoiding the need to write a client or using a REST requests generator like SoapUI.

Congratulations! your demo is completely set up and ready to go.

The swagger page result
The swagger page result
Anuncios