Skip to content

Commit 8f1eaa5

Browse files
authored
feat: error handling + withResponse + includeClient option (#92)
* wip vibecode * wip: ApiResponse infer withResponse * wip: fix response.error/status inference * fix: inference based on error status code * feat: TAllResponses for happy-case narrowing * feat: configurable status codes * chore: clean * feat: includeClient option * feat: add CLI options Adds CLI options for client inclusion and success codes Introduces flags to control API client generation and customize success status codes via the command line, enhancing flexibility for different use cases. * chore: add examples * docs: usage examples * docs * feat: return Response object directly when using withResponse Improves type-safe API error handling and response access Refactors API client response typing to unify success and error data under a consistent interface. Replaces separate error property with direct data access and ensures the Response object retains its methods. Updates documentation with clearer examples for type-safe error handling and data access patterns. Facilitates more ergonomic and predictable client usage, especially for error cases. * chore: update snapshots * fix: tanstack type-error due to withResponse * wip: allow passing withResponse to tanstack api Adds type-safe error handling to TanStack Query generator Introduces discriminated unions and configurable success status codes for more robust error handling in generated TanStack Query clients. Supports advanced mutation options with `withResponse` and `selectFn` to enable granular control over success and error transformations in API responses. Improves documentation to highlight new error handling features and integration patterns. * feat: type mutationOptions errors (mutate callback onError) * feat: configurable error status codes * chore: changeset * refactor: throw a Response * chore: update docs * chore: add pkg.pr.new badge * chore: add pkg.pr.new workflow https://github.com/stackblitz-labs/pkg.pr.new?tab=readme-ov-file#examples * ci * ci * chore: mv examples * feat: SuccessResponse/ErrorResponse * feat: TypedResponseError + expose successStatusCodes/errorStatusCodes * docs: link to example fetcher + inline example * refactor: ai slop + allow withResponse/throwOnStatusError on tanstack mutations test: add integration tests for the tanstack runtime feat: expose runtime status codes + new InferResponseByStatus type ci: fix * chore: update tests * docs
1 parent 1ec0e27 commit 8f1eaa5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+12642
-738
lines changed

.changeset/true-lemons-think.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"typed-openapi": major
3+
---
4+
5+
Add comprehensive type-safe error handling and configurable status codes
6+
7+
- **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
8+
- **TypedResponseError class**: Introduced `TypedResponseError` that extends the native Error class to include typed response data for easier error handling
9+
- Expose `successStatusCodes` and `errorStatusCodes` arrays on the generated API client instance for runtime access
10+
- **withResponse parameter**: Enhanced API clients to optionally return both the parsed data and the original Response object for advanced use cases
11+
- **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)
12+
- **TanStack Query integration**: The above features are fully integrated into the TanStack Query client generator:
13+
- Advanced mutation options supporting `withResponse` and `selectFn` parameters
14+
- Automatic error type inference based on OpenAPI error schemas instead of generic Error type
15+
- Type-safe error handling with discriminated unions for mutations
16+
- Response-like error objects that extend Response with additional `data` property for consistency
17+
- **Configurable status codes**: Made success and error status codes fully configurable:
18+
- New `--success-status-codes` and `--error-status-codes` CLI options
19+
- `GeneratorOptions` now accepts `successStatusCodes` and `errorStatusCodes` arrays
20+
- Default error status codes cover comprehensive 4xx and 5xx ranges
21+
- **Enhanced CLI options**: Added new command-line options for better control:
22+
- `--include-client` to control whether to generate API client types and implementation
23+
- `--include-client=false` to only generate the schemas and endpoints
24+
- **Enhanced types**: expose `SuccessStatusCode` / `ErrorStatusCode` type and their matching runtime typed arrays
25+
- **Comprehensive documentation**: Added detailed examples and guides for error handling patterns
26+
27+
This release significantly improves the type safety and flexibility of generated API clients, especially for error handling scenarios.

.github/workflows/build-and-test.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ jobs:
2424
run: pnpm build
2525

2626
- name: Run integration test (MSW)
27-
run: pnpm test:runtime
27+
run: pnpm -F typed-openapi test:runtime
2828

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

3232
- name: Test
3333
run: pnpm test
34+
35+
- name: Release package
36+
run: pnpm dlx pkg-pr-new publish './packages/typed-openapi'

README.md

