How to write less code? Automatic code generation with OpenApi
What is DTO?
When communicating with the backend, we need to regularly ensure that we continuously maintain the compatibility of the communication models. These models are commonly called DTO (Data Transfer Objects). This name distinguishes them from database models, which may contain more information. For example, a user model may contain the following fields:
type User = {
name: string;
password: string;
id: string;
}
When communicating with the frontend, we shouldn’t send the ‘password’ field, therefore our DTO should look like this:
type UserDto = {
name: string;
id: string;
}
When we have tens, hundreds, or thousands of these models in an application, the whole process of writing them can be very time-consuming, and prone to errors or simple typos. Then in addition there is the entire service code used for communication, and together that is a huge amount of code. But what if all this could be generated automatically without having to write a single line?
Swagger / OpenAPI
In 2011, Swagger was created to describe the API structure in a convenient JSON or YAML format. This format has been developed since, and under its new name, OpenAPI, enables to describe models and endpoints for communication of any given system. An example of an OpenAPI document looks like this.
{
"paths": {
"/pet": {
"post": {
"tags": [
"pet"
],
"summary": "Add a new pet to the store",
"description": "",
"operationId": "addPet",
"consumes": [
"application/json",
"application/xml"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "Pet object that needs to be added to the store",
"required": true,
"schema": {
"$ref": "#/definitions/Pet"
}
}
],
"responses": {
"405": {
"description": "Invalid input"
}
}
}
}
},
"definitions": {
"ApiResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"type": {
"type": "string"
},
"message": {
"type": "string"
}
}
},
"Pet": {
"type": "object",
"required": [
"name",
"photoUrls"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"category": {
"$ref": "#/definitions/Category"
},
"name": {
"type": "string",
"example": "doggie"
},
"photoUrls": {
"type": "array",
"xml": {
"wrapped": true
},
"items": {
"type": "string",
"xml": {
"name": "photoUrl"
}
}
},
"tags": {
"type": "array",
"xml": {
"wrapped": true
},
"items": {
"xml": {
"name": "tag"
},
"$ref": "#/definitions/Tag"
}
},
"status": {
"type": "string",
"description": "pet status in the store",
"enum": [
"available",
"pending",
"sold"
]
}
},
"xml": {
"name": "Pet"
}
},
"Tag": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
},
"xml": {
"name": "Tag"
}
},
}
}
When creating the API we can highlight two main approaches:
- 1. Code first - we write the code, and on its basis we determine the API structure
- 2. Contract first - we write the API structure in OpenAPI format, and then create the backend and frontend that will work on the basis of this structure
In this article we will look at case 1 (Code First) as it is the most common and universal.
The Backend Build
Endpoint APIs are grouped into controllers and methods. On the code side of the backend, it is a regular class method that does some work and returns a value. Suppose that we already have a code ready for our API, in order to describe the structure we need to mark the endpoints appropriately: Example in Java Spring:
@RestController
public class CustomController {
@RequestMapping(value = "/custom", method = RequestMethod.POST)
public String custom() {
return "custom";
}
}
Example in C# (.NET)
[HttpPost]
[ProducesResponseType (StatusCodes.Status201Created)]
[ProducesResponseType (StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(TodoItem item)
{
_context.TodoItems.Add(item);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = item.Id }, item);
}
After such marking, generally under the address /swagger.json we will have the description of our API available in the format in which we can use to generate the client.
Automation - generating API client
Openapi-generator https://github.com/OpenAPITools/openapi-generator serves as a useful tool for generating clients. It is able to generate all data models and services for communication. Firstly, the OpenAPI generator reads the description (swagger.json) and then generates an API client on the base of the selected generator. There are several available for TypeScript. By following this link: https://openapi-generator.tech/docs/generators/
You will find a complete list of generators. As you can see, there are many generators with which we can generate the client and even the server codes (when we are working according to the contract-first approach).
We install them with the help of npm
npm install @openapitools/openapi-generator-cli -D
We create the openapitools.json file in the root project directory
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "5.2.1"
}
}
Then we should add the action in package.json which will be used for this:
"scripts": {
"generate-api-client": "npx @openapitools/openapi-generator-cli generate --skip-validate-spec -i https://example.com/swagger.json --additional-properties=typescriptThreePlus=true -g typescript-fetch -o ./api-client"
},
Using a generated client
The complete client consists of models (DTOs) and services. What is DTO was described on begining. Service a complete logic which is required to load data.
Example autogenerated service looks like that:
export class HealthApi extends runtime.BaseAPI {
async healthGetRaw(initOverrides?: RequestInit): Promise<runtime.ApiResponse<boolean>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const response = await this.request({
path: `/Health`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.TextApiResponse(response) as any;
}
/**
*/
async healthGet(initOverrides?: RequestInit): Promise<boolean> {
const response = await this.healthGetRaw(initOverrides);
return await response.value();
}
}
As you can see the generator has created a method for getting health check. By implementing it anywhere, we can perform a direct operation on the API:
const result = await new HealthApi().healthGet({});