Typescript with the OpenAPI specification
Tags: openapi, swagger, schema, typescript
Published: 2022-01-31 21:10:36

---
title: Typescript with the OpenAPI specification
published: true
date: 2022-01-23 07:00:00 UTC
tags: openapi,swagger,schema,typescript
canonical_url: https://blog.simonireilly.com/posts/typescript-openapi
---

## Summary

[OpenAPI, formerly swagger](https://swagger.io/resources/open-api/) is a specification for RESTful interfaces that helps developers and engineers write and consume an API.

[Typescript](https://www.typescriptlang.org/) is a strongly typed programming language built on Javascript.

Most Typescript projects that utilise HTTP interfaces rely on an HTTP interface. There are many ways to create an OpenAPI specification for that interface, that is strongly typed to your Typescript.

I have conducted a review of the existing tooling, highlighting three ways that typescript and the OpenAPI specification can interact.

## Conclusion

If I had to pick, I would write my OpenAPI specification as a typed object, using typescript. Then I would want typechecking for my request/responses, but I would not want prescriptive code, and I would not use generation in the lifecycle, because it shouldn't be necessary.

### Compeller

So, because of that I am writing [compeller](https://github.com/simonireilly/compeller), A strong typescript binding for your OpenAPI Schema that doesn't need generation and is not prescriptive in coding style.

{% github simonireilly/compeller %}

You can check it out with:

```bash
npx compeller@alpha new
```

## Typescript - OpenAPI Mapping

At a high level we might say that we can either be OpenAPI specification driven, or Typescript code driven. These two approaches provide the following options.

1️⃣ Define an OpenAPI specification, and from that, generate the Typescript types for the API domain.

2️⃣ Use decorators on Typescript to produce an OpenAPI specification from the API domain.

There is a wildcard, type 3, where you an bind the typescript to the OpenAPI document by placing all or parts of the OpenAPI specification inside typescript itself. This approach does not use generation, but instead uses **type inference** to ensure the types and their schema are in agreement.

3️⃣ Combine the OpenAPI specification, and API domain types, into a single Typescript object.

* * *

## 1️⃣ OpenAPI => Typescript

The team behind the OpenAPI specification [provided an example here](https://github.com/OAI/OpenAPI-Specification/blob/42c153c36cd5226e299a2dd74e507aa288c13e8f/examples/v3.0/petstore.json).

We have our specification file from the above link. So lets review some of the tools we can ue to get our types from the specification:

### OpenAPI Generator

Link: [https://github.com/OpenAPITools/openapi-generator](https://github.com/OpenAPITools/openapi-generator)

Generate clients, and stub servers in multiple languages from the OpenAPI specification.

- <input type="checkbox" checked disabled> Generate client
- <input type="checkbox" disabled> Generate server - No TS support
- <input type="checkbox" checked disabled> Generate types

#### Usage

The below example creates a typescript API Client.

```bash
docker run \
  --rm \
  -v "${PWD}:/local" \
  openapitools/openapi-generator-cli generate \
  -i /local/spec.json \
  -g typescript-axios \
  -o /local/openapi-generator/ts
```

The created client will dutifully represent your types; here is the `NewPet` schema as an interface, automatically generated by the tool:

```ts
/**
 *
 * @export
 * @interface NewPet
 */
export interface NewPet {
    /**
     *
     * @type {string}
     * @memberof NewPet
     */
    'name': string;
    /**
     *
     * @type {string}
     * @memberof NewPet
     */
    'tag'?: string;
}
```

There is not yet support for generating a typescript server.

* * *

### OpenAPI Typescript Codegen

Link: [https://github.com/ferdikoomen/openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen)

Produce typescript api clients from OpenAPI specification. Build tooling is not Java

Supports:

- <input type="checkbox" checked disabled> Generate typescript clients

#### Usage

You can generate a client from the specification with:

```bash
npx openapi-typescript-codegen \
  --input ./spec.json \
  --output ./openapi-typescript-codegen \
  --exportSchemas true
```

The provided client can be used to fetch from the API:

```ts
import { PetsService } from './index';

const pets = await PetsService.listPets(5);
```

In this instance pets will be typed either as `Pet | Error` after the fetch promise resolves.

* * *

### Openapi Typescript

Link: [https://github.com/drwpow/openapi-typescript](https://github.com/drwpow/openapi-typescript)

Produce typescript interfaces, from OpenAPI schema

Supports:

- <input type="checkbox" checked disabled> Generate types

#### Usage

The below example creates Typescript types and interfaces that represent the API Document.

```bash
npx openapi-typescript spec.json \
  --output ./openapi-typescript/schema.ts
```

The created types include paths, operations, and components. Here is an example of the paths:

```ts
export interface paths {
  '/pets': {
    get: operations['listPets'];
    post: operations['createPets'];
  };
  '/pets/{petId}': {
    get: operations['showPetById'];
  };
}
```

The operations section is automatically generated. I can imagine this would be very useful.

You can build a simple response handler, that is not coupled to any particular framework like so:

```ts
import { operations } from './schema';

type showPetById = operations['showPetById'];

const response = <K extends keyof showPetById['responses']>(
  statusCode: K,
  body: showPetById['responses'][K]['content']['application/json']
) => {};
```

This binds the response codes to the response objects, which are inturn bound to your OpenAPI schema via the generation.

* * *

## 2️⃣ Typescript => OpenAPI

In this scenario we have our Typescript code, for our server, and we want to create an OpenAPI specification.

These decorator and binding methods require you to write your code in a specific way, which is restrictive, and not adoptable for existing large projects.

### NestJS

link: [https://docs.nestjs.com/openapi/introduction](https://docs.nestjs.com/openapi/introduction)

NestJS is a progressive typescript framework for building backend applications.

Supports:

- <input type="checkbox" checked disabled> Rendering documentation
- <input type="checkbox" checked disabled> Adding docs to controllers using decorators
- <input type="checkbox" checked disabled> Express and Fastify support

The documentation for this tool is extensive, so I will leave it to their talented team to explain the usage.

* * *

### TSOA

link: [https://tsoa-community.github.io/docs/](https://tsoa-community.github.io/docs/)

TSOA generates routes from your Typescript code for koa, fastify or express

- <input type="checkbox" disabled> Rendering documentation
- <input type="checkbox" disabled> Adding docs to controllers using decorators
- <input type="checkbox" disabled> Express and Fastify support

This tool has code bindings and decorators, but also makes use of generation, so there is a lot of configuration overhead.

* * *

## 3️⃣ Typescript <=> OpenAPI

This collection of tools bring the OpenAPI specification domain into the typescript typing system.

This gives some benefits when developers are more used to receiving type hints. Whilst it prevents the schema from being a portable JSON document, you can always export the javascript object to a JSON or YAML file.

This method is in my opinion less opaque, as it does not use generation like method 1️⃣, and less prescriptive than method 2️⃣, because you can write your code however you should like.

Basically I am all in on method 3️⃣!

### OpenAPI3 TS

#### Usage

The extensive type system of OpenAPI3-TS attempts to be true to the OpenAPI specification.

```ts
const spec: OpenAPIObject = {
  info: {
    title: 'Mandatory',
    version: '1.0.0',
  },
  openapi: '3.0.3',
  paths: {
    '/v1/version': {
      get: {
        responses: {
          '200': {
            description: 'example',
            content: {
              'application/json': {
                schema: {
                  type: 'string',
                },
              },
            },
          } as ResponseObject,
        },
      },
    } as PathItemObject,
  },
};
```

This has advantages over other type systems, because it will require a diligent documentation of the types to ensure the compiler agrees.

* * *

### AJV `JSONSchemaType`

Link: [https://ajv.js.org/guide/typescript.html#utility-types-for-schemas](https://ajv.js.org/guide/typescript.html#utility-types-for-schemas)

This utility type can take the API models that you declare as type aliases, or interfaces, and infer a fully JSON schema for the type.

Supports

- <input type="checkbox" checked disabled> Inferring schema type from object type.

#### Usage

This example is borrowed from te documentation.

```ts
import Ajv, {JSONSchemaType} from "ajv"
const ajv = new Ajv()

interface MyData {
  foo: number
  bar?: string
}

const schema: JSONSchemaType<MyData> = {
  type: "object",
  properties: {
    foo: {type: "integer"},
    bar: {type: "string", nullable: true}
  },
  required: ["foo"],
  additionalProperties: false
}

// validate is a type guard for MyData - type is inferred from schema type
const validate = ajv.compile(schema)

// or, if you did not use type annotation for the schema,
// type parameter can be used to make it type guard:
// const validate = ajv.compile<MyData>(schema)

const data = {
  foo: 1,
  bar: "abc"
}

if (validate(data)) {
  // data is MyData here
  console.log(data.foo)
} else {
  console.log(validate.errors)
}
```

The benefit of this method, is there is a runtime binding between the schema validation, and the typing system.

* * *

### JSON Schema to TS

Link: [https://github.com/ThomasAribart/json-schema-to-ts](https://github.com/ThomasAribart/json-schema-to-ts)

Given a schema object in typescript, the type it represents can be inferred.

Supports:

- <input type="checkbox" checked disabled> Inferring Schema to Object type

#### Usage

The `FromSchema` type requires the schema to be made a const, and this means the schema itself is missing type hints.

```ts
import { FromSchema } from 'json-schema-to-ts';

export const VersionSchema = {
  type: 'object',
  required: ['version'],
  additionalProperties: false,
  properties: {
    version: {
      type: 'string',
    },
  },
} as const;

export type Version = FromSchema<typeof VersionSchema>;
```

This is a little less convenient as you still need to know the OpenAPI specification, and the type will resolve to `never` with no error if you get it wrong.

## Closing

Thanks for reading this review, if there are any issues with the links let me know 👍