Lost in the code
blog about programming by Michal Zubkowicz

me

Michal Zubkowicz


Software architect | Full stack developer

"There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies" C.A.R. Hoare

Back to blog

Real Programmers Don't Write Code, They Generate Code - Frontend and Backend Communication

2022-02-09

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" } }, } }
As you can see above, we have the name of the endpoint (/pets), the type of query (get), and the type of data return.

When creating the API we can highlight two main approaches:
  1. 1. Code first - we write the code, and on its basis we determine the API structure
  2. 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"; } }
More info at https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api

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); }
More info at https://docs.microsoft.com/pl-pl/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-6.0&tabs=visual-studio

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

Summary

In this example we only have one endpoint and one model, but your application will probably have hundreds of them, so think about the amount of time that you can save and spend on the right programming. In addition, with this solution the number of communication errors is minimised to zero.

Muscula

Muscula is a tool that monitors your website or application and informs you as soon as an error occurs. It's like a developers console in browser but all errors are stored.



Muscula example
With this tool you can check the exact location of the issue in the code and what has caused it. You can get up to 1000 free errors per month. See more and sign up at: https://www.muscula.com

Comments