Skip to content

astahmer/typed-openapi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

typed-openapi

Generate a Typescript API client from an OpenAPI spec

See the online playground

Screenshot 2023-08-08 at 00 48 42

pkg.pr.new

Features

  • Headless API client, bring your own fetcher (fetch, axios, ky, etc...) ! (You can generate that file with --default-fetcher)
  • Generates a fully typesafe API client with just types by default (instant suggestions)
  • Type-safe error handling: with discriminated unions and configurable success/error status codes
  • withResponse & throwOnStatusError: Get a union-style response object or throw on configured error status codes, with full type inference
  • TanStack Query integration: with withResponse and selectFn options for advanced success/error handling
  • Or you can also generate a client with runtime validation using one of the following runtimes:

The generated client is a single file that can be used in the browser or in node. Runtime validation schemas are provided by the excellent typebox-codegen

Install & usage

pnpm add typed-openapi

It exports a bunch of functions that can be used to build your own tooling on top of it. You can look at the CLI code so see how to use them.

CLI

npx typed-openapi -h
typed-openapi/2.0.0

Usage:
  $ typed-openapi <input>

Commands:
  <input>  Generate

For more info, run any command with the `--help` flag:
  $ typed-openapi --help

Options:
  -o, --output <path>             Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)
  -r, --runtime <n>               Runtime to use for validation; defaults to `none`; available: Type<"arktype" | "io-ts" | "none" | "typebox" | "valibot" | "yup" | "zod"> (default: none)
  --schemas-only                  Only generate schemas, skipping client generation (defaults to false) (default: false)
  --include-client                Include API client types and implementation (defaults to true) (default: true)
  --success-status-codes <codes>  Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)
  --error-status-codes <codes>    Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)
  --tanstack [name]               Generate tanstack client, defaults to false, can optionally specify a name (will be generated next to the main file) or absolute path for the generated file
  --default-fetcher [name]        Generate default fetcher, defaults to false, can optionally specify a name (will be generated next to the main file) or absolute path for the generated file
  -h, --help                      Display this message
  -v, --version                   Display version number

Non-goals

  • Caring too much about the runtime validation code. If that works (thanks to typebox-codegen), that's great, otherwise I'm not really interested in fixing it. If you are, feel free to open a PR.

  • Supporting all the OpenAPI spec. Regex, dates, files, whatever, that's not the point here. openapi-zod-client does a great job at that, but it's slow to generate the client and the suggestions in the IDE are not instant. I'm only interested in supporting the subset of the spec that makes the API client typesafe and fast to provide suggetions in the IDE.

  • Splitting the generated client into multiple files. Nope. Been there, done that. Let's keep it simple.

Basically, let's focus on having a fast and typesafe API client generation instead.

Usage Examples

API Client Setup

The generated client is headless - you need to provide your own fetcher. Here are ready-to-use examples:

Type-Safe Error Handling & Response Modes

You can choose between two response styles:

  • Direct data return (default):

    const user = await api.get("/users/{id}", { path: { id: "123" } });
    // Throws TypedResponseError on error status (default)
  • Union-style response (withResponse):

    const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
    if (result.ok) {
      // result.data is typed as User
    } else {
      // result.data is typed as your error schema for that status
    }

You can also control error throwing with throwOnStatusError.

All errors thrown by the client are instances of TypedResponseError and include the parsed error data.

Generic Request Method

For dynamic endpoint calls or when you need more control:

// Type-safe generic request method
const response = await api.request("GET", "/users/{id}", {
  path: { id: "123" },
  query: { include: ["profile", "settings"] }
});

const user = await response.json(); // Fully typed based on endpoint

TanStack Query Integration

Generate TanStack Query wrappers for your endpoints with:

npx typed-openapi api.yaml --tanstack

You get:

  • Type-safe queries and mutations with full error inference
  • withResponse and selectFn for advanced error and response handling
  • All mutation errors are Response-like and type-safe, matching your OpenAPI error schemas

useQuery / fetchQuery / ensureQueryData

// Basic query
const accessiblePagesQuery = useQuery(
  tanstackApi.get('/authorization/accessible-pages').queryOptions
);

