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 14 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
195 changes: 188 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,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 (defaults to 2xx and 3xx ranges)
--tanstack [name] Generate tanstack client, 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 +74,178 @@ 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 API slightly differs as you do not need to pass any parameters initially but only when using the `mutate` method:

```ts
// Basic mutation
const mutation = useMutation(
tanstackApi.mutation("post", '/authorization/organizations/:organizationId/invitations').mutationOptions
);

// Usage:
mutation.mutate({
body: {
emailAddress: '[email protected]',
department: 'engineering',
roleName: 'admin'
}
});
```

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