Skip to main content

SDK Configuration

· 4 min read
Maciej Pyrc
Creator of Hyper Fetch | @BetterTyped
Tutorial

SDK Configuration

One config. Every endpoint. Fully typed.

sdk.ts×
const sdk = createSdk(client).$configure({
"users.$get": { cache: true, cacheTime: 30000 },
"/admin/*": { auth: true, retry: 0 },
});

Your API schema generates a fully typed SDK, but the generated code only knows about endpoints and types. Real projects still need caching policies, retry rules, auth settings, and response mappers. $configure lets you inject all of that into the SDK in one place, so every request comes pre-configured the moment you access it. It also makes testing easier by letting you swap in mocks without tools like msw or nock.

Version

This tutorial was written for Hyper Fetch 8.0.

Dot-path keys

The SDK uses property chains like sdk.users.$get. Configuration keys mirror that path:

const configured = sdk.$configure({
// Cache the user list for 30s
"users.$get": { cache: true, cacheTime: 30000 },
// Not idempotent, should not retry
"users.$post": { retry: 0 },
// Serve stale profile data while revalidating in background
"users.$userId.$get": { staleTime: 5000 },
});

"users.$get" targets exactly sdk.users.$get. It does not affect $post or nested routes.

To apply settings across an entire resource, use endpoint-group keys:

const configured = sdk.$configure({
// All /users endpoints get caching
"/users": { cache: true },
// Admin routes require auth, no retry on privileged actions
"/admin/*": { auth: true, retry: 0 },
});

Function values

Plain objects cover simple settings. When you need mappers or conditional logic, pass a function that receives the Request instance and returns a modified one:

const configured = sdk.$configure({
// Base retry for all requests
"*": { retry: 3 },

// Normalize the API's snake_case into camelCase for the frontend
"users.$get": (request) => request.setResponseMapper(snakeToCamelMapper).setCache(true).setCacheTime(30000),

// Attach the org header that the billing API requires
"billing.invoices.$get": (request) => request.setHeaders({ "X-Org-Id": getCurrentOrgId() }),
});

Every Request setter is available: mappers, headers, auth, mocks, interceptors.

SDK-level mocks for testing

Instead of maintaining a separate network mock layer, mock directly on the SDK:

export const testSdk = sdk.$configure({
"users.$get": (req) => req.setMock(() => ({ data: [{ id: 1, name: "Alice" }] })),
"users.$userId.$get": (req) => req.setMock(() => ({ data: { id: 1, name: "Alice" } })),
"billing.invoices.$get": (req) => req.setMock(() => ({ data: [] })),
});

Import testSdk instead of sdk in your test setup. No interceptors, no service workers, no polyfills.


Application order

When multiple keys match a single request, they stack in a fixed order:

  1. Global ("*") - applied first, base defaults
  2. Endpoint groups ("/users", "/admin/*") - domain-level policies
  3. Dot-path keys ("users.$get") - per-request overrides, wins on conflict

Each level preserves settings from earlier levels unless it explicitly overrides them.

const configured = sdk.$configure({
// Retry everything 3 times by default
"*": { retry: 3 },
// Cache all user-related endpoints
"/users": { cache: true },
// Deduplicate only the user list fetch
"users.$get": { deduplicate: true },
});

const request = configured.users.$get;
// Result: retry: 3 + cache: true + deduplicate: true

Set global defaults once, add domain-level policies per resource, and fine-tune individual endpoints only when needed.


Socket SDK

@hyper-fetch/sockets uses the same pattern. Leaf keys are $listener and $emitter instead of HTTP methods:

import { createSocketSdk } from "@hyper-fetch/sockets";

const sdk = createSocketSdk<typeof socket, MyChatSchema>(socket);

sdk.chat.messages.$listener.listen(({ data }) => {
appendMessage(data.text, data.user);
});

sdk.chat.messages.$emitter.emit({ payload: { text: "Hello!" } });

Configuration uses the same 3-level order and supports both objects and functions:

const configured = sdk.$configure({
// Auto-reconnect all socket listeners
"*": { options: { reconnect: true } },
// Prioritize chat topic delivery
"chat/*": { options: { priority: "high" } },
// Sanitize outgoing messages before sending
"chat.messages.$emitter": (instance) => instance.setPayloadMapper(sanitizeMessage),
});

Dot-path keys narrow the callback type automatically: $listener keys receive ListenerInstance, $emitter keys receive EmitterInstance.


Splitting config across files

When your app grows past a dozen endpoints, split configuration by domain:

config/users.ts
import { createConfiguration } from "@hyper-fetch/core";

export const usersConfig = createConfiguration<ApiSchema>()({
// Normalize and cache the user list
"users.$get": (request) => request.setResponseMapper(snakeToCamelMapper).setCache(true),
// Require auth, allow stale data for 5s on profile pages
"users.$userId.$get": (request) => request.setAuth(true).setStaleTime(5000),
});
config/billing.ts
export const billingConfig = createConfiguration<ApiSchema>()({
// Cache invoices for 1 minute
"billing.invoices.$get": { cache: true, cacheTime: 60000 },
// Never retry invoice creation
"billing.invoices.$post": { retry: 0 },
});
sdk.ts
export const api = createSdk(client).$configure(usersConfig).$configure(billingConfig);

Keys are validated against your schema at compile time. A typo in "users.$gett" fails the build, not a production request.