Skip to content

Commit d5bcebb

Browse files
committed
feat: type mutationOptions errors (mutate callback onError)
1 parent 7b58118 commit d5bcebb

File tree

5 files changed

+555
-22
lines changed

5 files changed

+555
-22
lines changed

packages/typed-openapi/API_CLIENT_EXAMPLES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,15 @@ if (result.ok) {
188188
}
189189
}
190190
```
191+
192+
## TanStack Query Integration
193+
194+
For React applications using TanStack Query, see:
195+
- [TANSTACK_QUERY_EXAMPLES.md](./TANSTACK_QUERY_EXAMPLES.md) for usage patterns with `withResponse` and `selectFn`
196+
- [TANSTACK_QUERY_ERROR_HANDLING.md](./TANSTACK_QUERY_ERROR_HANDLING.md) for type-safe error handling based on OpenAPI error schemas
197+
198+
Key features:
199+
- Type-safe mutations with `withResponse` option
200+
- Custom response transformation with `selectFn`
201+
- Automatic error type inference from OpenAPI specs
202+
- Full type inference for all scenarios
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# TanStack Query Error Handling Examples
2+
3+
This document demonstrates how the generated TanStack Query client provides type-safe error handling based on OpenAPI error schemas.
4+
5+
## Error Type Inference
6+
7+
The TanStack Query client automatically infers error types from your OpenAPI spec's error responses (status codes 400-511).
8+
9+
### OpenAPI Spec Example
10+
11+
```yaml
12+
paths:
13+
/users:
14+
post:
15+
responses:
16+
'201':
17+
description: Created
18+
content:
19+
application/json:
20+
schema:
21+
$ref: '#/components/schemas/User'
22+
'400':
23+
description: Validation Error
24+
content:
25+
application/json:
26+
schema:
27+
$ref: '#/components/schemas/ValidationError'
28+
'409':
29+
description: Conflict
30+
content:
31+
application/json:
32+
schema:
33+
$ref: '#/components/schemas/ConflictError'
34+
'500':
35+
description: Server Error
36+
content:
37+
application/json:
38+
schema:
39+
$ref: '#/components/schemas/ServerError'
40+
41+
components:
42+
schemas:
43+
User:
44+
type: object
45+
properties:
46+
id: { type: string }
47+
name: { type: string }
48+
email: { type: string }
49+
50+
ValidationError:
51+
type: object
52+
properties:
53+
message: { type: string }
54+
fields:
55+
type: array
56+
items: { type: string }
57+
58+
ConflictError:
59+
type: object
60+
properties:
61+
message: { type: string }
62+
existingId: { type: string }
63+
64+
ServerError:
65+
type: object
66+
properties:
67+
message: { type: string }
68+
code: { type: string }
69+
```
70+
71+
## Generated Error Types
72+
73+
For the above spec, the TanStack Query client generates:
74+
75+
```typescript
76+
// Error type is automatically inferred as:
77+
type CreateUserError =
78+
| { status: 400; data: ValidationError }
79+
| { status: 409; data: ConflictError }
80+
| { status: 500; data: ServerError }
81+
```
82+
83+
## Usage Examples
84+
85+
### Basic Usage with Type-Safe Error Handling
86+
87+
```typescript
88+
import { useMutation } from '@tanstack/react-query';
89+
import { tanstackApi } from './generated/tanstack-query-client';
90+
91+
function CreateUserForm() {
92+
const createUser = useMutation({
93+
...tanstackApi.mutation("post", "/users").mutationOptions,
94+
onError: (error) => {
95+
// error is fully typed based on your OpenAPI spec!
96+
if (error.status === 400) {
97+
// error.data is typed as ValidationError
98+
console.error('Validation failed:', error.data.message);
99+
console.error('Invalid fields:', error.data.fields);
100+
} else if (error.status === 409) {
101+
// error.data is typed as ConflictError
102+
console.error('User already exists:', error.data.existingId);
103+
} else if (error.status === 500) {
104+
// error.data is typed as ServerError
105+
console.error('Server error:', error.data.code, error.data.message);
106+
}
107+
},
108+
onSuccess: (user) => {
109+
// user is typed as User
110+
console.log('Created user:', user.name);
111+
}
112+
});
113+
114+
return (
115+
<form onSubmit={() => createUser.mutate({
116+
body: { name: 'John', email: '[email protected]' }
117+
})}>
118+
{/* form content */}
119+
</form>
120+
);
121+
}
122+
```
123+
124+
### Advanced Usage with withResponse
125+
126+
```typescript
127+
function AdvancedCreateUserForm() {
128+
const createUser = useMutation({
129+
...tanstackApi.mutation("post", "/users", {
130+
withResponse: true,
131+
selectFn: (response) => ({
132+
success: response.ok,
133+
user: response.ok ? response.data : null,
134+
error: response.ok ? null : response.data,
135+
statusCode: response.status,
136+
headers: response.headers
137+
})
138+
}).mutationOptions,
139+
onError: (error) => {
140+
// Same typed error handling as above
141+
switch (error.status) {
142+
case 400:
143+
toast.error(`Validation: ${error.data.fields.join(', ')}`);
144+
break;
145+
case 409:
146+
toast.error('Email already taken');
147+
break;
148+
case 500:
149+
toast.error(`Server error: ${error.data.code}`);
150+
break;
151+
}
152+
},
153+
onSuccess: (result) => {
154+
if (result.success) {
155+
toast.success(`Welcome ${result.user!.name}!`);
156+
// Access response headers
157+
const rateLimit = result.headers.get('x-rate-limit-remaining');
158+
}
159+
}
160+
});
161+
162+
// ... rest of component
163+
}
164+
```
165+
166+
### Error Type Discrimination in Action
167+
168+
```typescript
169+
// The error parameter is automatically discriminated based on status
170+
const handleError = (error: CreateUserError) => {
171+
switch (error.status) {
172+
case 400:
173+
// TypeScript knows error.data is ValidationError
174+
return {
175+
title: 'Validation Failed',
176+
message: error.data.message,
177+
details: error.data.fields.map(field => `${field} is invalid`)
178+
};
179+
180+
case 409:
181+
// TypeScript knows error.data is ConflictError
182+
return {
183+
title: 'User Exists',
184+
message: `User already exists with ID: ${error.data.existingId}`,
185+
action: 'login'
186+
};
187+
188+
case 500:
189+
// TypeScript knows error.data is ServerError
190+
return {
191+
title: 'Server Error',
192+
message: `Internal error (${error.data.code}): ${error.data.message}`,
193+
action: 'retry'
194+
};
195+
}
196+
};
197+
```
198+
199+
## Benefits
200+
201+
- **Full Type Safety**: Error types are automatically inferred from your OpenAPI spec
202+
- **No Manual Type Definitions**: Types are generated, not hand-written
203+
- **Discriminated Unions**: TypeScript can narrow error types based on status codes
204+
- **IDE Support**: Full autocomplete and type checking for error properties
205+
- **Runtime Safety**: Errors are thrown with consistent structure: `{ status, data }`
206+
207+
## Error Structure
208+
209+
All errors thrown by TanStack Query mutations follow this structure:
210+
211+
```typescript
212+
interface ApiError<TData = unknown> {
213+
status: number; // HTTP status code (400-511)
214+
data: TData; // Typed error response body
215+
}
216+
```
217+
218+
This makes error handling predictable and type-safe across your entire application.

0 commit comments

Comments
 (0)