Skip to content

Examples

The types generated by openapi-typescript are universal, and can be used in a variety of ways. While these examples are not comprehensive, hopefully they’ll spark ideas about how to use these in your app.

Data fetching

Fetching data can be done simply and safely using an automatically-typed fetch wrapper:

openapi-fetch (recommended)
ts
import createClient from "openapi-fetch";
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript

const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });

const {
  data, // only present if 2XX response
  error, // only present if 4XX or 5XX response
} = await client.GET("/blogposts/{post_id}", {
  params: {
    path: { post_id: "123" },
  },
});

await client.PUT("/blogposts", {
  body: {
    title: "My New Post",
  },
});
openapi-typescript-fetch by @ajaishankar
ts
import { Fetcher } from 'openapi-typescript-fetch';
import type { paths } from './my-openapi-3-schema'; // generated by openapi-typescript

const fetcher = Fetcher.for<paths>();

// GET request
const getBlogPost = fetcher.path('/blogposts/{post_id}').method('get').create();

try {
  const { status, data } = await getBlogPost({ pathParams: { post_id: '123' } });
  console.log(data);
} catch (error) {
  console.error('Error:', error);
}

// PUT request
const updateBlogPost = fetcher.path('/blogposts').method('put').create();

try {
  await updateBlogPost({ body: { title: 'My New Post' } });
} catch (error) {
  console.error('Error:', error);
}
feature-fetch by builder.group
ts
import { createOpenApiFetchClient } from 'feature-fetch';
import type { paths } from './my-openapi-3-schema'; // generated by openapi-typescript

// Create the OpenAPI fetch client
const fetchClient = createOpenApiFetchClient<paths>({
  prefixUrl: 'https://myapi.dev/v1'
});

// Send a GET request
const response = await fetchClient.get('/blogposts/{post_id}', {
  pathParams: {
    post_id: '123',
  },
});

// Handle the response (Approach 1: Standard if-else)
if (response.isOk()) {
  const data = response.value.data;
  console.log(data); // Handle successful response
} else {
  const error = response.error;
  if (error instanceof NetworkError) {
    console.error('Network error:', error.message);
  } else if (error instanceof RequestError) {
    console.error('Request error:', error.message, 'Status:', error.status);
  } else {
    console.error('Service error:', error.message);
  }
}

// Send a PUT request
const putResponse = await fetchClient.put('/blogposts', {
  body: {
    title: 'My New Post',
  },
});

// Handle the response (Approach 2: Try-catch)
try {
  const putData = putResponse.unwrap().data;
  console.log(putData); // Handle the successful response
} catch (error) {
  // Handle the error
  if (error instanceof NetworkError) {
    console.error('Network error:', error.message);
  } else if (error instanceof RequestError) {
    console.error('Request error:', error.message, 'Status:', error.status);
  } else {
    console.error('Service error:', error.message);
  }
}
openapi-axios by @web-bee-ru
ts
import { OpenApiAxios } from "@web-bee-ru/openapi-axios";
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
import Axios from "axios";

const axios = Axios.create({
  baseURL: "https://myapi.dev/v1",
  adapter: "fetch", // strongly recommended (available since [email protected])
});

// Example 1. Usage with "axios" (default) status handling strategy (validStatus: 'axios')

const api = new OpenApiAxios<paths, "axios">(axios, { validStatus: "axios" }); // throws like axios (e.g. status 400+, network errors, interceptor errors)
// const api =  new OpenApiAxios<paths>(axios) // same result

try {
  const { status, data, response } = await api.get("/users");
} catch (err) {
  if (api.isAxiosError(err)) {
    if (typeof err.status === "number") {
      // status >= 400
    }
    // request failed (e.g. network error)
  }
  throw err; // axios.interceptors error
}

// Example 2. Usage with "fetch" status handling strategy (validStatus: 'fetch')

const fetchApi = new OpenApiAxios<paths, "fetch">(axios, { validStatus: "fetch" }); // throws like browser's fetch() (e.g. network errors, interceptor errors)

try {
  const { status, data, error, response } = await api.get("/users");

  if (error) {
    // status >= 400
  }
} catch (err) {
  if (api.isAxiosError(err)) {
    // request failed (e.g. network error)
  }
  throw err; // axios.interceptors error
}

// Example 3. Usage with "safe" status handling strategy (validStatus: 'all')
// (No try/catch required)

const safeApi = new OpenApiAxios<paths, "all">(axios, { validStatus: "all" }); // never throws errors

const { status, data, error, response } = await api.get("/users");

if (error) {
  if (typeof status === "number") {
    // status >= 400
  } else if (api.isAxiosError(error)) {
    // request failed (e.g. network error)
  }
  throw error; // axios.interceptors error
}

TIP

A good fetch wrapper should never use generics. Generics require more typing and can hide errors!

Hono

