Featured image of post Microsoft Kiota - The Bespoke Premium REST API Client

Microsoft Kiota - The Bespoke Premium REST API Client

Nobody needs to write REST API clients manually anymore - generating a decent client from well-written definitions is a common practice. In this two-part series, I will introduce you to Microsoft Kiota, a new project that aims to provide a novel approach to the boring task of generating SDK clients. In the first part, I'll walk you through my sandbox REST API project, and demonstrate how to generate an OpenAPI definition file using the code-first approach.

🎒 Resources

The SDK Generation Dilemma

You might wonder: why bother with programmatically generating SDK clients? Isn’t it enough to pick one from the available HTTP client libraries, create the necessary models, write request/response and error handling code, and call it a day? Well… if you ask me, the answer is a no.

The Static Approach

While this approach — let’s call it the “static” method — might work well for polyglots in small projects, one should realize very soon that supporting multiple SDK clients is a nightmare.

Even if you focus only on popular languages - like Python, JavaScript, Java, and C#, you’ll find yourself responsible for writing and maintaining code across all these clients. And let’s not overlook the documentation—you’ll need to keep that in sync as well.

Sooner or later, the SDK client code falls behind the API. No need to explain why this is a bad thing. At the end, your users are going to use a raw HTTP client manually crafting the requests, or - even worse - they will leave your service for a better supported alternative.

Since SDK clients are not rocket science, we can automate the process of generating them. All we need is a well-defined API description and a tool capable of creating SDK clients. Fortunately, OpenAPI offers a solution to this challenge.

OpenAPI Standard & SDK Generators

The OpenAPI Specification (formerly known as Swagger) is a widely adopted standard for describing REST APIs.

☝️ Important to note that OpenAPI is a specification, and not a tool. Indeed, Swagger is the name of the toolset for designing, building, and documenting your APIs.

I am not going to dive deep into the OpenAPI Specification itself. There are so many great articles and tutorials out there that cover this topic. Instead, I will focus on the SDK generation.

The OpenAPI Specification is a JSON or YAML file that describes your API. It includes information about the endpoints, the request and response bodies, the parameters, and much more.

Here is a sample OpenAPI definition for a simple REST API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
openapi: 3.0.0
info:
  title: Greeter API
  version: 1.0.0

paths:
    /greeter:
        get:
            summary: Returns a greeting
            responses:
                '200':
                    description: A greeting
                    content:
                        application/json:
                            schema:
                                type: object
                                properties:
                                    message:
                                        type: string

By looking at this snippet, you can easily understand what this API does: It has a single endpoint (/greeter) that returns a greeting message in JSON format.

Since our APIs are described in a standard format, now it’s possible to generate SDK clients programmatically for various platforms. The evident solution would be to use the OpenAPI Generator, a popular tool that supports every major programming language and platform.

Naturally, I’ve chosen something different than the beaten path. As you might figure out from the title, I am going to talk about Microsoft Kiota.

Kiota what?

Kiota is a relatively new project from Microsoft. It came into my radar a few months ago, when I was reading a blog post from GitHub: Our move to generated SDKs.

Even though Kiota is not as mature as some other tools on the market, GitHub decided to use it for the following reasons:

  • it’s intuitive and developer friendly (great DX 👍)
  • it generates idiomatic and comprehensive SDKs
  • quality is a top priority: the stable subset of supported languages provides an intuitive and consistent experience for consumers
  • while it provides 100% coverage of the API, it’s also possible to narrow down the generated code to a specific subset of the API (curated, hand-picked SDKs)

Sounds promising, right? Let’s see how it looks like in practice.

Shall we?

Prerequisites

Kiota is a command line tool, so it fits perfectly into any CI/CD pipeline. Despite it’s a relatively new project, it has a decent ecosystem around it: you can use it as a CLI tool, or as a Maven plugin, or create an ad-hoc client using the Visual Studio Code extension. We cover all of these in the second part of this series.

To make it happen…

That’s for the Java part. For Python, I will use the Visual Studio Code Extension, which is a great tool for generating SDK clients from the comfort of the IDE.

💡 Kiota really shines here, as it provides a great developer experience to generate tailor-made SDK clients for your needs.

The REST API

All right, let’s start with the REST API. I’ve created a Spring Boot application which present some quotes of famous people. I removed all the stuff which does not help the understanding of the SDK generation process, so you might find this REST API incomplete - and you are right.

Before we dive into the details, let me share the GitHub repository of the sample project. Feel free to clone it and follow along.

💡 I prepared a handy DevContainer for this project, so you don’t have to install anything on your machine. Open the project in Visual Studio Code and reopen the project in the container. Or even better: use GitHub Codespaces, and you’re good to go! 🚀

