How To Generate Angular & Spring Code From OpenAPI Specification

If you are developing the backend and frontend part of an application you know that it can be tricky to keep the data models between the backend & frontend code in sync. Luckily, we can use generators that generate server stubs, models, configuration and more based on a OpenAPI specification.

In this article, I want to demonstrate how you can implement such an OpenAPI generator in a demo application with an Angular frontend and a Spring Boot backend.

The Demo Application

For this article, I have created a simple demo application that provides a backend REST endpoint based on Spring Boot that returns a list of gaming news. The frontend based on Angular requests this list from the backend and renders the list of news.

The source code is available on GitHub.

The Angular frontend was generated with the Angular CLI and the Spring Boot backend with Spring Initializr.

OpenAPI

The OpenAPI specification is defined as

a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection

Such an OpenAPI definition can be used by tools for testing, to generate documentation, server and client code in various programming languages, and many other use cases.

The specification has undergone three revisions since its initial creation in 2010. The latest version is 3.0.2 (as of 02.03.2020).

OpenAPI Generator

In this article, I want to focus on code generators, especially on the openapi-generator from OpenAPI Tools.

This picture taken from the project’s GitHub repository shows the impressive list of supported languages and frameworks:

OpenAPI Supported Languages & Frameworks

For this article’s demo project the @openapitools/openapi-generator-cli package is used to generate the Angular code via npm and openapi-generator-gradle-plugin to generate the Spring code using Gradle.

OpenAPI Schema Definition

The OpenAPI code generator needs a yaml schema definition file which includes all relevant information about the API code that should be generated.

Based on the official petstore.yaml example I created a simple schema.yaml file for the demo news application:

openapi: '3.0.0'
servers:
  - url: http://localhost:8080/api
info:
  version: 1.0.0
  title: Gaming News API
paths:
  /news:
    summary: Get list of latest gaming news
    get:
      tags:
        - News
      summary: Get list of latest gaming news
      operationId: getNews
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArticleList'

components:
  schemas:
    ArticleList:
      type: array
      items:
        $ref: '#/components/schema/Article'
    Article:
      required:
        - id
        - title
        - date
        - description
        - imageUrl
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        date:
          type: string
          format: date
        description:
          type: string
        imageUrl:
          type: string

Let’s take a look at the most important parts of this file:

  • openapi: The version of the OpenAPI specification
  • servers -> url: The backend URL
  • info: General API information
  • paths: This section defines the API endpoints. In our case, we have one GET endpoint at /news which returns a list of articles.
  • components: Describes the structure of the payload

For more information about the schema definition, you can take a look at the basic structure or at the full specification (in this case for v3).

Generate backend code based on this schema

In this section, I will demonstrate how the backend code for Spring Boot can be generated based on our schema definition.

The first step is to modify the build.gradle file:

plugins {
    id "org.openapi.generator" version "4.2.3"
}

compileJava.dependsOn('openApiGenerate')

sourceSets {
    main {
        java {
            srcDir "${rootDir}/backend/openapi/src/main/java"
        }
    }
}

openApiValidate {
    inputSpec = "${rootDir}/openapi/schema.yaml".toString()
}

openApiGenerate {
    generatorName = "spring"
    library = "spring-boot"
    inputSpec = "${rootDir}/openapi/schema.yaml".toString()
    outputDir = "${rootDir}/backend/openapi".toString()
    systemProperties = [
            modelDocs      : "false",
            models         : "",
            apis           : "",
            supportingFiles: "false"
    ]
    configOptions = [
            useOptional          : "true",
            swaggerDocketConfig  : "false",
            performBeanValidation: "false",
            useBeanValidation    : "false",
            useTags              : "true",
            singleContentTypes   : "true",
            basePackage          : "de.mokkapps.gamenews.api",
            configPackage        : "de.mokkapps.gamenews.api",
            title                : rootProject.name,
            java8                : "false",
            dateLibrary          : "java8",
            serializableModel    : "true",
            artifactId           : rootProject.name,
            apiPackage           : "de.mokkapps.gamenews.api",
            modelPackage         : "de.mokkapps.gamenews.api.model",
            invokerPackage       : "de.mokkapps.gamenews.api",
            interfaceOnly        : "true"
    ]
}

As you can see, two new Gradle tasks are defined: openApiValidate and openApiGenerate. The first task can be used to validate the schema definition, and the second task generates the code.

To be able to reference the generated code in the Spring Boot application it needs to be configured as sourceSet. Additionally, it is recommended to define compileJava.dependsOn('openApiGenerate') to ensure that the code is generated each time the Java code is compiled.

For the backend code, we just want to generate models and interfaces, which is done in configOptions by setting interfaceOnly: "true".

Detailed documentation about all possible configuration options can be found at the official GitHub repository.

Running ./gradlew openApiGenerate produces this code:

Generated Backend Code

Make sure to add this folder with generated code to your .gitignore file and exclude it from code coverage & analysis tools.

