loading...
Cover image for API-First Development with Spring Boot and Swagger

API-First Development with Spring Boot and Swagger

petros0 profile image Petros Stergioulas Updated on ・8 min read

This article was first published on reflectoring.io


Following an API-first approach, we specify an API before we start coding. Via API description languages, teams can collaborate without having implemented anything, yet.

Those description languages specify endpoints, security schemas, object schemas, and much more. Moreover, most of the time we can also generate code such a specification.

Often, an API specification also becomes the documentation of the API.

GitHub logo thombergs / code-examples

A collection of code examples from blog posts etc.

Benefits of API-First

To start working on an integration between components or systems, a team needs a contract. In our case, the contract is the API specification. API-first helps teams to communicate with each other, without implementing a thing. It also enables teams to work in parallel.

Where the API-first approach shines is on building a better API. Focusing on the functionality that it is needed to provide and only that. Minimalistic APIs mean less code to maintain.

Creating an API Spec with the Swagger Editor

Let's create our own OpenAPI specification in a YAML document. To make it easier to follow, we'll split the discussion into separate parts of the YAML document we're creating.

If you want to learn more details about the OpenAPI-Specification you can visit the Github repository.

General Information

We start with some general information about our API at the top of our document:

openapi: 3.0.2
info:
  title: Reflectoring
  description: "Tutorials on Spring Boot and Java."
  termsOfService: http://swagger.io/terms/
  contact:
    email: petros.stergioulas94@gmail.com
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: 0.0.1-SNAPSHOT
externalDocs:
  description: Find out more about Reflectoring
  url: https://reflectoring.io/about/
servers:
- url: https://reflectoring.swagger.io/v2

The openapi field allows us to define the version of the OpenAPI spec that our document follows.

Within the info section, we add some information about our API. The fields should be pretty self-explanatory.

Finally, in the servers section, we provide a list of servers that implement the API.

Tags

Then comes some additional metadata about our API:

tags:
- name: user
  description: Operations about user
  externalDocs:
    description: Find out more about our store
    url: http://swagger.io

The tags section provides fields for additional metadata which we can use to make our API more readable and easier to follow. We can add multiple tags, but each tag should be unique.

Paths

Next, we'll describe some paths. A path holds information about an individual endpoint and its operations:

paths:
  /user/{username}:
    get:
      tags:
      - user
      summary: Get user by user name
      operationId: getUserByName
      parameters:
      - name: username
        in: path
        description: 'The name that needs to be fetched. '
        required: true
        schema:
          type: string
      responses:
        200:
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        404:
          description: User not found
          content: {}

The $ref field allows us to refer to objects in a self-defined schema. In this case we refer to the User schema object (see the next section about Components).

The summary is a short description of what the operation does.

With the operationId, we can define a unique identifier for the operation. We can think about it as our method name.

Finally, the responses object allows us to define the outcomes of an operation. We must define at least one successful response code for any operation call.

Components

The objects of the API are all described in the components section. The objects defined within the components object will not affect the API unless they are explicitly referenced from properties outside the components object, as we have seen above:

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
        username:
          type: string
        firstName:
          type: string
        ... more attributes
        userStatus:
          type: integer
          description: User Status
          format: int32
  securitySchemes:
    reflectoring_auth:
      type: oauth2
      flows:
        implicit:
          authorizationUrl: http://reflectoring.swagger.io/oauth/dialog
          scopes:
            write:users: modify users
            read:users: read users
    api_key:
      type: apiKey
      name: api_key
      in: header

The schemas section allows us to define the objects we want to use in our API.

In the securitySchemes section, we can define security schemes that can be used by the operations.

There two possible ways to make use of security schemes.

First, we can add a security scheme to a specific operation using the security field:

paths:
  /user/{username}:
    get:
      tags:
      - user
      summary: Get user by user name
      security: 
        - api_key: []

In the above example we explicitly specify that the path /user/{username} is secured with the api_key scheme we defined above.

However, if we want to apply security on the whole project, we just need to specify it as a top-level field:

paths:
  /user/{username}:
    get:
      tags:
      - user
      summary: Get user by user name
security: 
  - api_key: []

Now, all of our paths should be secured with the api_key scheme.

Generating Code From an API Specification

Having defined an API, we'll now create code from the YAML document above.

We'll take a look at two different approaches to generating the code:

Generating Code from Swagger Editor

Although this is an approach that I wouldn't take, let's talk about it and discuss why I think it's a bad idea.

Let's go over to Swagger Editor and paste our YAML file into it. Then, we select Generate Server from the menu and pick what kind of a server we'd like to generate (I went with "Spring").

So why is this a bad idea?

First, the code that was generated for me is using Java 7 and Spring Boot 1.5.22, both of which are quite outdated.

Second, if we make a change to the specification (and changes happen all the time), we'd have to copy-and-paste the files that were changed manually.

Generating Code with the OpenAPI Maven plugin

A better alternative is to generate the code from within a Maven build with the OpenAPI Maven plugin.

Let's take a look at the folder structure. I chose to use a multi-module maven project, where we have two projects:

  • app, an application that implements the API from our specification.
  • specification, whose only job is to provide the API Specification for our app.

The folder structure looks like this:

spring-boot-openapi
├── app
│   └── pom.xml
│   └── src
│       └── main
│           └── java
│               └── io.reflectoring
│                   └── OpenAPIConsumerApp.java
├── specification
│   └── pom.xml
│   └── src
│       └── resources
│           └── openapi.yml
└── pom.xml

