Skip to main content

Announcing Hyper Fetch 8.0

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

Hyper Fetch 7.0

Version 8.0 is a foundational release — we replaced the entire HTTP layer with native fetch, added first-class streaming, modernized every React hook with useSyncExternalStore, and introduced client/server cache modes for safe server-side usage. Every change is driven by a single goal: fewer abstractions, more platform alignment, better DX.

Key features
  1. Unified Fetch Adapter: One adapter for browser and Node — no more XHR or dual builds.
  2. First-Class Streaming: Native ReadableStream support in core and a new useStream React hook.
  3. React Hooks Modernization: useSyncExternalStore, clearState, smart keepPreviousData, and readable console output.
  4. Client/Server Cache Modes: Safe server-side caching with explicit scope isolation via setScope().
  5. Request Lifecycle Hooks: Persistent $hooks on Request instances — no more repeating callbacks on every send().
  6. SDK Configuration: Type-safe per-endpoint defaults with createConfiguration() and sdk.$configure().
  7. CLI Auto-Init: Any CLI command auto-initializes when api.json is missing — just run and go.
  8. TypeScript Optimizations: Reduced type instantiation count and faster IDE responsiveness.
  9. Optimistic Mutations: First-class request.setOptimistic() with auto-rollback, cache invalidation, and typed mutationContext flowing through all callbacks and hooks.

What is Hyper Fetch?

HyperFetch is a data-fetching framework that unifies HTTP, GraphQL, Firebase, and real-time sources like WebSockets and Server-Sent Events into a single, type-safe API. It works across React, React Native, and Node.js with consistent patterns for caching, queuing, offline support, and request lifecycle management.

Whether you're building a small app or a large-scale platform, HyperFetch provides architectural uniformity, end-to-end type safety, and adapters that let you swap transports without changing your application code.

Get Started
Learn how to write your first request with Hyper-Fetch.

Highlights

1. Unified Fetch Adapter

We replaced both the browser XMLHttpRequest adapter and the Node.js http/https adapter with a single implementation built on the native fetch API.

One codebase. One build. No more dual isomorphic builds or conditional entry points.

  • Abort via AbortController: the standard AbortController / AbortSignal pattern replaces XHR .abort()
  • Download progress via response.body.getReader(): native streaming progress reporting
  • Full RequestInit passthrough: credentials, referrer policy, keepalive, and any other standard fetch option
import { createClient } from "@hyper-fetch/core";

const client = createClient({ url: "https://api.example.com" });

const getUsers = client.createRequest<{ response: User[] }>()({
endpoint: "/users",
method: "GET",
});

getUsers.setOptions({ credentials: "include", timeout: 5000 }).send();

2. First-Class Streaming

When you set streaming: true, the adapter returns the raw ReadableStream instead of buffering the entire response. Think real-time AI chat, large file downloads with progress, or server-sent data processing.

Core usage, get the stream directly:

const streamRequest = client.createRequest<{ response: ReadableStream }>()({
endpoint: "/ai/chat",
method: "POST",
});

const { data } = await streamRequest.setOptions({ streaming: true }).send();

React: the new useStream hook manages the full lifecycle for you:

import { useStream } from "@hyper-fetch/react";

function AiChat({ prompt }) {
const { text, streaming, start, abort } = useStream(chatRequest.setPayload({ prompt }));

return (
<div>
<p>{text}</p>
{streaming ? <button onClick={abort}>Stop</button> : <button onClick={start}>Send</button>}
</div>
);
}

It automatically clones the request with streaming: true, accumulates chunks via TextDecoder, and exposes start, abort, and reset controls. Works for text, files, or binary data.


3. React Hooks Modernization

Four interconnected improvements to the React hooks layer.

useSyncExternalStore

All hooks now use useSyncExternalStore (React 18+) for proper concurrent mode support. This fixes potential tearing issues while preserving field-level dependency tracking. A component that only reads data still won't re-render when error changes.

Smart keepPreviousData

