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!