Better Fetch Documentation

repository·main·Indexed 21 days ago

https://github.com/better-auth/better-fetch

A 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.

Tokens
22.4K
Snippets
73
Records
80
Agent score
76%

What's inside Better Fetch

  1. Overview of Better Fetch

    main
    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.
  2. Introduction to Better Fetch

    main

    Better 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 Schema to 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
  3. Define request and response shapes using `input` and `output`

    main

    In 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 an input schema is defined, Better Fetch defaults to using a POST method. To make the body optional, wrap the schema with z.optional().
    • output: Defines the schema for the response body. The returned data object will be typed according to this schema.

    If no input is defined, the default method is GET.

    // Example with input (POST) and output
    const schema = createSchema({
        "/path": {
            input: z.object({ id: z.number() }),
            output: z.object({ name: z.string() }),
        },
    });
  4. What are Plugins in Better Fetch?

    main

    Plugins 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 plugins array in the createFetch configuration 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],
    });
  5. Set headers at different levels

    main

    Better Fetch allows you to define headers at three distinct levels. Headers are merged in a specific order where later levels override earlier ones:

    1. Configuration Level: Set in createFetch({ headers: ... }). These are applied to every request made by the instance.
    2. Schema Level: Defined within createSchema to provide runtime validation and type safety for specific routes.
    3. 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",
        },
    });
  6. How hooks work in the request lifecycle

    main

    Hooks in @better-fetch/fetch allow you to tap into different stages of a fetch operation. The lifecycle follows this sequence:

    1. onRequest: Intercepts the request before it is sent. You must return the context to continue the chain.
    2. Network Operation: The actual fetch occurs.
    3. onResponse: Intercepts the incoming response. You must return the response to continue the chain.
    4. Outcome Hooks: Depending on the result, either onSuccess or onError is 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) {
        },
    })
  7. Understand the default error behavior in Better Fetch

    main

    By default, betterFetch returns errors as a value rather than throwing them. The returned error object contains the following properties:

    • status: The HTTP status code (always defined).
    • statusText: The HTTP status text (always defined).
    • message: A string or undefined. 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 data and error.

    import { betterFetch } from '@better-fetch/fetch';
    
    const { error } = await betterFetch("https://jsonplaceholder.typicode.com/todos/1");
    // error contains { status, statusText, message? }
  8. Define API contracts with Fetch Schema

    main

    Fetch 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,
        },
    });
  9. Use dynamic parameters in URL paths

    main

    You can define dynamic segments in a URL path using the : prefix. When making a request, provide a params object in the options to map these placeholders to actual values. The values provided in params will 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"
        }
    })
  10. Validate headers with schemas

    main

    You 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",
        },
    });
  11. How to define a Fetch Schema using the `schema` property

    main

    You 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 createSchema to map paths to input and output validation schemas. You can also pass a prefix to createSchema to 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,
        },
    });
  12. Install dependencies and run the dev project

    main

    To set up the development environment and run the project, use the following commands with bun:

    1. Install dependencies: bun install
    2. Run the entry point: bun run index.ts

    This project is configured to work with Bun.

    bun install
    bun run index.ts