Custom Extensions
APIful's true power lies in its extensibility. You can chain multiple extensions using the with
method to build exactly the API client you need.
Extension Types
Before creating your first extension, it is essential to understand the two types of extensions available.
APIful provides two types of extensions:
- Handler Extension: Adds the callable signature to a client instance (e.g.,
client('/path')
). This provides the core HTTP functionality. - Methods Extension: Adds methods to the client instance (e.g.,
client.login()
,client.cache.get()
). You can chain multiple method extensions.
Both extension types are created using a builder function that receives the client instance and returns the extension function (handler) or object (methods). This builder pattern serves two essential purposes: it provides access to the client instance for reading default options and existing extensions, and it enables TypeScript's type inference to work correctly across the extension chain.
IMPORTANT
Use satisfies HandlerExtensionBuilder
or satisfies MethodsExtensionBuilder
instead of declaring extension variables directly with these types. The satisfies
operator is crucial here – it validates that your extension conforms to the expected interface while preserving the exact return type for downstream type inference, ensuring seamless integration with other extensions.
WARNING
Only use one handler extension per client. Multiple handler extensions will override each other, with the last one taking precedence.
Handler Extension
Handler extensions form the foundation of any ApiClient
by providing the callable interface. While you can technically add multiple handler extensions, you should use only one per client.
ofetch works perfectly as a handler extension. The built-in ofetchBuilder
is essentially just an ofetch instance configured with your client's default options:
import type { HandlerExtensionBuilder } from 'apiful'
import { ofetch } from 'ofetch'
const callableExtension = (
client => ofetch.create(client.defaultOptions)
) satisfies HandlerExtensionBuilder
Use your custom handler extension by adding it to a client:
import { createClient } from 'apiful'
const client = createClient({ baseURL: 'https://api.example.com' })
.with(callableExtension)
// Now you can make requests
const response = await client('/users')
TIP
Use the HandlerExtensionBuilder
type for better type checking and IntelliSense support.
Methods Extension
While handler extensions are recommended to be used only once, you can add as many method extensions as you need. They allow you to add your own methods to the client and provide full type safety.
For example, let's add a logDefaults
function to the client that logs the default fetch options:
import type { MethodsExtensionBuilder } from 'apiful'
const logExtension = (client => ({
logDefaults() {
console.log('Default fetch options:', client.defaultOptions)
}
})) satisfies MethodsExtensionBuilder
const extendedClient = client
.with(logExtension)
extendedClient.logDefaults() // { baseURL: 'https://api.example.com', headers: { Authorization: 'Bearer <your-bearer-token>' } }
TIP
Use the MethodsExtensionBuilder
type for better type checking and IntelliSense support.
NOTE
Method extensions can access the client's defaultOptions
, _handler
, and any previously added extensions, making them powerful for building layered functionality.
Extension Priority and Resolution
When you chain multiple extensions, later extensions override earlier ones. This allows you to compose functionality in a predictable way:
import type { MethodsExtensionBuilder } from 'apiful'
import { createClient } from 'apiful'
const firstExtension = (client => ({
greet: () => 'Hello from first!'
})) satisfies MethodsExtensionBuilder
const secondExtension = (client => ({
greet: () => 'Hello from second!'
})) satisfies MethodsExtensionBuilder
const api = createClient({ baseURL: 'https://api.example.com' })
.with(firstExtension)
.with(secondExtension)
console.log(api.greet()) // "Hello from second!"
Best Practices
- Keep it Simple: Start with basic extensions and add complexity only when needed
- Use TypeScript: Always use
satisfies
to ensure type safety - Single Purpose: Each extension should have one clear responsibility
These patterns help you build maintainable API clients that grow with your needs while keeping the code clean and understandable.