Skip to content

Instantly share code, notes, and snippets.

@jamiebuilds
Created April 8, 2022 17:48
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamiebuilds/d0f40b036d70d8c2c7cc0e210d01b8a0 to your computer and use it in GitHub Desktop.
Save jamiebuilds/d0f40b036d70d8c2c7cc0e210d01b8a0 to your computer and use it in GitHub Desktop.

OpenAPI TypeScript Generator

First we start off with a very generic module for executing OpenAPI operations:

export interface ApiRequest {
  path: string;
  method: string;
  params?: Record<string, string>;
  queryParams?: Record<string, string>;
  headers?: Record<string, string>;
  body?: any;
}

export interface ApiResponse {
  status: number;
  body: any;
}

export function api(request: ApiRequest): Promise<ApiResponse> {
  let url = request.path
  for (let match of request.path.matchAll(/\{([a-z]+)\}/gi)) {
    let name = match[1]!;
    let param = request.params?.[name];
    if (param == null) throw new Error(`Missing path param: ${name}`)
    url = url.replace(new RegExp(`\\{${name}\\}`), param)
  }
  if (request.queryParams != null) {
    url = `${url}?${new URLSearchParams(request.queryParams).toString()}`
  }
  let response = await fetch(url, {
    method: request.method,
    headers: request.headers,
  });
  return response.json();
}

On its own it can be used like this:

let response = await api({
  path: "/users/{user}",
  params: { user: "123" },
})
// Example Response: `{ status: 200, body: { user: { id: "123", name: "Jamie" } }`

But then you can wrap by generating code like so:

import { api, ApiRequest, ApiResponse } from "openapi-ts-mod"

export interface User {
  id: string
  name: string
}

export interface GetUserRequest extends ApiRequest {
  path: "/users/{user}"
  method: "get"
  params: { user: string }
}

export interface GetUser200Response extends ApiResponse {
  status: 200
  body: { user: User }
}

export interface GetUser404Response extends ApiResponse {
  status: 404
  body: { message: string }
}

export type GetUserResponse = GetUser200Response | GetUser404Response

export function typedApi(request: GetUserRequest): Promise<GetUserResponse>
export function typedApi(request: ApiRequest): Promise<ApiResponse> {
  return api(request)
}

Which you would then use the same way, but TypeScript would enforce everything:

import { typedApi } from "./typedApi.generated"

let response = await api({
  path: "/users/{user}",
  params: { user: 123 }, // 'number' should be 'string'
})

if (response.status === 200) {
  console.log(response.body.user.name) // 200: "Jamie"
} else {
  console.log(response.body.message) // 404: "Not Found"
}

Because this relies so heavily on TypeScript, the actual code that ends up in your bundle would be around 400 bytes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment