← Back to posts
2 min read

Using openapi-typescript with Swagger

Syncing backend and frontend interfaces with openapi-typescript

Front-EndBack-EndTypeScript

While working on a new project at my company, we were using TypeScript but the response types were often unknown, which made TypeScript feel pointless in those spots. Manually hitting the API and writing the response types by hand on the client was slow, so I started looking for a better approach. When I mentioned this to my friend Yongjun, he suggested trying a module called openapi-typescript, so I looked into it.

openapi-typescript reads a Swagger schema document and turns its contents into TypeScript interfaces. Our backend team happened to manage the API with Swagger, so we could use it right away. I let our backend engineer know that this module exists and that team productivity would improve if the schema docs were well-maintained, and they agreed to keep the schema in good shape.

Let's give it a try.

 

npx openapi-typescript ${schema docs url} --output schema.ts

There's not much to it. Just run the command above in your project.

You pass the URL to the schema document and the path of the output file. That's it. You can change the output file name to whatever you like.

 

Here's a short example of what the generated schema looks like in practice.

export interface path {
  '/api/v1/groups/{groupId}': {
    /** Returns the group that matches the given groupId. */
    get: operations['GroupController_getGroup'];
    /** Removes the group that matches the given groupId. */
    delete: operations['GroupController_removeGroup'];
    /** Updates the group that matches the given groupId. */
    patch: operations['GroupController_updateGroup'];
  };
}
 
export interface components {
  schemas: {
    Group$mZFLkyvTelC5g8XnyQrpOw: {
      id: number;
      /** Format: date-time */
      createdAt: string;
      /** Format: date-time */
      updatedAt: string;
      /** @description The group's domain. Defaults to the group's id if not provided. */
      domain: string | null;
      /** @description Name of the group */
      name: string;
      /** @description id of the user who created the group */
      creatorId: number;
    };
  };
}
 
export interface operations {
  /** Returns the group that matches the given groupId. */
  GroupController_getGroup: {
    parameters: {
      path: {
        groupId: number;
      };
    };
    responses: {
      200: {
        content: {
          'application/json': components['schemas']['Group$mZFLkyvTelC5g8XnyQrpOw'];
        };
      };
      /** When the token is missing or invalid */
      401: unknown;
      /** When the token is valid but the user lacks permission, not found is returned instead of forbidden */
      404: unknown;
    };
  };
}

path, components, and operations come out neatly organized like this. If the backend engineer writes good comments in the schema docs, those comments are pulled in as well.

 

Once schema.ts is generated, you can import and use the types where they're needed.

import { components } from './schema.ts';
 
type APIResponse = components['schemas']['APIResponse'];

Just import from schema.ts and use components["schemas"]["response name"]. The last key in components matches whatever name the backend engineer used in the Swagger schema.

 

You can also get the types you need for API requests, not just responses.

import { operations } from './schema.ts';
 
type getUsersById = operations['getUsersById'];

By indexing operations with the operation key, you get the types needed to make that request.

 

Here's a short example of how I actually use it in production code.

import { authenticateRequest } from '@/api/authenticateRequest';
 
import type { components, operations } from '@/types/model';
 
type GetGroupInfoPath = operations['GroupController_getGroup']['parameters']['path'];
 
interface GetGroupInfoProps {
  path: GetGroupInfoPath;
}
 
type GetGroupInfoResponse = components['schemas']['Group$mZFLkyvTelC5g8XnyQrpOw'];
 
export async function getGroupInfo({
  path,
}: GetGroupInfoProps): Promise<GetGroupInfoResponse | false> {
  const response = await authenticateRequest<GetGroupInfoResponse>({
    method: 'get',
    url: `/api/v1/groups/${path.groupId}`,
    useGroupId: false,
  });
 
  return response;
}
 
export type { GetGroupInfoProps, GetGroupInfoResponse };

This is the code that requests group information. You can pull both request and response types from the generated schema and use them in your API code. authenticateRequest is an axios wrapper I wrote separately to fit our project.

 

When the server schema changes, just run the original command again. The newly generated schema will overwrite the existing file.

For more details, check the GitHub link below.

openapi-typescript (github)