Home | Send Feedback | Share on Bluesky |

Spring Boot and Ionic application development with OpenAPI

Published: 21. September 2017  •  Updated: 4. December 2018  •  spring, java, javascript, ionic

When you are writing REST services, you may have already heard of the OpenAPI (formerly 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 distinction 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 then implement the services. Or you could first develop the services and then 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. 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.

  private String description;

  public String getId() {
    return this.id;
  }

Todo.java

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();
  }

}

TodoDb.java

Then we create the Controller that implements the REST services. We create three services: save, delete, and list, which 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.

package ch.rasc.swagger.todo;

import java.util.ArrayList;
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@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)
  public void delete(@PathVariable(value = "id", required = true) String id) {
    this.todoDb.delete(id);
  }

  @GetMapping("/list")
  public List<Todo> list() {
    return new ArrayList<>(this.todoDb.list());
  }
}

TodoService.java

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 springdoc-openapi library to our project. springdoc-openapi is a Swagger/OpenAPI implementation for Spring. springdoc-openapi provides a starter library for Spring Boot that configures everything automatically.

    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.8.9</version>
    </dependency>

pom.xml

Start the Spring Boot application, open the browser with the URL http://localhost:8080/v3/api-docs, and you will 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.

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.codegen.v3</groupId>
              <artifactId>swagger-codegen-maven-plugin</artifactId>
              <version>3.0.71</version>
        <executions>
          <execution>
            <id>typescriptgen</id>
            <phase>none</phase>
            <goals>
              <goal>generate</goal>
            </goals>
            <configuration>
              <inputSpec>http://localhost:8080/v3/api-docs</inputSpec>
              <language>typescript-angular</language>
              <output>${basedir}/../client/src/app/swagger</output>
            </configuration>
          </execution>
        </executions>
      </plugin>

pom.xml

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 to run the plugin manually:

mvn io.swagger:swagger-codegen-maven-plugin:generate@typescriptgen

You can remove the none phase, and then the plugin will run 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, store it in the project directory, and configure a file path instead of a URL.

<inputSpec>src/main/resources/api.json</inputSpec>

You can find more information about the plugin on the project page: https://github.com/swagger-api/swagger-codegen

Developing the client

When the generator has finished without throwing an error, you will 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 will 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'
};

environment.ts

To use the generated service, we have to import it into our application. Open src/main.ts and import it with importProvidersFrom and ApiModule.forRoot. You also need to insert a configuration factory for the module where you can specify things like the base URL, API keys, and username/password. For this example, we only have to configure the base URL that points to the server.

function apiConfigFactory(): Configuration {
  const params: ConfigurationParameters = {
    basePath: environment.basePath
  };
  return new Configuration(params);
}


bootstrapApplication(AppComponent, {
  providers: [
    provideIonicAngular(),
    importProvidersFrom(ApiModule.forRoot(apiConfigFactory)),

main.ts


Then, we can inject the service class into other classes as usual.

export class HomePage implements ViewDidEnter {
  todos: Todo[] = [];
  private readonly navCtrl = inject(NavController);
  private readonly todoService = inject(TodoServiceService);

home.page.ts

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.list().subscribe(data => this.todos = data);
  }

home.page.ts

delete

  deleteTodo(slidingItem: IonItemSliding, todo: Todo): void {
    slidingItem.close();
    this.todoService._delete(todo.id).subscribe(() => this.ionViewDidEnter());
  }

home.page.ts

save

  save(): void {
    this.todoService.save(this.todo).subscribe(() => this.navCtrl.navigateBack(['home']));
  }

edit.page.ts


You can find the complete source for the Spring Boot and Ionic application on GitHub: https://github.com/ralscha/blog/tree/master/swagger