Working with REST API using swagger-typescript-api

Before starting to write this article, I was puzzled by an interesting question. How will anyone work with APIs in 2024? For me, having a Swagger contract or OpenAPI contract has been a must have for several years now. And frankly, it’s hard for me to imagine that people don’t use this framework to work with the REST API. However, if there are any among the readers, and you are still being sent “dottos”, then go ahead and master and promote OpenApi.

To understand the work swagger-typescript-api I'll first briefly describe the main points of the OpenAPI specification. Readers who are already familiar with this can immediately go to the part about swagger-typescript-api.

The OpenAPI specification allows for the description of a remote API accessible via HTTP or HTTP-like protocols. OpenAPI can be thought of as a series of building blocks within a specific structure. For convenience, I will immediately outline the structure and those same building blocks:

Let's start with the structure. Each OpenAPI contract is a JSON object, but it can be described both in JSON format and in YAML-format I will immediately give examples of different implementations so that you can clearly evaluate the differences:

{
  "anObject": {
    "aNumber": 42,
    "aString": "This is a string",
    "aBoolean": true,
    "nothing": null,
    "arrayOfNumbers": [
      1,
      2,
      3
    ]
  }
}
# Anything after a hash sign is a comment
anObject:
  aNumber: 42
  aString: This is a string
  aBoolean: true
  nothing: null
  arrayOfNumbers:
    - 1
    - 2
    - 3

JSON does not support comments and requires: commas separating fields, curly braces around objects, double quotes around strings, and square brackets around arrays.

On the other hand, YAML requires hyphens before array elements and relies heavily on indentation, which can be inconvenient for large files (indentation in JSON is completely optional).

YAML is usually preferred due to its slightly reduced file size, but the two formats are completely interchangeable.

Let's look at what the OpenAPI contract includes (OpenAPI root object). Only two OpenAPI object fields are required: openapi And infobut you must also specify at least one of the following fields:

Field openapi (string) indicates the version of the specification, for example 3.1.0. In field info (Info Object) you can provide general information about the API, such as description, author, and contact information, but the only required fields are title (string) and version (string).

And the field paths (Paths Object) describes all API Endpoints, including their parameters and all possible server responses. This is what it will look like in YAML format:

openapi: 3.1.0
info:
  title: A minimal OpenAPI Description
  version: 0.0.1
paths: {}  # No endpoints defined

Now about API Endpoints (also called Operations or Routes). These are called Paths in the OpenAPI specification. Each field in a Paths object is a path element object (Path Item Object) describing one API endpoint.

All paths must start with a slash /, since they are directly appended to the server URL. Consider an example from Tic Tac Toe sample API:

openapi: 3.1.0
info:
  title: Tic Tac Toe
  description: |
    This API allows writing down marks on a Tic Tac Toe board
    and requesting the state of the board or of individual squares.
  version: 1.0.0
paths:
  # Whole board operations
  /board:
    get:
      summary: Get the whole board
      description: Retrieves the current state of the board and the winner.
      responses:
        "200":
          description: "OK"
          content:
            ...

Each path element object (Path Item Object) describes an HTTP operation that can be performed at a specific path (EndPoint). The example above describes the operation for the method get. Allowed operations correspond to HTTP method names, e.g. get, put etc.

Each operation object (Operation Object) provides a description as well as parameters, payload, and possible server responses.

Server response object (ResponsesObject) is a container for expected responses. The name of each field in this object is the HTTP response code (at least one must be present, typically “200”), and its value is the response object (Response Object) containing information about the response.

Response object (Response Object) contains a field description with a human-readable description, as well as the most important field contentwhich describes the possible response payloads.

Consider the field content more details. It is used both in the response object (Response Object), and in request body objects (Request Body Objects) and combines the standard RFC6838 Media Types and OpenAPI Media Type Objects. Here is an example field content for method get request /board:

openapi: 3.1.0
info:
  title: Tic Tac Toe
  description: |
    This API allows writing down marks on a Tic Tac Toe board
    and requesting the state of the board or of individual squares.
  version: 1.0.0
