- name
- result-rpc
- description
- Build type-safe RPC between a server and a React app with result-rpc — one Result<T,E> and one wire-safe tagged-error union per procedure, error-owning React shells, a normalized entity cache, source-checked models, cursor pagination, and offline handling. Use when working in a project that depends on `result-rpc`, defining contracts/routers/procedures, wiring the browser client, writing hooks (useResultQuery/useResultMutation/useResultPaginatedQuery/useResultSubscription), boundaryShells, entity models, or migrating from tRPC.
# result-rpc
result-rpc is the RPC layer for a React app that outgrew tRPC's happy path:
production teams hitting offline, 5xx, session expiry, and observability. Every
procedure returns one **`Result<T, E>`** against a **declared, closed tagged-error
union**. The result-rpc wire reconstructs the Result behavior and real
`TaggedError` instances on the client; failures are values you narrow, not
exceptions you catch. React **shells** own failures by position, an entity cache patches by
identity, and models can be checked against **any upstream source type** without
pulling that source's runtime graph into the client.
This skill is a map. The documentation at **https://result-rpc.com** is
the single source of truth — every page is also served as raw Markdown by
appending `.md` to its URL, and the whole set is indexed at
`https://result-rpc.com/llms.txt` (full text at `/llms-full.txt`). For
any task below, **fetch the linked `.md` page** rather than guessing — the docs
carry the current API, and this file only routes you there.
## The foundation: better-result 3.0
`Result<T, E>` is [better-result](https://github.com/dmmulroy/better-result) 3.0's
class, brought in as a `peerDependencies` entry — one shared `Ok`/`Err` class
across the app, which is what the boundary's `instanceof` checks rely on.
result-rpc exports the boundary (procedures, errors, wire, client, server)
plus the constrained construction primitives `ok`/`err` and the types
(`Result`, `Ok`, `Err`, `InferErr`/`InferOk`/`GenErr`). The rest of the
algebra — `gen`, `map`, `andThen`, `match`, `all`, `tap`, `unwrap`,
`tryCatch`, `tryPromise`, `matchError`, ... — comes from better-result:
`import { Result, matchError } from "better-result"`.
- **Dialect:** `gen` bodies `return ok(x)` — better-result's convention, unchanged.
- **Division of labor:** better-result owns the Result algebra (`Result.codec`,
`retry`, `matchError`, Panic); result-rpc owns the boundary, transport, cache,
and shells — the `.handler()` return type enforces the declared-error lane.
- **Adoption:** a better-result `Result` whose error is already a declared
result-rpc tagged error flows into a handler unchanged (zero-copy); a foreign
error folds with `mapError` before the boundary.
The boundary rule is unchanged: only declared, serializable, reifiable tagged
errors may enter or leave a procedure. See `/reference/faq.md` (class identity)
and `/concepts/results.md` (dialect) for the full reasoning.
## Before writing code
Choose the relevant path before editing:
- Handler calls `fetch`, a database, the filesystem, or a third-party SDK →
read **Results and Errors** first. These two pages are mandatory for fallible
external I/O.
- Output contains `Date`, `Map`, `Set`, `BigInt`, URL-like, or custom values →
read **Wire**.
- Browser code is involved → read **Client Boundary** before creating imports.
- Query freshness, hydration, or refetching is involved → read **React hooks**.
- Operation is a mutation → read **Mutations** before adding invalidation or
optimistic callbacks.
- App-wide auth, access, offline, redirect, modal, or escalation behavior is
involved → read **Shells and Layers**.
Preflight before the first build:
- [ ] Browser imports a contract value, never the implemented router.
- [ ] Expected failures are declared and returned, not thrown.
- [ ] Provider/database failures are folded before entering a public contract.
- [ ] Procedure input, output, and error types are inferred instead of copied.
- [ ] Rich values use their actual wire codecs.
- [ ] At least one real wire round-trip is exercised.
## Read this first — the one rule that is a security bug if broken
result-rpc ships a **real client value** to the browser (not just a type, the
way tRPC ships `AppRouter`). So **what you import decides what bundles**:
> Build the browser client from a **`contract()`**, and define that contract in
> a module that **never imports handler or server code**. Importing the
> **router** (or a contract module that value-imports server code) ships your
> handlers, database driver, and any secret they close over to every visitor.
> Bundlers do **not** tree-shake this away.
If you touch client wiring, read
**https://result-rpc.com/concepts/client-boundary.md** before anything
else. `import type` from a server module is safe (erased); a value import is the
footgun.
## Task → page map
Fetch the `.md` version (append `.md`) of the page you need:
| When you're… | Read |
| --------------------------------------------- | ---------------------------------------------------- |
| Getting the mental model | `/start/introduction` · `/start/quickstart` |
| Installing / project layout | `/start/installation` |
| **Wiring the browser client (do this right)** | **`/concepts/client-boundary`** · `/concepts/client` |
| Fallible fetch/db/SDK handler (**mandatory**) | `/concepts/results` · `/concepts/errors` |
| Declaring errors + visibility | `/concepts/errors` |
| Working with Result values (gen, all, match) | `/concepts/results` |
| Defining procedures/routers/contracts | `/concepts/contract` |
| Passing request context | `/concepts/context` |
| Wire codecs (input/output shapes) | `/concepts/wire` |
| React hooks + provider | `/concepts/react` |
| Mutations, optimistic updates, `.affects()` | `/concepts/mutations` |
| Entity models + normalized cache | `/concepts/entities` |
| Cursor pagination | `/concepts/pagination` |
| Subscriptions | `/concepts/subscriptions` |
| Error-owning shells (boundaryShells) | `/concepts/shells` |
| Layered context/auth | `/concepts/layers` |
| Stale clients across deploys | `/concepts/deploys` |
| Forms + validateStandard | `/guides/forms` |
| Checking models against source types | `/concepts/model-sources` |
| Folding database constraint failures | `/guides/database-errors` |
| TanStack Router integration | `/guides/routing` |
| Testing (parity client, counter-pins) | `/guides/testing` |
| Sentry/observability | `/guides/observability` |
| Coming from tRPC | `/guides/migrating-from-trpc` |
| Worked examples (01–11) | `/reference/examples` |
| Known sharp edges | `/reference/sharp-edges` |
| FAQ (boundary union, peer dep, identity) | `/reference/faq` |
## Non-negotiables when writing result-rpc code
- **Errors are declared and closed.** Each procedure lists its error union with
`.errors({...})`; handlers `return err(...)`, they don't throw. Private
(`visibility: "private"`) errors are server-only composition currency and
are rejected from procedure, middleware, and layer `.errors()` maps.
- **Errors are instances, wire forms are structural.** Use a declared definition
such as `err(authErrors.unauthorized())`, never fabricate `{ _tag, data }`.
Only the result-rpc wire reifies Results and TaggedErrors; unwrap before passing
them through JSON, framework RPC, or component-prop serialization.
- **The contract is the error registry.** One tag → one definition across the
app; shells claim by tag.
- **Contracts are handler-free.** See the client-boundary rule above.
- **Mutations declare their blast radius** in the contract (`.affects()`,
`.writes()`, or by returning an entity) — not with ad-hoc `onSettled` at call
sites.
- **Paginated queries** use `.paginate({ cursor })` on the server and
`useResultPaginatedQuery` on the client; one cache entry per list.
When a page's guidance conflicts with this file, the page wins — it is
maintained; this map is not.
GitHub에서 보기