When you are writing REST services, you may be already heard of the OpenAPI (former Swagger) specification. OpenAPI is a specification for describing REST services. It's a text file in JSON, or Yaml format and tools can read it to generate code, create documentation and test cases.
The Swagger specification was renamed to OpenAPI in January 2016. The name change did not alter the specification. It made the difference between the tools (Swagger) and the specification (OpenAPI) more distinct.
You can incorporate OpenAPI in different ways into your application development workflow. You could first write the specification, generate code, and implement the services. Or you could first develop the services and use a tool to generate the OpenAPI specification file from the source code.
In this blog post, we create a simple todo application with a Spring Boot server that implements the REST services and an Ionic application that consumes the service.
We start with the REST service implementation and let a generator create the OpenAPI specification file.
Then we feed this specification into a code generator that produces TypeScript/Angular code. And finally, we use the generated code in an Ionic application to consume the REST services.
Developing the REST services ¶
I usually generate my Spring Boot applications with the https://start.spring.io/ website. For this demo project, we only need the Web
dependency.
When everything is set up, we first create a POJO that represents a todo record.
@ApiModel(description = "The todo record")
public class Todo {
@ApiModelProperty(required = true, value = "The primary key")
private String id;
private String title;
private String description;
To keep things simple, we store everything in memory. A simple database service stores the todo records in a Map.
@Service
public class TodoDb {
private final Map<String, Todo> todos = new HashMap<>();
public void save(Todo todo) {
this.todos.put(todo.getId(), todo);
}
public void delete(String id) {
this.todos.remove(id);
}
public Collection<Todo> list() {
return this.todos.values();
}
}
Then we create the Controller that implements the REST services. We create three services save
, delete
and list
, that are mapped to the URLs /todo/save
, /todo/delete/{id}
and /todo/list
.
Because save
and delete
do not return anything, we set the HTTP response code to 204 (No Content). The generated TypeScript code in the next section depends on this behavior. It throws an error when the server sends an empty response with status code 200.
@RestController
@RequestMapping("/todo")
@CrossOrigin
public class TodoService {
private final TodoDb todoDb;
public TodoService(TodoDb todoDb) {
this.todoDb = todoDb;
}
@PostMapping("/save")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void save(@RequestBody Todo todo) {
this.todoDb.save(todo);
}
@PostMapping("/delete/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ApiOperation("Deletes a todo entry")
public void delete(@ApiParam(value = "Primary key of the todo entity",
required = true) @PathVariable("id") String id) {
this.todoDb.delete(id);
}
@GetMapping("/list")
public List<Todo> list() {
return new ArrayList<>(this.todoDb.list());
}
}
This concludes our server-side implementation. You can start the application in your IDE or from the command line with mvn spring-boot:run
.
To test the application in this state, we have to generate and send the requests manually. You can use one of the many available REST clients or use curl
from the shell.
curl -i -X POST -H "Content-Type:application/json" http://localhost:8080/todo/save -d "{\"id\":\"1\",\"title\":\"Shopping\",\"description\":\"Buy milk\"}"
curl http://localhost:8080/todo/list
curl -i -X POST http://localhost:8080/todo/delete/1
Generating OpenAPI specification ¶
Our services are working, and we want to create an OpenAPI specification file for these three services.
For this purpose, we add the SpringFox library to our project.
SpringFox is a Swagger/OpenAPI implementation for Spring. SpringFox
provides a starter library for Spring Boot that configures everything automatically.
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
Start the Spring Boot application, open the browser with the URL
http://localhost:8080/v2/api-docs
, and you see the OpenAPI specification in JSON format.
To convert this into a more readable form, you can open the Online Swagger Editor and paste the JSON into the editor.
By default, Springfox adds all the HTTP services it finds inside the Spring application to the specification file. If you want to limit the exposed services in the specification, you need an additional configuration.
Stop the server and add the following bean definition to a configuration class.
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).useDefaultResponseMessages(false)
.apiInfo(new ApiInfoBuilder().title("Todo API Endpoints").version("1").build())
.select().apis(RequestHandlerSelectors.basePackage("ch.rasc.swagger.todo"))
.paths(PathSelectors.any()).build();
}
The Docket
bean allows an application to specify which services should appear in the OpenAPI specification file.
There are different ways to filter services. In this example, we select all services inside the package ch.rasc.swagger.todo
.
With the Docket, you can also configure other parts of SpringFox.
You find more information in the official Springfox documentation.
Besides the REST services, OpenAPI also describes the objects that are used as return values and parameters of the services. You find them at the end of the file in the definitions
section.
SpringFox not only provides the api-docs endpoint that returns a JSON it also generates a user friendly documentation page. This page is automatically configured and enabled when you followed the configuration above.
Open the browser with the URL: http://localhost:8080/swagger-ui/index.html, and you see a documentation page for your REST services. It's not just a static documentation page; it also allows you to test the services. For each operation, you find a "Try it out" button that sends a request to the server. You have to fill in the mandatory parameters before you can send the request.
By default, the documentation is very generic, but you can customize a lot of the information with SpringFox annotations.
You can describe the service with @ApiOperation
and the parameters with @ApiParam
.
@PostMapping("/delete/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ApiOperation("Deletes a todo entry")
public void delete(@ApiParam(value = "Primary key of the todo entity",
required = true) @PathVariable("id") String id) {
this.todoDb.delete(id);
}
You can also add annotations to the model classes.
@ApiModel(description = "The todo record")
public class Todo {
@ApiModelProperty(required = true, value = "The primary key")
private String id;
See the official SpringFox documentation for more information.
Generating client code ¶
Next, we create an Ionic app that consumes these REST services.
The application for this example is based on the Ionic blank starter template.
In this example, we want to assign the primary key (id) on the client-side when the user creates a new todo record.
For this purpose, we install a UUID JavaScript library.
npm install uuid
Next, we use a Swagger code generator that produces TypeScript code. In this project, we use a Maven plugin to run the code generator.
<plugin>
<groupId>io.swagger</groupId>
<artifactId>swagger-codegen-maven-plugin</artifactId>
<version>2.4.16</version>
<executions>
<execution>
<id>typescriptgen</id>
<phase>none</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>http://localhost:8080/v2/api-docs</inputSpec>
<language>typescript-angular</language>
<output>${basedir}/../client/src/app/swagger</output>
</configuration>
</execution>
</executions>
</plugin>
The output path points to the src
directory of the Ionic app. I set the phase of the plugin to none ( <phase>none</phase>
) to prevent the plugin from running during the normal Maven lifecycle. With this configuration, you have run the plugin manually:
mvn io.swagger:swagger-codegen-maven-plugin:generate@typescriptgen
You can remove the none-phase, and then the plugin runs every time you call the generate-sources lifecycle (mvn generate-sources
).
Either way, before you run the plugin, you have to start the Spring Boot application because the plugin downloads the OpenAPI file from the provided inputSpec address.
Alternatively, you could download the specification file and store it in the project directory and configure a file path instead of a URL.
<inputSpec>src/main/resources/api.json</inputSpec>
You find more information about the plugin on the project page: https://github.com/swagger-api/swagger-codegen
Developing the client ¶
When the generator did his job without throwing an error, you find the generated source code in the src/app/swagger
directory. The model
subdirectory contains a TypeScript class for the todo model, and in the api
subdirectory, you find the service class with methods for each of our services.
Next, we add a property baseUrl
to the environment objects (environment.ts and environment.prod.ts).
export const environment = {
production: false,
basePath: 'http://127.0.0.1:8080'
};
To use the generated service, we have to import it into our application. Open src/app/app.module.ts
and add ApiModule.forRoot
to the imports
array. You also need to insert a configuration factory for the module where you can specify things like base URL, API keys, and username/password. For this example, we only have to configure the base URL that points to the server.
export function apiConfigFactory(): Configuration {
const params: ConfigurationParameters = {
basePath: environment.basePath
};
return new Configuration(params);
}
@NgModule({
declarations: [AppComponent, HomePage, EditPage],
imports: [BrowserModule,
CommonModule,
HttpClientModule,
ApiModule.forRoot(apiConfigFactory),
FormsModule,
IonicModule.forRoot(),
RouterModule.forRoot(routes, {useHash: true})],
Make sure that you import HttpClientModule
. The generated code depends on this.
Then we can inject the service class into other classes as usual.
export class HomePage implements ViewDidEnter {
todos: Todo[] = [];
constructor(private readonly navCtrl: NavController,
private readonly todoService: TodoServiceService) {
}
After this setup, the application can call one of the provided methods to send a request to the server.
All the generated methods return an Observable, and we have to subscribe to it. Otherwise, nothing happens.
list
ionViewDidEnter(): void {
this.todoService.listUsingGET().subscribe(data => this.todos = data);
}
delete
deleteTodo(slidingItem: IonItemSliding, todo: Todo): void {
slidingItem.close();
this.todoService.deleteUsingPOST(todo.id).subscribe(() => this.ionViewDidEnter());
}
save
save(): void {
this.todoService.saveUsingPOST(this.todo).subscribe(() => this.navCtrl.navigateBack(['home']));
}
You find the complete source for the Spring Boot and Ionic application on GitHub: https://github.com/ralscha/blog/tree/master/swagger