// Query with query parameters
const membersQuery = useQuery(
  tanstackApi.get('/authorization/organizations/:organizationId/members/search', {
    path: { organizationId: 'org123' },
    query: { searchQuery: 'john' }
  }).queryOptions
);

// With additional query options
const departmentCostsQuery = useQuery({
  ...tanstackApi.get('/organizations/:organizationId/department-costs', {
    path: { organizationId: params.orgId },
    query: { period: selectedPeriod },
  }).queryOptions,
  staleTime: 30 * 1000,
  // placeholderData: keepPreviousData,
  // etc
});

or if you need it in a router beforeLoad / loader:

import { tanstackApi } from '#api';

await queryClient.fetchQuery(
  tanstackApi.get('/:organizationId/remediation/accounting-lines/metrics', {
    path: { organizationId: params.orgId },
  }).queryOptions,
);

useMutation

The mutation API supports both basic usage and advanced error handling with withResponse and custom transformations with selectFn. Note: All mutation errors are Response-like objects with type-safe error inference based on your OpenAPI error schemas.

// Basic mutation (returns data only)
const basicMutation = useMutation({
  // Will throws TypedResponseError on error status
  ...tanstackApi.mutation("post", '/authorization/organizations/:organizationId/invitations').mutationOptions,
  onError: (error) => {
    // error is a Response-like object with typed data based on OpenAPI spec
    console.log(error instanceof Response); // true
    console.log(error.status); // 400, 401, etc. (properly typed)
    console.log(error.data); // Typed error response body
  }
});

// With error handling using withResponse
const mutationWithErrorHandling = useMutation(
  tanstackApi.mutation("post", '/users', {
    // Returns union-style result, never throws
    withResponse: true
  }).mutationOptions
);

// With custom response transformation
const customMutation = useMutation(
  tanstackApi.mutation("post", '/users', {
    selectFn: (user) => ({ userId: user.id, userName: user.name })
  }).mutationOptions
);

// Advanced: withResponse + selectFn for comprehensive error handling
const advancedMutation = useMutation(
  tanstackApi.mutation("post", '/users', {
    withResponse: true,
    selectFn: (response) => ({
      success: response.ok,
      user: response.ok ? response.data : null,
      error: response.ok ? null : response.data,
      statusCode: response.status
    })
  }).mutationOptions
);

Usage Examples:

// Basic usage
basicMutation.mutate({
  body: {
    emailAddress: 'user@example.com',
    department: 'engineering',
    roleName: 'admin'
  }
});


// With error handling
// All errors thrown by mutations are type-safe and Response-like, with parsed error data attached.
mutationWithErrorHandling.mutate(
  { body: userData },
  {
    onSuccess: (response) => {
      if (response.ok) {
        toast.success(`User ${response.data.name} created!`);
      } else {
        if (response.status === 400) {
          toast.error(`Validation error: ${response.data.message}`);
        } else if (response.status === 409) {
          toast.error('User already exists');
        }
      }
    }
  }
);

// Advanced usage with custom transformation
advancedMutation.mutate(
  { body: userData },
  {
    onSuccess: (result) => {
      if (result.success) {
        console.log('Created user:', result.user.name);
      } else {
        console.error(`Error ${result.statusCode}:`, result.error);
      }
    }
  }
);

useMutation without the tanstack api

If you need to make a custom mutation you could use the api directly:

const { mutate: login, isPending } = useMutation({
  mutationFn: async (type: 'google' | 'microsoft') => {
    return api.post(`/authentication/${type}`, { body: { redirectUri: search.redirect } });
  },
  onSuccess: (data) => {
    window.location.replace(data.url);
  },
  onError: (error, type) => {
    console.error(error);
    toast({
      title: t(`toast.login.${type}.error`),
      icon: 'warning',
      variant: 'critical',
    });
  },
});

Alternatives

openapi-zod-client, which generates a zodios client but can be slow to provide IDE suggestions when the OpenAPI spec is large. Also, you might not always want to use zod or even runtime validation, hence this project.

Contributing

  • pnpm i
  • pnpm build
  • pnpm test

When you're done with your changes, please run pnpm changeset in the root of the repo and follow the instructions described here.

About

Generate a headless Typescript API client from an OpenAPI spec - optionally with a @tanstack/react-query client using queryOptions

Topics

Resources

License

Stars

Watchers

Forks

Languages