Better Fetch Documentation
repository·main·Indexed 21 days ago
https://github.com/better-auth/better-fetchA type-safe, advanced TypeScript fetch wrapper featuring schema validation (Zod, Valibot, Arktype), a plugin system, and cross-runtime compatibility for the browser, Node.js (v18+), Workers, Deno, and Bun. It provides tools for pre-defined routes via createSchema, dynamic URL parameters, and built-in support for Bearer and Basic authorization.
What's inside Better Fetch
- Better Fetch is an advanced TypeScript fetch wrapper designed for type-safe HTTP requests. It provides standard schema validation (supporting Zod, Valibot, Arktype, or any other compliant validator), pre-defined routes, lifecycle callbacks, and a plugin system. It is cross-runtime compatible, working in the browser, Node.js (v18+), Workers, Deno, and Bun.
Introduction to Better Fetch
mainBetter Fetch is an advanced TypeScript fetch wrapper designed for runtime validation and type inference. It is schema-agnostic, meaning it supports any validator compliant with the Standard Schema specification (such as Zod, Valibot, or ArkType).
Key capabilities include:
- Runtime Validation: Ensures incoming data matches your expected types.
- Error-as-Value Handling: Provides a more functional approach to error management.
- Smart Parsing: Automatically handles response parsing for various content types.
- Extensibility: Supports plugins and hooks to customize behavior.
- Pre-defined Routes: Use
Fetch Schemato define routes with built-in schema validations. - Resilience: Includes advanced retry mechanisms and timeout controls.
Supported Runtimes:
- Browser
- Node.js (version 18+)
- Workers
- Deno
- Bun
Define request and response shapes using `input` and `output`
mainIn a Fetch Schema, you can define the shape of the data sent to and received from the server:
input: Defines the schema for the request body. If aninputschema is defined, Better Fetch defaults to using aPOSTmethod. To make the body optional, wrap the schema withz.optional().output: Defines the schema for the response body. The returneddataobject will be typed according to this schema.
If no
inputis defined, the default method isGET.// Example with input (POST) and output const schema = createSchema({ "/path": { input: z.object({ id: z.number() }), output: z.object({ name: z.string() }), }, });What are Plugins in Better Fetch?
mainPlugins are functions used to modify various parts of the request lifecycle, including the request, response, and error handling. They can also be used to define a Fetch Schema to document and validate API usage. Plugins are registered via the
pluginsarray in thecreateFetchconfiguration object.import { createFetch, BetterFetchPlugin } from "@better-fetch/fetch"; const myPlugin = { id: "my-plugin", name: "My Plugin", } satisfies BetterFetchPlugin; const $fetch = createFetch({ baseURL: "https://jsonplaceholder.typicode.com", plugins: [myPlugin], });Set headers at different levels
mainBetter Fetch allows you to define headers at three distinct levels. Headers are merged in a specific order where later levels override earlier ones:
- Configuration Level: Set in
createFetch({ headers: ... }). These are applied to every request made by the instance. - Schema Level: Defined within
createSchemato provide runtime validation and type safety for specific routes. - Request Level: Set on individual calls to the fetch function (e.g.,
betterFetch(url, { headers: ... })).
Aggregation Order (Later overrides earlier):
Configuration headers$\rightarrow$Schema headers$\rightarrow$Request headers.import { createFetch, createSchema } from "@better-fetch/fetch"; import { z } from "zod"; const $fetch = createFetch({ baseURL: "http://localhost:3000", headers: { "x-api-key": "config-key", // 1. Configuration level }, schema: createSchema({ "/api/data": { headers: z.object({ "x-tenant-id": z.string(), // 2. Schema level }), }, }), }); // 3. Request level: Final headers will include x-api-key, x-tenant-id, and x-request-id const { data } = await $fetch("/api/data", { headers: { "x-tenant-id": "tenant-123", "x-request-id": "req-456", }, });- Configuration Level: Set in
How hooks work in the request lifecycle
mainHooks in
@better-fetch/fetchallow you to tap into different stages of a fetch operation. The lifecycle follows this sequence:onRequest: Intercepts the request before it is sent. You must return the context to continue the chain.- Network Operation: The actual fetch occurs.
onResponse: Intercepts the incoming response. You must return the response to continue the chain.- Outcome Hooks: Depending on the result, either
onSuccessoronErroris triggered. These are for side effects and do not return values to the chain.
import { createFetch } from "@better-fetch/fetch"; const $fetch = createFetch({ baseURL: "http://localhost:3000", onRequest(context) { return context; }, onResponse(context) { return context.response }, onError(context) { }, onSuccess(context) { }, })Understand the default error behavior in Better Fetch
mainBy default,
betterFetchreturns errors as a value rather than throwing them. The returnederrorobject contains the following properties:status: The HTTP status code (always defined).statusText: The HTTP status text (always defined).message: A string orundefined. If the API returns a JSON error object, it will be parsed and included in the error object.
When an error occurs, the function returns an object containing both
dataanderror.import { betterFetch } from '@better-fetch/fetch'; const { error } = await betterFetch("https://jsonplaceholder.typicode.com/todos/1"); // error contains { status, statusText, message? }Define API contracts with Fetch Schema
mainFetch Schema allows you to pre-define your API's structure, including URL paths, request inputs, and response outputs. This provides a way to document your API and ensures that both request and response data are validated.
When using a schema, the output is validated against your provided schema; if validation fails, an error is thrown. You can validate headers, request bodies, query parameters, and more using
createSchema.import { createSchema, createFetch } from "@better-fetch/fetch"; import { z } from "zod"; export const zodSchema = createSchema({ "/path": { input: z.object({ userId: z.string(), id: z.number(), title: z.string(), completed: z.boolean(), }), output: z.object({ userId: z.string(), id: z.number(), title: z.string(), completed: z.boolean(), }), } }) const $fetch = createFetch({ baseURL: "https://jsonplaceholder.typicode.com", schema: zodSchema }); const { data, error } = await $fetch("/path", { body: { userId: "1", id: 1, title: "title", completed: true, }, });Use dynamic parameters in URL paths
mainYou can define dynamic segments in a URL path using the
:prefix. When making a request, provide aparamsobject in the options to map these placeholders to actual values. The values provided inparamswill replace the placeholders in the URL string.import { createFetch } from "@better-fetch/fetch"; const $fetch = createFetch({ baseURL: "http://localhost:3000", }) // Single parameter const res = await $fetch("/path/:id", { params: { id: "1" } }) // Multiple parameters const res2 = await $fetch("/repos/:owner/:repo", { params: { owner: "octocat", repo: "hello-world" } })Validate headers with schemas
mainYou can use Zod or any StandardSchema library to define header schemas. This provides runtime validation and TypeScript type inference for your headers.
Important: Always define header keys in lowercase within your schemas (e.g.,
"x-user-id") to ensure reliable case-insensitive matching. While keys must be lowercase in the schema, you can pass headers using any casing (e.g.,"X-User-Id") in your actual requests.import { createFetch, createSchema } from "@better-fetch/fetch"; import { z } from "zod"; const $fetch = createFetch({ baseURL: "http://localhost:3000", schema: createSchema({ "/api/users": { headers: z.object({ "x-user-id": z.string().uuid(), "x-api-version": z.string().regex(/^\d+\.\d+\.\d+$/), }), }, }), }); // Validates headers at runtime const { data, error } = await $fetch("/api/users", { headers: { "x-user-id": "123e4567-e89b-12d3-a456-426614174000", "x-api-version": "1.0.0", }, });How to define a Fetch Schema using the `schema` property
mainYou can define a schema for a plugin to document and validate API usage. Better Fetch uses the Standard Schema specification, meaning you can use any Standard Schema-compliant validator (like
zod), not just Zod.Use
createSchemato map paths toinputandoutputvalidation schemas. You can also pass aprefixtocreateSchemato prefix all routes defined within that schema.import { createFetch, createSchema, BetterFetchPlugin } from "@better-fetch/fetch"; import { z } from "zod"; const plugin = { id: "my-plugin", name: "My Plugin", schema: createSchema({ "/path": { input: z.object({ userId: z.string(), id: z.number(), }), output: z.object({ title: z.string(), completed: z.boolean(), }), } },{ baseURL: "https://jsonplaceholder.typicode.com", }) } satisfies BetterFetchPlugin; const $fetch = createFetch({ baseURL: "localhost:3000" }) const { data, error } = await $fetch("https://jsonplaceholder.typicode.com/path", { body: { userId: "1", id: 1, title: "title", completed: true, }, });Install dependencies and run the dev project
mainTo set up the development environment and run the project, use the following commands with
bun:- Install dependencies:
bun install - Run the entry point:
bun run index.ts
This project is configured to work with Bun.
bun install bun run index.ts- Install dependencies: