npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@immediate_media/openapi-client

v5.0.1

Published

A type-safe OpenAPI client generator.

Downloads

288

Readme

OpenAPI client

A type-safe OpenAPI client generator.

Table of Contents

Installation

yarn add @immediate_media/openapi-client

Generating the client

This repository exposes a command line tool that you can run to generate the OpenAPI client. After installing the package you can generate the client from an OpenAPI specification with the following command:

yarn oac http://example.api.com/docs.json

Alternatively, you can use a JSON file:

yarn oac -f spec.json

Where spec.json is the location of the OpenAPI specification file from which you want to generate the client.

Usage

Once the API client has been generated it can be instantiated as follows:

import { createMyApiClient } from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
  getAccessToken: () => 'my-access-token',
  refreshAccessToken: () => 'my-new-access-token',
  onError: console.error,
});

Where createMyApiClient is generated based on the title in the OpenAPI spec. For example, if the title is "Mobile BFF" then this function will be createMobileBffClient.

The client object exposes functions for each API operation, as defined by the OpenAPI specification. Each function is called with an object containing the following properties:

params

An object containing properties that are mapped to any named route parameters. For example, if you have the route /user/:name, then the name property should be passed in as params: { name: 'Alex' }.

query

An object containing a property for each query string parameter.

data

An object containing key-value to submit as the request body (i.e. for POST or PUT requests).


For example, given the following (simplified) OpenAPI specification:

{
  "openapi": "3.0.1",
  "info": {
    "title": "My API"
  },
  "paths": {
    "/example/{id}/get-stuff": {
      "get": {
        "operationId": "myExampleOperation",
        "parameters": [
          {
            "name": "id",
            "in": "path"
          },
          {
            "name": "limit",
            "in": "query"
          }
        ]
      }
    }
  }
}

When we run this code:

import { createMyApiClient } from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
});

client.myExampleOperation({
  params: { id: 123 },
  query: { limit: 1 },
});

A request like this would be made:

GET /example/123/get-stuff?limit=1

Query parameter serialization

Arrays are serialized in the brackets format, for example:

import { createMyApiClient } from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
});

client.search({
  params: { id: 123 },
  query: {
    text: 'hello',
    filter: ['world'],
    sort: {
      asc: 'foo',
    }
  },
});

Becomes:

GET /example/123/get-stuff?text=hello&filter[]=world&sort[asc]=foo

A custom serializer can be passed in via the paramsSerializer property, for example:

import qs from 'qs';
import { createMyApiClient } from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
  paramsSerializer: (params) => {
    return qs.stringify(params, {
      encodeValuesOnly: true,
      arrayFormat: 'brackets',
    });
  },
});

Typescript

Two types are generated for each API operation. One for the options (params, query and data) and one for the response, for example:

import { createMyApiClient, MyApiMethods } from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
});

export const getMyExample = (
  options: MyApiMethods['myExampleOperation']['options']
): MyApiMethods['myExampleOperation']['response'] => (
  client.myExampleOperation(options)
);

Types are also generated for each OpenAPI component present in your specification. These can be imported from MyApiModels.

MyApi is generated based on the title of the API as defined in the OpenAPI specification, transformed to pascal case.

For example, given the following specification:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Mobile BFF"
  },
  "components": {
    "schemas": {
      "Post": {
        "properties": {
          "title": {
            "type": "string"
          }
        },
        "type": "object",
        "required": ["title"]
      }
    }
  }
}

the Post model can be referenced as follows:

import { MobileBff } from '@immediate_media/openapi-client';

const post: MobileBffModels['Post'] = {
  title: 'My Post',
};

Authorization

The API client supports JWT token-based authentication. Any access token provided via the getAccessToken() function will be automatically attached to requests that require it. That is, those marked where the operation in the OpenAPI specs has a security property.

If a request fails an attempt is made to refresh the token by calling the refreshAccessToken() function and the request is retried. If the retry fails a 401 error will be thrown, at which point the consuming application can handle this error as appropriate (e.g. redirect the user to sign in again). If the access token has expired an attempt will be made to refresh the token before making the initial request, thus saving on unnecessary API calls.

You can optionally modify the refreshStatusCodes to be respected. For example, you may want to log an error and attempt a refresh when a 403 is returned, as well as a 401:

import { createMyApiClient } from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
  refreshStatusCodes: [401, 403],
});

Error handling

HTTP errors

Any HTTP errors encountered when using the client will be thrown as error object that includes the following properties:

| Property | Description | |--------------|------------------------------------------------------------------------| | statusCode | The HTTP status code. | | name | The name of the error (e.g. BadRequest). | | message | An error message. | | errors | An array containing any validation errors (see below). | | type | A key that can be set via the API to uniquely identify the type of error (e.g. /probs/the-thing-expired). |