paths:
  # Whole board operations
  /board:
    get:
      summary: Get the whole board
      description: Retrieves the current state of the board and the winner.
      responses:
        "200":
          description: "OK"
          content:
            application/json:
              schema:
                type: object
                properties:
                    winner:
                      type: string
                      enum: [".", "X", "O"]
                      description: |
                        Winner of the game. `.` means nobody has won yet.
                    board:
                      type: array
                      maxItems: 3
                      minItems: 3
                      items:
                          type: array
                          maxItems: 3
                          minItems: 3
                          items:
                            type: string
                            enum: [".", "X", "O"]
                            description: |
                              Possible values for a board square.
                              `.` means empty square.
  ...

Media Type Object describes the type and structure of content, and may also contain examples (more details in documentation). For type application/json the structure is described in the field schema.

Schema object (Schema Object) defines the data type, which can be a primitive (integer, string, …), array or object depending on the field type. Each data type may have its own limitations, such as length and possible values ​​for strings, or maximum and minimum values ​​for integers or lists.

We've talked about describing responses, now let's move on to requests. OpenAPI provides two mechanisms for specifying input data – parameters and the request body (payload). Parameters are typically used to identify a resource (path parameters or query parameters), while payloads provide the content for that resource (body parameters). As always, it’s easier to understand with an example:

paths:
  # Single square operations
  /board/{row}/{column}:
    parameters:
      - name: row
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
          maximum: 3
      - name: column
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
          maximum: 3
    get:
      summary: Get a single board square
      responses:
        ...
    put:
      summary: Set a single board square
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: string
              enum: [".", "X", "O"]
      responses:
        ...

In method get we see a list of parameters. Each parameter object (Parameter Object) describes a single parameter with the following required fields:

  • in (string) – this field indicates the location of the parameter (path, query or header)

  • name (string) – this field indicates the name of the parameter and must be unique (case sensitive)

Additional fields may also be specified description And requiredas well as the type of the parameter using a schema object (Schema Object) in field schema.

When data is transferred using methods such as e.g. POST or PUTthey are placed in the field requestBody. Request body object (Request Body Object) contains only one field – contentthe same thing that is used in the response object (Response Object).

Besides the field paths in the OpenAPI root object (OpenAPI Object), there is a very useful field components. This field contains object definitions (ComponentsObject), which can be reused in other parts of the description. In other words, you can take out repeating patterns in componentsto avoid duplication and clutter. In an example it looks like this:

components:
  schemas:
    coordinate:
      type: integer
      minimum: 1
      maximum: 3
  parameters:
    rowParam:
      name: row
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/coordinate"
    columnParam:
      name: column
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/coordinate"
paths:
  /board/{row}/{column}:
    parameters:
      - $ref: "#/components/parameters/rowParam"
      - $ref: "#/components/parameters/columnParam"

Two are given here ComponentsObjectschemas And parameters. rowParam And columnParam referred to by Reference Objects on coordinate from schemas.

Also worth noting separately is the field serverswhich can be in the OpenAPI root object (OpenAPI Object), path element object (Path Item Object) and operation object (Operation Object).

Each field element servers represents a server object (Server Object), providing the field url with the base URL for this server, plus an optional field description. Here's what it looks like in an example:

servers:
- url: https://europe.server.com/v1
  description: Server located in Germany.
- url: https://america.server.com/v1
  description: Server located in Atlanta, GA.
- url: https://asia.server.com/v1
  description: Server located in Shenzhen
paths:
  /users:
    get:
      servers:
      - url: https://europe.server2.com/v1

Now that everyone knows how OpenAPI works, you can move on to working with swagger-typescript-api.

How could you guess swagger-typescript-api is a generator that uses Swagger or OpenAPI contract as a basis. To generate it, it uses the `server` field to indicate the address of the server in the HTTP client, based on which it creates a JS class where it maps the fields paths into methods. Accordingly, these methods will use ts interfaces that will be generated based on the fields schemeusing the types described there or links to the section components.

This library is quite easy to use, has good documentation and examples of usingas well as many options that allow you to use it quite flexibly.

You can install the library using the command npm i -D swagger-typescript-api.

First of all, I’ll tell you a couple of tips that will allow you to use this generator more efficiently:

  1. I recommend writing all response and request schemes in components, even if they are not reused. This approach will allow you to generate separate ts interfaces, which may have to be reused in the front project. Example from documentation:

Example of mapping components to ts interfaces

