Skip to main content
Version: v8.0.0

Custom Adapter

In Hyper Fetch, an adapter is a class responsible for handling the actual data exchange with a server. While Hyper Fetch comes with a built-in http-adapter, you might need to create a custom adapter to support different protocols (like WebSockets), integrate with a specific service (like Firebase), or use a different HTTP client (like axios).

This guide will walk you through creating your own custom adapter, from a basic setup to advanced typing.


What you'll learn
  1. How to create a custom adapter for Hyper Fetch using the Adapter class.
  2. How to use the setFetcher method to implement the core logic.
  3. The role of the adapter bindings in the adapter lifecycle.
  4. How to strongly-type your adapter with custom options and return values.
  5. How to implement adapter unions for conditional typing.

Basic Setup

At its core, an adapter is a class that extends the Adapter class and implements the fetching logic. You can connect any adapter to the client by instantiating it.

Here's a minimal example of a custom adapter:

import { Adapter, Client } from "@hyper-fetch/core";

const customAdapter = new Adapter({
name: "custom-adapter",
defaultMethod: "GET",
defaultExtra: {},
systemErrorStatus: 500,
systemErrorExtra: {},
}).setFetcher(async ({ request, onSuccess }) => {
onSuccess({
data: { message: "success" },
error: null,
status: 200,
extra: null,
});
});

const client = new Client({ url: "http://localhost:3000" }).setAdapter(customAdapter);

This adapter doesn't do much—it returns a static response. To make it useful, we need to add logic to actually make a network request.

Core Logic with setFetcher

The setFetcher method is where you'll implement your adapter's core functionality. It takes a callback function that receives a set of bindings—utilities and request details—to help you build your fetching logic. This approach abstracts away the complex internal logic of Hyper Fetch and provides a clean way to integrate your adapter.

This ensures that all lifecycle hooks, request queueing, and other features work correctly.

Let's implement a simple adapter using the native fetch API:

import { Adapter, Client, RequestInstance } from "@hyper-fetch/core";
import { User } from "shared";

const fetchAdapter = new Adapter({
name: "fetch-adapter",
defaultMethod: "GET",
defaultExtra: {},
systemErrorStatus: 500,
systemErrorExtra: {},
}).setFetcher(async ({ request, headers, payload, onSuccess, onError }) => {
const { client, endpoint, method } = request;
const fullUrl = `${client.url}${endpoint}`;

try {
const response = await fetch(fullUrl, {
method,
headers: headers as HeadersInit,
body: JSON.stringify(payload),
});

const data = await response.json();

if (response.ok) {
onSuccess({ data, status: response.status, extra: null });
} else {
onError({ error: data, status: response.status, extra: null });
}
} catch (error) {
onError({ error, status: 0, extra: null });
}
});

const client = new Client({ url: "http://localhost:3000" }).setAdapter(fetchAdapter);

Adapter Bindings

The callback passed to setFetcher receives an object with a set of utilities and request details. Here are the key properties you'll use:

  • Core Request Data: request, headers, payload, and adapterOptions provide the essential details of the request.
  • Lifecycle Callbacks: A set of on... callbacks (onSuccess, onError, onAbort, onTimeout, etc.) that you must use to report the outcome of the request.
  • Abort Logic: createAbortListener allows you to implement request cancellation.
  • Progress Callbacks: onRequestProgress and onResponseProgress for tracking upload and download progress.

Advanced Typing

The Adapter class is a generic type that allows you to define custom options for your adapter, specify the structure of the extra return property, and more. This enables you to create strongly-typed adapters that are both powerful and easy to use.

Here's how you can define an adapter with custom options and extra data:

import { Adapter, HttpMethodsType, HttpStatusType, QueryParamsType } from "@hyper-fetch/core";

type MyCustomAdapterOptions = {
timeout?: number;
someOtherOption?: boolean;
};

type MyCustomExtra = {
headers: Headers;
raw: string;
};

const customHttpAdapter = new Adapter<
MyCustomAdapterOptions,
HttpMethodsType,
HttpStatusType,
MyCustomExtra,
QueryParamsType
>({
name: "custom-http-adapter",
defaultMethod: "GET",
defaultExtra: { headers: new Headers(), raw: "" },
systemErrorStatus: 500,
systemErrorExtra: { headers: new Headers(), raw: "" },
});

const client = new Client({ url: "https://api.example.com" }).setAdapter(customHttpAdapter);

Adapter Unions

For more complex scenarios, you can create a union of Adapter types. This is a powerful feature used extensively in the firebase-adapter. However, creating a single adapter instance that can handle different configurations requires a factory function or a class that internally manages different adapter instances. The simplest way to achieve union-like behavior is to define a type and then create instances that conform to different parts of that union.

import { Adapter, HttpMethodsType, HttpStatusType, QueryParamsType } from "@hyper-fetch/core";

type MyAdapter =
| Adapter<{ timeout?: number }, "GET", HttpStatusType, { raw: string }, QueryParamsType>
| Adapter<{ headers?: Headers }, "POST", HttpStatusType, { json: string }, QueryParamsType>;

const getAdapter: MyAdapter = new Adapter({
name: "get-adapter",
defaultMethod: "GET",
defaultExtra: { raw: "" },
systemErrorStatus: 500,
systemErrorExtra: { raw: "" },
});
const postAdapter: MyAdapter = new Adapter({
name: "post-adapter",
defaultMethod: "POST",
defaultExtra: { json: "" },
systemErrorStatus: 500,
systemErrorExtra: { json: "" },
});

const client = new Client({ url: "https://api.example.com" }).setAdapter(getAdapter);

// This request will have the type of the first union member
const request1 = client.createRequest()({
endpoint: "/product",
method: "GET",
adapterOptions: { timeout: 1000 },
});
const { extra: extra1 } = await request1.send();
// extra1 is { raw: string }

// To use the post adapter, you would need to set it on the client
client.setAdapter(postAdapter);

// This request will have the type of the second union member
const request2 = client.createRequest()({
endpoint: "/user",
method: "POST",
adapterOptions: { headers: new Headers() },
});
const { extra: extra2 } = await request2.send();
// extra2 is { json: string }

Congratulations!

You've learned how to create custom adapters for Hyper Fetch!

  • You know how to create a basic adapter using the Adapter class.
  • You can use the setFetcher method to implement the core logic of your adapter.
  • You understand how to strongly-type your adapter to accept custom options and return additional data.
  • You are able to create adapter unions for complex, conditional typing scenarios.