|
| 1 | +--- |
| 2 | +title: ofetch Client |
| 3 | +description: Generate a type-safe ofetch client from OpenAPI with the ofetch client for openapi-ts. Fully compatible with validators, transformers, and all core features. |
| 4 | +--- |
| 5 | + |
| 6 | +<script setup lang="ts"> |
| 7 | +import { embedProject } from '../../embed' |
| 8 | +</script> |
| 9 | + |
| 10 | +# ofetch |
| 11 | + |
| 12 | +### About |
| 13 | + |
| 14 | +[ofetch](https://github.com/unjs/ofetch) is a lightweight wrapper around the Fetch API that adds useful defaults and features such as automatic response parsing, request/response hooks, and Node.js support. |
| 15 | + |
| 16 | +The ofetch client for Hey API generates a type-safe client from your OpenAPI spec, fully compatible with validators, transformers, and all core features. |
| 17 | + |
| 18 | +## Features |
| 19 | + |
| 20 | +- seamless integration with `@hey-api/openapi-ts` ecosystem |
| 21 | +- type-safe response data and errors |
| 22 | +- response data validation and transformation |
| 23 | +- access to the original request and response |
| 24 | +- granular request and response customization options |
| 25 | +- minimal learning curve thanks to extending the underlying technology |
| 26 | +- support bundling inside the generated output |
| 27 | + |
| 28 | +## Installation |
| 29 | + |
| 30 | +In your [configuration](/openapi-ts/get-started), add `@hey-api/client-ofetch` to your plugins and you'll be ready to generate client artifacts. :tada: |
| 31 | + |
| 32 | +::: code-group |
| 33 | + |
| 34 | +```js [config] |
| 35 | +export default { |
| 36 | + input: 'hey-api/backend', // sign up at app.heyapi.dev |
| 37 | + output: 'src/client', |
| 38 | + plugins: ['@hey-api/client-ofetch'], // [!code ++] |
| 39 | +}; |
| 40 | +``` |
| 41 | + |
| 42 | +```sh [cli] |
| 43 | +npx @hey-api/openapi-ts \ |
| 44 | + -i hey-api/backend \ |
| 45 | + -o src/client \ |
| 46 | + -c @hey-api/client-ofetch # [!code ++] |
| 47 | +``` |
| 48 | + |
| 49 | +::: |
| 50 | + |
| 51 | +## Configuration |
| 52 | + |
| 53 | +The ofetch client is built as a thin wrapper on top of ofetch, extending its functionality to work with Hey API. If you're already familiar with ofetch, configuring your client will feel like working directly with ofetch. |
| 54 | + |
| 55 | +When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. |
| 56 | + |
| 57 | +### `setConfig()` |
| 58 | + |
| 59 | +This is the simpler approach. You can call the `setConfig()` method at the beginning of your application or anytime you need to update the client configuration. You can pass any ofetch configuration option to `setConfig()` (including ofetch hooks), and even your own [ofetch](#custom-ofetch) instance. |
| 60 | + |
| 61 | +```js |
| 62 | +import { client } from 'client/client.gen'; |
| 63 | + |
| 64 | +client.setConfig({ |
| 65 | + baseUrl: 'https://example.com', |
| 66 | +}); |
| 67 | +``` |
| 68 | + |
| 69 | +The disadvantage of this approach is that your code may call the `client` instance before it's configured for the first time. Depending on your use case, you might need to use the second approach. |
| 70 | + |
| 71 | +### Runtime API |
| 72 | + |
| 73 | +Since `client.gen.ts` is a generated file, we can't directly modify it. Instead, we can tell our configuration to use a custom file implementing the Runtime API. We do that by specifying the `runtimeConfigPath` option. |
| 74 | + |
| 75 | +```js |
| 76 | +export default { |
| 77 | + input: 'hey-api/backend', // sign up at app.heyapi.dev |
| 78 | + output: 'src/client', |
| 79 | + plugins: [ |
| 80 | + { |
| 81 | + name: '@hey-api/client-ofetch', |
| 82 | + runtimeConfigPath: './src/hey-api.ts', // [!code ++] |
| 83 | + }, |
| 84 | + ], |
| 85 | +}; |
| 86 | +``` |
| 87 | + |
| 88 | +In our custom file, we need to export a `createClientConfig()` method. This function is a simple wrapper allowing us to override configuration values. |
| 89 | + |
| 90 | +::: code-group |
| 91 | + |
| 92 | +```ts [hey-api.ts] |
| 93 | +import type { CreateClientConfig } from './client/client.gen'; |
| 94 | + |
| 95 | +export const createClientConfig: CreateClientConfig = (config) => ({ |
| 96 | + ...config, |
| 97 | + baseUrl: 'https://example.com', |
| 98 | +}); |
| 99 | +``` |
| 100 | + |
| 101 | +::: |
| 102 | + |
| 103 | +With this approach, `client.gen.ts` will call `createClientConfig()` before initializing the `client` instance. If needed, you can still use `setConfig()` to update the client configuration later. |
| 104 | + |
| 105 | +### `createClient()` |
| 106 | + |
| 107 | +You can also create your own client instance. You can use it to manually send requests or point it to a different domain. |
| 108 | + |
| 109 | +```js |
| 110 | +import { createClient } from './client/client'; |
| 111 | + |
| 112 | +const myClient = createClient({ |
| 113 | + baseUrl: 'https://example.com', |
| 114 | +}); |
| 115 | +``` |
| 116 | + |
| 117 | +You can also pass this instance to any SDK function through the `client` option. This will override the default instance from `client.gen.ts`. |
| 118 | + |
| 119 | +```js |
| 120 | +const response = await getFoo({ |
| 121 | + client: myClient, |
| 122 | +}); |
| 123 | +``` |
| 124 | + |
| 125 | +### SDKs |
| 126 | + |
| 127 | +Alternatively, you can pass the client configuration options to each SDK function. This is useful if you don't want to create a client instance for one-off use cases. |
| 128 | + |
| 129 | +```js |
| 130 | +const response = await getFoo({ |
| 131 | + baseUrl: 'https://example.com', // <-- override default configuration |
| 132 | +}); |
| 133 | +``` |
| 134 | + |
| 135 | +## Interceptors |
| 136 | + |
| 137 | +Interceptors (middleware) can be used to modify requests before they're sent or responses before they're returned to your application. |
| 138 | + |
| 139 | +The ofetch client supports two complementary options: |
| 140 | + |
| 141 | +- Built-in interceptors exposed via `client.interceptors` (request/response/error) — same API across Hey API clients. |
| 142 | +- Native ofetch hooks passed through config: `onRequest`, `onRequestError`, `onResponse`, `onResponseError`. |
| 143 | + |
| 144 | +Below is an example request interceptor using the built-in API. |
| 145 | + |
| 146 | +::: code-group |
| 147 | + |
| 148 | +```js [use] |
| 149 | +import { client } from 'client/client.gen'; |
| 150 | +// Supports async functions |
| 151 | +async function myInterceptor(request) { |
| 152 | + // do something |
| 153 | + return request; |
| 154 | +} |
| 155 | +interceptorId = client.interceptors.request.use(myInterceptor); |
| 156 | +``` |
| 157 | + |
| 158 | +```js [eject] |
| 159 | +import { client } from 'client/client.gen'; |
| 160 | + |
| 161 | +// eject interceptor by interceptor id |
| 162 | +client.interceptors.request.eject(interceptorId); |
| 163 | + |
| 164 | +// eject interceptor by reference to interceptor function |
| 165 | +client.interceptors.request.eject(myInterceptor); |
| 166 | +``` |
| 167 | + |
| 168 | +```js [update] |
| 169 | +import { client } from 'client/client.gen'; |
| 170 | + |
| 171 | +async function myNewInterceptor(request) { |
| 172 | + // do something |
| 173 | + return request; |
| 174 | +} |
| 175 | +// update interceptor by interceptor id |
| 176 | +client.interceptors.request.update(interceptorId, myNewInterceptor); |
| 177 | + |
| 178 | +// update interceptor by reference to interceptor function |
| 179 | +client.interceptors.request.update(myInterceptor, myNewInterceptor); |
| 180 | +``` |
| 181 | + |
| 182 | +::: |
| 183 | + |
| 184 | +and an example response interceptor |
| 185 | + |
| 186 | +::: code-group |
| 187 | + |
| 188 | +```js [use] |
| 189 | +import { client } from 'client/client.gen'; |
| 190 | +async function myInterceptor(response) { |
| 191 | + // do something |
| 192 | + return response; |
| 193 | +} |
| 194 | +// Supports async functions |
| 195 | +interceptorId = client.interceptors.response.use(myInterceptor); |
| 196 | +``` |
| 197 | + |
| 198 | +```js [eject] |
| 199 | +import { client } from 'client/client.gen'; |
| 200 | + |
| 201 | +// eject interceptor by interceptor id |
| 202 | +client.interceptors.response.eject(interceptorId); |
| 203 | + |
| 204 | +// eject interceptor by reference to interceptor function |
| 205 | +client.interceptors.response.eject(myInterceptor); |
| 206 | +``` |
| 207 | + |
| 208 | +```js [update] |
| 209 | +import { client } from 'client/client.gen'; |
| 210 | + |
| 211 | +async function myNewInterceptor(response) { |
| 212 | + // do something |
| 213 | + return response; |
| 214 | +} |
| 215 | +// update interceptor by interceptor id |
| 216 | +client.interceptors.response.update(interceptorId, myNewInterceptor); |
| 217 | + |
| 218 | +// update interceptor by reference to interceptor function |
| 219 | +client.interceptors.response.update(myInterceptor, myNewInterceptor); |
| 220 | +``` |
| 221 | + |
| 222 | +::: |
| 223 | + |
| 224 | +You can also use native ofetch hooks by passing them in config: |
| 225 | + |
| 226 | +```js |
| 227 | +import { client } from 'client/client.gen'; |
| 228 | + |
| 229 | +client.setConfig({ |
| 230 | + onRequest: ({ options }) => { |
| 231 | + // mutate ofetch options (headers, query, etc.) |
| 232 | + }, |
| 233 | + onResponse: ({ response }) => { |
| 234 | + // inspect/transform the raw Response |
| 235 | + }, |
| 236 | + onRequestError: (ctx) => { |
| 237 | + // handle request errors |
| 238 | + }, |
| 239 | + onResponseError: (ctx) => { |
| 240 | + // handle response errors |
| 241 | + }, |
| 242 | +}); |
| 243 | +``` |
| 244 | + |
| 245 | +::: tip |
| 246 | +To eject, you must provide the id or reference of the interceptor passed to `use()`, the id is the value returned by `use()` and `update()`. |
| 247 | +::: |
| 248 | + |
| 249 | +## Auth |
| 250 | + |
| 251 | +The SDKs include auth mechanisms for every endpoint. You will want to configure the `auth` field to pass the right token for each request. The `auth` field can be a string or a function returning a string representing the token. The returned value will be attached only to requests that require auth. |
| 252 | + |
| 253 | +```js |
| 254 | +import { client } from 'client/client.gen'; |
| 255 | + |
| 256 | +client.setConfig({ |
| 257 | + auth: () => '<my_token>', // [!code ++] |
| 258 | + baseUrl: 'https://example.com', |
| 259 | +}); |
| 260 | +``` |
| 261 | + |
| 262 | +If you're not using SDKs or generating auth, using interceptors is a common approach to configuring auth for each request. |
| 263 | + |
| 264 | +```js |
| 265 | +import { client } from 'client/client.gen'; |
| 266 | + |
| 267 | +client.interceptors.request.use((request, options) => { |
| 268 | + request.headers.set('Authorization', 'Bearer <my_token>'); // [!code ++] |
| 269 | + return request; |
| 270 | +}); |
| 271 | +``` |
| 272 | + |
| 273 | +or with ofetch hooks: |
| 274 | + |
| 275 | +```js |
| 276 | +import { client } from 'client/client.gen'; |
| 277 | + |
| 278 | +client.setConfig({ |
| 279 | + onRequest: ({ options }) => { |
| 280 | + options.headers.set('Authorization', 'Bearer <my_token>'); // [!code ++] |
| 281 | + }, |
| 282 | +}); |
| 283 | +``` |
| 284 | + |
| 285 | +## Build URL |
| 286 | + |
| 287 | +If you need to access the compiled URL, you can use the `buildUrl()` method. It's loosely typed by default to accept almost any value; in practice, you will want to pass a type hint. |
| 288 | + |
| 289 | +```ts |
| 290 | +type FooData = { |
| 291 | + path: { |
| 292 | + fooId: number; |
| 293 | + }; |
| 294 | + query?: { |
| 295 | + bar?: string; |
| 296 | + }; |
| 297 | + url: '/foo/{fooId}'; |
| 298 | +}; |
| 299 | + |
| 300 | +const url = client.buildUrl<FooData>({ |
| 301 | + path: { |
| 302 | + fooId: 1, |
| 303 | + }, |
| 304 | + query: { |
| 305 | + bar: 'baz', |
| 306 | + }, |
| 307 | + url: '/foo/{fooId}', |
| 308 | +}); |
| 309 | +console.log(url); // prints '/foo/1?bar=baz' |
| 310 | +``` |
| 311 | + |
| 312 | +## Custom `ofetch` |
| 313 | + |
| 314 | +You can provide a custom ofetch instance. This is useful if you need to extend ofetch with extra functionality (hooks, retry behavior, etc.) or replace it altogether. |
| 315 | + |
| 316 | +```js |
| 317 | +import { ofetch } from 'ofetch'; |
| 318 | +import { client } from 'client/client.gen'; |
| 319 | + |
| 320 | +const $ofetch = ofetch.create({ |
| 321 | + onRequest: ({ options }) => { |
| 322 | + // customize request |
| 323 | + }, |
| 324 | + onResponse: ({ response }) => { |
| 325 | + // customize response |
| 326 | + }, |
| 327 | +}); |
| 328 | + |
| 329 | +client.setConfig({ |
| 330 | + ofetch: $ofetch, |
| 331 | +}); |
| 332 | +``` |
| 333 | + |
| 334 | +You can use any of the approaches mentioned in [Configuration](#configuration), depending on how granular you want your custom instance to be. |
| 335 | + |
| 336 | +## API |
| 337 | + |
| 338 | +You can view the complete list of options in the [UserConfig](https://github.com/hey-api/openapi-ts/blob/main/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/types.d.ts) interface. |
| 339 | + |
| 340 | +<!--@include: ../../partials/examples.md--> |
| 341 | +<!--@include: ../../partials/sponsors.md--> |
0 commit comments