Hono is a modern server framework for Node.js that can be deployed to the edge (e.g. Cloudflare Workers) just as easily as a standard container. It also has TypeScript baked-in, so it’s a great fit for generated types.

After generating your types using the CLI, pass in the proper paths response for each endpoint:

ts
import { Hono } from "hono";
import { components, paths } from "./my-openapi-3-schema"; // generated by openapi-typescript

const app = new Hono();

/** /users */
app.get("/users", async (ctx) => {
  try {
    const users = db.get("SELECT * from users");
    return ctx.json<
      paths["/users"]["responses"][200]["content"]["application/json"]
    >(users);
  } catch (err) {
    return ctx.json<components["schemas"]["Error"]>({
      status: 500,
      message: err ?? "An error occurred",
    });
  }
});

export default app;

TIP

TypeChecking in server environments can be tricky, as you’re often querying databases and talking to other endpoints that TypeScript can’t introspect. But using generics will alert you of the obvious errors that TypeScript can catch (and more things in your stack may have types than you realize!).

Hono with @blgc/openapi-router

Instead of manually typing each route with generics as in the Hono example, @blgc/openapi-router wraps around the Hono router to deliver full typesafety and enforce your OpenAPI-Schema with validators.

Good To Know

While TypeScript types ensure compile-time safety, they don't enforce runtime schema validation. For runtime compliance, you need to integrate with validation libraries like Zod or Valibot. Although you must define the validation rules manually, they are type-safe to ensure these rules are correctly defined.

ts
import { createHonoOpenApiRouter } from '@blgc/openapi-router';
import { Hono } from 'hono';
import { zValidator } from 'validation-adapters/zod';
import * as z from 'zod';

import { paths } from './gen/v1'; // OpenAPI-generated types
import { PetSchema } from './schemas'; // Custom reusable Zod schema for validation

export const router = new Hono();
export const openApiRouter = createHonoOpenApiRouter<paths>(router);

// GET /pet/{petId}
openApiRouter.get('/pet/{petId}', {
  pathValidator: zValidator(
    z.object({
      petId: z.number() // Validate that petId is a number
    })
  ),
  handler: (c) => {
    const { petId } = c.req.valid('param'); // Access validated params
    return c.json({ name: 'Falko', photoUrls: [] }); 
  }
});

// POST /pet
openApiRouter.post('/pet', {
  bodyValidator: zValidator(PetSchema), // Validate request body using PetSchema
  handler: (c) => {
    const { name, photoUrls } = c.req.valid('json'); // Access validated body data
    return c.json({ name, photoUrls }); 
  }
});

Full example

Express with @blgc/openapi-router

@blgc/openapi-router wraps around the Express router to deliver full typesafety and enforce your OpenAPI-Schema with validators.

Good To Know

While TypeScript types ensure compile-time safety, they don't enforce runtime schema validation. For runtime compliance, you need to integrate with validation libraries like Zod or Valibot. Although you must define the validation rules manually, they are type-safe to ensure these rules are correctly defined.

ts
import { createExpressOpenApiRouter } from '@blgc/openapi-router';
import { Router } from 'express';
import * as v from 'valibot';
import { vValidator } from 'validation-adapters/valibot';

import { paths } from './gen/v1'; // OpenAPI-generated types
import { PetSchema } from './schemas'; // Custom reusable Zod schema for validation

export const router: Router = Router();
export const openApiRouter = createExpressOpenApiRouter<paths>(router);

// GET /pet/{petId}
openApiRouter.get('/pet/{petId}', {
  pathValidator: vValidator(
    v.object({
      petId: v.number() // Validate that petId is a number
    })
  ),
  handler: (req, res) => {
    const { petId } = req.params; // Access validated params
    res.send({ name: 'Falko', photoUrls: [] }); 
  }
});

// POST /pet
openApiRouter.post('/pet', {
  bodyValidator: vValidator(PetSchema), // Validate request body using PetSchema
  handler: (req, res) => {
    const { name, photoUrls } = req.body; // Access validated body data
    res.send({ name, photoUrls }); 
  }
});

Full example

Mock-Service-Worker (MSW)

If you are using Mock Service Worker (MSW) to define your API mocks, you can use a small, automatically-typed wrapper around MSW, which enables you to address conflicts in your API mocks easily when your OpenAPI specification changes. Ultimately, you can have the same level of confidence in your application's API client and API mocks.

Using openapi-typescript and a wrapper around fetch, such as openapi-fetch, ensures that our application's API client does not have conflicts with your OpenAPI specification.

However, while you can address issues with the API client easily, you have to "manually" remember to adjust API mocks since there is no mechanism that warns you about conflicts.

We recommend the following wrapper, which works flawlessly with openapi-typescript:

Test Mocks

One of the most common causes of false positive tests is when mocks are out-of-date with the actual API responses.

openapi-typescript offers a fantastic way to guard against this with minimal effort. Here’s one example how you could write your own helper function to typecheck all mocks to match your OpenAPI schema (we’ll use vitest/vitest-fetch-mock but the same principle could work for any setup):

Let’s say we want to write our mocks in the following object structure, so we can mock multiple endpoints at once:

ts
{
  [pathname]: {
    [HTTP method]: { status: [status], body: { …[some mock data] } };
  }
}

Using our generated types we can then infer the correct data shape for any given path + HTTP method + status code. An example test would look like this:

ts
import { mockResponses } from "../test/utils";

describe("My API test", () => {
  it("mocks correctly", async () => {
    mockResponses({
      "/users/{user_id}": {
        // ✅ Correct 200 response
        get: { status: 200, body: { id: "user-id", name: "User Name" } },
        // ✅ Correct 403 response
        delete: { status: 403, body: { code: "403", message: "Unauthorized" } },
      },
      "/users": {
        // ✅ Correct 201 response
        put: { 201: { status: "success" } },
      },
    });

    // test 1: GET /users/{user_id}: 200
    await fetch("/users/user-123");

    // test 2: DELETE /users/{user_id}: 403
    await fetch("/users/user-123", { method: "DELETE" });

    // test 3: PUT /users: 200
    await fetch("/users", {
      method: "PUT",
      body: JSON.stringify({ id: "new-user", name: "New User" }),
    });

    // test cleanup
    fetchMock.resetMocks();
  });
});

Note: this example uses a vanilla fetch() function, but any fetch wrapper—including openapi-fetch—could be dropped in instead without any changes.

And the magic that produces this would live in a test/utils.ts file that can be copy + pasted where desired (hidden for simplicity):

📄 test/utils.ts
ts
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript

// Settings
// ⚠️ Important: change this! This prefixes all URLs
const BASE_URL = "https://myapi.com/v1";
// End Settings

// type helpers — ignore these; these just make TS lookups better
type FilterKeys<Obj, Matchers> = {
  [K in keyof Obj]: K extends Matchers ? Obj[K] : never;
}[keyof Obj];
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
type MediaType = `${string}/${string}`;
type MockedResponse<T, Status extends keyof T = keyof T> =
  FilterKeys<OperationContent<T[Status]>, MediaType> extends never
    ? { status: Status; body?: never }
    : {
        status: Status;
        body: FilterKeys<OperationContent<T[Status]>, MediaType>;
      };

/**
 * Mock fetch() calls and type against OpenAPI schema
 */
export function mockResponses(responses: {
  [Path in keyof Partial<paths>]: {
    [Method in keyof Partial<paths[Path]>]: MockedResponse<
      PathResponses<paths[Path][Method]>
    >;
  };
}) {
  fetchMock.mockResponse((req) => {
    const mockedPath = findPath(
      req.url.replace(BASE_URL, ""),
      Object.keys(responses)
    )!;
    // note: we get lazy with the types here, because the inference is bad anyway and this has a `void` return signature. The important bit is the parameter signature.
    if (!mockedPath || (!responses as any)[mockedPath])
      throw new Error(`No mocked response for ${req.url}`); // throw error if response not mocked (remove or modify if you’d like different behavior)
    const method = req.method.toLowerCase();
    if (!(responses as any)[mockedPath][method])
      throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // likewise throw error if other parts of response aren’t mocked
    if (!(responses as any)[mockedPath][method]) {
      throw new Error(`${req.method} called but not mocked on ${mockedPath}`);
    }
    const { status, body } = (responses as any)[mockedPath][method];
    return { status, body: JSON.stringify(body) };
  });
}

// helper function that matches a realistic URL (/users/123) to an OpenAPI path (/users/{user_id}
export function findPath(
  actual: string,
  testPaths: string[]
): string | undefined {
  const url = new URL(
    actual,
    actual.startsWith("http") ? undefined : "http://testapi.com"
  );
  const actualParts = url.pathname.split("/");
  for (const p of testPaths) {
    let matched = true;
    const testParts = p.split("/");
    if (actualParts.length !== testParts.length) continue; // automatically not a match if lengths differ
    for (let i = 0; i < testParts.length; i++) {
      if (testParts[i]!.startsWith("{")) continue; // path params ({user_id}) always count as a match
      if (actualParts[i] !== testParts[i]) {
        matched = false;
        break;
      }
    }
    if (matched) return p;
  }
}

Additional Explanation

That code is quite above is quite a doozy! For the most part, it’s a lot of implementation detail you can ignore. The mockResponses(…) function signature is where all the important magic happens—you’ll notice a direct link between this structure and our design. From there, the rest of the code is just making the runtime work as expected.

ts
export function mockResponses(responses: {
  [Path in keyof Partial<paths>]: {
    [Method in keyof Partial<paths[Path]>]: MockedResponse<
      PathResponses<paths[Path][Method]>
    >;
  };
});

Now, whenever your schema updates, all your mock data will be typechecked correctly 🎉. This is a huge step in ensuring resilient, accurate tests.