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.