Using Swagger or OpenAPI generated clients on Deno
Tags: deno, openapi, tutorial
Published: 2023-04-21 03:14:41

### Summary
I will share my experience on how to generate a client from a Swagger/OpenAPI definition, and how to better use it in a project.

FYI:[What is the difference between Swagger and OpenAPI](https://swagger.io/blog/api-strategy/difference-between-swagger-and-openapi/)

## Why?

**TL;DR: I found no docs explaining how to do this properly**

The [OpenAPI](https://www.openapis.org/) Specification is a specification language for HTTP APIs that provides a standardized means to define your API to others.

Its may not be the best or perfect solution, but it is a popular solution. It's supported by many languages and frameworks, and AFAIK mainly used to provide automatically generated documentation.

A very interesting part of it feature set is the ability to generate properly typed client code from its definition, with the FOSS project [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) being the main reference for such utilities.

However, there is little to no documentation on how to actually generate and use those clients on projects, and to make matters worse the available/generated documentation is wrong at times.

## Getting the definition

Given the definition often automated nature, I ran into three main issues:
 1. Given the dynamic nature of my project, those definitions are changing very frequently 
 2. The provided specification version may not be compatible with the generator
 3. The provided specification is slight off for its intended use

In order to solve those issues:
1. If you are in the same scenario, I strongly recommend writing an script to get the definition and fix all following issues. You can start getting the definition and writing it to disk with:```typescript
const schemaUrl = "http://localhost:8000/openapi.json"
const schemaResponse = await fetch(schemaUrl)
const textSchema = await definitionResponse.text()
Deno.writeTextFileSync('schema.json', textSchema)
```
2. There are many conversion tools, however I found the [Swagger Editor](https://editor.swagger.io/) to be a good tool to both inspect and convert your schema (On the web UI the conversion options are under `Edit`). We can automate the conversion step using a script as well.```typescript
const schemaUrl = "http://localhost:8000/openapi.json"
const schemaResponse = await fetch(schemaUrl)
const textSchema = await definitionResponse.text()
const convertedResponse = await fetch('https://converter.swagger.io/api/convert', {
        headers: {
            accept: 'application/json',
            'Content-Type': 'application/yaml',
        },
        method: "POST",
        body: textSchema
    }
) // despite its content type being yaml, it will accept json definition as well
const convertedText = await convertedResponse.text()
Deno.writeTextFileSync('schema.json', convertedText)
```
3. This might be not as obvious to everyone, but you can easily edit the schema by code before saving the final version.```typescript
const schemaUrl = "http://localhost:8000/openapi.json"
const schemaResponse = await fetch(schemaUrl)
const textSchema = await definitionResponse.text()
const convertedResponse = await fetch('https://converter.swagger.io/api/convert', {
        headers: {
            accept: 'application/json',
            contentType: 'application/yaml',
        },
        method: "POST",
        body: textSchema
    }
) // despite its content type being yaml, it will accept json definition as well
const convertedSchema = await convertedResponse.json()
delete convertedSchema['paths']['/undesired_route'] // delete a route
delete convertedSchema['paths']['/some_route']['get'] // delete a specific method from a route
Deno.writeTextFileSync('schema.json', JSON.stringify(convertedSchema))
```

## Generating the client

Next, we use the [NPX](https://docs.npmjs.com/cli/v8/commands/npx) to run the [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) CLI to generate a client definition from the saved schema.

`npx @openapitools/openapi-generator-cli generate -i ./schema.json -g typescript --additional-properties=platform=deno -o ./Client`

If you inspect the contents of the created Client, you will find a lot of boilerplate code, definitions and even some Markdown documentation.

## How to use the generated client

### Basic Usage
Under its generated documentation you should be use to able the client in the following way:
```typescript
import {SomeRouteApi, createConfiguration} from "./Client/index.ts"

const someRouteApi = new SomeRouteApi(createConfiguration())
const body = {
    someStringParameter: "someValue",
    someObjectParameter: { foo: "this", bar: "that"}
}
const response = someRouteApi.someRoutePost(body)
```

This however seems to be wrong, and your IDE would point out that the method `someRoutePost` defines its first argument as `someStringParameter` and its expects a `string`. You could still call it passing positional arguments such as in:
```typescript
import {SomeRouteApi, createConfiguration} from "./Client/index.ts"

const someRouteApi = new SomeRouteApi(createConfiguration())
const body = {
    someStringParameter: "someValue",
    someObjectParameter: { foo: "this", bar: "that"}
}
const response = someRouteApi.someRoutePost("someValue",{ foo: "this", bar: "that"})
```
The positional argument might be acceptable to some use cases, but if you have many optional arguments you might end with a call such as:
```typescript
const _unusedBodyObject:{
    paramZero?: string,
    paramOne?: string,
    paramTwo?: string,
    paramThree?: string,
    paramFour?: string,
    paramFive?: string
} = {
   paramFive: "someValue"
}
const response = someRouteApi.someRoutePost(undefined, undefined, undefined, undefined, undefined, "someValue")
```

In order to "fix" that, there are specific API classes in the `Client/types/ObjectParamAPI.ts` file that will allow calls using objects. For the example above, we could use the following code:
```typescript
import {createConfiguration} from "./Client/index.ts"
import {ObjectSomeRouteApi} from "Client/types/ObjectParamAPI.ts"

const someRouteApi = new ObjectSomeRouteApi(createConfiguration())
const response = someRouteApi.someRoutePost({paramFive: "someValue"})
```

### Changing default host and headers

As you might expect, those settings belong to the `Configurations` object created with the `createConfiguration` function.

However, the function don't expose anyway to update its values after creation neither does it expose direct APIs to set those values at creation.

In order to properly change those values, you could use the following example to generate custom configuration for your clients:
```typescript
import {createConfiguration, RequestContext} from "/Client/index.ts"
const myConfiguration = (host: string, token?: string, ) => createConfiguration({
    baseServer: {makeRequestContext: (endpoint, httpMethod)=> {
        const context: RequestContext = new RequestContext(host+endpoint,httpMethod)
        context.setHeaderParam("Some-Custom-Header", "someValue")
        if (token) context.setHeaderParam('Authorization', `Bearer ${token}`)
        return context
    }}
})
```
Going further, we can call every method passing specific configurations as the second argument on the `ObjectParamApi`

```typescript
const localService = "http://localhost:3000"
const someRouteApi = new ObjectSomeRouteApi(myConfiguration(localService))
const resp = await someRouteApi.someRouteGet() // Call without an Authorization Header
const resp = await someRouteApi.someRouteGet({}, myConfiguration(localService, "userToken")) // Call with an Authorization Header
```

### Better names for multiple instances or reuse

So far, you might have noticed that the methods are verbose, which is fine should you to use like:

```typescript
import {createConfiguration} from "./Client/index.ts"
import {ObjectSomeRouteApi} from "Client/types/ObjectParamAPI.ts"

const api = new ObjectSomeRouteApi(createConfiguration())
const response = api.someRoutePost({paramFive: "someValue"})
```

However, if you are using and/or exporting multiple route APIs at the same time, this gets very verbose like in this example:
```typescript
\\ apiClients.ts
import {createConfiguration} from "./Client/index.ts"
import {ObjectSomeRouteApi, ObjectOtherRouteApi, ObjectYetAnotherRouteApi } from "Client/types/ObjectParamAPI.ts"

export const someRoute = new ObjectSomeRouteApi(createConfiguration())
export const otherRoute = new ObjectOtherRouteApi(createConfiguration())
export const yetAnotherRoute = new ObjectYetAnotherRouteApi(createConfiguration())
\\ apiCaller.ts
import {someRoute, otherRoute, yetAnotherRoute} from "./apiClients.ts"
const some = await someRoute.someGet()
const other = await otherRoute.otherGet()
const yetAnother = await (await yetAnotherRoute.yetAnotherGet()
```

In this scenario, I strong recommend remapping the route client methods to an object with better usability, such as:
```typescript
\\ apiClients.ts
import {createConfiguration} from "./Client/index.ts"
import {ObjectSomeRouteApi, ObjectOtherRouteApi, ObjectYetAnotherRouteApi } from "Client/types/ObjectParamAPI.ts"

const someRoute = new ObjectSomeRouteApi(createConfiguration())
const otherRoute = new ObjectOtherRouteApi(createConfiguration())
const yetAnotherRoute = new ObjectYetAnotherRouteApi(createConfiguration())

export default const apiClient = {
    some: {
        get: someRoute.someGet.bind(someRoute)
    },
    other: {
        get: otherRoute.otherGet.bind(otherRoute)
    },
    yetAnother: {
        get: yetAnotherRoute.yetAnotherGet.bind(yetAnotherRoute)
    },
}
\\ apiCaller.ts
import apiClient from "./apiClients.ts"
const some = await apiClient.some.get()
const other = await apiClient.other.get()
const yetAnother = await apiClient.yetAnother.get()
```
## Final Considerations

This is a somewhat basic tutorial, it might not be the "best way" to do it, but its a way to get started. 

I really wish that someone had written this tutorial before me, it would have saved me a few hours and a lot of frustration.

I had to dig through multiple sources (code and otherwise) and test a few generators and client alternatives before reaching a working solution.