Skip to content

How Extensions Work ​

A client on its own holds nothing but default fetch options – calling it throws. Extensions give it behavior, and with adds them.

Every .with() call returns a new client, wrapped in a proxy that routes property access to the extension that provides it, so a chain of extensions stays a single callable client and TypeScript keeps the types of each one. The client you called it on is left as it was, which means you can extend one client two different ways without the two interfering.

Extensions come in two kinds:

  • A handler extension provides the call signature that makes requests. A client takes one – adding a second replaces the first, on the client .with() returns. The built-in extensions are all handler extensions.
  • A methods extension adds named methods to the client. Add as many as you like.

Every extension is handed the client, so all of them read the same defaultOptions. Where two methods extensions provide the same name, the later one wins, and a method always shadows a property of the handler extension:

ts
import type { MethodsExtensionBuilder } from 'apiful'
import { createClient } from 'apiful'

const firstExtension = (() => ({
  greet: () => 'Hello from first!',
})) satisfies MethodsExtensionBuilder

const secondExtension = (() => ({
  greet: () => 'Hello from second!',
})) satisfies MethodsExtensionBuilder

const client = createClient({ baseURL: 'https://api.example.com' })
  .with(firstExtension)
  .with(secondExtension)

console.log(client.greet()) // "Hello from second!"

The three built-in extensions differ only in how you write a request; that page compares the call signatures side by side. To write your own, see Custom Extensions.