Create an API client in Typescript

last updated: February 23, 2022

Having an API client with defined types can make building out an app a smoother and easier process as we can rely on the types to build out the payload we need to send in the request, and what we should expect in response.

This is where we can use some tools which work with an OpenAPI specification to help build out a client and keep it in sync with changes to the API. You can see more details of the API implementation on my previous post on Documenting a REST API in Typescript

There are a couple of tools we will cover in this post that take advantage of the OpenAPI standard to help build a API client and keep it up to date with API server.

Option 1: Generating Types

This option is useful if you have a client that contains custom logic or configuration within it or the OpenAPI specification doesn't fully document all aspects of the API, and you want to easily generate the Typescript types and keep them up to date with your REST API.

We will make use of a library called openapi-typescript which can generate Typescript types from an OpenAPI specification.

For this example we will use the Pet Store OpenAPI definition, which is an example schema provided by OpenAPI.

Getting started

We will create a bare-bones Typescript project for this demo, but the following steps are applicable to whichever Typescript based project you are working on.


# Create a new folder for the project
mkdir typescript-api-client
cd typescript-api-client

# Install dev dependencies
npm i --save-dev typescript ts-node

# Install dependencies
npm i axios

# Create tsconfig.json
npx tsc --init

Now we are going to use openapi-typescript to generate the types for our api.

npx openapi-typescript https://petstore3.swagger.io/api/v3/openapi.json --output schema.ts

The command takes in a link to an OpenAPI specification and we also specify the output file that will contain the types, we should now have a schema.ts file in our project.

Utilizing the Types

We will use axios to make the actual requests, so lets make a basic axios configuration first.

client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

let config: AxiosRequestConfig = {
  baseURL: 'https://petstore3.swagger.io/api/v3',
};

let client: AxiosInstance = axios.create(config);

export { client };

This will allow us to access an instance of axios with the baseUrl configured to the API's base URL.

Now lets create a PetStore class

PetStore.ts
import { client } from './client';
import { components } from './schema';

// We are using the auto generated type
type Pet = components['schemas']['Pet'];

export class PetStore {
  public async getPet(petId: number) {
    return client.get<Pet>(`/pet/${petId}`).then((res) => {
      return res.data;
    });
  }
}

We have added a getPet method which we can now use to retrieve a pet by its ID. We are importing the types from the previously generated schema file, so now we will know what type to expect from the getPet method.

If we were to call this method somewhere within our application, we will be aware of the type it is returning.

let petClient = new PetStore();

// We now know the object type of pet
let pet = await petClient.getPet(10);

The strength of auto generated types in this scenario is if the pet resource has a new property added to it, all we have to do is re-run our generation script and now our client is instantly updated and aware of the new types and we can keep our client up to date with our API very easily.

Option 2: Fully Generated Client

This option works well if the OpenAPI specification is accurate, kept up to date and fully describes details such as response and error structures, we can then auto generate a fully functioning API client.

We can use openapi-generator, which is a powerful tool that can generate API clients in many different languages.

There are a few different ways to install it, but I have found the easiest way to use the @openapitools/openapi-generator-cli npm package.

Getting started

npm install @openapitools/openapi-generator-cli

The simplest way to run it now is to add a script to the package.json file of your project.

package.json
{
  "name": "my-app",
  "version": "0.0.0",
  "scripts": {
    "generate-client": "openapi-generator-cli generate -i https://petstore3.swagger.io/api/v3/openapi.json -g typescript-fetch -o src/api --additional-properties=supportsES6=true,npmVersion=6.9.0,typescriptThreePlus=true",
  }
}

The command accepts the following arguments:

  • -i location of a OpenAPI specification
  • -g the language and format of the generated client (in this case we are asking for a typescript client using the fetch library)
  • -o specify the output directory
  • --additional-properties to enable specific properties related to our client format

We should now have generated code in the src/api directory.

Using the generated code

First of all we need to create a Configuration object. The generated code has a default configuration which should work without needing to modify it, but in a real application we likely have a dev or staging server as we develop and the configuration object allows us to specify these.

import { Configuration } from './api/runtime';

let apiConfiguration = new Configuration({
  // We could set this from an environment variable
  basePath: 'https://petstore3.swagger.io/api/v3',
});

Now we can pass the configuration object into the various api's we want to interact with.

let petApi = new PetApi(apiConfiguration);

let pet: Pet = await petApi.getPetById({ petId: 10 });

Now we have a fully working API client that is completely generated with a script, so as the API changes and grows we can re-run the generator script and our client library will be fully in sync.

Hi! I'm Niall McKenna

Technologist, Maker & Software Developer