6

API first services with Spring Boot

 3 years ago
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.
neoserver,ios ssh client

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK