Skip to content

feat: error handling + withResponse + includeClient option #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Aug 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6f2c646
wip vibecode
astahmer Jul 31, 2025
2635631
wip: ApiResponse infer withResponse
astahmer Jul 31, 2025
d1def58
wip: fix response.error/status inference
astahmer Jul 31, 2025
1131429
fix: inference based on error status code
astahmer Aug 1, 2025
257add7
feat: TAllResponses for happy-case narrowing
astahmer Aug 1, 2025
0b7bcfb
feat: configurable status codes
astahmer Aug 1, 2025
2dc9ee5
chore: clean
astahmer Aug 1, 2025
34a9672
feat: includeClient option
astahmer Aug 1, 2025
cbbb98d
feat: add CLI options
astahmer Aug 1, 2025
dc870dc
chore: add examples
astahmer Aug 1, 2025
1f90499
docs: usage examples
astahmer Aug 1, 2025
75e5431
docs
astahmer Aug 1, 2025
f7864d5
feat: return Response object directly when using withResponse
astahmer Aug 1, 2025
fa672e7
chore: update snapshots
astahmer Aug 1, 2025
2a376d6
fix: tanstack type-error due to withResponse
astahmer Aug 1, 2025
d1ef389
wip: allow passing withResponse to tanstack api
astahmer Aug 1, 2025
299a632
feat: type mutationOptions errors (mutate callback onError)
astahmer Aug 1, 2025
ba3f941
feat: configurable error status codes
astahmer Aug 1, 2025
04ed53f
chore: changeset
astahmer Aug 1, 2025
6e6b047
refactor: throw a Response
astahmer Aug 1, 2025
8b48b00
chore: update docs
astahmer Aug 1, 2025
7f234f2
chore: add pkg.pr.new badge
astahmer Aug 3, 2025
3cb85e5
chore: add pkg.pr.new workflow
astahmer Aug 3, 2025
ebe8108
ci
astahmer Aug 3, 2025
ccdadba
ci
astahmer Aug 3, 2025
8abed72
chore: mv examples
astahmer Aug 24, 2025
22df339
feat: SuccessResponse/ErrorResponse
astahmer Aug 24, 2025
96b665a
feat: TypedResponseError + expose successStatusCodes/errorStatusCodes
astahmer Aug 24, 2025
d286060
docs: link to example fetcher + inline example
astahmer Aug 24, 2025
7a95a55
refactor: ai slop + allow withResponse/throwOnStatusError on tanstack…
astahmer Aug 24, 2025
7d35869
chore: update tests
astahmer Aug 24, 2025
9952737
docs
astahmer Aug 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/true-lemons-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"typed-openapi": major
---

Add comprehensive type-safe error handling and configurable status codes

- **Type-safe error handling**: Added discriminated unions for API responses with `SafeApiResponse` and `InferResponseByStatus` types that distinguish between success and error responses based on HTTP status codes
- **TypedResponseError class**: Introduced `TypedResponseError` that extends the native Error class to include typed response data for easier error handling
- Expose `successStatusCodes` and `errorStatusCodes` arrays on the generated API client instance for runtime access
- **withResponse parameter**: Enhanced API clients to optionally return both the parsed data and the original Response object for advanced use cases
- **throwOnStatusError option**: Added `throwOnStatusError` option to automatically throw `TypedResponseError` for error status codes, simplifying error handling in async/await patterns, defaulting to `true` (unless `withResponse` is set to true)
- **TanStack Query integration**: The above features are fully integrated into the TanStack Query client generator:
- Advanced mutation options supporting `withResponse` and `selectFn` parameters
- Automatic error type inference based on OpenAPI error schemas instead of generic Error type
- Type-safe error handling with discriminated unions for mutations
- Response-like error objects that extend Response with additional `data` property for consistency
- **Configurable status codes**: Made success and error status codes fully configurable:
- New `--success-status-codes` and `--error-status-codes` CLI options
- `GeneratorOptions` now accepts `successStatusCodes` and `errorStatusCodes` arrays
- Default error status codes cover comprehensive 4xx and 5xx ranges
- **Enhanced CLI options**: Added new command-line options for better control:
- `--include-client` to control whether to generate API client types and implementation
- `--include-client=false` to only generate the schemas and endpoints
- **Enhanced types**: expose `SuccessStatusCode` / `ErrorStatusCode` type and their matching runtime typed arrays
- **Comprehensive documentation**: Added detailed examples and guides for error handling patterns