For the sake of simplicity, we omit the test folders.

Our app is a simple Spring Boot project that we can automatically generate on start.spring.io, so let's focus on the pom.xml from the specification module, where we configure
the OpenAPI Maven plugin:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>4.2.3</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>
                  ${project.basedir}/src/main/resources/openapi.yml
                </inputSpec>
                <generatorName>spring</generatorName>
                <apiPackage>io.reflectoring.api</apiPackage>
                <modelPackage>io.reflectoring.model</modelPackage>
                <supportingFilesToGenerate>
                  ApiUtil.java
                </supportingFilesToGenerate>
                <configOptions>
                    <delegatePattern>true</delegatePattern>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

You can see the full pom.xml file on GitHub.

For this tutorial, we're using the spring generator.

Simply running the command ./mvnw install will generate code that implements our OpenAPI specification!

Taking a look into the folder target/generated-sources/openapi/src/main/java/io/reflectoring/model, we find the code for the User model we defined in our YAML:

@javax.annotation.Generated(...)
public class User   {
  @JsonProperty("id")
  private Long id;

  @JsonProperty("username")
  private String username;

  @JsonProperty("firstName")
  private String firstName;

  // ... more properties

  @JsonProperty("userStatus")
  private Integer userStatus;

  // ... getters and setters

}

The generator does not only generate the models but also the endpoints. Let's take a quick look at what we generated:

public interface UserApiDelegate {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * POST /user : Create user
     * Create user functionality
     *
     * @param body Created user object (required)
     * @return successful operation (status code 200)
     * @see UserApi#createUser
     */
    default ResponseEntity<Void> createUser(User body) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }
  // ... omit deleteUser, getUserByName and updateUser
}

Of course, the generator cannot generate our business logic for us, but it does generate interfaces like UserApiDelegate above for us to implement.

It also creates a UserApi interface which delegates calls to UserApiDelegate:

@Validated
@Api(value = "user", description = "the user API")
public interface UserApi {

    default UserApiDelegate getDelegate() {
        return new UserApiDelegate() {};
    }

    /**
     * POST /user : Create user
     * Create user functionality
     *
     * @param body Created user object (required)
     * @return successful operation (status code 200)
     */
    @ApiOperation(value = "Create user", 
      nickname = "createUser", 
      notes = "Create user functionality", 
      tags={ "user", })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "successful operation") })
    @RequestMapping(value = "/user",
        method = RequestMethod.POST)
    default ResponseEntity<Void> createUser(
      @ApiParam(value = "Created user object" ,required=true )  
      @Valid 
      @RequestBody User body) {
        return getDelegate().createUser(body);
    }

    // ... other methods omitted
}

The generator also creates a Spring controller for us that implements the UserApi interface:

@javax.annotation.Generated(...)
@Controller
@RequestMapping("${openapi.reflectoring.base-path:/v2}")
public class UserApiController implements UserApi {

    private final UserApiDelegate delegate;

    public UserApiController(
      @Autowired(required = false) UserApiDelegate delegate) {
        this.delegate = Optional.ofNullable(delegate)
            .orElse(new UserApiDelegate() {});
    }

    @Override
    public UserApiDelegate getDelegate() {
        return delegate;
    }
}

Spring will inject our implementation of UserApiDelegate into the controller's constructor if it finds it in the application context. Otherwise, the default implementation will be used.

Let's start our application and hit the GET endpoint /v2/user/{username}.

curl -I http://localhost:8080/v2/user/Petros
HTTP/1.1 501
Content-Length: 0

But why do we get a 501 response (Not Implemented)?

Because we did not implement the UserApiDelegate interface and the UserApiController used the default one, which returns HttpStatus.NOT_IMPLEMENTED.

Now let's implement the UserApiDelegate:

@Service
public class UserApiDelegateImpl implements UserApiDelegate {

    @Override
    public ResponseEntity<User> getUserByName(String username) {
        User user = new User();
        user.setId(123L);
        user.setFirstName("Petros");

        // ... omit other initialization

        return ResponseEntity.ok(user);
    }
}

It's important to add a @Service or @Component annotation to the class so that Spring can pick it up and inject it into the UserApiController.

If we run curl http://localhost:8080/v2/user/Petros again now, we'll receive a valid JSON response:

{
  "id": 123,
  "firstName": "Petros",
  // ... omit other properties
}

The UserApiDelegate is the single point of truth. That enables us to make fast changes in our API. For example, if we change the specification and generate it again, we only have to implement the newly generated methods.

The good thing is that if we won't implement them, our application doesn't break. By default, those endpoints would return HTTP status 501 (Not Implemented).

In my opinion, generating the OpenAPI Specification with Maven plugin instead of Swagger Editor is the better choice.
That's because we have more control over our options. The plugin provides some configuration and with Git as a version control tool, we can safely track any changes in either pom.xml and openapi.yml.

Conclusion

With OpenAPI we can create an API specification that we can share among teams to communicate contracts. The OpenAPI Maven plugin allows us to generate boilerplate code for Spring Boot from such a specification so that we only need to implement the business logic ourselves.

You can browse the example code on GitHub.

Posted on by:

petros0 profile

Petros Stergioulas

@petros0

Software developer, gamer, enthusiast

Discussion

pic
Editor guide