@tesyl/hapi
v0.5.0
Published
Pipeline-based, type-safe API layer wrapping TanStack Query with Zod.
Maintainers
Readme
@tesyl/hapi
Pipeline-based, type-safe API layer wrapping TanStack Query with Zod.
The name. The unscoped package
hapiis the Walmart Node framework. This package is always@tesyl/hapi— inpackage.json, in imports, everywhere.
Install
npm install @tesyl/hapi @tanstack/react-query zod@tanstack/react-query and react are peer dependencies. zod is an optional
one — hapi accepts two validator protocols, and Zod is only the most familiar
implementation of the first:
// 1. The safeParse shape, which Zod implements.
type Validator<T> = {
safeParse: (x: unknown) => { success: true; data: T } | { success: false; error: unknown };
};
// 2. Standard Schema V1 — Effect Schema, Valibot, ArkType, and Zod 4.What it does
You declare an endpoint once — method, path, request schema, response schema — and get back a single object that carries every way you might call it: the raw promise, the query hook, the suspense hook, the mutation, the infinite query, and the query-options object for prefetching. The types flow from the schemas, so the call sites need no annotations.
import { defineService, createApi } from '@tesyl/hapi';
const usersService = defineService({
service: 'users',
basePath: '/users',
endpoints: {
detail: {
endpoint: 'detail',
method: 'GET',
path: '/:id',
pathParamsSchema: z.object({ id: z.string() }),
responseSchema: userSchema,
},
},
});
export const api = createApi({
baseUrl: 'https://api.example.com',
services: { users: usersService },
});function UserProfile({ id }: { id: string }) {
const user = api.users.detail.withPathParams({ id });
const { data } = user.useQuery();
// ^? User | undefined
return <h1>{data?.name}</h1>;
}createApi cascades baseUrl, headers, hooks, and defaultOptions down
through service and endpoint. Header providers may be async, so auth tokens
resolve per request.
Every endpoint is a fluent pipeline, and every operation returns a new
endpoint rather than mutating the original — so api.users.detail stays
reusable no matter what a call site binds or instruments:
const traced = api.users.detail
.onRequest((ctx) => log('out', ctx))
.onResponseValidationError(() => ({ suppress: true, fallback: EMPTY_USER }));Every failure is a tagged member of one closed union, so error handling is a
switch, not a chain of instanceof:
switch (err.tag) {
case 'http': return err.status === 404 ? notFound() : rethrow(err);
case 'abort': return err.timedOut ? retry() : ignore();
case 'transport': return offline();
case 'request-validation':
case 'path-validation':
case 'response-validation': return report(err.failure.issues);
default: throw err;
}Requests can be cancelled everywhere — a signal or a timeout on fetch(), a
timeout on any endpoint, and opt-in cancellation for mutations:
await api.users.list.fetch({ page: 1 }, { timeout: 5_000 });Documentation
hapi.tesyl.tech — the full documentation site: getting started, guides for each concept, and an API reference.
docs/tanstack-parity.md records exactly how hapi
lines up against TanStack Query v5 — what passes through, what composes, and
what is still missing.
docs/usage.md is the full walk-through: two services composed
into one API object, with path params, validation, pagination, header providers,
and the hook pipeline.
Development
npm install
npm test # vitest, includes type-level tests
npm run typecheck # tsc --noEmit
npm run build # tsup — esm + cjs + d.tsprepack runs the build, so npm pack and npm publish always ship a fresh
dist/.
License
MIT — see LICENSE.