If the request resulted in validation errors, such as a query parameter being in the wrong format, then errors will include one or more objects with the following properties:

| Property | Description | |--------------|---------------------------------------------------------| | property | The name of the property that failed validation. | | constraint | The name of the constraint that failed. | | message | A message explaining why the constraint failed. |

The isOpenApiClientError() function may be used to determine if an error is an expected OpenAPI client error (i.e. an HTTP error), for example:

import { createMyApiClient, isOpenApiClientError }
from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
});

try {
  await client.myExampleOperation();
} catch(err) {
  if (!isOpenApiClientError(err)) {
    throw err;
  }

  if (err.type === '/probs/the-thing-expired') {
    // Handle this specific case

    return;
  }

  console.error(`HTTP Error: ${err.statusCode}`);
}

Errors will be logged to the console. To implement custom error handling you can pass onError() and onClientError() callbacks when setting up the client, to handle server errors (5xx) and client errors (4xx), respectively. By default server errors will be logged via console.error and client errors via console.warn.

Timeout errors

Any timeout errors encountered when using the client will be thrown as an error object that includes the following properties:

| Property | Description | |--------------|------------------------------------------------------------------| | code | The timeout code see: https://www.npmjs.com/package/axios#error-types. | | name | The name of the error - OpenApiClientTimeoutError. | | message | An error message. |

The isOpenApiClientTimeoutError() function may be used to determine if an error is an expected OpenAPI timeout error, for example:

import { createMyApiClient, isOpenApiClientTimeoutError }
from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
});

try {
  await client.myExampleOperation();
} catch(err) {
  if (!isOpenApiClientTimeoutError(err)) {
    throw err;
  }

  console.error(`Request timed out: ${err.code}`);
}

By default errors will be logged to the console as a warning via console.warn. To implement custom error handling you can pass an onTimeoutError() callback when setting up the client.

Accept header

The API client will send an Accept header with every request in the format:

application/vnd.[NAME]+json; version=[VERSION]

Where NAME is generated based on the title defined in the OpenAPI specification, transformed to lower case, and VERSION is the version from the package.json of your repository.

This header is here in case the API wants to respond differently, or perhaps log some error based on the version recieved.

Debugging

To log all outgoing requests you can pass in an onRequest() function, which is called with details about every request. For example:

import { createMyApiClient } from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
  onRequest: ({ method, url }) => {
    console.debug(`${method.toUpperCase()} ${url.href}`);
  },
});

Request timeout

To prevent requests remaining unresolved for a long period, a default timeout of 15000ms is set on all requests.

This may be overridden using the timeout argument on open api client creation.

import { createMyApiClient } from '@immediate_media/openapi-client';

const client = createMyApiClient({
  baseURL: 'http://example.api.com',
  timeout: 5000,
});

The timeout can be specified as a number or an object of type Timeout which is an object that has optional keys for the method name with the value a number. The corresponding value will be attached to the method request via an interceptor.

Mocking

You can create a type-safe mock API client by installing the jest-mock-extended package:

yarn add jest-mock-extended -D

Creating a file containing something like the following, where the MyApi in createMyApiClient and MyApiClient is swapped out for the title in the OpenAPI spec, converted to pascal case (e.g. createMobileBffClient and MobileBffClient):

// jest.mockApiClient.ts
import { DeepMockProxy, mockDeep } from 'jest-mock-extended';
import {
  getMyApiOperations,
  createMyApiClient,
  MyApiClient,
} from '@immediate_media/openapi-client';

const noop = {} as MyApiClient;
const operations = getMyApiOperations(noop);

jest.mock('@immediate_media/openapi-client', () => ({
  ...jest.requireActual('@immediate_media/openapi-client'),
  createMyApiClient: jest.fn(),
}));

const mockClient = mockDeep<OpenApiClient>() as DeepMockProxy<OpenApiClient> & {
  [x: string]: jest.Mock;
};

(createMyApiClient as jest.Mock).mockReturnValue(mockClient);

Object.keys(operations).forEach((key) => {
  mockClient[key].mockImplementation(() => {
    console.warn(
      `No mock return value set for API client operation ${key}. ` +
        'Try adding a mock resolved value, for example: ' +
        `\`apiClient.${key}.mockResolvedValue({ foo: 'bar' })\``,
    );
  });
});

Adding the following to your Jest setupFilesAfterEnv array:

module.exports = {
  setupFilesAfterEnv: [
    './node_modules/@immediate_media/openapi-client/mock.ts',
  ],
};

Then in your tests you can then create a mock client by calling the createMyApiClient() function. All operations will have been replaced with Jest mocks, meaning you can mock API responses like so:

const client = createMyApiClient({ baseURL: 'http://example.api.com' });

client.myOperation.mockResolvedValue({ foo: 'bar' });