@danieljvdm/dev-kit 0.11.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +111 -78
  2. package/dev-kit.example.jsonc +0 -4
  3. package/package.json +10 -6
  4. package/schema/dev-kit.schema.json +1 -46
  5. package/skills/build-effect-apis/SKILL.md +77 -0
  6. package/skills/build-effect-apis/agents/openai.yaml +4 -0
  7. package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
  8. package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
  9. package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
  10. package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
  11. package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
  12. package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
  13. package/skills/build-effect-apis/references/shared-contracts.md +108 -0
  14. package/skills/build-effect-apis/references/tanstack-start.md +86 -0
  15. package/skills/build-effect-apis/references/verification.md +50 -0
  16. package/skills/build-effect-clis/SKILL.md +61 -0
  17. package/skills/build-effect-clis/agents/openai.yaml +4 -0
  18. package/skills/build-effect-clis/references/command-design.md +93 -0
  19. package/skills/build-effect-clis/references/entrypoints-and-testing.md +59 -0
  20. package/skills/build-effect-clis/references/processes-and-platform.md +73 -0
  21. package/skills/dev-kit/SKILL.md +77 -48
  22. package/skills/effect-architecture-audit/SKILL.md +26 -0
  23. package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
  24. package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
  25. package/skills/effect-ts/SKILL.md +21 -256
  26. package/skills/effect-ts/agents/openai.yaml +3 -3
  27. package/skills/testing/SKILL.md +5 -0
  28. package/src/catalog-manager.ts +16 -17
  29. package/src/catalog.ts +71 -16
  30. package/src/effect-source.ts +46 -24
  31. package/src/effect-tsgo.ts +86 -24
  32. package/src/gitignore.ts +5 -5
  33. package/src/index.ts +3 -6
  34. package/src/manifest.ts +0 -34
  35. package/src/node-symbolic-link.ts +2 -2
  36. package/src/oxfmt.js +5 -0
  37. package/src/oxfmt.ts +5 -0
  38. package/src/oxlint.js +5 -0
  39. package/src/oxlint.ts +5 -0
  40. package/src/package-skill-source.ts +51 -59
  41. package/src/path-digest.ts +7 -7
  42. package/src/project-package.ts +8 -7
  43. package/src/project-process-lock.ts +17 -12
  44. package/src/project-state.ts +1 -1
  45. package/src/skill-manager.ts +18 -16
  46. package/src/skill-selector.ts +12 -0
  47. package/src/sync.ts +181 -131
  48. package/src/tool-ignore-patterns.js +9 -0
  49. package/src/tool-ignore-patterns.ts +15 -0
  50. package/src/vendor.ts +67 -61
  51. package/src/vite-plus-dependency.ts +10 -11
  52. package/src/vite-plus-hooks.ts +24 -14
  53. package/src/vite-plus-quality.ts +21 -172
  54. package/src/vite-plus.js +81 -0
  55. package/src/vite-plus.ts +102 -0
  56. package/templates/AGENTS.md +1 -1
  57. package/skills/effect-ts/UPSTREAM.md +0 -28
  58. package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
  59. package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
  60. package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
  61. package/skills/effect-ts/references/atom-testing.md +0 -67
  62. package/skills/effect-ts/references/audit-services.md +0 -144
  63. package/skills/effect-ts/references/features.md +0 -525
  64. package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
  65. package/skills/effect-ts/references/guide-cli.md +0 -107
  66. package/skills/effect-ts/references/guide-datetime.md +0 -72
  67. package/skills/effect-ts/references/guide-effect.md +0 -440
  68. package/skills/effect-ts/references/guide-error-handling.md +0 -565
  69. package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
  70. package/skills/effect-ts/references/guide-layers.md +0 -989
  71. package/skills/effect-ts/references/guide-observability.md +0 -746
  72. package/skills/effect-ts/references/guide-retries.md +0 -434
  73. package/skills/effect-ts/references/guide-schedule.md +0 -343
  74. package/skills/effect-ts/references/guide-schema.md +0 -664
  75. package/skills/effect-ts/references/guide-sql.md +0 -536
  76. package/skills/effect-ts/references/guide-testing.md +0 -532
  77. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
  78. package/skills/effect-ts/references/version-and-source.md +0 -86
  79. package/templates/vite-plus/vite.config.ts +0 -22
@@ -0,0 +1,161 @@
1
+ # Effect API clients
2
+
3
+ Derive clients from the shared `HttpApi`; keep request types, response types,
4
+ and expected errors owned by the contract.
5
+
6
+ - [Inventory the client boundary](#inventory-the-client-boundary)
7
+ - [Build one Atom API service](#build-one-atom-api-service)
8
+ - [Define queries](#define-queries)
9
+ - [Define mutations and invalidation](#define-mutations-and-invalidation)
10
+ - [Use a direct client outside React](#use-a-direct-client-outside-react)
11
+
12
+ ## Inventory the client boundary
13
+
14
+ Before changing Atom code, locate every `RegistryProvider`, runtime factory,
15
+ `AtomHttpApi.Service`, query atom, mutation atom, reactivity-key constructor,
16
+ and route aggregate. Confirm the installed `effect` and `@effect/atom-react`
17
+ versions before copying signatures.
18
+
19
+ ## Build one Atom API service
20
+
21
+ Create one module-scoped runtime factory and one `AtomHttpApi.Service`. Share a
22
+ memo map when multiple client services must reuse layers.
23
+
24
+ ```ts
25
+ import { Effect, Layer } from "effect";
26
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
27
+ import { HttpApiMiddleware } from "effect/unstable/httpapi";
28
+ import { Atom, AtomHttpApi } from "effect/unstable/reactivity";
29
+ import { ApplicationApi, Authenticate } from "@app/domain";
30
+
31
+ export const AuthenticateClient = HttpApiMiddleware.layerClient(
32
+ Authenticate,
33
+ Effect.fn("AuthenticateClient")(function* ({ next, request }) {
34
+ const token = yield* readAccessToken;
35
+ return yield* next(HttpClientRequest.bearerToken(request, token));
36
+ }),
37
+ );
38
+
39
+ const ApiHttpClient = Layer.mergeAll(FetchHttpClient.layer, AuthenticateClient);
40
+
41
+ export const appRuntime = Atom.context({
42
+ memoMap: Layer.makeMemoMapUnsafe(),
43
+ });
44
+
45
+ export const ApiClient = AtomHttpApi.Service()("ApiClient", {
46
+ api: ApplicationApi,
47
+ httpClient: ApiHttpClient,
48
+ runtime: appRuntime,
49
+ baseUrl: "/api",
50
+ transformClient: (client) =>
51
+ HttpClient.mapRequest(client, (request) =>
52
+ HttpClientRequest.setHeader(request, "x-request-id", requestId()),
53
+ ),
54
+ });
55
+ ```
56
+
57
+ Define the runtime, service, key constructors, queries, and mutations outside
58
+ React renders. Keep browser-only identity access behind the client boundary.
59
+ When contract middleware sets `requiredForClient`, satisfy it with
60
+ `HttpApiMiddleware.layerClient` in the `httpClient` layer. Reserve
61
+ `transformClient` for transport-wide behavior such as correlation headers,
62
+ base URL changes, tracing, or retries; do not bypass declared API security with
63
+ an unrelated raw header transform.
64
+
65
+ ## Define queries
66
+
67
+ ```ts
68
+ export const projectKeys = {
69
+ collection: ["projects"] as const,
70
+ project: (id: ProjectId) => [`project:${id}`] as const,
71
+ };
72
+
73
+ export const projectAtom = Atom.family((projectId: ProjectId) =>
74
+ ApiClient.query("projects", "getProject", {
75
+ params: { projectId },
76
+ timeToLive: "5 minutes",
77
+ reactivityKeys: projectKeys.project(projectId),
78
+ }).pipe(Atom.swr({ staleTime: "30 seconds", revalidateOnMount: true })),
79
+ );
80
+ ```
81
+
82
+ `query(group, endpoint, request)` returns an `Atom<AsyncResult<...>>`. The
83
+ service memoizes encoded request keys internally; a public `Atom.family` still
84
+ expresses domain ownership and cache policy through a stable scalar or Effect
85
+ `Hash`/`Equal` key.
86
+
87
+ Query options have distinct jobs:
88
+
89
+ - `timeToLive` controls idle registry retention;
90
+ - `reactivityKeys` registers invalidation subscriptions;
91
+ - `serializationKey` enables decoded-only hydration serialization and is not
92
+ the runtime cache key;
93
+ - `responseMode` changes the response and error shape.
94
+
95
+ Keep secrets out of serialization keys, URL state, hydration payloads, and
96
+ client-visible layers.
97
+
98
+ ## Define mutations and invalidation
99
+
100
+ ```ts
101
+ export const updateProjectMutation = ApiClient.mutation("projects", "updateProject");
102
+
103
+ const updateProject = useAtomSet(updateProjectMutation, { mode: "promise" });
104
+
105
+ await updateProject({
106
+ params: { projectId },
107
+ payload: patch,
108
+ reactivityKeys: [...projectKeys.collection, ...projectKeys.project(projectId)],
109
+ });
110
+ ```
111
+
112
+ Successful mutations invalidate matching keys; failed mutations do not.
113
+ Centralize reactivity-key constructors because mismatched strings fail silently.
114
+ When one mutation affects several exact keys, flatten their arrays; an array
115
+ containing key arrays registers those nested arrays as different keys.
116
+
117
+ Exact array keys invalidate only exact matches. Record-form keys are
118
+ hierarchical in current Effect v4 implementations: a property registers both
119
+ its broad name and each `property:id` combination. Confirm this against the
120
+ installed version before relying on it.
121
+
122
+ Choose invalidation breadth from the server write:
123
+
124
+ - invalidate an entity key when only its detail changed;
125
+ - invalidate the collection when membership, ordering, totals, or filters can
126
+ change;
127
+ - invalidate every affected namespace when a write crosses aggregates.
128
+
129
+ Combine invalidation with manual refresh only when two requests are intended.
130
+ Keep navigation, toasts, form reset, and presentation-level optimistic state
131
+ with the initiating UI.
132
+
133
+ ## Use a direct client outside React
134
+
135
+ ```ts
136
+ import { Effect } from "effect";
137
+ import { FetchHttpClient, HttpClientRequest } from "effect/unstable/http";
138
+ import { HttpApiClient, HttpApiMiddleware } from "effect/unstable/httpapi";
139
+ import { ApplicationApi, Authenticate } from "@app/domain";
140
+ import { AccessToken, AccessTokenLive } from "./AccessToken";
141
+
142
+ const AuthenticateClient = HttpApiMiddleware.layerClient(
143
+ Authenticate,
144
+ Effect.fn("AuthenticateClient")(function* ({ next, request }) {
145
+ const token = yield* AccessToken;
146
+ return yield* next(HttpClientRequest.bearerToken(request, token.value));
147
+ }),
148
+ );
149
+
150
+ const program = Effect.gen(function* () {
151
+ const client = yield* HttpApiClient.make(ApplicationApi, {
152
+ baseUrl: "https://api.example.com",
153
+ });
154
+ return yield* client.projects.getProject({ params: { projectId } });
155
+ }).pipe(Effect.provide([FetchHttpClient.layer, AuthenticateClient, AccessTokenLive]));
156
+ ```
157
+
158
+ Use the direct client in services, scripts, tests, and non-React applications.
159
+ Provide every required contract middleware at the client composition root.
160
+ Transform the underlying `HttpClient` once for non-contract headers, tracing,
161
+ retry, or base behavior rather than repeating it at every call site.
@@ -0,0 +1,78 @@
1
+ # Effect Atom lifecycle
2
+
3
+ Treat query atoms as shared read state and mutations as actions owned by an
4
+ initiating UI or workflow. Choose registry scope, atom identity, retention,
5
+ freshness, polling, and cancellation independently.
6
+
7
+ ## Registry and runtime scope
8
+
9
+ `RegistryProvider` creates its registry on first render; later option changes do
10
+ not rebuild it. Provider unmount schedules disposal after a short grace period,
11
+ while moving or keying the provider changes the cache boundary. Place one
12
+ provider around the application subtree that should share client state; nested
13
+ or route-local providers create separate caches.
14
+
15
+ The registry stores nodes, values, subscriptions, idle timers, and finalizers.
16
+ An Atom runtime supplies Effect services, and `Atom.context({ memoMap })` lets
17
+ several runtimes share layer construction. Create neither a registry nor a memo
18
+ map per query or render. Keep request-specific authentication out of a
19
+ process-global server memo map.
20
+
21
+ ## Stable identity and families
22
+
23
+ Export fixed queries directly. Use `Atom.family` when a parameter selects the
24
+ resource. Prefer a primitive family argument; give object keys deliberate Effect
25
+ `Equal`/`Hash` semantics or reuse the same object. Calling a family with a stable
26
+ primitive during render already returns its stable member; an extra `useMemo`
27
+ does not improve identity.
28
+
29
+ ## Three independent clocks
30
+
31
+ | Control | Clock starts | Effect | It does not mean |
32
+ | ----------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------- | -------------------------- |
33
+ | Registry/default idle TTL or query `timeToLive` | When the atom becomes unused | Retain the cached node until idle eviction | The value remains fresh |
34
+ | `Atom.swr({ staleTime })` | At the latest success | Revalidate stale data on configured mount/focus signals while showing the prior success | The node survives unmount |
35
+ | `Atom.withRefresh(interval)` | While its wrapper is mounted | Force periodic refresh until disposal | Fresh requests are skipped |
36
+
37
+ Set retention long enough for the intended navigation/remount reuse window.
38
+ `staleTime` cannot rescue an already evicted node. Manual refresh, invalidation,
39
+ and polling are forceful and do not consult SWR freshness. Applying `keepAlive`
40
+ to a polling wrapper intentionally keeps that polling lifetime alive.
41
+
42
+ Atom registries are in-memory caches, not durable or shared storage. New tabs,
43
+ hard reloads, processes, and providers have separate caches. Idle TTL is not a
44
+ maximum-entry bound; finite TTLs are important for unbounded family keys.
45
+
46
+ ## React mounts and action ownership
47
+
48
+ - `useAtomValue(atom)` subscribes for rendering;
49
+ - `useAtomSet(atom)` mounts a writable atom and returns a setter;
50
+ - `useAtom(atom)` subscribes and writes when one component owns both behaviors.
51
+
52
+ Use `useAtom` instead of pairing `useAtomValue` and `useAtomSet` for the same
53
+ atom in one component. The split form creates a value subscription plus a
54
+ separate mount and obscures ownership.
55
+
56
+ Unmount releases only that hook's subscription or mount. Registry eviction waits
57
+ for remaining consumers and idle TTL, then runs node finalizers. Component
58
+ unmount, registry eviction, and `Atom.Interrupt` are distinct events.
59
+
60
+ Use `Atom.Interrupt` for explicit cancellation. A cleanup write publishes an
61
+ interrupted `AsyncResult.Failure`; it is not passive unmount release, can run
62
+ during React Strict Mode replay, and can cancel work owned by another consumer.
63
+
64
+ Place a multi-request action in an owner that lives for the whole sequence. If
65
+ navigation or disconnect must not leave a partially completed fan-out, use a
66
+ stable workflow atom/service or one durable server-side command.
67
+
68
+ ## Keep aggregates stable
69
+
70
+ `AsyncResult.all` returns the first non-success input, so an aggregate is only
71
+ as reusable as its least-stable input. A newly allocated or evicted input
72
+ returns to `Initial` and makes the aggregate appear reset even while other
73
+ queries remain cached.
74
+
75
+ Define every input as a module-scoped singleton or stable family member with
76
+ compatible retention. Aggregate inside a derived atom so the registry owns
77
+ recomputation; memoizing only the `AsyncResult.all` call does not repair an
78
+ unstable input atom.
@@ -0,0 +1,74 @@
1
+ # Deterministic Effect Atom testing
2
+
3
+ Test cache policy below React first with `AtomRegistry.make()`. Add a React
4
+ integration test for provider placement, hook behavior, browser-only SSR
5
+ boundaries, hydration, or Strict Mode ownership.
6
+
7
+ Use fake timers, a request counter, controllable Effects, and explicit mounts:
8
+
9
+ ```ts
10
+ const registry = AtomRegistry.make({ defaultIdleTTL: 1_000 });
11
+ const unmount = registry.mount(queryAtom);
12
+ const first = registry.get(queryAtom);
13
+
14
+ // Flush Effect work and advance the test runner's fake timers.
15
+
16
+ unmount();
17
+ ```
18
+
19
+ Use the repository's established `Effect.yieldNow` or test-clock pattern rather
20
+ than wall-clock sleeps. Prefer installed APIs and predicates such as
21
+ `AsyncResult.isSuccess`; use the cleanup returned by `registry.mount(atom)`.
22
+
23
+ ## Remount reuse
24
+
25
+ 1. Mount and resolve the query; assert request count `1`.
26
+ 2. Unmount, advance less than idle TTL, and remount the same atom identity.
27
+ 3. Assert cached success is immediately available and no fresh request occurs.
28
+
29
+ ## Stale refresh
30
+
31
+ 1. Resolve once through an SWR wrapper.
32
+ 2. Advance past `staleTime` but not idle TTL.
33
+ 3. Remount or emit the injected focus signal.
34
+ 4. Assert the previous success remains with `waiting: true`, then a second
35
+ success arrives and request count becomes `2`.
36
+ 5. Prove a fresh mount or focus signal does not request.
37
+
38
+ ## TTL eviction
39
+
40
+ 1. Resolve and unmount.
41
+ 2. Advance to just before TTL and assert reuse.
42
+ 3. Advance through TTL, flush disposal, and remount.
43
+ 4. Assert initial/waiting behavior and a new request.
44
+
45
+ ## Polling cleanup
46
+
47
+ 1. Mount the `Atom.withRefresh` wrapper and resolve once.
48
+ 2. Advance one interval and assert one forced refresh.
49
+ 3. Unmount, advance several intervals, and assert the counter stops changing.
50
+
51
+ ## Mutation invalidation
52
+
53
+ 1. Mount list and detail queries with explicit keys.
54
+ 2. Run a successful mutation and assert only intended queries refresh.
55
+ 3. Run a failed mutation and assert nothing invalidates.
56
+ 4. Dispose the queries and assert invalidation handlers are removed.
57
+
58
+ ## Strict Mode and action ownership
59
+
60
+ Render the owner inside `StrictMode`. Prove development setup-cleanup replay
61
+ does not write `Atom.Interrupt` or publish an interrupted failure before an
62
+ explicit cancellation. For multi-request actions, unmount during the sequence
63
+ and prove the chosen policy. If the atom is shared, prove unmounting one
64
+ consumer does not cancel work still owned by another.
65
+
66
+ ## Aggregate stability
67
+
68
+ Mount a route atom using `AsyncResult.all`, resolve every input, unmount, and
69
+ remount inside the retention window. Assert the aggregate never returns to
70
+ `Initial`. Then expire one input and prove that input causes the reset.
71
+
72
+ For runtime/layer tests, seed `runtime.layer` through `RegistryProvider`
73
+ `initialValues` with a deterministic test layer. Replace network services
74
+ without changing the production atom graph.
@@ -0,0 +1,56 @@
1
+ # Standard runtime assembly
2
+
3
+ Use this branch for conventional Node/Bun processes, generated documentation,
4
+ and Web-standard handlers. Confirm platform package names against the installed
5
+ versions.
6
+
7
+ ## Compose routes before choosing the runtime
8
+
9
+ ```ts
10
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
11
+ import { Layer } from "effect";
12
+ import { HttpRouter, HttpServer } from "effect/unstable/http";
13
+ import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi";
14
+ import { createServer } from "node:http";
15
+ import { ApplicationApi } from "@app/domain/http";
16
+
17
+ const ApplicationHttpLive = Layer.mergeAll(ProjectsHandlersLive, AuthenticateLive).pipe(
18
+ Layer.provide(ApplicationServicesLive),
19
+ );
20
+
21
+ const ApiRoutes = HttpApiBuilder.layer(ApplicationApi, {
22
+ openapiPath: "/openapi.json",
23
+ }).pipe(Layer.provide(ApplicationHttpLive));
24
+
25
+ const DocsRoute = HttpApiScalar.layer(ApplicationApi, { path: "/docs" });
26
+ const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute);
27
+ ```
28
+
29
+ Build the typed API route and documentation route from the same contract. Merge
30
+ raw routes here only when the API schema cannot represent their transport, and
31
+ ensure they independently enforce equivalent identity and security behavior.
32
+
33
+ ## Run a long-lived server
34
+
35
+ ```ts
36
+ const HttpServerLive = HttpRouter.serve(AllRoutes).pipe(
37
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
38
+ );
39
+
40
+ Layer.launch(HttpServerLive).pipe(NodeRuntime.runMain);
41
+ ```
42
+
43
+ Use the matching platform server layer for Bun or another runtime. Keep the
44
+ platform choice at this composition root.
45
+
46
+ ## Export a Web handler
47
+
48
+ ```ts
49
+ export const { handler, dispose } = HttpRouter.toWebHandler(
50
+ AllRoutes.pipe(Layer.provide(HttpServer.layerServices)),
51
+ );
52
+ ```
53
+
54
+ Use the Web handler for serverless and framework adapters that accept standard
55
+ `Request` and `Response` values. Preserve and call `dispose` when the host has a
56
+ lifecycle hook; do not recreate the handler and its runtime for every request.
@@ -0,0 +1,174 @@
1
+ # Server handlers and middleware
2
+
3
+ Implement the shared contract with group handlers, application services, and
4
+ layers. Confirm names against the installed Effect version; examples here use
5
+ the Effect v4 `effect/unstable/httpapi` surface.
6
+
7
+ - [Implement thin group handlers](#implement-thin-group-handlers)
8
+ - [Declare security middleware in the shared contract](#declare-security-middleware-in-the-shared-contract)
9
+ - [Implement security middleware on the server](#implement-security-middleware-on-the-server)
10
+ - [Assemble layers at the runtime edge](#assemble-layers-at-the-runtime-edge)
11
+ - [Use response escape hatches deliberately](#use-response-escape-hatches-deliberately)
12
+
13
+ ## Implement thin group handlers
14
+
15
+ Resolve application services once in the group builder, then map every endpoint
16
+ identifier to one handler. Use `Effect.fn` for callbacks that return Effects.
17
+
18
+ ```ts
19
+ import { Effect } from "effect";
20
+ import { HttpApiBuilder } from "effect/unstable/httpapi";
21
+ import { ApplicationApi } from "@app/domain/http";
22
+ import { Projects } from "../services/Projects";
23
+
24
+ export const ProjectsHandlersLive = HttpApiBuilder.group(
25
+ ApplicationApi,
26
+ "projects",
27
+ Effect.fn("ProjectsApi.handlers")(function* (handlers) {
28
+ const projects = yield* Projects;
29
+
30
+ return handlers
31
+ .handle("listProjects", () => projects.list().pipe(Effect.orDie))
32
+ .handle("getProject", ({ params }) =>
33
+ projects
34
+ .get(params.projectId)
35
+ .pipe(Effect.catchReasons("ProjectsError", { ProjectNotFound: Effect.fail }, Effect.die)),
36
+ );
37
+ }),
38
+ );
39
+ ```
40
+
41
+ Handlers receive decoded `params`, `query`, `headers`, and `payload`. Check
42
+ cross-field invariants at this boundary when schemas cannot express them, then
43
+ call one application workflow. Keep persistence, transactions, retries, and
44
+ multi-service orchestration in services.
45
+
46
+ Reconcile the service error channel with the endpoint contract here. Use
47
+ `Effect.orDie` when an endpoint declares no service failures. Use
48
+ `Effect.catchReason`, `Effect.catchReasons`, or `Effect.unwrapReason` when an
49
+ application service wraps domain reasons: re-fail only reasons declared by the
50
+ endpoint and turn unexpected infrastructure or invariant failures into defects.
51
+ Finish when each handler's expected error type is a subset of the endpoint's
52
+ declared error schema.
53
+
54
+ ## Declare security middleware in the shared contract
55
+
56
+ Keep the middleware declaration in the shared contract package so server,
57
+ OpenAPI, and generated clients see the same requirement. Keep its implementation
58
+ out of that package.
59
+
60
+ ```ts
61
+ // packages/domain/src/http/authorization.ts
62
+ import { Context, Schema } from "effect";
63
+ import { HttpApiGroup, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi";
64
+
65
+ export class CurrentActor extends Context.Service<CurrentActor, { readonly id: string }>()(
66
+ "app/CurrentActor",
67
+ ) {}
68
+
69
+ export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
70
+ "Unauthorized",
71
+ { message: Schema.String },
72
+ { httpApiStatus: 401 },
73
+ ) {}
74
+
75
+ export class Authenticate extends HttpApiMiddleware.Service<
76
+ Authenticate,
77
+ {
78
+ readonly provides: CurrentActor;
79
+ readonly requires: never;
80
+ }
81
+ >()("app/Authenticate", {
82
+ requiredForClient: true,
83
+ security: { bearer: HttpApiSecurity.bearer },
84
+ error: Unauthorized,
85
+ }) {}
86
+
87
+ export const ProjectsApi = HttpApiGroup.make("projects")
88
+ .add(GetProjectEndpoint, ListProjectsEndpoint)
89
+ .middleware(Authenticate)
90
+ .prefix("/projects");
91
+ ```
92
+
93
+ `requiredForClient` makes the generated client require a matching client-side
94
+ middleware layer. The security declaration also owns credential decoding and
95
+ OpenAPI security metadata; do not manually decode an `Authorization` header for
96
+ this case.
97
+
98
+ ## Implement security middleware on the server
99
+
100
+ A security middleware implementation is keyed by the names in its `security`
101
+ object. It can use server-only services without leaking them into clients.
102
+
103
+ ```ts
104
+ // apps/api/src/http/Authenticate.ts
105
+ import { Effect, Layer, Redacted } from "effect";
106
+ import { Authenticate, CurrentActor, Unauthorized } from "@app/domain/http";
107
+ import { Actors } from "../services/Actors";
108
+
109
+ export const AuthenticateLive = Layer.effect(
110
+ Authenticate,
111
+ Effect.gen(function* () {
112
+ const actors = yield* Actors;
113
+
114
+ return Authenticate.of({
115
+ bearer: Effect.fn("Authenticate.bearer")(function* (httpEffect, { credential }) {
116
+ const actor = yield* actors
117
+ .authenticate(Redacted.value(credential))
118
+ .pipe(
119
+ Effect.catch(() => Effect.fail(new Unauthorized({ message: "Invalid bearer token" }))),
120
+ );
121
+
122
+ return yield* Effect.provideService(httpEffect, CurrentActor, actor);
123
+ }),
124
+ });
125
+ }),
126
+ );
127
+ ```
128
+
129
+ Use ordinary `HttpApiMiddleware.Service` implementations for request logging,
130
+ tenancy, correlation IDs, or other cross-cutting work that is not a declared
131
+ security scheme. Those implementations may access `HttpServerRequest`, but they
132
+ must decode raw headers, cookies, and external values before providing services.
133
+ When middleware uses `requires` and `provides`, confirm ordering from the
134
+ installed docs: the outer middleware that requires a service is attached before
135
+ the inner middleware that provides it.
136
+
137
+ Every error a middleware can return belongs in its `error` schema. Keep the
138
+ server layer separate from the shared declaration so importing the contract
139
+ never pulls secrets, repositories, or platform services into a client bundle.
140
+
141
+ ## Assemble layers at the runtime edge
142
+
143
+ ```ts
144
+ const ProjectsHandlersProvided = ProjectsHandlersLive.pipe(Layer.provide(ApplicationServicesLive));
145
+
146
+ const ApiLive = HttpApiBuilder.layer(ApplicationApi, {
147
+ openapiPath: "/api/openapi.json",
148
+ }).pipe(
149
+ Layer.provide(ProjectsHandlersProvided),
150
+ Layer.provide(AuthenticateLive),
151
+ Layer.provideMerge(HttpRuntimePrerequisitesLive),
152
+ );
153
+ ```
154
+
155
+ The exact HTTP platform/router/server layers vary by runtime. Keep that wiring
156
+ at the entrypoint, provide all group and middleware layers, and let the type
157
+ system expose missing requirements. Verify that the OpenAPI path composes with
158
+ any API prefix as intended.
159
+
160
+ For a conventional Node/Bun process, generated docs route, or serverless web
161
+ handler, read [runtime-assembly.md](runtime-assembly.md).
162
+
163
+ Transport-wide middleware such as CORS may wrap the final HTTP application.
164
+ Keep API middleware for contract-visible behavior and runtime HTTP middleware
165
+ for concerns that also apply to raw or non-API routes.
166
+
167
+ ## Use response escape hatches deliberately
168
+
169
+ Return typed success values and declared errors for ordinary endpoints. Use
170
+ `HttpServerResponse` when the boundary must pass through an upstream response,
171
+ redirect, set cookies, stream, or control a non-default success status or body.
172
+ Use Effect request and cookie APIs to parse boundary state. Reserve unsafe JSON
173
+ response constructors for values whose safety is established elsewhere; encode
174
+ ordinary typed 4xx failures through the endpoint's declared error schemas.
@@ -0,0 +1,108 @@
1
+ # Shared contracts
2
+
3
+ Use a runtime-neutral domain package as the protocol boundary shared by the
4
+ server and every typed client.
5
+
6
+ ## Package shape
7
+
8
+ ```text
9
+ packages/domain/
10
+ ├── package.json
11
+ └── src/
12
+ ├── models/
13
+ │ ├── ids.ts
14
+ │ ├── errors.ts
15
+ │ └── views.ts
16
+ └── http/
17
+ ├── endpoints/
18
+ │ ├── get-project.ts
19
+ │ └── update-project.ts
20
+ ├── authorization.ts
21
+ ├── project-api.ts
22
+ ├── application-api.ts
23
+ └── index.ts
24
+ ```
25
+
26
+ Export the root API, contract-visible middleware declarations, and any genuinely
27
+ reusable schemas. Keep middleware implementations, server handlers, React,
28
+ database clients, secrets, platform bindings, and runtime layers out of this
29
+ package. A workspace export map can expose `./http`, `./models`, and endpoint
30
+ subpaths when consumers need them.
31
+
32
+ ## Model the wire
33
+
34
+ Use schemas for every external value. Prefer reusable named classes for DTOs,
35
+ branded scalar schemas for identifiers, and serializable tagged errors with
36
+ HTTP status metadata for expected failures.
37
+
38
+ ```ts
39
+ import { Schema } from "effect";
40
+ import { HttpApiSchema } from "effect/unstable/httpapi";
41
+
42
+ export const ProjectId = Schema.String.pipe(Schema.brand("ProjectId"));
43
+ export type ProjectId = typeof ProjectId.Type;
44
+
45
+ export class ProjectDto extends Schema.Class<ProjectDto>("ProjectDto")({
46
+ id: ProjectId,
47
+ name: Schema.NonEmptyString,
48
+ }) {}
49
+
50
+ export class ProjectNotFoundError extends Schema.TaggedError<ProjectNotFoundError>()(
51
+ "ProjectNotFoundError",
52
+ { id: ProjectId },
53
+ ) {}
54
+
55
+ export const ProjectNotFound = ProjectNotFoundError.pipe(HttpApiSchema.status("NotFound"));
56
+ ```
57
+
58
+ Keep success schemas limited to successful values. Put every expected 4xx/5xx
59
+ failure in `error`; defects remain defects. Reuse a tuple of transport errors
60
+ only when the same set is genuinely standard across endpoints.
61
+
62
+ Make DTOs describe the wire exactly. If clients receive `id`, expose `id` even
63
+ when a persistence model uses `publicId`; map between them in a small boundary
64
+ function. Keep endpoint-specific request DTOs beside their endpoint and move a
65
+ DTO into shared models only when multiple contracts reuse the same concept.
66
+
67
+ ## Define one endpoint per file
68
+
69
+ An endpoint declares the complete request and response contract inline.
70
+
71
+ ```ts
72
+ import { HttpApiEndpoint } from "effect/unstable/httpapi";
73
+ import { ProjectDto, ProjectId, ProjectNotFound } from "../../models";
74
+ import { RequestHeaders } from "../headers";
75
+
76
+ export const GetProjectEndpoint = HttpApiEndpoint.get("getProject", "/projects/:projectId", {
77
+ params: { projectId: ProjectId },
78
+ headers: RequestHeaders,
79
+ success: ProjectDto,
80
+ error: ProjectNotFound,
81
+ });
82
+ ```
83
+
84
+ Use the operation identifier as a stable protocol name: handlers and generated
85
+ clients address it directly. Represent optional query values with the installed
86
+ version's optional schema operator, such as `Schema.optionalKey`.
87
+
88
+ If headers use `Schema.Class`, generated client input may require a constructed
89
+ class instance rather than a structurally similar object. Encode a representative
90
+ instance in a test so this contract stays explicit.
91
+
92
+ ## Compose groups and the root
93
+
94
+ ```ts
95
+ import { HttpApi, HttpApiGroup } from "effect/unstable/httpapi";
96
+ import { GetProjectEndpoint, UpdateProjectEndpoint } from "./endpoints";
97
+
98
+ export const ProjectsApi = HttpApiGroup.make("projects")
99
+ .add(GetProjectEndpoint, UpdateProjectEndpoint)
100
+ .prefix("/projects");
101
+
102
+ export const ApplicationApi = HttpApi.make("application").add(ProjectsApi).prefix("/api");
103
+ ```
104
+
105
+ Group and root files compose; endpoint files specify. Re-export through index
106
+ files so server and clients import the same `ApplicationApi` value. A contract
107
+ change is complete only after all affected handlers, callers, tests, and
108
+ OpenAPI output agree with it.