Autoupdating controllers layer based on OpenAPI contract - lstefaniszyn/e-bank_ GitHub Wiki
Applying documentation first approach, we decided to use OpenAPI Specification to define an API contract (openapi.yaml) and consider this file as our primary, preferably the only, source of API structure. To provide the most efficient and consistent way of implementing controllers layer, we took advantage of OpenAPI tools to auto-generate controller interfaces and data transfer objects (DTOs) based on defined OpenAPI contract.
To trigger auto-generation you need to compile the project
mvn clean compile
Auto-generated files are located in /target/generated-sources/openapi/src/main/java
path.
The Api interfaces are stored in package com.example.ebank.generated.api
and DTOs in com.example.ebank.generated.dto
.
For each tag defined in openapi.yaml the Api interface is generated. Each interface contains all endpoints defined as controller methods with Swagger annotations.
For instance the below path in OpenAPI contract
"/api": get: tags: - app summary: Get API status operationId: getStatus responses: "200": description: successful operation content: application/json: schema: $ref: "#/components/schemas/AppStatus" "400": description: Error content: {} "404": description: Error content: {}
triggers generation of such an Api interface
@Api(value = "App", description = "the App API") public interface AppApi { /** * GET /api : Get API status * * @return successful operation (status code 200) * or Error (status code 400) * or Error (status code 404) */ @ApiOperation(value = "Get API status", nickname = "getStatus", notes = "", response = AppStatusDto.class, tags={ "app", }) @ApiResponses(value = { @ApiResponse(code = 200, message = "successful operation", response = AppStatusDto.class), @ApiResponse(code = 400, message = "Error"), @ApiResponse(code = 404, message = "Error") }) @RequestMapping(value = "/api", produces = { "application/json" }, method = RequestMethod.GET) ResponseEntity<AppStatusDto> getStatus(); }
DTO is generated for each schema defined in openapi.yaml.
For instance the below schema in OpenAPI contract
AppStatus: type: object properties: app-version: type: string description: https://schema.org/version
triggers generation of such an object
public class AppStatusDto { @JsonProperty("app-version") private String appVersion; public AppStatusDto appVersion(String appVersion) { this.appVersion = appVersion; return this; } /** * https://schema.org/version * @return appVersion */ @ApiModelProperty(value = "https://schema.org/version") {...} }
To implement logic behind the endpoints, you need to create a RestController which implement specific Api interface.
For instance, for the AppApi interface
@Api(tags = "app") @RestController public class AppStatusController implements AppApi { @Value("${app.version}") private final String appVersion = "1.0.0"; @Override public ResponseEntity<AppStatusDto> getStatus() { AppStatusDto dto = new AppStatusDto(); dto.setAppVersion(appVersion); return ResponseEntity.ok(dto); } }
For more complicated endpoints, it is important to map entities to generated Data Transfer Objects. It can be done easily using MapStruct which generates mapping between similar objects. To see example mappers go to com/example/ebank/mappers.
Below you can find required dependency for OpenAPI tools and configured maven plugin
<dependency> <groupId>org.openapitools</groupId> <artifactId>openapi-generator</artifactId> <version>${openapitools-version}</version> </dependency>
<plugin> <groupId>org.openapitools</groupId> <artifactId>openapi-generator-maven-plugin</artifactId> <version>${openapitools-version}</version> <executions> <execution> <goals> <goal>generate</goal> </goals> <configuration> <generatorName>spring</generatorName> <inputSpec>${project.basedir}/docs/api/openapi.yml</inputSpec> <configOptions> <sourceFolder>src/main/java</sourceFolder> <library>spring-boot</library> <dateLibrary>java11</dateLibrary> <java11>true</java11> <interfaceOnly>true</interfaceOnly> <skipDefaultInterface>true</skipDefaultInterface> <useTags>true</useTags> </configOptions> <modelPackage>com.example.ebank.generated.dto</modelPackage> <modelNameSuffix>Dto</modelNameSuffix> <apiPackage>com.example.ebank.generated.api</apiPackage> <generateSupportingFiles>false</generateSupportingFiles> </configuration> </execution> </executions> </plugin>
-
configOptions.interfaceOnly=true
- only Api interface is generated -
configOptions.skipDefaultInterface=true
- methods in Api interface do not have default implementation -
configOptions.useTags=true
- generated Api interfaces corresponds to defined tags -
generateSupportingFiles=false
- no supporting files are generated (only api and model)