Entities, Repositories, Services and Boilerplate - The Boring Stuff

If you’ve ever authored a REST API with (or without) Spring Boot, feel free to skip ahead to the next section. Or bear with me, and let’s see what I’ve prepared for you. First, let’s delve into the essential entities needed to accurately represent quotes and their respective authors.

The Person Entity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity
public class Person {
    @Schema(description = "The unique identifier of the person.", example = "1")
    @Id
    @GeneratedValue
    private Long id;

    @Schema(description = "The name of the person.", example = "John Doe")
    private String name;

    @Schema(description = "The occupation of the person.", example = "ACTOR")
    private Occupation occupation;

    public Person(String name, Occupation occupation) {
        this.name = name;
        this.occupation = occupation;
    }

    public Person() {

    }

    // Getters and setters are omitted for brevity
}

In this code snippet, you’ll notice an unusual annotation: @Schema. Unlike Spring Framework annotations, this one belongs to the springdoc-openapi library. Its purpose is to provide metadata for the OpenAPI definition. Here, I’ve used it to offer a concise description and an example for the properties of the Person entity.

Person entity visualized

Additionally, take note of the Occupation enum. It stands for a person’s occupation:

1
2
3
4
5
6
7
8
public enum Occupation {
    WRITER,
    ACTOR,
    MUSICIAN,
    POLITICIAN,
    SCIENTIST,
    ATHLETE
}

I hope it speaks for itself, so let’s move on to the Quote entity.

The Quote Entity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
public class Quote {
    @Schema(description = "The unique identifier of the quote.", example = "1")
    @Id
    @GeneratedValue
    private Long id;

    @Schema(description = "The author of the quote - a Person entity", example = "John Doe")
    @ManyToOne(targetEntity = Person.class)
    private Person author;

    @Schema(description = "The content of the quote.", example = "To be or not to be, that is the question.")
    private String content;

    public Quote(Person author, String content) {
        this.author = author;
        this.content = content;
    }

    // Getters and setters are omitted for brevity
}

As you can see, the Quote entity has a Person property, which represents the author of the quote via a many-to-one relationship, since a quote can have only one author, but an author can have multiple quotes.

The Repositories and Services

Since we need to persist the entities, I created a repository for each of them - they’re pretty standard Spring Data JPA repositories. I added a custom method to each Repository, to fetch authors by their name and to fetch quotes by their author’s.

1
2
3
public interface PersonRepository extends JpaRepository<Person, Long> {
    Optional<Person> findByName(String name);
}
1
2
3
public interface QuotesRepository extends JpaRepository<Quote, Long> {
    List<Quote> findAllByAuthorId(Long authorId);
}