This release significantly improves the type safety and flexibility of generated API clients, especially for error handling scenarios.
7 changes: 5 additions & 2 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ jobs:
run: pnpm build

- name: Run integration test (MSW)
run: pnpm test:runtime
run: pnpm -F typed-openapi test:runtime

- name: Type check generated client and integration test
run: pnpm exec tsc --noEmit tmp/generated-client.ts tests/integration-runtime-msw.test.ts
run: pnpm --filter typed-openapi exec tsc -b ./tsconfig.ci.json

- name: Test
run: pnpm test

- name: Release package
run: pnpm dlx pkg-pr-new publish './packages/typed-openapi'
247 changes: 239 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ See [the online playground](https://typed-openapi-astahmer.vercel.app/)

![Screenshot 2023-08-08 at 00 48 42](https://github.com/astahmer/typed-openapi/assets/47224540/3016fa92-e09a-41f3-a95f-32caa41325da)

[![pkg.pr.new](https://pkg.pr.new/badge/astahmer/typed-openapi)](https://pkg.pr.new/~/astahmer/typed-openapi)

## Features

- Headless API client, bring your own fetcher ! (fetch, axios, ky, etc...)
- Headless API client, [bring your own fetcher](packages/typed-openapi/API_CLIENT_EXAMPLES.md#basic-api-client-api-client-examplets) (fetch, axios, ky, etc...) !
- 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:
- [zod](https://zod.dev/)
- [typebox](https://github.com/sinclairzx81/typebox)
Expand Down Expand Up @@ -37,17 +42,26 @@ npx typed-openapi -h
```

```sh
typed-openapi/0.1.3
typed-openapi/1.5.0

Usage: $ typed-openapi <input>
Usage:
$ typed-openapi <input>

Commands: <input> Generate
Commands:
<input> Generate

For more info, run any command with the `--help` flag: $ typed-openapi --help
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
<name> Runtime to use for validation; defaults to `none`; available: 'none' | 'arktype' | 'io-ts' | 'typebox' |
'valibot' | 'yup' | 'zod' (default: none) -h, --help Display this message -v, --version Display version number
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 for type-safe error handling (defaults to 2xx and 3xx ranges)
--tanstack [name] Generate tanstack client with withResponse support for error handling, defaults to false, can optionally specify a name for the generated file
-h, --help Display this message
-v, --version Display version number
```

## Non-goals
Expand All @@ -65,6 +79,223 @@ Options: -o, --output <path> Output path for the api client ts file (defaults to

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:

- **[Basic API Client](packages/typed-openapi/API_CLIENT_EXAMPLES.md#basic-api-client-api-client-examplets)** - Simple, dependency-free wrapper
- **[Validating API Client](packages/typed-openapi/API_CLIENT_EXAMPLES.md#validating-api-client-api-client-with-validationts)** - With request/response validation


### Type-Safe Error Handling & Response Modes

You can choose between two response styles:

- **Direct data return** (default):
```ts
const user = await api.get("/users/{id}", { path: { id: "123" } });
// Throws TypedResponseError on error status (default)
```

- **Union-style response** (withResponse):
```ts
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:

```typescript
// 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:
```sh
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

```ts
// 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`:

```ts
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.

```ts
// 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:

```ts
// Basic usage
basicMutation.mutate({
body: {
emailAddress: '[email protected]',
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:

```ts
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](https://github.com/astahmer/openapi-zod-client), which generates a
Expand Down
Loading