Contents
API First Approach
is when we emphasize API
specification, rather than building a project and evolving our API
specification around it over time. We define API
specifications ahead of development time to create a strict contract between the backend and the frontend.
We are going to utilize the stoplight.io
platform for creating API
design and documentation to generate YAML
file with openapi
specifications for our app. Then we are going to use openapi-generator-cli
to generate code for both the backend and the frontend.
API
API
structure is specified on one place only and enforced on all apps using the API
.API
.DTOs
(Data transfer objects) generated with the correct structure and data validation.API
, as the method signature is defined by the code generator, therefor developers can focus only on business logic.
Go to https://stoplight.io/ and create a free account. With the free account you will be able to create one project which is enough for you to follow along.
After creating an account click on the start new project
button on the left navigation bar of your dashboard.
On the next page enter a desired project name and set it to internal to make it accessible only to workspace members. Now click on the Create API Project
button.
First we need to create an API
.
Enter the API
name and make sure you select 3.0.0
as the openapi
version, as the openapi-generator-cli
tool we are going to use later on doesn’t support the 3.1.0
version at the time of writing this blog. Leave format as YAML
. Click on Create
button.
After creating the API
we will be redirected to a page with an overview of our API
. We are only going to enter our server address for this example, but you can play around and explore all possible definitions you can declare, like API
description, global security and license information.
You can also view your API
definition as YAML
by clicking on the code
button on the top right. This way you can edit API
directly if you are familiar with openapi
and how to make one in YAML
.
Let us check some example models and endpoint specifications that come together with the newly created API
definition on stoplight.io
. First, go to Models
on the left panel and click on User
to inspect its definition.
We can see that the User
model is defined to have multiple fields of which only dateOfBirth
and createDate
are optional, while the rest are required. That is indicated by a red exclamation mark on the right of each field. Also, we can see that only id
, emailVerified
, and createDate
have a description which is indicated by the blue book right of the exclamation mark.
Each field has its type defined, which is going to be used to generate data validation. Especially useful for strings, as we can define formats like email
, UUID
or date
that are going to be embedded into our DTOs
. as regex validation
.
If we click on the Examples
button, we can see that we can create examples of valid objects of type User
. These examples can be used as responses defined on a mock server that is going to be used while running tests or to allow frontend developers to simulate working endpoints, although they are not implemented yet.
Now let us see an example of how to define an API
endpoint. Click on /users/{userId}
under Paths
on the left panel to open the endpoint definition.
We can define the following for the /users/{userId}
endpoint:
HTTP
request.HTTP
request should look like.
Possibilities are endless, you can define pretty much anything you can think of when talking about APIs
, but we are going to stop here and use what we have defined for now. You can take your time here and define the perfect API
definition for your project.
It is time for us to export our definition to the YAML
file and generate code from it. Click on the Code
button we talked about earlier and copy the whole definition to a file with a .yaml
extension. Save that file for later.
This is how your YAML
file should look like:
openapi: 3.0.0
x-stoplight:
id: x3hxq03l6hfwv
info:
title: TestAPI
version: '1.0'
description: API example for API First Approach.
servers:
- url: 'http://localhost:8080'
paths:
'/users/{userId}':
parameters:
- schema:
type: integer
name: userId
in: path
required: true
description: Id of an existing user.
get:
summary: Get User Info by User ID
tags: []
responses:
'200':
description: User Found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
examples:
Get User Alice Smith:
value:
id: 142
firstName: Alice
lastName: Smith
email: alice.smith@gmail.com
dateOfBirth: '1997-10-31'
emailVerified: true
signUpDate: '2019-08-24'
'404':
description: User Not Found
operationId: get-users-userId
description: Retrieve the information of the user with the matching user ID.
patch:
summary: Update User Information
operationId: patch-users-userId
responses:
'200':
description: User Updated
content:
application/json:
schema:
$ref: '#/components/schemas/User'
examples:
Updated User Rebecca Baker:
value:
id: 13
firstName: Rebecca
lastName: Baker
email: rebecca@gmail.com
dateOfBirth: '1985-10-02'
emailVerified: false
createDate: '2019-08-24'
'404':
description: User Not Found
'409':
description: Email Already Taken
description: Update the information of an existing user.
requestBody:
content:
application/json:
schema:
type: object
properties:
firstName:
type: string
lastName:
type: string
email:
type: string
description: 'If a new email is given, the user''s email verified property will be set to false.'
dateOfBirth:
type: string
examples:
Update First Name:
value:
firstName: Rebecca
Update Email:
value:
email: rebecca@gmail.com
Update Last Name & Date of Birth:
value:
lastName: Baker
dateOfBirth: '1985-10-02'
description: Patch user properties to update.
/user:
post:
summary: Create New User
operationId: post-user
responses:
'200':
description: User Created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
examples:
New User Bob Fellow:
value:
id: 12
firstName: Bob
lastName: Fellow
email: bob.fellow@gmail.com
dateOfBirth: '1996-08-24'
emailVerified: false
createDate: '2020-11-18'
'400':
description: Missing Required Information
'409':
description: Email Already Taken
requestBody:
content:
application/json:
schema:
type: object
properties:
firstName:
type: string
lastName:
type: string
email:
type: string
dateOfBirth:
type: string
format: date
required:
- firstName
- lastName
- email
- dateOfBirth
examples:
Create User Bob Fellow:
value:
firstName: Bob
lastName: Fellow
email: bob.fellow@gmail.com
dateOfBirth: '1996-08-24'
description: Post the necessary fields for the API to create a new user.
description: Create a new user.
components:
schemas:
User:
title: User
type: object
x-examples:
Alice Smith:
id: 142
firstName: Alice
lastName: Smith
email: alice.smith@gmail.com
dateOfBirth: '1997-10-31'
emailVerified: true
signUpDate: '2019-08-24'
properties:
id:
type: integer
description: Unique identifier for the given user.
firstName:
type: string
lastName:
type: string
email:
type: string
format: email
dateOfBirth:
type: string
format: date
example: '1997-10-31'
emailVerified:
type: boolean
description: Set to true if the user's email has been verified.
createDate:
type: string
format: date
description: The date that the user was created.
required:
- id
- firstName
- lastName
- email
- emailVerified
securitySchemes: {}
Now it is time to set up our Spring Boot
project. Open IntelliJ
and create a new project. Choose maven
as the build system and click on Create
button.
Right-click on the project name, then go New, then Module.
We are creating a module that is going to be responsible to generate new code each time a change in the YAML
file happens. Name the new module openapi
and choose maven
as the build system again.
Now go to https://start.spring.io/ and initialize your Spring Boot
project. Choose maven
as the build system and add Spring Web
dependency. Click on GENERATE
to download the project.
Extract the downloaded zip to the root of your project. And update your root pom.xml
to include the newly added module. Here is an example of a pom.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>openapi-example</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>openapi</module>
<module>demo</module> <!-- Add your project name to list of modules -->
</modules>
<properties>
<maven.compiler.source>18</maven.compiler.source>
<maven.compiler.target>18</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
Don’t forget to load maven
changes by clicking on the button that appears on the top right.
If you have done everything correctly, both modules should have blue squares on their root folders. Also, you can delete the src folder in the root project as we won’t be adding anything there.
Now open pom.xml
of the openapi
module and add the following dependencies and build plugins after <properties>...</properties>
:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!--https://openapi-generator.tech/docs/plugins/-->
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>5.1.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<id>buildApi</id>
<configuration>
<!-- path to the specification -->
<inputSpec>${basedir}/src/main/resources/openapi.yaml</inputSpec>
<!--https://openapi-generator.tech/docs/generators/spring -->
<generatorName>spring</generatorName>
<library>spring-boot</library>
<modelNameSuffix>${swagger.modelNameSuffix}</modelNameSuffix>
<generateApis>true</generateApis>
<generateModels>true</generateModels>
<configOptions>
<useSpringBoot3>true</useSpringBoot3>
<interfaceOnly>true</interfaceOnly>
<useBeanValidation>true</useBeanValidation>
<performBeanValidation>true</performBeanValidation>
<modelPackage>${swagger.modelPackage}</modelPackage>
<apiPackage>${swagger.basePackage}.controller</apiPackage>
<sourceFolder>/src/main/java</sourceFolder>
<implFolder>/src/main/java</implFolder>
<serializableModel>true</serializableModel>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
<!-- This plugin is required for spring boot 3.0.0 and higher as javax got replaced with jakarta-->
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
<version>1.5.3</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>
<include>${project.basedir}/target/generated-sources/openapi/src/**/*.java</include>
</includes>
<regex>false</regex>
<token>javax</token>
<value>jakarta</value>
</configuration>
</plugin>
<plugin>
<!-- used to read swagger.properties-->
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<phase>initialize</phase>
<goals>
<goal>read-project-properties</goal>
</goals>
<configuration>
<files>
<file>${basedir}/src/main/resources/swagger.properties</file>
</files>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<addClasspath>false</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Build-Number>${buildNumber}</Build-Number>
<Build-Time>${maven.build.timestamp}</Build-Time>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
Again, don’t forget to load maven
changes. Now create openapi.yaml
and swagger.properties
in openapi/src/main/resources
. Copy the API
specification defined before with stoplight.io
in openapi.yaml
file and copy the following to swagger.properties
:
# define path to yaml file
swagger.yaml.path=src/main/resources
# add suffix to generated class names if you want or leave it blank for no suffix
swagger.modelNameSuffix=
# define package name where controller interfaces will be generated
swagger.basePackage=com.miloszeljko.backend.api
# define package name where DTO classes will be generated
swagger.modelPackage=com.miloszeljko.backend.api.model
Now run mvn clean install
for the openapi
module to see if everything works. You can do that by opening Execute Maven Goal
on the maven panel on the right and typing the command there. Also, don’t forget to select the openapi
module.
You can see that the target folder has appeared and if you open it and go to target/generated-sources/openapi/src/main/java
you will see two packages, controller
and model
, with generated code from your specification. Take a look around and check what has been generated.
If you used an unchanged demo specification from spotlight.io
as I did, you will have two DTOs
with weird names: InlineObject
and InlineObject1
. That only means we didn’t declare the endpoint response or request body as a model, we rather defined a custom object directly on endpoint specification. We are going to leave it as is for demo purposes, but you should have your models properly named for real-world applications.
Let us configure our demo
module so it can use generated classes. Add the following dependencies in the pom.xml
of the demo
module:
<!-- Imports needed for open-api specification -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<!-- Update groupId, artifactId and version to match your openapi module -->
<groupId>com.miloszeljko</groupId>
<artifactId>openapi</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>2.1.10</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
<version>2.1.10</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-models</artifactId>
<version>2.1.10</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.0.3</version>
</dependency>
Make sure you update groupId
, artifactId
and version
to match your openapi
module. Load maven
changes and you can start using generated code in your project! Let us create UserController
and implement UserApi
:
package com.miloszeljko.demo.controller;
import com.miloszeljko.backend.api.controller.UserApi;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController implements UserApi {
}
By default, each endpoint we don’t override will return a 501 NOT_IMPLEMENTED
HTTP
status code. To easily override methods press ALT+Ins
on the keyboard, select Override Methods...
and select postUser()
. Now provide some dummy implementation for that endpoint so we can test that everything is working. For example:
package com.miloszeljko.demo.controller;
import com.miloszeljko.backend.api.controller.UserApi;
import com.miloszeljko.backend.api.model.InlineObject1;
import com.miloszeljko.backend.api.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
@RestController
public class UserController implements UserApi {
@Override
public ResponseEntity<User> postUser(InlineObject1 inlineObject1) {
//TODO: create implementation instead of calling method from UserApi
//return UserApi.super.postUser(inlineObject1);
var response = new User();
response.setId(1);
response.setFirstName(inlineObject1.getFirstName());
response.setLastName(inlineObject1.getLastName());;
response.setEmail(inlineObject1.getEmail());
response.setDateOfBirth(inlineObject1.getDateOfBirth());
response.setEmailVerified(true);
response.setCreateDate(LocalDate.now());
return new ResponseEntity(response, HttpStatus.OK);
}
}
We can specify a custom path for a swagger
to create documentation for our API
by adding this line in the application.properties
of the demo
module:
# swagger-ui custom path
springdoc.swagger-ui.path=/api-docs
Start the demo application now and open http://localhost:8080/api-docs in your browser. You can see the endpoint we implemented listed in the swagger
documentation. If we open up /user
we can see a button Try it out
on the right. If we click it we can define a request and test out our endpoint.
We can see we are getting 200 OK
with the response body, although the email seems to be in invalid format, it is ok, as InlineObject1
wasn’t designed to have email validation.
If we go back to openapi.yaml
and change /user
configuration by adding format: email
below type: string
on email
field we will add email regex validation
. We can also go back to stoplight.io
and add format by clicking on the field name and selecting format. After the change, we need to copy generated YAML
file content into our openapi.yaml
.
Now if we do mvn clean install
from Execute Maven Goal
for the openapi
module and run our demo
module, we can see that when we send a post request with an invalid email format we are getting 400 BAD REQUEST
responses from the server. Which indicates validation is working properly.
Our backend is configured, now let us focus on the frontend. First, we are going to generate a new angular
application.
ng new demo-client
Another thing we need to do is to install the npm package openapi-generator-cli
.
npm i @openapitools/openapi-generator-cli -D
To rerun the generator easily when needed (in case of openapi.yaml
change) we can define the script in package.json
under the scripts property:
"scripts": {
.
.
.
"generate-api": "openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o src/app/api -p=removeOperationIdPrefix=true"
},
Let try generating the code:
npm run generate-api
You can see generate code under src/app/api
. If you look inside api
folder, you can see there is only one service called default.service.ts
. To tell the generator where to place each endpoint, we can define tags. Keep in mind that openapi 3.0.0
allows multiple tags to be added, therefor generator will put the same method in multiple services in case you define multiple tags.
We can define tag by editing the YAML
file directly, adding tags
property to each endpoint method like this:
.
.
.
/user:
post:
tags:
- user
.
.
.
.
.
.
.
.
.
We can also define tags
through the stoplight.io
platform by clicking the tag icon
on the top left when having the endpoint definition opened.
Each tag
will result in a service
named: tagName.service.ts
To auto-delete
the src/app/api
folder when running the generator again, we are going to use del-cli
, first, let us install it.
npm install --save-dev del-cli
After installation change your generate-api
script in package.json
to:
"scripts": {
.
.
.
"generate-api": "del-cli --force ./src/app/api && openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o src/app/api -p=removeOperationIdPrefix=true"
},
Try running npm run generate-api
again to see the difference.
To use generated classes, you’ll need to import HttpClientModule
in AppModule
and provide it with configuration
, your AppModule
should look like this:
import { ApiModule } from './api/api.module';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { Configuration, ConfigurationParameters } from './api';
export function apiConfigFactory(): Configuration {
const params: ConfigurationParameters = {
basePath: 'http://localhost:8080',
};
return new Configuration(params);
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
ApiModule.forRoot(apiConfigFactory)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
And that is it! You can now use generated classes simply by importing them like this:
import { PostUserRequest } from './api/model/postUserRequest';
import { UserService } from './api/api/user.service';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'demo-client';
constructor(private userService: UserService) {
let postUserRequest: PostUserRequest = {
firstName: 'test',
lastName: 'test',
email: 'test@test.com',
dateOfBirth: '2023-01-01',
}
this.userService.postUser(postUserRequest).subscribe({
next: response => {
console.log(response);
},
error: error => {
console.log(error);
}
});
}
}
For more information on how to customize generator configuration, you can visit the official documentation:
https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/spring.md
https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/typescript-angular.md