Skip to main content

result-rpc

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.

소스 정보

저장소
jokull/result-rpc
최근 소스 활동
2026년 8월 7일 17:07
감지된 SKILL.md 언어
영어
스타
21
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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에서 보기