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:

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!).

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.