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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
24 changes: 24 additions & 0 deletions .changeset/true-lemons-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"typed-openapi": minor
---

Add comprehensive type-safe error handling and configurable status codes

- **Type-safe error handling**: Added discriminated unions for API responses with `SafeApiResponse` and `TypedApiResponse` types that distinguish between success and error responses based on HTTP status codes
- **withResponse parameter**: Enhanced API clients to optionally return both the parsed data and the original Response object for advanced use cases
- **TanStack Query integration**: Added complete TanStack Query client generation with:
- 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**: Renamed `StatusCode` to `TStatusCode` and added reusable `ErrorStatusCode` type
- **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.
3 changes: 3 additions & 0 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ jobs:

- name: Test
run: pnpm test

- name: Release package
run: pnpm dlx pkg-pr-new publish './packages/typed-openapi'
268 changes: 261 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ 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...)
- Generates a fully typesafe API client with just types by default (instant suggestions)
- **Type-safe error handling** with discriminated unions and configurable success status codes
- **TanStack Query integration** with `withResponse` and `selectFn` options for advanced 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 +41,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 +78,247 @@ 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

The generated client supports two response modes:

```typescript
// Default: Direct data return (simpler, but no error details)
const user = await api.get("/users/{id}", {
path: { id: "123" }
}); // user is directly typed as User object

// WithResponse: Full Response object with typed ok/status and data
const result = await api.get("/users/{id}", {
path: { id: "123" },
withResponse: true
});

// result is the actual Response object with typed ok/status overrides plus data access
if (result.ok) {
// Access data directly (already parsed)
const user = result.data; // Type: User
console.log("User:", user.name);

// Or use json() method for compatibility
const userFromJson = await result.json(); // Same as result.data
console.log("User from json():", userFromJson.name);

console.log("Status:", result.status); // Typed as success status codes
console.log("Headers:", result.headers); // Access to all Response properties
} else {
// Access error data directly
const error = result.data; // Type based on status code
if (result.status === 404) {
console.log("User not found:", error.message);
} else if (result.status === 401) {
console.log("Unauthorized:", error.details);
}
}
```### Success Response Type-Narrowing

When endpoints have multiple success responses (200, 201, etc.), the type is automatically narrowed based on status:

```typescript
const result = await api.post("/users", {
body: { name: "John" },
withResponse: true
});

if (result.ok) {
if (result.status === 201) {
// result.data typed as CreateUserResponse (201)
console.log("Created user:", result.data.id);
} else if (result.status === 200) {
// result.data typed as ExistingUserResponse (200)
console.log("Existing user:", result.data.email);
}
}
```

### 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:

```bash
npx typed-openapi api.yaml --runtime zod --tanstack
```

## 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({
...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', {
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
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