Finally, for a decent separation of concerns, I created services for the quotes, and for their authors. Look, here is the PersonService, implemented:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class PersonServiceImpl implements PersonService {

    private final PersonRepository personRepository;

    public PersonServiceImpl(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Override
    public List<Person> getPersons() {
        return this.personRepository.findAll();
    }

    @Override
    public Optional<Person> getPersonById(Long id) {
        return this.personRepository.findById(id);
    }

    @Override
    public Person upsertPerson(Person person) {
        return this.personRepository.save(person);
    }
}

The Endpoints

Let’s start with the endpoint for the quotes:

HTTP MethodEndpointDescription
GET/quotesReturns a list of quotes
GET/quotes/{id}Returns a quote by ID
POST/quotesUpserts* a quote**

Also, I prepared an endpoint for the authors of the quotes:

HTTP MethodEndpointDescription
GET/authorsReturns a list of authors
GET/authors/{id}Returns an author by ID
POST/authorsUpserts* an author**

*Inserts, or if it already exists, updates the quote
**requires authentication - more on this later

☝️ Note that I’ve avoided using the PUT and PATCH methods — while the former is used for full updates, the latter is for partial updates. I wonder if Kiota can handle my lazy approach to REST API design. 🤔

As I mentioned earlier, the OpenAPI definition is generated by the springdoc-openapi-maven-plugin during the build process. You can find the generated file (openapi.json) in the target directory.

This scenario is called code-first approach, where you begin with writing the code itself, and then generate the API definition from it. This method allows for rapid development but may lead to inconsistencies if not carefully managed.

In the professional world, you should consider the design-first approach, where the API is thoughtfully planned out using the OpenAPI Specification prior to actual development. This offers a significant advantage: you can generate SDK clients before the actual implementation, which allows - for example - your frontend team to begin working on the client side while the backend team focuses on implementation. Although it requires more effort and discipline, the productivity gains are worth it.

Given that this is a sandbox project for demonstration purposes, we are good to go with the first approach. 🤷

Annotations

Using clever annotations, you will end up with a properly documented API, which can be feed into Kiota to generate the SDK clients we need.

While these annotations may seem self-explanatory, it’s worth taking the time to explain each of them individually.

Visual explanation of annotations on an endpoint

@Operation

Describes a single operation on a path. It could have more properties, but in our case, we only provide a brief summary (summary is a required property).

@ApiResponse

This annotation is quite important for SDK generation. Usually, I would probably go with the plural form of this annotation (@ApiResponses), but for the sake of simplicity, I focused only on the “happy path” - when our service could return all the quotes of the given author.

@Content

Pay attention at the @Content annotation at the content property! Kiota uses this as a hint to make the generated SDKs type-safe. Without this, the generated SDKs would return a byte array, or a Stream (depending on the language).

@ArraySchema

This one is in tandem with the aforementioned @Content annotation. Use this, if your API operation returns a collection of an entity. In our case, I indicated that the consumer should expect a collection (well, an array) of Quote entities.

Let’s see how these annotations look like in practice. I put here the QuotesController from my sample project - sans the imports and package declaration, for brevity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController
@RequestMapping("/quotes")
class QuotesController {
    private final QuoteService quoteService;

    public QuotesController(QuoteService quoteService) {
        this.quoteService = quoteService;
    }

    @Operation(summary = "Get a list of all quotes")
    @ApiResponse(
        responseCode = "200",
        description = "Quotes are returned",
        content = @Content(
            mediaType = "application/json",
            array = @ArraySchema(schema = @Schema(implementation = Quote.class))
        )
    )

    @GetMapping
    public List<Quote> getQuotes() {
        return this.quoteService.getQuotes();
    }

    @Operation(summary = "Get all the quotes by an author")
    @ApiResponse(
        responseCode = "200",
        description = "Quotes are returned",
        content = @Content(
            mediaType = "application/json",
            array = @ArraySchema(schema = @Schema(implementation = Quote.class))
        )
    )

    @GetMapping("/author/{authorId}")
    public List<Quote> getQuotesByAuthor(@PathVariable("authorId") Long authorId) {
        return this.quoteService.getQuotesByAuthor(authorId);
    }
}

Okay, we have the REST API ready. Now, let’s generate the OpenAPI definition file using the springdoc-openapi-maven-plugin.

Here is the relevant part from my pom.xml in the api module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <jvmArguments>-Dspring.application.admin.enabled=true</jvmArguments>
            </configuration>
            <executions>
                <execution>
                    <id>pre-integration-test</id>
                    <goals>
                        <goal>start</goal>
                    </goals>
                </execution>
                <execution>
                    <id>post-integration-test</id>
                    <goals>
                        <goal>stop</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-maven-plugin</artifactId>
            <version>1.4</version>
            <executions>
                <execution>
                    <id>api-docs</id>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <outputDir>${project.build.directory}</outputDir>
                <outputFileName>openapi.json</outputFileName>
                <skip>false</skip>
            </configuration>
        </plugin>
    </plugins>
</build>

This configuration tells Maven to generate the OpenAPI definition file (openapi.json) during the build process. You can find the generated file in the target directory of the api module. The springdoc-openapi-maven-plugin works a bit tricky - the generate goal is executed during the integration test phase, that’s why we need the spring-boot-maven-plugin to start and stop the application before and after the integration tests.

Additionally, the plugin documentation suggests that it can be configured to generate the OpenAPI definition in YAML format, instead of JSON. However, despite all my efforts, I couldn’t get it to work. Kiota works seamlessly with JSON, so I decided not to spend more time with the YAML option. If any readers have a solution for generating a YAML from the plugin, I’d greatly appreciate hearing about it in the comments! 🙏

The Swagger UI

I understand that following this tutorial on a theoretical level can be challenging. Fortunately, you have the option to run the api module on your local machine and access the Swagger UI hosted by the Spring Boot application. I highly recommend doing so because it is an excellent way to gain knowledge about all the moving parts of any API. The Swagger UI is generated automatically by the springdoc-openapi Maven plugin - feel free to play around with the settings to customize it according to your preference.

1
2
cd api
mvn spring-boot:run

After a few seconds, the Spring Boot application will be up and running. Navigate to http://localhost:8080/swagger-ui/index.html in your browser, and you will see the Swagger UI. Feel free to explore the API endpoints and the models.

Screenshot of the generated Swagger UI

Thank you for your patience during this lengthy introduction. By now, I hope you have a better grasp of the REST API I’ve prepared for this demonstration. In the next part, we’ll delve into generating SDK clients using Microsoft Kiota.

Above you can find the OpenAPI definition in both JSON and YAML format for your reference. We’ll use this file in the next part to generate the SDK clients.

See you soon! 👋

Appendix A: The OpenAPI Definition, as a JSON File

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
{
  "openapi": "3.0.1",
  "info": {
    "title": "OpenAPI definition",
    "version": "v0"
  },
  "servers": [
    {
      "url": "http://localhost:8080",
      "description": "Generated server url"
    }
  ],
  "paths": {
    "/persons": {
      "get": {
        "tags": [
          "persons-controller"
        ],
        "summary": "Get a list of all persons",
        "operationId": "getPersons",
        "responses": {
          "404": {
            "description": "Not Found"
          },
          "200": {
            "description": "Persons are returned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Person"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "persons-controller"
        ],
        "summary": "Adds a new person to the system, or updates it if it already exists",
        "operationId": "upsertPerson",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Person"
              }
            }
          },
          "required": true
        },
        "responses": {
          "404": {
            "description": "Not Found"
          },
          "201": {
            "description": "The person is created or updated",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              }
            }
          }
        }
      }
    },
    "/quotes": {
      "get": {
        "tags": [
          "quotes-controller"
        ],
        "summary": "Get a list of all quotes",
        "operationId": "getQuotes",
        "responses": {
          "200": {
            "description": "Quotes are returned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Quote"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/quotes/author/{authorId}": {
      "get": {
        "tags": [
          "quotes-controller"
        ],
        "summary": "Get all the quotes by an author",
        "operationId": "getQuotesByAuthor",
        "parameters": [
          {
            "name": "authorId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Quotes are returned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Quote"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/persons/{id}": {
      "get": {
        "tags": [
          "persons-controller"
        ],
        "summary": "Get a person by its ID",
        "operationId": "getPersonById",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "There is no person with the given ID"
          },
          "200": {
            "description": "The person is returned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Person": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The unique identifier of the person.",
            "format": "int64",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "The name of the person.",
            "example": "John Doe"
          },
          "occupation": {
            "type": "string",
            "description": "The occupation of the person.",
            "example": "ACTOR",
            "enum": [
              "WRITER",
              "ACTOR",
              "MUSICIAN",
              "POLITICIAN",
              "SCIENTIST",
              "ATHLETE"
            ]
          }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The unique identifier of the quote.",
            "format": "int64",
            "example": 1
          },
          "author": {
            "$ref": "#/components/schemas/Person"
          },
          "content": {
            "type": "string",
            "description": "The content of the quote.",
            "example": "To be or not to be, that is the question."
          }
        }
      }
    }
  }
}

Appendix B: The OpenAPI Definition, as a YAML File

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
openapi: 3.0.1
info:
  title: OpenAPI definition
  version: v0
servers:
  - url: http://localhost:8080
    description: Generated server url
paths:
  /persons:
    get:
      tags:
        - persons-controller
      summary: Get a list of all persons
      operationId: getPersons
      responses:
        '200':
          description: Persons are returned
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Person'
        '404':
          description: Not Found
    post:
      tags:
        - persons-controller
      summary: Adds a new person to the system, or updates it if it already exists
      operationId: upsertPerson
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Person'
        required: true
      responses:
        '201':
          description: The person is created or updated
          content:
            '*/*':
              schema:
                $ref: '#/components/schemas/Person'
        '401':
          description: Unauthorized
          content:
            '*/*':
              schema:
                $ref: '#/components/schemas/Person'
        '404':
          description: Not Found
  /quotes:
    get:
      tags:
        - quotes-controller
      summary: Get a list of all quotes
      operationId: getQuotes
      responses:
        '200':
          description: Quotes are returned
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Quote'
  /quotes/author/{authorId}:
    get:
      tags:
        - quotes-controller
      summary: Get all the quotes by an author
      operationId: getQuotesByAuthor
      parameters:
        - name: authorId
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Quotes are returned
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Quote'
  /persons/{id}:
    get:
      tags:
        - persons-controller
      summary: Get a person by its ID
      operationId: getPersonById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: The person is returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Person'
        '404':
          description: There is no person with the given ID
components:
  schemas:
    Person:
      type: object
      properties:
        id:
          type: integer
          description: The unique identifier of the person.
          format: int64
          example: 1
        name:
          type: string
          description: The name of the person.
          example: John Doe
        occupation:
          type: string
          description: The occupation of the person.
          example: ACTOR
          enum:
            - WRITER
            - ACTOR
            - MUSICIAN
            - POLITICIAN
            - SCIENTIST
            - ATHLETE
    Quote:
      type: object
      properties:
        id:
          type: integer
          description: The unique identifier of the quote.
          format: int64
          example: 1
        author:
          $ref: '#/components/schemas/Person'
        content:
          type: string
          description: The content of the quote.
          example: To be or not to be, that is the question.
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy