Skip to content

feat(examples): add the oRPC API layer - #5

Merged
btravers merged 4 commits into
mainfrom
feat/examples-orpc
Aug 11, 2026
Merged

feat(examples): add the oRPC API layer#5
btravers merged 4 commits into
mainfrom
feat/examples-orpc

Conversation

@btravers

Copy link
Copy Markdown
Contributor

Task 3 of the clean-architecture example slice: the transport layer. A real Runtime over @unthrown/orpc, serving the application the previous packages built.

This is the package the examples existed for. Every runtime in the kernel's own suite is Runtime<never>testRuntime declares no needs — so Context<InstanceType<Needs>> and the phantom needs-gate had never been exercised against a real implementation until now.

What it proves

The needs gate is real, verified in both directions. With the @ts-expect-error in place test:types is clean; delete it and tsc fails with TS2554: Expected 4 arguments, but got 2, naming the unprovided ["UNSATISFIED RUNTIME NEEDS", Exclude<InstanceType<Needs>, X>] rest tuple. Both outputs are in the task report; the directive is restored.

Result → transport, at the edge and only there. A domain Err arrives at the client as a typed, inferable ORPCError — a value with its code, not a thrown 500 — while a Defect collapses to INTERNAL_SERVER_ERROR. The mapping lives in the runtime's mapErrCases, with every case named; P._ is banned by this repo's own no-catch-all-pattern rule, and demonstrating the named-case discipline is part of the example's job.

The runtime publishes its bound port through Serving.info ({ port, prefix }), read back via app.runtimeInfo(). No onListening hook, no boundPort() accessor — that channel was added in #2 precisely so runtimes stop reinventing it, and this is its first real consumer.

Per-request scope. Each call forks a RequestModule over the built parent with Module.forkScope, providing a RequestSpan whose onStop logs through the parent Logger — di's documented request-scope pattern, which is also what makes Logger a genuine third entry in the runtime's needs.

Plus: real oRPC calls reaching the DI-wired use case, distinct trace ids per call, draining letting an in-flight call finish with { inFlightAtStart: 1, completed: 1, abandoned: 0 }, a hung call abandoned at a zero deadline, and probes answering alongside the runtime.

Two repo-config changes, both fixing real failures

  • turbo.json gains a generate task with ^generate edges. Without it, order-api's typecheck races Prisma client generation on a fresh clone — a genuine CI failure, not a theoretical one.
  • knip.json gains a workspaces block registering the composition root as an entry.

One honest limitation

There is no pnpm start. node src/main.ts cannot work: the workspace packages are source-only with .js specifiers over untranspiled TypeScript, and Node's type-stripping does not rewrite specifiers. main.ts remains as the typechecked composition root and the README says so. This matches the sibling repos — btravstack/di's examples carry only test and typecheck scripts too — so it is the house convention rather than a workaround. Running one would need a TS runner dependency.

Verification

121 tests, up from 113; the 113 existing ones unchanged. Suite run 7× with no flake. Full six-command gate green, no Docker. packages/start/ diff is zero lines.

Copilot AI lite review requested due to automatic review settings August 11, 2026 21:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the clean-architecture example’s transport layer by introducing an oRPC-based API package that implements a real Runtime for @btravstack/start, while updating repo tooling to ensure generated Prisma artifacts are available before typechecking/testing.

Changes:

  • Added examples/order-api, including an oRPC contract/router, a node:http-backed Runtime, and an AsyncResult client.
  • Added end-to-end runtime specs covering typed error mapping, per-request scope forking, runtime Serving.info, draining behavior, and probe integration.
  • Updated Turborepo + Knip configuration to sequence generate (Prisma) ahead of dependent typecheck/test and to register the new example’s composition root as an entry.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated no comments.

Show a summary per file
File Description
turbo.json Adds a generate pipeline step and wires ^generate into typecheck/test dependencies to avoid Prisma generation races.
pnpm-workspace.yaml Pins oRPC v2 beta dependencies in the workspace catalog and adds @unthrown/orpc.
pnpm-lock.yaml Locks new oRPC/@unthrown-orpc dependency graph.
knip.json Registers examples/order-api/src/main.ts as a workspace entry for Knip.
examples/order-api/vitest.config.ts Adds Vitest config for the new example package.
examples/order-api/tsconfig.test-d.json Adds a dedicated tsconfig for type-level tests.
examples/order-api/tsconfig.json Adds TS config for the package (including allowImportingTsExtensions for generated Prisma client compatibility).
examples/order-api/src/vitest.d.ts Ensures @unthrown/vitest matchers are in-scope for the example’s tests.
examples/order-api/src/router.ts Implements the oRPC router and the explicit domain-error → transport-error mapping boundary.
examples/order-api/src/request-scope.ts Adds a per-request DI scope module (RequestModule) and request-lifetime port (RequestSpan).
examples/order-api/src/orpc-runtime.ts Implements the real oRPC runtime (start/drain/stop), including per-request unit submission and Serving.info.
examples/order-api/src/orpc-runtime.spec.ts Adds integration tests for transport behavior, typed errors, runtime info, draining, and probes.
examples/order-api/src/needs-gate.test-d.ts Adds type-level verification that the runtime needs-gate fails/succeeds as intended.
examples/order-api/src/module.ts Adds the composition root (OrderApiModule) exporting the runtime’s required ports.
examples/order-api/src/main.ts Provides the “real process shape” entrypoint (start + runMain) for typechecking.
examples/order-api/src/index.ts Adds the public barrel exports for the example package.
examples/order-api/src/contract.ts Declares the oRPC contract, wire types, and inferable error codes.
examples/order-api/src/client.ts Adds an AsyncResult-based client for the declared oRPC router.
examples/order-api/README.md Documents the transport layer, the two-channel mapping, runtime behavior, and usage.
examples/order-api/package.json Adds the new example workspace package definition and scripts.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread examples/order-api/src/main.ts Outdated
Comment thread examples/order-api/src/orpc-runtime.spec.ts Outdated
Comment thread examples/order-api/src/orpc-runtime.spec.ts Outdated
Comment thread examples/order-api/src/orpc-runtime.spec.ts Outdated
`main.ts` read `Number(process.env["PORT"] ?? 3000)` inline, so a malformed
`PORT` bound `NaN` instead of failing. `src/env.ts` now validates the
environment through `@unthrown/standard-schema`'s `fromSchema` — the curried
form, `fromSchema(schema)(input)` — rather than a schema's own `.parse()`,
which throws and would contradict the example it appears in.

The schema reads strings rather than `z.coerce.number()`: coercion is
`Number()` underneath, so it would turn `PORT=abc` into `NaN` and `PORT=` into
the ephemeral port `0`. `main.ts` folds the `Result` into a message on stderr
and exit code 78 (sysexits EX_CONFIG).
…EN/THEN

Four review conventions, applied to every spec in `examples/` and written into
CLAUDE.md so they outlive the PR comment.

- `describe` is now the first statement after the imports. Every helper moved
  into a sibling `src/test-fixtures.ts` exporting an extended `it`, so what a
  test needs arrives through its own parameter list instead of module scope.
  `orpc-runtime.spec.ts` went from 144 lines of preamble to none.
- Teardown lives in the fixtures. The `try`/`finally` in all eight oRPC runtime
  tests is gone: the `serve` fixture registers each app's shutdown, and its
  cleanup runs on every exit path — including a failing assertion, verified,
  and it still asserts the app exited `Ok`.
- Every test body carries `// GIVEN`, `// WHEN`, `// THEN`.

No assertion weakened: the assertion set of `orpc-runtime.spec.ts` plus its
fixtures is identical to the old file's, and the test counts are unchanged
(8 / 9 / 5 / 6). Two bare `toBeDefect()` calls in the Prisma spec were
STRENGTHENED to name their causes — `InvalidEntity` for a corrupt row, an
`Error` for the disconnected client.

`.oxlintrc.json` extends the `no-get-or-throw` exemption from `**/*.spec.ts` to
`**/test-fixtures.ts`: a fixture module is test code that merely does not end in
`.spec.ts`.

`packages/start`'s 14 kernel spec files are deliberately untouched and recorded
as a deferred sweep in CLAUDE.md's Status, with the per-file scope measured.
Comment thread examples/order-api/src/orpc-runtime.spec.ts Outdated
…ecline to run

The review comment on the oRPC runtime spec named a convention the examples were
not holding: one deep `expect` per test, asserting once against one resource.

It removes a real class of silent pass, not just verbosity. Nine of the flagged
test's assertions sat inside `if (conflict.isErr()) { … }` and
`if (result.isDefect()) …`; if the narrowing were ever false every assertion
inside would not run at all and the test would still pass on the outer
`toBeErr()`. The same hole was open in `env.spec.ts` (`env.isErr() && …`, whose
subject silently becomes `false`) and in `order.spec.ts` (`descriptor?.writable`,
which asserts against `undefined` instead of the entity). All six sites are gone.

Across `examples/`: 72 assertion `expect`s over 32 tests becomes 46 over 46 —
exactly one each. Fourteen tests split out, every one of them a test that asserted
about two or more distinct resources. Nothing asserted before is asserted less:
three properties are logically subsumed (a `not.toBeErrTagged` implied by the
positive `toBeErrTagged`, two `toBeErr`s implied by the `toBeErrWith` that follows
them), six are strengthened, three are new — including the "the raw cause does NOT
leak over the wire" claim, which the comment made but nothing checked.

Two idioms carry the collapse. A class is pinned inside the one assertion with
`constructor: TheClass` in an `objectContaining` — asymmetric matchers read through
the prototype chain, so it is `toBeInstanceOf` without a second `expect` (verified
to reject a structural impostor, which plain `objectContaining` accepts). Facts
that are not properties of one object are asserted as a projection. Setup asserts
nothing: it is chained into the subject with `flatMap`/`flatTap`, which also keeps
every intermediate `Result` consumed rather than dropped.

The two `expect.poll` calls were barriers, not assertions, and read as the latter;
they are `vi.waitUntil` now, with the state they waited for asserted in the test's
one `expect`.
@btravers
btravers merged commit f8a796c into main Aug 11, 2026
13 checks passed
@btravers
btravers deleted the feat/examples-orpc branch August 11, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants