API first services with Spring Boot
source link: https://blog.codecentric.de/en/2021/08/api-first-services-spring-boot/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
In several posts, I’ve touched on the API first paradigm. I would now like to review the basic statements of API first before we take a look at the concrete implementation.
API first recap
The API first approach is about a product-centric mindset and the understanding that development is never finished and the customer is fundamentally the focus of every iteration. Likewise, the technology behind the specification does not play a leading role. Furthermore, it is about parallelism in an already accelerated software development. The goal is to create a system in which users can effortlessly obtain information and build applications based on it.
The implementation
In the role of an API producer, I would like to try to implement a service drawing on the idea of API first. Specifically, I want to create a Spring Boot service that will deliver a list of news items via an API with the endpoint /api/news
.
At the beginning, the focus is on creating the OpenAPI specification. It is the starting point for any code generated later. The specification is kept very simple and is presented as follows:
openapi: 3.0.3
servers:
- url: 'http://localhost:8080/api'
info:
version: 1.0.0
title: News API
contact:
name: Daniel Kocot
url: 'http://www.codecentric.de'
email: [email protected]
license:
name: MIT
url: https://www.tldrelgal.com/mit
description: An API to provide news
tags:
- name: news
paths:
/news:
get:
description: gets latest news
operationId: getNews
tags:
- news
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleList'
examples: {}
'404':
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
ArticleList:
title: ArticleList
type: array
items:
$ref: '#/components/schemas/Article'
Article:
title: Article
description: A article is a part of a news.
type: object
properties:
id:
type: integer
title:
type: string
date:
type: string
format: date
description:
type: string
imageUrl:
type: string
required:
- id
- title
- date
- description
- imageUrl
Error:
type: object
properties:
code:
type: string
messages:
type: string
required:
- code
- message
securitySchemes: {}
examples:
news:
value:
description: Example shared example
type: object
properties:
id:
type: string
required:
- id
The specification was created using Stoplight Studio and is based on OpenAPI version 3.0.3. Stoplight Studio offers integration with GitHub, making it easier to version the models and specifications. In the GitHub repo itself, there is also a GitHub action configured that checks the specifications via spectral linter on pushes to the main branch or pull requests on it. In order not to blow up the blogpost, we will fall back on the built-in ruleset for Spectral. Spectral is also used as a linter in Stoplight Studio at the same time.
After the OpenAPI Spec is available, a service can be built based on Spring Boot and Gradle. The gradle.build file looks like this:
plugins {
id 'org.springframework.boot' version '2.5.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'de.undercouch.download' version '4.1.2'
id 'io.openapiprocessor.openapi-processor' version '2021.3'
id 'java'
}
group = 'de.codecentric'
version = '0.0.2-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
testImplementation('org.springframework.boot:spring-boot-starter-test')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.7.2')
testRuntime('org.junit.jupiter:junit-jupiter-engine:5.7.2')
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
test {
useJUnitPlatform()
}
task downloadFile(type: Download) {
src "https://raw.githubusercontent.com/codecentric/api-showcases/main/specs/news.yaml"
dest "${projectDir}/src/api"
onlyIfModified true
useETag true
}
openapiProcessor {
spring {
processor 'io.openapiprocessor:openapi-processor-spring:2021.5'
apiPath "$projectDir/src/api/news.yaml"
targetDir "$projectDir/build/openapi"
mapping "$projectDir/mapping.yaml"
showWarnings true
}
}
afterEvaluate {
tasks.processSpring.dependsOn(tasks.downloadFile)
}
sourceSets {
main {
java {
srcDir "build/openapi"
}
}
test {
resources {
srcDir file('src/test/java')
exclude '**/*.java'
}
}
}
compileJava.dependsOn('processSpring')
springBoot {
mainClassName = "de.codecentric.apifirstspringboot.ApifirstSpringbootApplication"
}
Two plug-ins are needed to generate the model and interfaces of the API. Because the specification is located in a repository on GitHub, a download task is required. This is provided by the plug-in from Michel Krämer. For the actual generation step, OpenAPI Processor is used instead of OpenAPI Generator. OpenAPI Processor is a fairly lightweight framework that converts the OpenAPI YAML file into a target format to be selected. Besides the plug-in for Gradle, there is also one available for Maven. Currently there are so-called processors for the following target formats:
- Spring
- Micronaut
The conversion of the OpenAPI spec is configured by using a mapping.yaml
. For this post only a simple configuration is used.
openapi-processor-mapping: v2
options:
package-name: de.codecentric.generated.news
map:
result: org.springframework.http.ResponseEntity
types:
- type: array => java.util.List
To start the conversion for the first time, gradle clean compileJava
is sufficient. Now the models and the interface are available in the package de.codecentric.generated.news
. To avoid accessing the API model of the article entity directly, it is recommended to create a separate entity. It is also possible that the entity does not correspond exactly to the API model. In the example, the Article entity contains another attribute Author, which is not provided to the outside via API. To create a mapping between the entity and the model, a corresponding mapper class is created.
package de.codecentric.apifirstspringboot.mapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.codecentric.apifirstspringboot.entities.Article;
import java.util.List;
import java.util.stream.Collectors;
public class ArticleModelMapper {
private static ObjectMapper objectMapper = new ObjectMapper();
public static Article toEntity(de.codecentric.news.api.model.Article article) {
return Article.builder()
.description(article.getDescription())
.title(article.getTitle())
.date(article.getDate())
.imageUrl(article.getImageUrl())
.build();
}
public static de.codecentric.news.api.model.Article toApi(Article article) {
de.codecentric.news.api.model.Article articleModel = new de.codecentric.news.api.model.Article();
articleModel.setId(article.getId());
articleModel.setTitle(article.getTitle());
articleModel.setDate(article.getDate());
articleModel.setDescription(article.getDescription());
articleModel.setImageUrl(article.getImageUrl());
return articleModel;
}
public static List<de.codecentric.news.api.model.Article> toApi(List<Article> retrieveAllArticles) {
return retrieveAllArticles.stream()
.map(ArticleModelMapper::toApi)
.collect(Collectors.toList());
}
public static de.codecentric.news.api.model.Article jsonToArticle(String json) throws JsonProcessingException {
return objectMapper.readValue(json, de.codecentric.news.api.model.Article.class);
}
}
A framework such as Mapstruct can also be used for mapping.
In order to make an API available to the outside world via the service, a repository class and a controller class are required.
package de.codecentric.apifirstspringboot.repository;
import de.codecentric.apifirstspringboot.entities.Article;
import org.springframework.data.jpa.repository.JpaRepository;
public interface NewsRepository extends JpaRepository<Article, Long> {
}
The controller will implement the generated API interface.
package de.codecentric.apifirstspringboot.controller;
import de.codecentric.apifirstspringboot.service.NewsService;
import de.codecentric.generated.news.api.NewsApi;
import de.codecentric.generated.news.model.Article;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
import static de.codecentric.apifirstspringboot.mapper.ArticleModelMapper.toApi;
@Controller
@RequestMapping("/api")
public class NewsController implements NewsApi {
private NewsService newsService;
public NewsController(NewsService newsService) {
this.newsService = newsService;
}
@Override
public ResponseEntity<List<Article>> getNews() {
List<de.codecentric.apifirstspringboot.entities.Article> allArticles = newsService.retrieveAllArticles();
return ResponseEntity.ok().body(toApi(allArticles));
}
}
Based on schema.sql and data.sql, the /api/news endpoint returns the following JSON response.
[
{
"date": "2021-07-08",
"description": "codecentric is...",
"id": 1,
"imageUrl": "http://picserve.codecentric.de/1/bild",
"title": "Company news"
}
]
Pitfalls: OpenAPI spec
Finally, I would like to mention two pitfalls that came up during the development. First of all, you should have noticed that the latest OpenAPI specification is not used. This is due to the fact that the parsers in the Java environment (swagger and openapi4j) only support OpenAPI Spec 3.0 (more precisely 3.0.3). This then affects other code generators, such as the OpenAPI generator.
If you decide to develop based on API first, it is important to keep an eye on which version of the specification is already supported by the generators.
In the OpenAPI spec, the URL of the server also contains the base path (/api
). The servers.url
field is not read by the OpenAPI Processor. There are now two possible solutions. Either the base path is always stored in the paths or it must be manually stored in the controller for the API as an annotation. The second variant can be found in the repo.
Summary
We can see that it is possible, with limitations, to develop services with Spring Boot based on the idea of API first. If one is also aware of the current pitfalls, it will be possible to make services available with the appropriate speed and quality.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK