Miloš Zeljko

How to make API First Approach Spring + Angular

Miloš Zeljko
March 5, 2023 20 mins to read
Share

What is API First Approach?

 

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.

 

What is our goal?

 

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.

By doing so we will achieve the following:

      • More standardized API

      • Our backend and frontend will always be in sync, as the API structure is specified on one place only and enforced on all apps using the API.

      • Backend and fronted developers can work independently, as both of them have specifications on how data is passed by which makes it easy to mock other side of API.

      • We will get all DTOs (Data transfer objects) generated with the correct structure and data validation.

      • Less complexity when implementing API, as the method signature is defined by the code generator, therefor developers can focus only on business logic.

     

    Creating API specification using the stoplight.io platform

     

    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.

     

    api first approach 1

     

    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.

     

    api first approach 2

     

    First we need to create an API.

     

    api first approach 3

     

    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.

     

    api first approach 4

     

    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.

     

    api first approach 5

     

    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.

     

    api first approach 6

     

    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.

     

    api first approach 7

     

    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.

     

    api first approach 8

     

    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.

     

    api first approach 9

     

    We can define the following for the /users/{userId} endpoint:

    1. Endpoint name.
    2. Endpoint path.
    3. Allowed methods.
    4. Add some additional headers, define how the endpoint is secured and how to authorize, define query parameters and cookies for the HTTP request.
    5. Define how body of the HTTP request should look like.
    6. Define possible responses that the server could respond with with a status message, additional headers and response body.

     

    api first approach 10

     

    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: {}

     

    How to configure the Spring Boot project to generate code from API specification?

     

    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.

     

    api first approach 11

     

    Right-click on the project name, then go New, then Module.

     

    api first approach 12

     

    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.

     

    api first approach 13

     

    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.

     

    api first approach 14

     

    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.

     

    How to use generated classes in Spring Boot?

     

    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.

     

    api first approach 15

     

    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.

     

    api first approach 16

     

    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.

     

    api first approach 17

     

    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.

     

    api first approach 18

     

    How to configure the Angular application to generate code from API specification?

     

    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.

    api first approach 19

     

    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.

     

    How to use generated classes in Angular?

     

    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

    Leave a comment

    Your email address will not be published. Required fields are marked *