Switching between resources (product #1 → product #2) now clears stale data by default instead of flashing the previous resource. Three modes give you full control:

ModeBehavior
"auto" (default)Clears on identity change (method + endpoint + path params), preserves on query-only change. Pagination just works
"preserve"Always keeps previous data visible during loading (old v7 behavior)
"clean"Always clears state on any key change

 

// Pagination: data stays visible while loading the next page
const { data } = useFetch(getUsers.setQueryParams({ page }), {
keepPreviousData: "preserve",
});

// Detail view: clears immediately when switching products
const { data } = useFetch(getProduct.setParams({ productId }));

clearState() and readable console output

  • New clearState() on useFetch, useSubmit, and useCache resets all tracked fields back to their initial values
  • Hook return values now implement toJSON() so console.log shows actual values instead of (...) getter placeholders

4. Client/Server Cache Modes

Server-side HyperFetch has a cache safety problem: the in-process cache is shared across all incoming requests, meaning user A could see user B's data.

v8 solves this with a mode option on the client:

const client = createClient({
url: "https://api.example.com",
// mode: "auto" (default) → detects environment automatically
// mode: "client" → cache works as before
// mode: "server" → cache disabled by default, opt-in with setScope()
});

On the server, caching only activates when you explicitly scope it, typically with a session ID, or when you set mode to "client".

// NOT cached, safe by default (no scope)
await getUser.setParams({ userId: 1 }).send();

// CACHED, scoped to this user's session
await getUser.setParams({ userId: 1 }).setScope(req.session.id).send();

Cache keys are automatically namespaced: ${scope}__${originalCacheKey}. No scope = no caching on the server. On the client side, setScope is optional and acts as a key prefix for organizing cache by tenant, workspace, or feature.


5. Request Lifecycle Hooks

Define persistent lifecycle hooks directly on a request via $hooks. Unlike per-send() callbacks you have to repeat every time, these travel with the request instance and survive .clone().

const getUser = client.createRequest<{ response: User }>()({
endpoint: "/users/:userId",
method: "GET",
});

getUser.$hooks.onResponse(({ response }) => {
console.log("User loaded:", response.data);
});

getUser.$hooks.onRequestStart(({ requestId }) => {
console.log("Loading user...", requestId);
});

// Hooks fire on every send, no need to repeat them
await getUser.setParams({ userId: 1 }).send();
await getUser.setParams({ userId: 2 }).send();

Each hook method returns an unsubscribe function. Multiple listeners per hook are supported. The $ prefix means runtime-only, so hooks don't interfere with serialization, persistence, or DevTools.


6. SDK Configuration

For projects using createSdk, v8 adds a typed configuration system. Define defaults per request once, and they apply automatically. No more repeating .setHeaders() or .setResponseMapper() on every call.

import { createSdk, createConfiguration } from "@hyper-fetch/core";
import type { OurSdk } from "./sdk";

const authConfig = createConfiguration<OurSdk>()({
"auth.login.$post": (req) => req.setResponseMapper(loginMapper).setHeaders({ "X-Custom": "value" }),
"auth.register.$post": (req) => req.setResponseMapper(registerMapper),
});

const sdk = createSdk(client);
const configuredSdk = sdk.$configure(authConfig);

// Configured defaults are applied automatically
await configuredSdk.auth.login.$post.send({ payload: credentials });

Use dot-path keys (like "users.$get") to target a specific method, or endpoint groups (like "/users") to configure all methods at once. Split configs across files by domain and merge with chained $configure() calls.


7. CLI Auto-Init

No more "run init first" errors. Every CLI command auto-initializes when api.json is missing. Just run your command and go, similar to how shadcn works.

# Fresh project: auto-creates api.json and src/api directory
npx hyper-fetch generate --url https://api.example.com/openapi.json

8. TypeScript Optimizations

We profiled and optimized the heaviest type constructs:

  • Flattened nested conditional chains into lookup types in Request and Adapter generics
  • Split ExtractUnionAdapter narrowing to avoid "excessively deep" instantiation errors
  • Cached intermediate type expressions via aliases instead of re-instantiating at every usage

The result: noticeably faster IDE autocomplete and type-checking, especially in files with many request definitions.


9. Optimistic Mutations

Define cache manipulation, rollback logic, and invalidation targets directly on the request with setOptimistic(). The framework orchestrates everything, including auto-rollback on failure and retry awareness.

const patchUser = client
.createRequest<{ response: User; payload: Partial<User> }>()({
endpoint: "/users/:userId",
method: "PATCH",
})
.setOptimistic(({ client, payload, request }) => {
const snapshot = client.cache.get(getUsers.cacheKey);

client.cache.update(getUsers, (prev) => ({
...prev,
data: prev.data.map((u) => (u.id === Number(request.params?.userId) ? { ...u, ...payload } : u)),
}));

return {
context: { snapshot },
rollback: () => {
if (snapshot) client.cache.set(getUsers, snapshot);
},
invalidate: [], // use the requests here to invalidate t hem without writing rollback!
};
});

Use it with React hooks. Rollback happens automatically, and mutationContext is fully typed:

const { submit, onSubmitSuccess, onSubmitError } = useSubmit(patchUser);

onSubmitSuccess(({ mutationContext }) => {
console.log("Updated! Previous:", mutationContext?.snapshot);
});

onSubmitError(() => {
alert("Update failed, changes reverted.");
});
What makes it robust
  • Auto-rollback on failure or abort, no manual error handling
  • Retry-aware: rollback only fires on final failure, not intermediate retries
  • Concurrent-safe: each submit() call gets its own isolated optimistic result
  • Type inference: mutationContext type is inferred from your context return value

Infrastructure Changes

Vitest migration. The entire test suite moved from Jest to Vitest, bringing faster execution, native ESM support, and better TypeScript integration. All packages now share a single vitest.workspace.ts configuration.

Vite build pipeline. All packages are now built with Vite, replacing the previous Rollup and esbuild setup.

E2E test infrastructure. A new reusable test server covers every HTTP method, file upload/download, redirects, streaming responses, error codes, and timeouts for both Node.js and browser contexts.


Breaking Changes

Heads up

Review these changes before upgrading. Most migrations are straightforward.

1. Native Fetch Replaces XHR

The XHR browser adapter and Node http/https adapter have been removed. The unified fetch adapter requires Node.js 18+. If you relied on XHR-specific features, migrate to fetch/RequestInit equivalents.

// v7: Two separate adapters, chosen by build target
// http-adapter.browser.ts (XHR)
// http-adapter.server.ts (Node http)
// v8: Single fetch-based adapter for all environments
request.setOptions({ credentials: "include", redirect: "follow" });

2. keepPreviousData Default

State is now cleared by default when the resource identity changes. The old behavior is opt-in:

const { data } = useFetch(getProduct.setParams({ productId }));
const { data } = useFetch(getProduct.setParams({ productId }), {
keepPreviousData: "preserve",
});

3. React 18+ Required

The useSyncExternalStore migration means React 18 or later is now required.

4. Jest → Vitest

The test infrastructure now uses Vitest. The public testing API (@hyper-fetch/testing) remains compatible.


Other Changes

  • ESM-only: continues to be ESM-only, aligning with the broader ecosystem direction
  • Simplified build: dual isomorphic build system and Rollup removed in favor of Vite
  • GraphQL E2E tests: comprehensive end-to-end tests for the GraphQL adapter with a real server
  • Improved CLI error handling: better error messages and edge case handling across all commands

What's Next?

v8 completes the modernization of HyperFetch's foundation. With native fetch, streaming, and safe server-side caching in place, here's what we're building next:

Roadmap
  1. Expanded E2E coverage: completing test suites for streaming, cache modes, React hooks, and SDK configuration.
  2. Framework integrations: first-class support for Next.js App Router, Remix, and other server-first frameworks.
  3. AI/LLM patterns: guides and utilities for common AI streaming patterns built on useStream.
  4. Plugin ecosystem: growing the plugin system with community-contributed adapters and utilities.

Thank you for all the support. We can't wait to see what you build with Hyper Fetch 8.0!