Skip to content

createClient

Creates an ApiClient from a set of default fetch options. Every APIful client starts here; the extensions you chain onto it decide how you write a request.

Example

ts
import { createClient, ofetchBuilder } from 'apiful'

const client = createClient({
  baseURL: 'https://api.example.com',
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
  },
})
  .with(ofetchBuilder())

// GET request to https://api.example.com/users/1
const user = await client('users/1', { method: 'GET' })

// POST request with JSON body
const newUser = await client('users', {
  method: 'POST',
  body: { name: 'John Doe', email: 'john@example.com' }
})

Type Definition

ts
declare function createClient<const BaseURL extends string = '/'>(
  defaultOptions?: ClientOptions<BaseURL>,
  factoryOptions?: ClientFactoryOptions
): ApiClient<BaseURL>

interface ClientFactoryOptions {
  fetch?: $Fetch
}

ClientOptions is defined with the ApiClient type.

Supplying Your Own Fetch

Every extension makes its requests through client.fetch. By default that is an ofetch instance built from defaultOptions, and building it is also what applies them. Pass your own to put a test double, an instrumented fetch, or an entirely different client underneath all of them at once:

ts
import type { $Fetch } from 'ofetch'
import { apiRouterBuilder, createClient } from 'apiful'

const requests: string[] = []
const client = createClient({ baseURL: 'https://api.example.com' }, {
  fetch: (async (url: string) => {
    requests.push(url)
    return { id: 1 }
  }) as unknown as $Fetch,
})
  .with(apiRouterBuilder())

A fetch you supply is handed the request as it stands and owns its own options, so defaultOptions no longer reaches it – the exception is baseURL, which apiRouterBuilder reads directly to build its routes. To keep ofetch's option handling while swapping only the transport, create the instance yourself with ofetch.create(defaultOptions, { fetch }).

NOTE

FetchOptions and $Fetch are imported from ofetch.