A backend route and the frontend call that hits it should never disagree about the shape of the data. Hype Stack closes that loop with a type bridge that reads your Hono routes and produces a fully typed SDK the frontend imports as a single object.

No hand-written request types. No redeclared payloads. The SDK's type is the backend's type, full stop.

The two halves

On the backend, routes are Hono apps. You declare a path, a validator, and a handler, and Hono infers the request and response types from that declaration. On the frontend, the SDK is built on HyperFetch. Reads go through useFetch(sdk.<path>.$get), mutations through useSubmit(sdk.<path>.$post), and WebSocket events through useListener(socketSdk.<domain>.<event>.$listener).

The bridge between them is generated. The frontend SDK is created from typeof client and the backend's ApiRoutesSdk type, so the moment you add a route on the backend, it shows up on sdk.

import { createSdk } from "@hyper-fetch/core";
import type { ApiRoutesSdk, WsSocketSdk } from "@internal/backend";

import { client, socket } from "./client";

export const sdk = createSdk<typeof client, ApiRoutesSdk>(client);
export const socketSdk = createSocketSdk<typeof socket, WsSocketSdk>(socket);

What that buys you

Change a response shape and every useFetch consuming it flags the mismatch at typecheck time. Rename a path and the call site breaks with a real error, not a 404 at runtime. Add a new endpoint and it is on sdk immediately, with its request and response types carried over for free.

The same applies to sockets. A new backend event appears on socketSdk.<domain>.<event>, and the payload type is whatever the backend emitter declared. You import the payload type directly from the backend package instead of re-exporting it through a listener index file.

Before and after showing a renamed backend path breaking a frontend call at typecheck time Rename a route on the backend and the frontend call fails at typecheck, not as a 404 in production.

Cache invalidation, done right

HyperFetch caches by endpoint key. After a mutation that affects data another component reads, you invalidate with client.cache.invalidate(/\/invitations\/pending/), using the client import directly. The regex matches every endpoint whose path fits, so every useFetch sharing that cache key refetches on its own.

The one rule that matters: never traverse through an SDK request to reach the cache. sdk.foo.$get.client.cache.invalidate(...) looks like it should work and quietly does the wrong thing. Always go through the client import.

import { client } from "@/api/client";

// Invalidate every endpoint whose path matches after a mutation.
client.cache.invalidate(/\/invitations\/pending/);

WebSockets without the usual mess

Raw useEffect plus .listen() is the default way people wire sockets, and it is where bugs live: stale closures, double subscriptions, listeners that never clean up. The typed socket SDK replaces that with useListener(socketSdk.<domain>.<event>.$listener) and an onEvent callback. Subscribe in one line, unsubscribe automatically, and the payload is typed.

Emitting is symmetric: useEmitter(socketSdk.<domain>.<event>.$emitter) and emit().

A typed useListener hook with the socket event payload type inferred from the backend One useListener call subscribes, auto-unsubscribes, and infers the payload type straight from the backend emitter.

Why this is worth the setup

The alternative is a frontend that knows the backend exists only through string URLs and hand-maintained types. Every endpoint is a leap of faith, and every refactor is a runtime gamble. With the bridge, a typecheck is a contract check. If the build passes, the calls line up.

For a template that ships auth, billing, and notifications across a real frontend and a real backend, that is the difference between a demo and a starting point you can ship. The HTTP data-fetching docs and WebSocket docs cover the exact patterns.