Example of mapping components to ts interfaces

  1. Select a separate directory in the project for your contracts and generated files services and write the commands in package.json by generation for each service separately. Saves time because you don't have to constantly copy the command and fill in values ​​for options. We have a monorepo, so we generally moved the services into a separate library, which we share with all host applications. This is what it looks like:

An example of a list of commands for generating APIs for services

An example of a list of commands for generating APIs for services

Example commands:

npx swagger-typescript-api -p ./swagger.json -o ./src -n myApi.ts
swagger-typescript-api -p ./swagger.json -o ./src -n myApi.ts
sta -p ./swagger.json -o ./src -n myApi.ts

Now I will describe a number of options that I use or have used, and which, in my opinion, will be useful.

I'll mark the flag first --axios, which makes it possible to switch the HTTP client from native fetch to axios. There is, of course, a question of taste here. I used to love axios, because before fetch it was extremely convenient. Now I abandoned it due to the problem of receiving custom headers in the response from the server.

There is one problem, which can be solved by additional settings on the backend. But in my case, it was faster to switch to fetch than to pull the guys, especially since fetch is also convenient.

Also I use flags --path And --outputto indicate where to get the contract and where to put the generated content.

Flags for everyone – --responses And --type-prefix. I add —-responsesto see possible response options for specific HTTP codes, as shown in the screenshot below:

Mapping response codes in JS Doc

Mapping response codes in JS Doc

This is useful when you have to handle business errors specially created by the backend.

Flag --type-prefix it adds the prefix you need before the types. There is a drawback that this prefix is ​​placed not only before ts-interfaces, but also ts-types and ts-enums. This can be solved, but more on that later.

Another useful flag --api-class-name, which will help if you have many different backend services. Here, I think, everything is clear and so.

How to use the generated content? As a result, you will have a JS class with the methods of your API, as well as all the interfaces that are used in these methods. For example, here is a miniclass for loading data:

export class DataLoadingModuleApi<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
  facade = {
    /**
     * @description Загрузка файла в хранилище
     *
     * @name LoadFileFacadeLoadFilePost
     * @summary Load File
     * @request POST:/facade/load_file
     * @response `200` `FileUploadResponse` Successful Response
     * @response `422` `HTTPValidationError` Validation Error
     */
    loadFileFacadeLoadFilePost: (
      data: BodyLoadFileFacadeLoadFilePost,
      query?: {
        /**
         * Force
         * @default false
         */
        force?: boolean;
      },
      params: RequestParams = {},
    ) =>
      this.request<FileUploadResponse, HTTPValidationError>({
        path: `/facade/load_file`,
        method: 'POST',
        query: query,
        body: data,
        type: ContentType.FormData,
        format: 'json',
        ...params,
      }),
  };
}

Method requestwhich is taken from the parent class HttpClient will return Promise With HttpResponse. For Axios there will be AxiosResponse accordingly. There are no limits to your imagination – you can create an instance of the class and pull the method directly in useEffect, you can use third-party libraries to work with the API, for example @tanstack/react-queryyou can write your own hook.

Here is an example hook with useMutation from @tanstack/react-query and using the generated class DataLoadingModuleApi:

export const useLoadFilePostMutation = () => {
  const [token] = useLocalStorage('accessToken', { value: '' });

  return useMutation<
    HttpResponse<FileUploadResponse, HTTPValidationError>,
    HttpResponse<FileUploadResponse, HTTPValidationError>,
    BodyLoadFileFacadeLoadFilePost
  >({
    mutationFn: (data: BodyLoadFileFacadeLoadFilePost) =>
      new DataLoadingModuleApi().facade.loadFileFacadeLoadFilePost(
        data,
        { force: true },
        { headers: { authorization: `Bearer ${token.value}` } },
      ),
  });
};

And lastly, I would like to note the flag —-templates. This flag makes it possible to provide the generator with your custom eta-templates. This solves the problem with prefixes, and also makes it possible to sharpen this tool for your projects.

At work we use RemoteData And fp-ts, so we created a custom hook to work with the API. Also, for convenience, there is a helper function to work with this hook in order to be able to abstractly retrieve the methods inside it.

And we will now supplement this helper function with handles when changing the contract. For a long time now there has been a desire to switch to our own templates to solve this issue.

If you are interested, please write about it in the comments. I’ll quickly help you out, write my templates and also write about it here.

Thank you all for your attention!

Similar Posts

Leave a Reply

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