At this point, we can use the generated code in our Spring Boot backend. The first step is to create a Controller which implements the generated OpenAPI interface:

import de.mokkapps.gamenews.api.NewsApi;

@RequestMapping("/api")
@Controller
public class NewsController implements NewsApi {
    private final NewsService newsService;

    public NewsController(NewsService newsService) {
        this.newsService = newsService;
    }

    @Override
    @GetMapping("/news")
    @CrossOrigin(origins = "http://localhost:4200")
    @ApiOperation("Returns list of latest news")
    public ResponseEntity<List<Article>> getNews() {
        return new ResponseEntity<>(this.newsService.getNews(), HttpStatus.OK);
    }
}

This GET endpoint is available at /api/news and returns a list of news that is provided by NewsService which just returns a dummy news article:

@Service
public class NewsService {
    public List<Article> getNews() {
        List<Article> articles = new ArrayList<>();
        Article article = new Article();
        article.setDate(LocalDate.now());
        article.setDescription("An article description");
        article.setId(UUID.randomUUID());
        article.setImageUrl("https://images.unsplash.com/photo-1493711662062-fa541adb3fc8?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3450&q=80");
        article.setTitle("A title");
        articles.add(article);
        return articles;
    }
}

@CrossOrigin(origins = "http://localhost:4200") allows requests from our frontend during local development and @ApiOperation("Returns list of latest news") is used for Swagger UI which is configured in SpringConfig.jav.

Finally, we can run the backend using ./gradlew bootRun and trigger the news endpoint

curl -v http://localhost:8080/api/news

which returns this JSON payload:

[
  {
    "id": "75f71b92-d1e5-43dd-862f-739b69cdf3aa",
    "title": "A title",
    "date": "2020-02-26",
    "description": "An article description",
    "imageUrl": "https://images.unsplash.com/photo-1493711662062-fa541adb3fc8?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3450&q=80"
  }
]

Generate frontend code based on this schema

In this section, I want to describe how Angular code can be generated based on our schema definition.

First, the OpenAPI generator CLI needs to be added as npm dependency:

npm add @openapitools/openapi-generator-cli

Next step is to create a new npm script in package.json that generates the code based on the OpenAPI schema:

{
  "scripts": {
    "generate:api": "openapi-generator generate -g typescript-angular -i ../openapi/schema.yaml -o ./build/openapi"
  }
}

This script generates the code inside the frontend/build/openapi folder:

Generated Frontend Code

Make sure to add this folder with generated code to your .gitignore file and exclude it from code coverage & analysis tools.

It is also important to run this code generation script each time you run, test or build your application. I would, therefore, recommend using the pre syntax for npm scripts:

{
  "scripts": {
    "generate:api": "openapi-generator generate -g typescript-angular -i ../openapi/schema.yaml -o ./build/openapi",
    "prestart": "npm run generate:api",
    "start": "ng serve",
    "prebuild": "npm run generate:api",
    "build": "ng build"
  }
}

Finally, we can import the generated module in our Angular application in app.module.ts:

import { ApiModule } from 'build/openapi/api.module';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule, ApiModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Now we are ready and can use the generated code in the frontend part of the demo application. This is done in app.component.ts:

import { NewsService } from 'build/openapi/api/news.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  title = 'frontend';
  $articles = this.newsService.getNews();

  constructor(private readonly newsService: NewsService) {}
}

Last step is to use the AsyncPipe in the HTML to render the articles:

<h1>News</h1>

<div *ngFor="let article of $articles | async">
  <p>Title: {{article.title}}</p>
  <img [src]="article.imageUrl" width="400" alt="Article image" />
  <p>Date: {{article.date}}</p>
  <p>Description: {{article.description}}</p>
  <p>ID: {{article.id}}</p>
</div>

If your backend is running locally, you can now serve the frontend by calling npm start and open a browser on http://localhost:4200 and you should see the dummy article:

Running Demo

Alternative

Of course, it is also possible to generate the frontend code if you have no control over the backend code but is supports OpenAPI.

It is then necessary to adjust the npm script to use the backend URL instead of referencing the local schema file:

{
  "scripts": {
    "generate:api": "openapi-generator generate -g typescript-angular -i http://my.backend.example/swagger/v1/swagger.json -o ./build/openapi"
  }
}

Conclusion

Having one file to define your API is helpful and can save you a lot of development time and prevent possible bugs caused by different models or API implementations in your frontend and backend code.

OpenAPI provides a good specification with helpful documentation. Additionally, many existing backends use Swagger for their API documentation, therefore it should also be possible to use this code generation for frontend applications where you cannot to modify the corresponding backend.

Due to the many supported languages and frameworks, it can be used in nearly every project, and the initial setup is not very hard.

In my current project, we use OpenAPI code generation for every new project and are very happy with it.

Let me know in the comments what you think about this approach and if you also have some OpenAPI experiences to share.

Michael Hoffmann

German freelance software engineer with focus on Angular