Lines changed: 239 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ See [the online playground](https://typed-openapi-astahmer.vercel.app/)
66

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

9+
[![pkg.pr.new](https://pkg.pr.new/badge/astahmer/typed-openapi)](https://pkg.pr.new/~/astahmer/typed-openapi)
10+
911
## Features
1012

11-
- Headless API client, bring your own fetcher ! (fetch, axios, ky, etc...)
13+
- Headless API client, [bring your own fetcher](packages/typed-openapi/API_CLIENT_EXAMPLES.md#basic-api-client-api-client-examplets) (fetch, axios, ky, etc...) !
1214
- Generates a fully typesafe API client with just types by default (instant suggestions)
15+
- **Type-safe error handling**: with discriminated unions and configurable success/error status codes
16+
- **withResponse & throwOnStatusError**: Get a union-style response object or throw on configured error status codes, with full type inference
17+
- **TanStack Query integration**: with `withResponse` and `selectFn` options for advanced success/error handling
1318
- Or you can also generate a client with runtime validation using one of the following runtimes:
1419
- [zod](https://zod.dev/)
1520
- [typebox](https://github.com/sinclairzx81/typebox)
@@ -37,17 +42,26 @@ npx typed-openapi -h
3742
```
3843

3944
```sh
40-
typed-openapi/0.1.3
45+
typed-openapi/1.5.0
4146

42-
Usage: $ typed-openapi <input>
47+
Usage:
48+
$ typed-openapi <input>
4349

44-
Commands: <input> Generate
50+
Commands:
51+
<input> Generate
4552

46-
For more info, run any command with the `--help` flag: $ typed-openapi --help
53+
For more info, run any command with the `--help` flag:
54+
$ typed-openapi --help
4755

48-
Options: -o, --output <path> Output path for the api client ts file (defaults to `<input>.<runtime>.ts`) -r, --runtime
49-
<name> Runtime to use for validation; defaults to `none`; available: 'none' | 'arktype' | 'io-ts' | 'typebox' |
50-
'valibot' | 'yup' | 'zod' (default: none) -h, --help Display this message -v, --version Display version number
56+
Options:
57+
-o, --output <path> Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)
58+
-r, --runtime <n> Runtime to use for validation; defaults to `none`; available: Type<"arktype" | "io-ts" | "none" | "typebox" | "valibot" | "yup" | "zod"> (default: none)
59+
--schemas-only Only generate schemas, skipping client generation (defaults to false) (default: false)
60+
--include-client Include API client types and implementation (defaults to true) (default: true)
61+
--success-status-codes <codes> Comma-separated list of success status codes for type-safe error handling (defaults to 2xx and 3xx ranges)
62+
--tanstack [name] Generate tanstack client with withResponse support for error handling, defaults to false, can optionally specify a name for the generated file
63+
-h, --help Display this message
64+
-v, --version Display version number
5165
```
5266

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

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

82+
## Usage Examples
83+
84+
### API Client Setup
85+
86+
The generated client is headless - you need to provide your own fetcher. Here are ready-to-use examples:
87+
88+
- **[Basic API Client](packages/typed-openapi/API_CLIENT_EXAMPLES.md#basic-api-client-api-client-examplets)** - Simple, dependency-free wrapper
89+
- **[Validating API Client](packages/typed-openapi/API_CLIENT_EXAMPLES.md#validating-api-client-api-client-with-validationts)** - With request/response validation
90+
91+
92+
### Type-Safe Error Handling & Response Modes
93+
94+
You can choose between two response styles:
95+
96+
- **Direct data return** (default):
97+
```ts
98+
const user = await api.get("/users/{id}", { path: { id: "123" } });
99+
// Throws TypedResponseError on error status (default)
100+
```
101+
102+
- **Union-style response** (withResponse):
103+
```ts
104+
const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
105+
if (result.ok) {
106+
// result.data is typed as User
107+
} else {
108+
// result.data is typed as your error schema for that status
109+
}
110+
```
111+
112+
You can also control error throwing with `throwOnStatusError`.
113+
114+
**All errors thrown by the client are instances of `TypedResponseError` and include the parsed error data.**
115+
116+
### Generic Request Method
117+
118+
For dynamic endpoint calls or when you need more control:
119+
120+
```typescript
121+
// Type-safe generic request method
122+
const response = await api.request("GET", "/users/{id}", {
123+
path: { id: "123" },
124+
query: { include: ["profile", "settings"] }
125+
});
126+
127+
const user = await response.json(); // Fully typed based on endpoint
128+
```
129+
130+
131+
### TanStack Query Integration
132+
133+
Generate TanStack Query wrappers for your endpoints with:
134+
```sh
135+
npx typed-openapi api.yaml --tanstack
136+
```
137+
138+
You get:
139+
- Type-safe queries and mutations with full error inference
140+
- `withResponse` and `selectFn` for advanced error and response handling
141+
- All mutation errors are Response-like and type-safe, matching your OpenAPI error schemas
142+
143+
## useQuery / fetchQuery / ensureQueryData
144+
145+
```ts
146+
// Basic query
147+
const accessiblePagesQuery = useQuery(
148+
tanstackApi.get('/authorization/accessible-pages').queryOptions
149+
);
150+
151+
// Query with query parameters
152+
const membersQuery = useQuery(
153+
tanstackApi.get('/authorization/organizations/:organizationId/members/search', {
154+
path: { organizationId: 'org123' },
155+
query: { searchQuery: 'john' }
156+
}).queryOptions
157+
);
158+
159+
// With additional query options
160+
const departmentCostsQuery = useQuery({
161+
...tanstackApi.get('/organizations/:organizationId/department-costs', {
162+
path: { organizationId: params.orgId },
163+
query: { period: selectedPeriod },
164+
}).queryOptions,
165+
staleTime: 30 * 1000,
166+
// placeholderData: keepPreviousData,
167+
// etc
168+
});
169+
```
170+
171+
or if you need it in a router `beforeLoad` / `loader`:
172+
173+
```ts
174+
import { tanstackApi } from '#api';
175+
176+
await queryClient.fetchQuery(
177+
tanstackApi.get('/:organizationId/remediation/accounting-lines/metrics', {
178+
path: { organizationId: params.orgId },
179+
}).queryOptions,
180+
);
181+
```
182+
183+
## useMutation
184+
185+
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.
186+
187+
```ts
188+
// Basic mutation (returns data only)
189+
const basicMutation = useMutation({
190+
// Will throws TypedResponseError on error status
191+
...tanstackApi.mutation("post", '/authorization/organizations/:organizationId/invitations').mutationOptions,
192+
onError: (error) => {
193+
// error is a Response-like object with typed data based on OpenAPI spec
194+
console.log(error instanceof Response); // true
195+
console.log(error.status); // 400, 401, etc. (properly typed)
196+
console.log(error.data); // Typed error response body
197+
}
198+
});
199+
200+
// With error handling using withResponse
201+
const mutationWithErrorHandling = useMutation(
202+
tanstackApi.mutation("post", '/users', {
203+
// Returns union-style result, never throws
204+
withResponse: true
205+
}).mutationOptions
206+
);
207+
208+
// With custom response transformation
209+
const customMutation = useMutation(
210+
tanstackApi.mutation("post", '/users', {
211+
selectFn: (user) => ({ userId: user.id, userName: user.name })
212+
}).mutationOptions
213+
);
214+
215+
// Advanced: withResponse + selectFn for comprehensive error handling
216+
const advancedMutation = useMutation(
217+
tanstackApi.mutation("post", '/users', {
218+
withResponse: true,
219+
selectFn: (response) => ({
220+
success: response.ok,
221+
user: response.ok ? response.data : null,
222+
error: response.ok ? null : response.data,
223+
statusCode: response.status
224+
})
225+
}).mutationOptions
226+
);
227+
```
228+
229+
### Usage Examples:
230+
231+
```ts
232+
// Basic usage
233+
basicMutation.mutate({
234+
body: {
235+
emailAddress: '[email protected]',
236+
department: 'engineering',
237+
roleName: 'admin'
238+
}
239+
});
240+
241+
242+
// With error handling
243+
// All errors thrown by mutations are type-safe and Response-like, with parsed error data attached.
244+
mutationWithErrorHandling.mutate(
245+
{ body: userData },
246+
{
247+
onSuccess: (response) => {
248+
if (response.ok) {
249+
toast.success(`User ${response.data.name} created!`);
250+
} else {
251+
if (response.status === 400) {
252+
toast.error(`Validation error: ${response.data.message}`);
253+
} else if (response.status === 409) {
254+
toast.error('User already exists');
255+
}
256+
}
257+
}
258+
}
259+
);
260+
261+
// Advanced usage with custom transformation
262+
advancedMutation.mutate(
263+
{ body: userData },
264+
{
265+
onSuccess: (result) => {
266+
if (result.success) {
267+
console.log('Created user:', result.user.name);
268+
} else {
269+
console.error(`Error ${result.statusCode}:`, result.error);
270+
}
271+
}
272+
}
273+
);
274+
```
275+
276+
## useMutation without the tanstack api
277+
278+
If you need to make a custom mutation you could use the `api` directly:
279+
280+
```ts
281+
const { mutate: login, isPending } = useMutation({
282+
mutationFn: async (type: 'google' | 'microsoft') => {
283+
return api.post(`/authentication/${type}`, { body: { redirectUri: search.redirect } });
284+
},
285+
onSuccess: (data) => {
286+
window.location.replace(data.url);
287+
},
288+
onError: (error, type) => {
289+
console.error(error);
290+
toast({
291+
title: t(`toast.login.${type}.error`),
292+
icon: 'warning',
293+
variant: 'critical',
294+
});
295+
},
296+
});
297+
```
298+
68299
## Alternatives
69300

70301
[openapi-zod-client](https://github.com/astahmer/openapi-zod-client), which generates a

0 commit comments

Comments
 (0)