@dojocho/effect-ts 0.0.1

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 (149) hide show
  1. package/DOJO.md +22 -0
  2. package/dojo.json +50 -0
  3. package/katas/001-hello-effect/SENSEI.md +72 -0
  4. package/katas/001-hello-effect/solution.test.ts +35 -0
  5. package/katas/001-hello-effect/solution.ts +16 -0
  6. package/katas/002-transform-with-map/SENSEI.md +72 -0
  7. package/katas/002-transform-with-map/solution.test.ts +33 -0
  8. package/katas/002-transform-with-map/solution.ts +16 -0
  9. package/katas/003-generator-pipelines/SENSEI.md +72 -0
  10. package/katas/003-generator-pipelines/solution.test.ts +40 -0
  11. package/katas/003-generator-pipelines/solution.ts +29 -0
  12. package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
  13. package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
  14. package/katas/004-flatmap-and-chaining/solution.ts +18 -0
  15. package/katas/005-pipe-composition/SENSEI.md +81 -0
  16. package/katas/005-pipe-composition/solution.test.ts +41 -0
  17. package/katas/005-pipe-composition/solution.ts +19 -0
  18. package/katas/006-handle-errors/SENSEI.md +86 -0
  19. package/katas/006-handle-errors/solution.test.ts +53 -0
  20. package/katas/006-handle-errors/solution.ts +30 -0
  21. package/katas/007-tagged-errors/SENSEI.md +79 -0
  22. package/katas/007-tagged-errors/solution.test.ts +82 -0
  23. package/katas/007-tagged-errors/solution.ts +37 -0
  24. package/katas/008-error-patterns/SENSEI.md +89 -0
  25. package/katas/008-error-patterns/solution.test.ts +41 -0
  26. package/katas/008-error-patterns/solution.ts +38 -0
  27. package/katas/009-option-type/SENSEI.md +96 -0
  28. package/katas/009-option-type/solution.test.ts +49 -0
  29. package/katas/009-option-type/solution.ts +26 -0
  30. package/katas/010-either-and-exit/SENSEI.md +86 -0
  31. package/katas/010-either-and-exit/solution.test.ts +33 -0
  32. package/katas/010-either-and-exit/solution.ts +17 -0
  33. package/katas/011-services-and-context/SENSEI.md +82 -0
  34. package/katas/011-services-and-context/solution.test.ts +23 -0
  35. package/katas/011-services-and-context/solution.ts +17 -0
  36. package/katas/012-layers/SENSEI.md +73 -0
  37. package/katas/012-layers/solution.test.ts +23 -0
  38. package/katas/012-layers/solution.ts +26 -0
  39. package/katas/013-testing-effects/SENSEI.md +88 -0
  40. package/katas/013-testing-effects/solution.test.ts +41 -0
  41. package/katas/013-testing-effects/solution.ts +20 -0
  42. package/katas/014-schema-basics/SENSEI.md +81 -0
  43. package/katas/014-schema-basics/solution.test.ts +35 -0
  44. package/katas/014-schema-basics/solution.ts +25 -0
  45. package/katas/015-domain-modeling/SENSEI.md +85 -0
  46. package/katas/015-domain-modeling/solution.test.ts +46 -0
  47. package/katas/015-domain-modeling/solution.ts +42 -0
  48. package/katas/016-retry-and-schedule/SENSEI.md +72 -0
  49. package/katas/016-retry-and-schedule/solution.test.ts +26 -0
  50. package/katas/016-retry-and-schedule/solution.ts +23 -0
  51. package/katas/017-parallel-effects/SENSEI.md +70 -0
  52. package/katas/017-parallel-effects/solution.test.ts +33 -0
  53. package/katas/017-parallel-effects/solution.ts +17 -0
  54. package/katas/018-race-and-timeout/SENSEI.md +75 -0
  55. package/katas/018-race-and-timeout/solution.test.ts +30 -0
  56. package/katas/018-race-and-timeout/solution.ts +27 -0
  57. package/katas/019-ref-and-state/SENSEI.md +72 -0
  58. package/katas/019-ref-and-state/solution.test.ts +29 -0
  59. package/katas/019-ref-and-state/solution.ts +16 -0
  60. package/katas/020-fibers/SENSEI.md +80 -0
  61. package/katas/020-fibers/solution.test.ts +23 -0
  62. package/katas/020-fibers/solution.ts +23 -0
  63. package/katas/021-acquire-release/SENSEI.md +57 -0
  64. package/katas/021-acquire-release/solution.test.ts +23 -0
  65. package/katas/021-acquire-release/solution.ts +22 -0
  66. package/katas/022-scoped-layers/SENSEI.md +52 -0
  67. package/katas/022-scoped-layers/solution.test.ts +35 -0
  68. package/katas/022-scoped-layers/solution.ts +19 -0
  69. package/katas/023-resource-patterns/SENSEI.md +52 -0
  70. package/katas/023-resource-patterns/solution.test.ts +20 -0
  71. package/katas/023-resource-patterns/solution.ts +13 -0
  72. package/katas/024-streams-basics/SENSEI.md +61 -0
  73. package/katas/024-streams-basics/solution.test.ts +30 -0
  74. package/katas/024-streams-basics/solution.ts +16 -0
  75. package/katas/025-stream-operations/SENSEI.md +59 -0
  76. package/katas/025-stream-operations/solution.test.ts +26 -0
  77. package/katas/025-stream-operations/solution.ts +17 -0
  78. package/katas/026-combining-streams/SENSEI.md +54 -0
  79. package/katas/026-combining-streams/solution.test.ts +20 -0
  80. package/katas/026-combining-streams/solution.ts +16 -0
  81. package/katas/027-data-pipelines/SENSEI.md +58 -0
  82. package/katas/027-data-pipelines/solution.test.ts +22 -0
  83. package/katas/027-data-pipelines/solution.ts +16 -0
  84. package/katas/028-logging-and-spans/SENSEI.md +58 -0
  85. package/katas/028-logging-and-spans/solution.test.ts +50 -0
  86. package/katas/028-logging-and-spans/solution.ts +20 -0
  87. package/katas/029-http-client/SENSEI.md +59 -0
  88. package/katas/029-http-client/solution.test.ts +49 -0
  89. package/katas/029-http-client/solution.ts +24 -0
  90. package/katas/030-capstone/SENSEI.md +63 -0
  91. package/katas/030-capstone/solution.test.ts +67 -0
  92. package/katas/030-capstone/solution.ts +55 -0
  93. package/katas/031-config-and-environment/SENSEI.md +77 -0
  94. package/katas/031-config-and-environment/solution.test.ts +38 -0
  95. package/katas/031-config-and-environment/solution.ts +11 -0
  96. package/katas/032-cause-and-defects/SENSEI.md +90 -0
  97. package/katas/032-cause-and-defects/solution.test.ts +50 -0
  98. package/katas/032-cause-and-defects/solution.ts +23 -0
  99. package/katas/033-pattern-matching/SENSEI.md +86 -0
  100. package/katas/033-pattern-matching/solution.test.ts +36 -0
  101. package/katas/033-pattern-matching/solution.ts +28 -0
  102. package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
  103. package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
  104. package/katas/034-deferred-and-coordination/solution.ts +24 -0
  105. package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
  106. package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
  107. package/katas/035-queue-and-backpressure/solution.ts +21 -0
  108. package/katas/036-schema-advanced/SENSEI.md +81 -0
  109. package/katas/036-schema-advanced/solution.test.ts +55 -0
  110. package/katas/036-schema-advanced/solution.ts +19 -0
  111. package/katas/037-cache-and-memoization/SENSEI.md +73 -0
  112. package/katas/037-cache-and-memoization/solution.test.ts +47 -0
  113. package/katas/037-cache-and-memoization/solution.ts +24 -0
  114. package/katas/038-metrics/SENSEI.md +91 -0
  115. package/katas/038-metrics/solution.test.ts +39 -0
  116. package/katas/038-metrics/solution.ts +23 -0
  117. package/katas/039-managed-runtime/SENSEI.md +75 -0
  118. package/katas/039-managed-runtime/solution.test.ts +29 -0
  119. package/katas/039-managed-runtime/solution.ts +19 -0
  120. package/katas/040-request-batching/SENSEI.md +87 -0
  121. package/katas/040-request-batching/solution.test.ts +56 -0
  122. package/katas/040-request-batching/solution.ts +32 -0
  123. package/package.json +22 -0
  124. package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
  125. package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
  126. package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
  127. package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
  128. package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
  129. package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
  130. package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
  131. package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
  132. package/skills/effect-patterns-error-management/SKILL.md +1668 -0
  133. package/skills/effect-patterns-getting-started/SKILL.md +237 -0
  134. package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
  135. package/skills/effect-patterns-observability/SKILL.md +1586 -0
  136. package/skills/effect-patterns-platform/SKILL.md +1195 -0
  137. package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
  138. package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
  139. package/skills/effect-patterns-resource-management/SKILL.md +827 -0
  140. package/skills/effect-patterns-scheduling/SKILL.md +451 -0
  141. package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
  142. package/skills/effect-patterns-streams/SKILL.md +2052 -0
  143. package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
  144. package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
  145. package/skills/effect-patterns-testing/SKILL.md +1632 -0
  146. package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
  147. package/skills/effect-patterns-value-handling/SKILL.md +676 -0
  148. package/tsconfig.json +20 -0
  149. package/vitest.config.ts +3 -0
@@ -0,0 +1,2393 @@
1
+ ---
2
+ name: effect-patterns-building-apis
3
+ description: Effect-TS patterns for Building Apis. Use when working with building apis in Effect-TS applications.
4
+ ---
5
+ # Effect-TS Patterns: Building Apis
6
+ This skill provides 13 curated Effect-TS patterns for building apis.
7
+ Use this skill when working on tasks related to:
8
+ - building apis
9
+ - Best practices in Effect-TS applications
10
+ - Real-world patterns and solutions
11
+
12
+ ---
13
+
14
+ ## 🟢 Beginner Patterns
15
+
16
+ ### Handle a GET Request
17
+
18
+ **Rule:** Use Http.router.get to associate a URL path with a specific response Effect.
19
+
20
+ **Good Example:**
21
+
22
+ This example defines two separate GET routes, one for the root path (`/`) and one for `/hello`. We create an empty router and add each route to it. The resulting `app` is then served. The router automatically handles sending a `404 Not Found` response for any path that doesn't match.
23
+
24
+ ```typescript
25
+ import { Data, Effect } from "effect";
26
+
27
+ // Define response types
28
+ interface RouteResponse {
29
+ readonly status: number;
30
+ readonly body: string;
31
+ }
32
+
33
+ // Define error types
34
+ class RouteNotFoundError extends Data.TaggedError("RouteNotFoundError")<{
35
+ readonly path: string;
36
+ }> {}
37
+
38
+ class RouteHandlerError extends Data.TaggedError("RouteHandlerError")<{
39
+ readonly path: string;
40
+ readonly error: string;
41
+ }> {}
42
+
43
+ // Define route service
44
+ class RouteService extends Effect.Service<RouteService>()("RouteService", {
45
+ sync: () => {
46
+ // Create instance methods
47
+ const handleRoute = (
48
+ path: string
49
+ ): Effect.Effect<RouteResponse, RouteNotFoundError | RouteHandlerError> =>
50
+ Effect.gen(function* () {
51
+ yield* Effect.logInfo(`Processing request for path: ${path}`);
52
+
53
+ try {
54
+ switch (path) {
55
+ case "/":
56
+ const home = "Welcome to the home page!";
57
+ yield* Effect.logInfo(`Serving home page`);
58
+ return { status: 200, body: home };
59
+
60
+ case "/hello":
61
+ const hello = "Hello, Effect!";
62
+ yield* Effect.logInfo(`Serving hello page`);
63
+ return { status: 200, body: hello };
64
+
65
+ default:
66
+ yield* Effect.logWarning(`Route not found: ${path}`);
67
+ return yield* Effect.fail(new RouteNotFoundError({ path }));
68
+ }
69
+ } catch (e) {
70
+ const error = e instanceof Error ? e.message : String(e);
71
+ yield* Effect.logError(`Error handling route ${path}: ${error}`);
72
+ return yield* Effect.fail(new RouteHandlerError({ path, error }));
73
+ }
74
+ });
75
+
76
+ // Return service implementation
77
+ return {
78
+ handleRoute,
79
+ // Simulate GET request
80
+ simulateGet: (
81
+ path: string
82
+ ): Effect.Effect<RouteResponse, RouteNotFoundError | RouteHandlerError> =>
83
+ Effect.gen(function* () {
84
+ yield* Effect.logInfo(`GET ${path}`);
85
+ const response = yield* handleRoute(path);
86
+ yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`);
87
+ return response;
88
+ }),
89
+ };
90
+ },
91
+ }) {}
92
+
93
+ // Create program with proper error handling
94
+ const program = Effect.gen(function* () {
95
+ const router = yield* RouteService;
96
+
97
+ yield* Effect.logInfo("=== Starting Route Tests ===");
98
+
99
+ // Test different routes
100
+ for (const path of ["/", "/hello", "/other", "/error"]) {
101
+ yield* Effect.logInfo(`\n--- Testing ${path} ---`);
102
+
103
+ const result = yield* router.simulateGet(path).pipe(
104
+ Effect.catchTags({
105
+ RouteNotFoundError: (error) =>
106
+ Effect.gen(function* () {
107
+ const response = { status: 404, body: `Not Found: ${error.path}` };
108
+ yield* Effect.logWarning(`${response.status} ${response.body}`);
109
+ return response;
110
+ }),
111
+ RouteHandlerError: (error) =>
112
+ Effect.gen(function* () {
113
+ const response = {
114
+ status: 500,
115
+ body: `Internal Error: ${error.error}`,
116
+ };
117
+ yield* Effect.logError(`${response.status} ${response.body}`);
118
+ return response;
119
+ }),
120
+ })
121
+ );
122
+
123
+ yield* Effect.logInfo(`Final Response: ${JSON.stringify(result)}`);
124
+ }
125
+
126
+ yield* Effect.logInfo("\n=== Route Tests Complete ===");
127
+ });
128
+
129
+ // Run the program
130
+ Effect.runPromise(Effect.provide(program, RouteService.Default));
131
+ ```
132
+
133
+ **Anti-Pattern:**
134
+
135
+ The anti-pattern is to create a single, monolithic handler that uses conditional logic to inspect the request URL. This imperative approach is difficult to maintain and scale.
136
+
137
+ ```typescript
138
+ import { Effect } from "effect";
139
+ import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
140
+
141
+ // A single app that manually checks the URL
142
+ const app = Http.request.ServerRequest.pipe(
143
+ Effect.flatMap((req) => {
144
+ if (req.url === "/") {
145
+ return Effect.succeed(Http.response.text("Welcome to the home page!"));
146
+ } else if (req.url === "/hello") {
147
+ return Effect.succeed(Http.response.text("Hello, Effect!"));
148
+ } else {
149
+ return Effect.succeed(Http.response.empty({ status: 404 }));
150
+ }
151
+ })
152
+ );
153
+
154
+ const program = Http.server
155
+ .serve(app)
156
+ .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
157
+
158
+ NodeRuntime.runMain(program);
159
+ ```
160
+
161
+ This manual routing logic is verbose, error-prone (a typo in a string breaks the route), and mixes the "what" (the response) with the "where" (the routing). It doesn't scale to handle different HTTP methods, path parameters, or middleware gracefully. The `Http.router` is designed to solve all of these problems elegantly.
162
+
163
+ **Rationale:**
164
+
165
+ To handle specific URL paths, create individual routes using `Http.router` functions (like `Http.router.get`) and combine them into a single `Http.App`.
166
+
167
+ ---
168
+
169
+
170
+ A real application needs to respond differently to different URLs. The `Http.router` provides a declarative, type-safe, and composable way to manage this routing logic. Instead of a single handler with complex conditional logic, you define many small, focused handlers and assign them to specific paths and HTTP methods.
171
+
172
+ This approach has several advantages:
173
+
174
+ 1. **Declarative and Readable**: Your code clearly expresses the mapping between a URL path and its behavior, making the application's structure easy to understand.
175
+ 2. **Composability**: Routers are just values that can be created, combined, and passed around. This makes it easy to organize routes into logical groups (e.g., a `userRoutes` router and a `productRoutes` router) and merge them.
176
+ 3. **Type Safety**: The router ensures that the handler for a route is only ever called for a matching request, simplifying the logic within the handler itself.
177
+ 4. **Integration**: Each route handler is an `Effect`, meaning it has full access to dependency injection, structured concurrency, and integrated error handling, just like any other part of an Effect application.
178
+
179
+ ---
180
+
181
+ ---
182
+
183
+ ### Send a JSON Response
184
+
185
+ **Rule:** Use Http.response.json to automatically serialize data structures into a JSON response.
186
+
187
+ **Good Example:**
188
+
189
+ This example defines a route that fetches a user object and returns it as a JSON response. The `Http.response.json` function handles all the necessary serialization and header configuration.
190
+
191
+ ```typescript
192
+ import { Effect, Context, Duration, Layer } from "effect";
193
+ import { NodeContext, NodeHttpServer } from "@effect/platform-node";
194
+ import { createServer } from "node:http";
195
+
196
+ const PORT = 3459; // Changed port to avoid conflicts
197
+
198
+ // Define HTTP Server service
199
+ class JsonServer extends Effect.Service<JsonServer>()("JsonServer", {
200
+ sync: () => ({
201
+ handleRequest: () =>
202
+ Effect.succeed({
203
+ status: 200,
204
+ headers: { "Content-Type": "application/json" },
205
+ body: JSON.stringify({
206
+ message: "Hello, JSON!",
207
+ timestamp: new Date().toISOString(),
208
+ }),
209
+ }),
210
+ }),
211
+ }) {}
212
+
213
+ // Create and run the server
214
+ const program = Effect.gen(function* () {
215
+ const jsonServer = yield* JsonServer;
216
+
217
+ // Create and start HTTP server
218
+ const server = createServer((req, res) => {
219
+ const requestHandler = Effect.gen(function* () {
220
+ try {
221
+ const response = yield* jsonServer.handleRequest();
222
+ res.writeHead(response.status, response.headers);
223
+ res.end(response.body);
224
+ // Log the response for demonstration
225
+ yield* Effect.logInfo(`Sent JSON response: ${response.body}`);
226
+ } catch (error: any) {
227
+ res.writeHead(500, { "Content-Type": "application/json" });
228
+ res.end(JSON.stringify({ error: "Internal Server Error" }));
229
+ yield* Effect.logError(`Request error: ${error.message}`);
230
+ }
231
+ });
232
+
233
+ Effect.runPromise(requestHandler);
234
+ });
235
+
236
+ // Start server with error handling
237
+ yield* Effect.async<void, Error>((resume) => {
238
+ server.on("error", (error: NodeJS.ErrnoException) => {
239
+ if (error.code === "EADDRINUSE") {
240
+ resume(Effect.fail(new Error(`Port ${PORT} is already in use`)));
241
+ } else {
242
+ resume(Effect.fail(error));
243
+ }
244
+ });
245
+
246
+ server.listen(PORT, () => {
247
+ resume(Effect.succeed(void 0));
248
+ });
249
+ });
250
+
251
+ yield* Effect.logInfo(`Server running at http://localhost:${PORT}`);
252
+ yield* Effect.logInfo("Try: curl http://localhost:3459");
253
+
254
+ // Run for a short time to demonstrate
255
+ yield* Effect.sleep(Duration.seconds(3));
256
+
257
+ // Shutdown gracefully
258
+ yield* Effect.sync(() => server.close());
259
+ yield* Effect.logInfo("Server shutdown complete");
260
+ }).pipe(
261
+ Effect.catchAll((error) =>
262
+ Effect.gen(function* () {
263
+ yield* Effect.logError(`Server error: ${error.message}`);
264
+ return error;
265
+ })
266
+ ),
267
+ // Merge layers and provide them in a single call to ensure proper lifecycle management
268
+ Effect.provide(Layer.merge(JsonServer.Default, NodeContext.layer))
269
+ );
270
+
271
+ // Run the program
272
+ // Use Effect.runFork for server applications that shouldn't resolve the promise
273
+ Effect.runPromise(
274
+ program.pipe(
275
+ // Ensure the Effect has no remaining context requirements for runPromise
276
+ Effect.map(() => undefined)
277
+ )
278
+ );
279
+ ```
280
+
281
+ **Anti-Pattern:**
282
+
283
+ The anti-pattern is to manually serialize the data to a string and set the headers yourself. This is verbose and introduces opportunities for error.
284
+
285
+ ```typescript
286
+ import { Effect } from "effect";
287
+ import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
288
+
289
+ const getUserRoute = Http.router.get(
290
+ "/users/1",
291
+ Effect.succeed({ id: 1, name: "Paul", team: "Effect" }).pipe(
292
+ Effect.flatMap((user) => {
293
+ // Manually serialize the object to a JSON string.
294
+ const jsonString = JSON.stringify(user);
295
+ // Create a text response with the string.
296
+ const response = Http.response.text(jsonString);
297
+ // Manually set the Content-Type header.
298
+ return Effect.succeed(
299
+ Http.response.setHeader(
300
+ response,
301
+ "Content-Type",
302
+ "application/json; charset=utf-8"
303
+ )
304
+ );
305
+ })
306
+ )
307
+ );
308
+
309
+ const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));
310
+
311
+ const program = Http.server
312
+ .serve(app)
313
+ .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
314
+
315
+ NodeRuntime.runMain(program);
316
+ ```
317
+
318
+ This manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the `setHeader` call, many clients will fail to parse the response correctly. The `Http.response.json` helper eliminates this entire class of potential bugs.
319
+
320
+ **Rationale:**
321
+
322
+ To return a JavaScript object or value as a JSON response, use the `Http.response.json(data)` constructor.
323
+
324
+ ---
325
+
326
+
327
+ APIs predominantly communicate using JSON. The `Http` module provides a dedicated `Http.response.json` helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone.
328
+
329
+ Using `Http.response.json` is superior because:
330
+
331
+ 1. **Automatic Serialization**: It safely handles the `JSON.stringify` operation for you, including handling potential circular references or other serialization errors.
332
+ 2. **Correct Headers**: It automatically sets the `Content-Type: application/json; charset=utf-8` header. This is critical for clients to correctly interpret the response body. Forgetting this header is a common source of bugs in manually constructed APIs.
333
+ 3. **Simplicity and Readability**: Your intent is made clear with a single, declarative function call. The code is cleaner and focuses on the data being sent, not the mechanics of HTTP.
334
+ 4. **Composability**: It creates a standard `Http.response` object that works seamlessly with all other parts of the Effect `Http` module.
335
+
336
+ ---
337
+
338
+ ---
339
+
340
+ ### Extract Path Parameters
341
+
342
+ **Rule:** Define routes with colon-prefixed parameters (e.g., /users/:id) and access their values within the handler.
343
+
344
+ **Good Example:**
345
+
346
+ This example defines a route that captures a `userId`. The handler for this route accesses the parsed parameters and uses the `userId` to construct a personalized greeting. The router automatically makes the parameters available to the handler.
347
+
348
+ ```typescript
349
+ import { Data, Effect } from "effect";
350
+
351
+ // Define tagged error for invalid paths
352
+ interface InvalidPathErrorSchema {
353
+ readonly _tag: "InvalidPathError";
354
+ readonly path: string;
355
+ }
356
+
357
+ const makeInvalidPathError = (path: string): InvalidPathErrorSchema => ({
358
+ _tag: "InvalidPathError",
359
+ path,
360
+ });
361
+
362
+ // Define service interface
363
+ interface PathOps {
364
+ readonly extractUserId: (
365
+ path: string
366
+ ) => Effect.Effect<string, InvalidPathErrorSchema>;
367
+ readonly greetUser: (userId: string) => Effect.Effect<string>;
368
+ }
369
+
370
+ // Create service
371
+ class PathService extends Effect.Service<PathService>()("PathService", {
372
+ sync: () => ({
373
+ extractUserId: (path: string) =>
374
+ Effect.gen(function* () {
375
+ yield* Effect.logInfo(
376
+ `Attempting to extract user ID from path: ${path}`
377
+ );
378
+
379
+ const match = path.match(/\/users\/([^/]+)/);
380
+ if (!match) {
381
+ yield* Effect.logInfo(`No user ID found in path: ${path}`);
382
+ return yield* Effect.fail(makeInvalidPathError(path));
383
+ }
384
+
385
+ const userId = match[1];
386
+ yield* Effect.logInfo(`Successfully extracted user ID: ${userId}`);
387
+ return userId;
388
+ }),
389
+
390
+ greetUser: (userId: string) =>
391
+ Effect.gen(function* () {
392
+ const greeting = `Hello, user ${userId}!​`;
393
+ yield* Effect.logInfo(greeting);
394
+ return greeting;
395
+ }),
396
+ }),
397
+ }) {}
398
+
399
+ // Compose the functions with proper error handling
400
+ const processPath = (
401
+ path: string
402
+ ): Effect.Effect<string, InvalidPathErrorSchema, PathService> =>
403
+ Effect.gen(function* () {
404
+ const pathService = yield* PathService;
405
+ yield* Effect.logInfo(`Processing path: ${path}`);
406
+ const userId = yield* pathService.extractUserId(path);
407
+ return yield* pathService.greetUser(userId);
408
+ });
409
+
410
+ // Run examples with proper error handling
411
+ const program = Effect.gen(function* () {
412
+ // Test valid paths
413
+ yield* Effect.logInfo("=== Testing valid paths ===");
414
+ const result1 = yield* processPath("/users/123");
415
+ yield* Effect.logInfo(`Result 1: ${result1}`);
416
+
417
+ const result2 = yield* processPath("/users/abc");
418
+ yield* Effect.logInfo(`Result 2: ${result2}`);
419
+
420
+ // Test invalid path
421
+ yield* Effect.logInfo("\n=== Testing invalid path ===");
422
+ const result3 = yield* processPath("/invalid/path").pipe(
423
+ Effect.catchTag("InvalidPathError", (error) =>
424
+ Effect.succeed(`Error: Invalid path ${error.path}`)
425
+ )
426
+ );
427
+ yield* Effect.logInfo(result3);
428
+ });
429
+
430
+ Effect.runPromise(Effect.provide(program, PathService.Default));
431
+ ```
432
+
433
+ **Anti-Pattern:**
434
+
435
+ The anti-pattern is to manually parse the URL string inside the handler. This approach is brittle, imperative, and mixes concerns.
436
+
437
+ ```typescript
438
+ import { Effect } from "effect";
439
+ import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
440
+
441
+ // This route matches any sub-path of /users/, forcing manual parsing.
442
+ const app = Http.router.get(
443
+ "/users/*", // Using a wildcard
444
+ Http.request.ServerRequest.pipe(
445
+ Effect.flatMap((req) => {
446
+ // Manually split the URL to find the ID.
447
+ const parts = req.url.split("/"); // e.g., ['', 'users', '123']
448
+ if (parts.length === 3 && parts[2]) {
449
+ const userId = parts[2];
450
+ return Http.response.text(`Hello, user ${userId}!​`);
451
+ }
452
+ // Manual handling for missing ID.
453
+ return Http.response.empty({ status: 404 });
454
+ })
455
+ )
456
+ );
457
+
458
+ const program = Http.server
459
+ .serve(app)
460
+ .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
461
+
462
+ NodeRuntime.runMain(program);
463
+ ```
464
+
465
+ This manual method is highly discouraged. It's fragile—a change in the base path or an extra slash could break the logic (`parts[2]`). It's also not declarative; the intent is hidden inside imperative code. The router's built-in parameter handling is safer, clearer, and the correct approach.
466
+
467
+ **Rationale:**
468
+
469
+ To capture dynamic parts of a URL, define your route path with a colon-prefixed placeholder (e.g., `/users/:userId`) and access the parsed parameters within your handler `Effect`.
470
+
471
+ ---
472
+
473
+
474
+ APIs often need to operate on specific resources identified by a unique key in the URL, such as `/products/123` or `/orders/abc`. The `Http.router` provides a clean, declarative way to handle these dynamic paths without resorting to manual string parsing.
475
+
476
+ By defining parameters directly in the path string, you gain several benefits:
477
+
478
+ 1. **Declarative**: The route's structure is immediately obvious from its definition. The code clearly states, "this route expects a dynamic segment here."
479
+ 2. **Safe and Robust**: The router handles the logic of extracting the parameter. This is less error-prone and more robust than manually splitting or using regular expressions on the URL string.
480
+ 3. **Clean Handler Logic**: The business logic inside your handler is separated from the concern of URL parsing. The handler simply receives the parameters it needs to do its job.
481
+ 4. **Composability**: This pattern composes perfectly with the rest of the `Http` module, allowing you to build complex and well-structured APIs.
482
+
483
+ ---
484
+
485
+ ---
486
+
487
+ ### Create a Basic HTTP Server
488
+
489
+ **Rule:** Use Http.server.serve with a platform-specific layer to run an HTTP application.
490
+
491
+ **Good Example:**
492
+
493
+ This example creates a minimal server that responds to all requests with "Hello, World!". The application logic is a simple `Effect` that returns an `Http.response`. We use `NodeRuntime.runMain` to execute the server effect, which is the standard way to launch a long-running application.
494
+
495
+ ```typescript
496
+ import { Effect, Duration } from "effect";
497
+ import * as http from "http";
498
+
499
+ // Create HTTP server service
500
+ class HttpServer extends Effect.Service<HttpServer>()("HttpServer", {
501
+ sync: () => ({
502
+ start: () =>
503
+ Effect.gen(function* () {
504
+ const server = http.createServer(
505
+ (req: http.IncomingMessage, res: http.ServerResponse) => {
506
+ res.writeHead(200, { "Content-Type": "text/plain" });
507
+ res.end("Hello, World!");
508
+ }
509
+ );
510
+
511
+ // Add cleanup finalizer
512
+ yield* Effect.addFinalizer(() =>
513
+ Effect.gen(function* () {
514
+ yield* Effect.sync(() => server.close());
515
+ yield* Effect.logInfo("Server shut down");
516
+ })
517
+ );
518
+
519
+ // Start server with timeout
520
+ yield* Effect.async<void, Error>((resume) => {
521
+ server.on("error", (error) => resume(Effect.fail(error)));
522
+ server.listen(3456, "localhost", () => {
523
+ resume(Effect.succeed(void 0));
524
+ });
525
+ }).pipe(
526
+ Effect.timeout(Duration.seconds(5)),
527
+ Effect.catchAll((error) =>
528
+ Effect.gen(function* () {
529
+ yield* Effect.logError(`Failed to start server: ${error}`);
530
+ return yield* Effect.fail(error);
531
+ })
532
+ )
533
+ );
534
+
535
+ yield* Effect.logInfo("Server running at http://localhost:3456/");
536
+
537
+ // Run for a short duration to demonstrate the server is working
538
+ yield* Effect.sleep(Duration.seconds(3));
539
+ yield* Effect.logInfo("Server demonstration complete");
540
+ }),
541
+ }),
542
+ }) {}
543
+
544
+ // Create program with proper error handling
545
+ const program = Effect.gen(function* () {
546
+ const server = yield* HttpServer;
547
+
548
+ yield* Effect.logInfo("Starting HTTP server...");
549
+
550
+ yield* server.start();
551
+ }).pipe(
552
+ Effect.scoped // Ensure server is cleaned up properly
553
+ );
554
+
555
+ // Run the server with proper error handling
556
+ const programWithErrorHandling = Effect.provide(
557
+ program,
558
+ HttpServer.Default
559
+ ).pipe(
560
+ Effect.catchAll((error) =>
561
+ Effect.gen(function* () {
562
+ yield* Effect.logError(`Program failed: ${error}`);
563
+ return yield* Effect.fail(error);
564
+ })
565
+ )
566
+ );
567
+
568
+ Effect.runPromise(programWithErrorHandling).catch(() => {
569
+ process.exit(1);
570
+ });
571
+
572
+ /*
573
+ To test:
574
+ 1. Server will timeout after 5 seconds if it can't start
575
+ 2. Server runs on port 3456 to avoid conflicts
576
+ 3. Proper cleanup on shutdown
577
+ 4. Demonstrates server lifecycle: start -> run -> shutdown
578
+ */
579
+ ```
580
+
581
+ **Anti-Pattern:**
582
+
583
+ The common anti-pattern is to use the raw Node.js `http` module directly, outside of the Effect runtime. This approach creates a disconnect between your application logic and the server's lifecycle.
584
+
585
+ ```typescript
586
+ import * as http from "http";
587
+
588
+ // Manually create a server using the Node.js built-in module.
589
+ const server = http.createServer((req, res) => {
590
+ res.writeHead(200, { "Content-Type": "text/plain" });
591
+ res.end("Hello, World!");
592
+ });
593
+
594
+ // Manually start the server and log the port.
595
+ const port = 3000;
596
+ server.listen(port, () => {
597
+ console.log(`Server running at http://localhost:${port}/`);
598
+ });
599
+ ```
600
+
601
+ This imperative approach is discouraged when building an Effect application because it forfeits all the benefits of the ecosystem. It runs outside of Effect's structured concurrency, cannot be managed by its resource-safe `Scope`, does not integrate with `Layer` for dependency injection, and requires manual error handling, making it less robust and much harder to compose with other effectful logic.
602
+
603
+ **Rationale:**
604
+
605
+ To create and run a web server, define your application as an `Http.App` and execute it using `Http.server.serve`, providing a platform-specific layer like `NodeHttpServer.layer`.
606
+
607
+ ---
608
+
609
+
610
+ In Effect, an HTTP server is not just a side effect; it's a managed, effectful process. The `@effect/platform` package provides a platform-agnostic API for defining HTTP applications, while packages like `@effect/platform-node` provide the concrete implementation.
611
+
612
+ The core function `Http.server.serve(app)` takes your application logic and returns an `Effect` that, when run, starts the server. This `Effect` is designed to run indefinitely, only terminating if the server crashes or is gracefully shut down.
613
+
614
+ This approach provides several key benefits:
615
+
616
+ 1. **Lifecycle Management**: The server's lifecycle is managed by the Effect runtime. This means structured concurrency applies, ensuring graceful shutdowns and proper resource handling automatically.
617
+ 2. **Integration**: The server is a first-class citizen in the Effect ecosystem. It can seamlessly access dependencies provided by `Layer`, use `Config` for configuration, and integrate with `Logger`.
618
+ 3. **Platform Agnosticism**: By coding to the `Http.App` interface, your application logic remains portable across different JavaScript runtimes (Node.js, Bun, Deno) by simply swapping out the platform layer.
619
+
620
+ ---
621
+
622
+ ---
623
+
624
+
625
+ ## 🟡 Intermediate Patterns
626
+
627
+ ### Add Rate Limiting to APIs
628
+
629
+ **Rule:** Use a rate limiter service to enforce request quotas per client.
630
+
631
+ **Good Example:**
632
+
633
+ ```typescript
634
+ import { Effect, Context, Layer, Ref, HashMap, Data, Duration } from "effect"
635
+ import { HttpServerRequest, HttpServerResponse } from "@effect/platform"
636
+
637
+ // ============================================
638
+ // 1. Define rate limit types
639
+ // ============================================
640
+
641
+ interface RateLimitConfig {
642
+ readonly maxRequests: number
643
+ readonly windowMs: number
644
+ }
645
+
646
+ interface RateLimitState {
647
+ readonly count: number
648
+ readonly resetAt: number
649
+ }
650
+
651
+ class RateLimitExceededError extends Data.TaggedError("RateLimitExceededError")<{
652
+ readonly retryAfter: number
653
+ readonly limit: number
654
+ }> {}
655
+
656
+ // ============================================
657
+ // 2. Rate limiter service
658
+ // ============================================
659
+
660
+ interface RateLimiter {
661
+ readonly check: (key: string) => Effect.Effect<void, RateLimitExceededError>
662
+ readonly getStatus: (key: string) => Effect.Effect<{
663
+ remaining: number
664
+ resetAt: number
665
+ }>
666
+ }
667
+
668
+ class RateLimiterService extends Context.Tag("RateLimiter")<
669
+ RateLimiterService,
670
+ RateLimiter
671
+ >() {}
672
+
673
+ // ============================================
674
+ // 3. In-memory rate limiter implementation
675
+ // ============================================
676
+
677
+ const makeRateLimiter = (config: RateLimitConfig) =>
678
+ Effect.gen(function* () {
679
+ const state = yield* Ref.make(HashMap.empty<string, RateLimitState>())
680
+
681
+ const getOrCreateState = (key: string, now: number) =>
682
+ Ref.modify(state, (map) => {
683
+ const existing = HashMap.get(map, key)
684
+
685
+ if (existing._tag === "Some") {
686
+ // Check if window expired
687
+ if (now >= existing.value.resetAt) {
688
+ // Start new window
689
+ const newState: RateLimitState = {
690
+ count: 0,
691
+ resetAt: now + config.windowMs,
692
+ }
693
+ return [newState, HashMap.set(map, key, newState)]
694
+ }
695
+ return [existing.value, map]
696
+ }
697
+
698
+ // Create new entry
699
+ const newState: RateLimitState = {
700
+ count: 0,
701
+ resetAt: now + config.windowMs,
702
+ }
703
+ return [newState, HashMap.set(map, key, newState)]
704
+ })
705
+
706
+ const incrementCount = (key: string) =>
707
+ Ref.modify(state, (map) => {
708
+ const existing = HashMap.get(map, key)
709
+ if (existing._tag === "Some") {
710
+ const updated = { ...existing.value, count: existing.value.count + 1 }
711
+ return [updated.count, HashMap.set(map, key, updated)]
712
+ }
713
+ return [1, map]
714
+ })
715
+
716
+ const limiter: RateLimiter = {
717
+ check: (key) =>
718
+ Effect.gen(function* () {
719
+ const now = Date.now()
720
+ const currentState = yield* getOrCreateState(key, now)
721
+
722
+ if (currentState.count >= config.maxRequests) {
723
+ const retryAfter = Math.ceil((currentState.resetAt - now) / 1000)
724
+ return yield* Effect.fail(
725
+ new RateLimitExceededError({
726
+ retryAfter,
727
+ limit: config.maxRequests,
728
+ })
729
+ )
730
+ }
731
+
732
+ yield* incrementCount(key)
733
+ }),
734
+
735
+ getStatus: (key) =>
736
+ Effect.gen(function* () {
737
+ const now = Date.now()
738
+ const currentState = yield* getOrCreateState(key, now)
739
+ return {
740
+ remaining: Math.max(0, config.maxRequests - currentState.count),
741
+ resetAt: currentState.resetAt,
742
+ }
743
+ }),
744
+ }
745
+
746
+ return limiter
747
+ })
748
+
749
+ // ============================================
750
+ // 4. Rate limit middleware
751
+ // ============================================
752
+
753
+ const withRateLimit = <A, E, R>(
754
+ handler: Effect.Effect<A, E, R>
755
+ ): Effect.Effect<
756
+ A | HttpServerResponse.HttpServerResponse,
757
+ E,
758
+ R | RateLimiterService | HttpServerRequest.HttpServerRequest
759
+ > =>
760
+ Effect.gen(function* () {
761
+ const request = yield* HttpServerRequest.HttpServerRequest
762
+ const rateLimiter = yield* RateLimiterService
763
+
764
+ // Use IP address as key (in production, might use user ID or API key)
765
+ const clientKey = request.headers["x-forwarded-for"] || "unknown"
766
+
767
+ const result = yield* rateLimiter.check(clientKey).pipe(
768
+ Effect.matchEffect({
769
+ onFailure: (error) =>
770
+ Effect.succeed(
771
+ HttpServerResponse.json(
772
+ {
773
+ error: "Rate limit exceeded",
774
+ retryAfter: error.retryAfter,
775
+ },
776
+ {
777
+ status: 429,
778
+ headers: {
779
+ "Retry-After": String(error.retryAfter),
780
+ "X-RateLimit-Limit": String(error.limit),
781
+ "X-RateLimit-Remaining": "0",
782
+ },
783
+ }
784
+ )
785
+ ),
786
+ onSuccess: () => handler,
787
+ })
788
+ )
789
+
790
+ return result
791
+ })
792
+
793
+ // ============================================
794
+ // 5. Usage example
795
+ // ============================================
796
+
797
+ const RateLimiterLive = Layer.effect(
798
+ RateLimiterService,
799
+ makeRateLimiter({
800
+ maxRequests: 100, // 100 requests
801
+ windowMs: 60 * 1000, // per minute
802
+ })
803
+ )
804
+
805
+ const apiEndpoint = withRateLimit(
806
+ Effect.gen(function* () {
807
+ // Your actual handler logic
808
+ return HttpServerResponse.json({ data: "Success!" })
809
+ })
810
+ )
811
+ ```
812
+
813
+ **Rationale:**
814
+
815
+ Implement rate limiting as a service that tracks request counts and enforces limits per client (IP, API key, or user).
816
+
817
+ ---
818
+
819
+
820
+ Rate limiting protects your API:
821
+
822
+ 1. **Prevent abuse** - Stop malicious flooding
823
+ 2. **Fair usage** - Share resources among clients
824
+ 3. **Cost control** - Limit expensive operations
825
+ 4. **Stability** - Prevent cascading failures
826
+
827
+ ---
828
+
829
+ ---
830
+
831
+ ### Validate Request Body
832
+
833
+ **Rule:** Use Http.request.schemaBodyJson with a Schema to automatically parse and validate request bodies.
834
+
835
+ **Good Example:**
836
+
837
+ This example defines a `POST` route to create a user. It uses a `CreateUser` schema to validate the request body. If validation passes, it returns a success message with the typed data. If it fails, the platform automatically sends a descriptive 400 error.
838
+
839
+ ```typescript
840
+ import { Duration, Effect } from "effect";
841
+ import * as S from "effect/Schema";
842
+ import { createServer, IncomingMessage, ServerResponse } from "http";
843
+
844
+ // Define user schema
845
+ const UserSchema = S.Struct({
846
+ name: S.String,
847
+ email: S.String.pipe(S.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
848
+ });
849
+ type User = S.Schema.Type<typeof UserSchema>;
850
+
851
+ // Define user service interface
852
+ interface UserServiceInterface {
853
+ readonly validateUser: (data: unknown) => Effect.Effect<User, Error, never>;
854
+ }
855
+
856
+ // Define user service
857
+ class UserService extends Effect.Service<UserService>()("UserService", {
858
+ sync: () => ({
859
+ validateUser: (data: unknown) => S.decodeUnknown(UserSchema)(data),
860
+ }),
861
+ }) {}
862
+
863
+ // Define HTTP server service interface
864
+ interface HttpServerInterface {
865
+ readonly handleRequest: (
866
+ request: IncomingMessage,
867
+ response: ServerResponse
868
+ ) => Effect.Effect<void, Error, never>;
869
+ readonly start: () => Effect.Effect<void, Error, never>;
870
+ }
871
+
872
+ // Define HTTP server service
873
+ class HttpServer extends Effect.Service<HttpServer>()("HttpServer", {
874
+ // Define effect-based implementation that uses dependencies
875
+ effect: Effect.gen(function* () {
876
+ const userService = yield* UserService;
877
+
878
+ return {
879
+ handleRequest: (request: IncomingMessage, response: ServerResponse) =>
880
+ Effect.gen(function* () {
881
+ // Only handle POST /users
882
+ if (request.method !== "POST" || request.url !== "/users") {
883
+ response.writeHead(404, { "Content-Type": "application/json" });
884
+ response.end(JSON.stringify({ error: "Not Found" }));
885
+ return;
886
+ }
887
+
888
+ try {
889
+ // Read request body
890
+ const body = yield* Effect.async<unknown, Error>((resume) => {
891
+ let data = "";
892
+ request.on("data", (chunk) => {
893
+ data += chunk;
894
+ });
895
+ request.on("end", () => {
896
+ try {
897
+ resume(Effect.succeed(JSON.parse(data)));
898
+ } catch (e) {
899
+ resume(
900
+ Effect.fail(e instanceof Error ? e : new Error(String(e)))
901
+ );
902
+ }
903
+ });
904
+ request.on("error", (e) =>
905
+ resume(
906
+ Effect.fail(e instanceof Error ? e : new Error(String(e)))
907
+ )
908
+ );
909
+ });
910
+
911
+ // Validate body against schema
912
+ const user = yield* userService.validateUser(body);
913
+
914
+ response.writeHead(200, { "Content-Type": "application/json" });
915
+ response.end(
916
+ JSON.stringify({
917
+ message: `Successfully created user: ${user.name}`,
918
+ })
919
+ );
920
+ } catch (error) {
921
+ response.writeHead(400, { "Content-Type": "application/json" });
922
+ response.end(JSON.stringify({ error: String(error) }));
923
+ }
924
+ }),
925
+
926
+ start: function (this: HttpServer) {
927
+ const self = this;
928
+ return Effect.gen(function* () {
929
+ // Create HTTP server
930
+ const server = createServer((req, res) =>
931
+ Effect.runFork(self.handleRequest(req, res))
932
+ );
933
+
934
+ // Add cleanup finalizer
935
+ yield* Effect.addFinalizer(() =>
936
+ Effect.gen(function* () {
937
+ yield* Effect.sync(() => server.close());
938
+ yield* Effect.logInfo("Server shut down");
939
+ })
940
+ );
941
+
942
+ // Start server
943
+ yield* Effect.async<void, Error>((resume) => {
944
+ server.on("error", (error) => resume(Effect.fail(error)));
945
+ server.listen(3456, () => {
946
+ Effect.runFork(
947
+ Effect.logInfo("Server running at http://localhost:3456/")
948
+ );
949
+ resume(Effect.succeed(void 0));
950
+ });
951
+ });
952
+
953
+ // Run for demonstration period
954
+ yield* Effect.sleep(Duration.seconds(3));
955
+ yield* Effect.logInfo("Demo completed - shutting down server");
956
+ });
957
+ },
958
+ };
959
+ }),
960
+ // Specify dependencies
961
+ dependencies: [UserService.Default],
962
+ }) {}
963
+
964
+ // Create program with proper error handling
965
+ const program = Effect.gen(function* () {
966
+ const server = yield* HttpServer;
967
+
968
+ yield* Effect.logInfo("Starting HTTP server...");
969
+
970
+ yield* server.start().pipe(
971
+ Effect.catchAll((error) =>
972
+ Effect.gen(function* () {
973
+ yield* Effect.logError(`Server error: ${error}`);
974
+ return yield* Effect.fail(error);
975
+ })
976
+ )
977
+ );
978
+ }).pipe(
979
+ Effect.scoped // Ensure server is cleaned up
980
+ );
981
+
982
+ // Run the server
983
+ Effect.runFork(Effect.provide(program, HttpServer.Default));
984
+
985
+ /*
986
+ To test:
987
+ - POST http://localhost:3456/users with body {"name": "Paul", "email": "paul@effect.com"}
988
+ -> Returns 200 OK with message "Successfully created user: Paul"
989
+
990
+ - POST http://localhost:3456/users with body {"name": "Paul"}
991
+ -> Returns 400 Bad Request with error message about missing email field
992
+ */
993
+ ```
994
+
995
+ **Anti-Pattern:**
996
+
997
+ The anti-pattern is to manually parse the JSON and then write imperative validation checks. This approach is verbose, error-prone, and not type-safe.
998
+
999
+ ```typescript
1000
+ import { Effect } from "effect";
1001
+ import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
1002
+
1003
+ const createUserRoute = Http.router.post(
1004
+ "/users",
1005
+ Http.request.json.pipe(
1006
+ // Http.request.json returns Effect<unknown, ...>
1007
+ Effect.flatMap((body) => {
1008
+ // Manually check the type and properties of the body.
1009
+ if (
1010
+ typeof body === "object" &&
1011
+ body !== null &&
1012
+ "name" in body &&
1013
+ typeof body.name === "string" &&
1014
+ "email" in body &&
1015
+ typeof body.email === "string"
1016
+ ) {
1017
+ // The type is still not safely inferred here without casting.
1018
+ return Http.response.text(`Successfully created user: ${body.name}`);
1019
+ } else {
1020
+ // Manually create and return a generic error response.
1021
+ return Http.response.text("Invalid request body", { status: 400 });
1022
+ }
1023
+ })
1024
+ )
1025
+ );
1026
+
1027
+ const app = Http.router.empty.pipe(Http.router.addRoute(createUserRoute));
1028
+
1029
+ const program = Http.server
1030
+ .serve(app)
1031
+ .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
1032
+
1033
+ NodeRuntime.runMain(program);
1034
+ ```
1035
+
1036
+ This manual code is significantly worse. It's hard to read, easy to get wrong, and loses all static type information from the parsed body. Crucially, it forces you to reinvent the wheel for error reporting, which will likely be less detailed and consistent than the automatic responses provided by the platform.
1037
+
1038
+ **Rationale:**
1039
+
1040
+ To process an incoming request body, use `Http.request.schemaBodyJson(YourSchema)` to parse the JSON and validate its structure in a single, type-safe step.
1041
+
1042
+ ---
1043
+
1044
+
1045
+ Accepting user-provided data is one of the most critical and sensitive parts of an API. You must never trust incoming data. The `Http` module's integration with `Schema` provides a robust, declarative solution for this.
1046
+
1047
+ Using `Http.request.schemaBodyJson` offers several major advantages:
1048
+
1049
+ 1. **Automatic Validation and Error Handling**: If the incoming body does not match the schema, the server automatically rejects the request with a `400 Bad Request` status and a detailed JSON response explaining the validation errors. You don't have to write any of this boilerplate logic.
1050
+ 2. **Type Safety**: If the validation succeeds, the value produced by the `Effect` is fully typed according to your `Schema`. This eliminates `any` types and brings static analysis benefits to your request handlers.
1051
+ 3. **Declarative and Clean**: The validation rules are defined once in the `Schema` and then simply applied. This separates the validation logic from your business logic, keeping handlers clean and focused on their core task.
1052
+ 4. **Security**: It acts as a security gateway, ensuring that malformed or unexpected data structures never reach your application's core logic.
1053
+
1054
+ ---
1055
+
1056
+ ---
1057
+
1058
+ ### Provide Dependencies to Routes
1059
+
1060
+ **Rule:** Define dependencies with Effect.Service and provide them to your HTTP server using a Layer.
1061
+
1062
+ **Good Example:**
1063
+
1064
+ This example defines a `Database` service. The route handler for `/users/:userId` requires this service to fetch a user. We then provide a "live" implementation of the `Database` to the entire server using a `Layer`.
1065
+
1066
+ ```typescript
1067
+ import * as HttpRouter from "@effect/platform/HttpRouter";
1068
+ import * as HttpResponse from "@effect/platform/HttpServerResponse";
1069
+ import * as HttpServer from "@effect/platform/HttpServer";
1070
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
1071
+ import { Effect, Duration, Fiber } from "effect/index";
1072
+ import { Data } from "effect";
1073
+
1074
+ // 1. Define the service interface using Effect.Service
1075
+ export class Database extends Effect.Service<Database>()("Database", {
1076
+ sync: () => ({
1077
+ getUser: (id: string) =>
1078
+ id === "123"
1079
+ ? Effect.succeed({ name: "Paul" })
1080
+ : Effect.fail(new UserNotFoundError({ id })),
1081
+ }),
1082
+ }) {}
1083
+
1084
+ class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
1085
+ id: string;
1086
+ }> {}
1087
+
1088
+ // handler producing a `HttpServerResponse`
1089
+ const userHandler = Effect.flatMap(HttpRouter.params, (p) =>
1090
+ Effect.flatMap(Database, (db) => db.getUser(p["userId"] ?? "")).pipe(
1091
+ Effect.flatMap(HttpResponse.json)
1092
+ )
1093
+ );
1094
+
1095
+ // assemble router & server
1096
+ const app = HttpRouter.empty.pipe(
1097
+ HttpRouter.get("/users/:userId", userHandler)
1098
+ );
1099
+
1100
+ // Create the server effect with all dependencies
1101
+ const serverEffect = HttpServer.serveEffect(app).pipe(
1102
+ Effect.provide(Database.Default),
1103
+ Effect.provide(
1104
+ NodeHttpServer.layer(() => require("node:http").createServer(), {
1105
+ port: 3458,
1106
+ })
1107
+ )
1108
+ );
1109
+
1110
+ // Create program that manages server lifecycle
1111
+ const program = Effect.gen(function* () {
1112
+ yield* Effect.logInfo("Starting server on port 3458...");
1113
+
1114
+ const serverFiber = yield* Effect.scoped(serverEffect).pipe(Effect.fork);
1115
+
1116
+ yield* Effect.logInfo("Server started successfully on http://localhost:3458");
1117
+ yield* Effect.logInfo("Try: curl http://localhost:3458/users/123");
1118
+ yield* Effect.logInfo("Try: curl http://localhost:3458/users/456");
1119
+
1120
+ // Run for a short time to demonstrate
1121
+ yield* Effect.sleep(Duration.seconds(3));
1122
+
1123
+ yield* Effect.logInfo("Shutting down server...");
1124
+ yield* Fiber.interrupt(serverFiber);
1125
+ yield* Effect.logInfo("Server shutdown complete");
1126
+ });
1127
+
1128
+ // Run the program
1129
+ NodeRuntime.runMain(program);
1130
+ ```
1131
+
1132
+ **Anti-Pattern:**
1133
+
1134
+ The anti-pattern is to manually instantiate and pass dependencies through function arguments. This creates tight coupling and makes testing difficult.
1135
+
1136
+ ```typescript
1137
+ import { Effect } from "effect";
1138
+ import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
1139
+
1140
+ // Manual implementation of a database client
1141
+ class LiveDatabase {
1142
+ getUser(id: string) {
1143
+ if (id === "123") {
1144
+ return Effect.succeed({ name: "Paul" });
1145
+ }
1146
+ return Effect.fail("User not found"); // Untyped error
1147
+ }
1148
+ }
1149
+
1150
+ // The dependency must be passed explicitly to the route definition
1151
+ const createGetUserRoute = (db: LiveDatabase) =>
1152
+ Http.router.get(
1153
+ "/users/:userId",
1154
+ Effect.flatMap(Http.request.ServerRequest, (req) =>
1155
+ db.getUser(req.params.userId)
1156
+ ).pipe(
1157
+ Effect.map(Http.response.json),
1158
+ Effect.catchAll(() => Http.response.empty({ status: 404 }))
1159
+ )
1160
+ );
1161
+
1162
+ // Manually instantiate the dependency
1163
+ const db = new LiveDatabase();
1164
+ const getUserRoute = createGetUserRoute(db);
1165
+
1166
+ const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));
1167
+
1168
+ const program = Http.server
1169
+ .serve(app)
1170
+ .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
1171
+
1172
+ NodeRuntime.runMain(program);
1173
+ ```
1174
+
1175
+ This approach is flawed because the route handler is now aware of the concrete `LiveDatabase` class. Swapping it for a mock in a test would be cumbersome. Furthermore, if a service deep within the call stack needs a dependency, it must be "drilled" down through every intermediate function, which is a significant maintenance burden.
1176
+
1177
+ **Rationale:**
1178
+
1179
+ Define your application's services using `class MyService extends Effect.Service("MyService")`, provide a live implementation via a `Layer`, and use `Effect.provide` to make the service available to your entire HTTP application.
1180
+
1181
+ ---
1182
+
1183
+
1184
+ As applications grow, route handlers need to perform complex tasks like accessing a database, calling other APIs, or logging. Hard-coding this logic or manually passing dependencies leads to tightly coupled, untestable code.
1185
+
1186
+ Effect's dependency injection system (`Service` and `Layer`) solves this by decoupling a service's interface from its implementation. This is the cornerstone of building scalable, maintainable applications in Effect.
1187
+
1188
+ 1. **Modern and Simple**: `Effect.Service` is the modern, idiomatic way to define services. It combines the service's definition and its access tag into a single, clean class structure, reducing boilerplate.
1189
+ 2. **Testability**: By depending on a service interface, you can easily provide a mock implementation in your tests (e.g., `Database.Test`) instead of the real one (`Database.Live`), allowing for fast, isolated unit tests of your route logic.
1190
+ 3. **Decoupling**: Route handlers don't know or care _how_ the database connection is created or managed. They simply ask for the `Database` service from the context, and the runtime provides the configured implementation.
1191
+ 4. **Composability**: `Layer`s are composable. You can build complex dependency graphs (e.g., a `Database` layer that itself requires a `Config` layer) that Effect will automatically construct and wire up for you.
1192
+
1193
+ ---
1194
+
1195
+ ---
1196
+
1197
+ ### Handle API Errors
1198
+
1199
+ **Rule:** Model application errors as typed classes and use Http.server.serveOptions to map them to specific HTTP responses.
1200
+
1201
+ **Good Example:**
1202
+
1203
+ This example defines two custom error types, `UserNotFoundError` and `InvalidIdError`. The route logic can fail with either. The `unhandledErrorResponse` function inspects the error and returns a `404` or `400` response accordingly, with a generic `500` for any other unexpected errors.
1204
+
1205
+ ```typescript
1206
+ import { Cause, Data, Effect } from "effect";
1207
+
1208
+ // Define our domain types
1209
+ export interface User {
1210
+ readonly id: string;
1211
+ readonly name: string;
1212
+ readonly email: string;
1213
+ readonly role: "admin" | "user";
1214
+ }
1215
+
1216
+ // Define specific, typed errors for our domain
1217
+ export class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
1218
+ readonly id: string;
1219
+ }> {}
1220
+
1221
+ export class InvalidIdError extends Data.TaggedError("InvalidIdError")<{
1222
+ readonly id: string;
1223
+ readonly reason: string;
1224
+ }> {}
1225
+
1226
+ export class UnauthorizedError extends Data.TaggedError("UnauthorizedError")<{
1227
+ readonly action: string;
1228
+ readonly role: string;
1229
+ }> {}
1230
+
1231
+ // Define error handler service
1232
+ export class ErrorHandlerService extends Effect.Service<ErrorHandlerService>()(
1233
+ "ErrorHandlerService",
1234
+ {
1235
+ sync: () => ({
1236
+ // Handle API errors with proper logging
1237
+ handleApiError: <E>(error: E): Effect.Effect<ApiResponse, never, never> =>
1238
+ Effect.gen(function* () {
1239
+ yield* Effect.logError(`API Error: ${JSON.stringify(error)}`);
1240
+
1241
+ if (error instanceof UserNotFoundError) {
1242
+ return {
1243
+ error: "Not Found",
1244
+ message: `User ${error.id} not found`,
1245
+ };
1246
+ }
1247
+ if (error instanceof InvalidIdError) {
1248
+ return { error: "Bad Request", message: error.reason };
1249
+ }
1250
+ if (error instanceof UnauthorizedError) {
1251
+ return {
1252
+ error: "Unauthorized",
1253
+ message: `${error.role} cannot ${error.action}`,
1254
+ };
1255
+ }
1256
+
1257
+ return {
1258
+ error: "Internal Server Error",
1259
+ message: "An unexpected error occurred",
1260
+ };
1261
+ }),
1262
+
1263
+ // Handle unexpected errors
1264
+ handleUnexpectedError: (
1265
+ cause: Cause.Cause<unknown>
1266
+ ): Effect.Effect<void, never, never> =>
1267
+ Effect.gen(function* () {
1268
+ yield* Effect.logError("Unexpected error occurred");
1269
+
1270
+ if (Cause.isDie(cause)) {
1271
+ const defect = Cause.failureOption(cause);
1272
+ if (defect._tag === "Some") {
1273
+ const error = defect.value as Error;
1274
+ yield* Effect.logError(`Defect: ${error.message}`);
1275
+ yield* Effect.logError(
1276
+ `Stack: ${error.stack?.split("\n")[1]?.trim() ?? "N/A"}`
1277
+ );
1278
+ }
1279
+ }
1280
+
1281
+ return Effect.succeed(void 0);
1282
+ }),
1283
+ }),
1284
+ }
1285
+ ) {}
1286
+
1287
+ // Define UserRepository service
1288
+ export class UserRepository extends Effect.Service<UserRepository>()(
1289
+ "UserRepository",
1290
+ {
1291
+ sync: () => {
1292
+ const users = new Map<string, User>([
1293
+ [
1294
+ "user_123",
1295
+ {
1296
+ id: "user_123",
1297
+ name: "Paul",
1298
+ email: "paul@example.com",
1299
+ role: "admin",
1300
+ },
1301
+ ],
1302
+ [
1303
+ "user_456",
1304
+ {
1305
+ id: "user_456",
1306
+ name: "Alice",
1307
+ email: "alice@example.com",
1308
+ role: "user",
1309
+ },
1310
+ ],
1311
+ ]);
1312
+
1313
+ return {
1314
+ // Get user by ID with proper error handling
1315
+ getUser: (
1316
+ id: string
1317
+ ): Effect.Effect<User, UserNotFoundError | InvalidIdError> =>
1318
+ Effect.gen(function* () {
1319
+ yield* Effect.logInfo(`Attempting to get user with id: ${id}`);
1320
+
1321
+ // Validate ID format
1322
+ if (!id.match(/^user_\d+$/)) {
1323
+ yield* Effect.logWarning(`Invalid user ID format: ${id}`);
1324
+ return yield* Effect.fail(
1325
+ new InvalidIdError({
1326
+ id,
1327
+ reason: "ID must be in format user_<number>",
1328
+ })
1329
+ );
1330
+ }
1331
+
1332
+ const user = users.get(id);
1333
+ if (user === undefined) {
1334
+ yield* Effect.logWarning(`User not found with id: ${id}`);
1335
+ return yield* Effect.fail(new UserNotFoundError({ id }));
1336
+ }
1337
+
1338
+ yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
1339
+ return user;
1340
+ }),
1341
+
1342
+ // Check if user has required role
1343
+ checkRole: (
1344
+ user: User,
1345
+ requiredRole: "admin" | "user"
1346
+ ): Effect.Effect<void, UnauthorizedError> =>
1347
+ Effect.gen(function* () {
1348
+ yield* Effect.logInfo(
1349
+ `Checking if user ${user.id} has role: ${requiredRole}`
1350
+ );
1351
+
1352
+ if (user.role !== requiredRole && user.role !== "admin") {
1353
+ yield* Effect.logWarning(
1354
+ `User ${user.id} with role ${user.role} cannot access ${requiredRole} resources`
1355
+ );
1356
+ return yield* Effect.fail(
1357
+ new UnauthorizedError({
1358
+ action: "access_user",
1359
+ role: user.role,
1360
+ })
1361
+ );
1362
+ }
1363
+
1364
+ yield* Effect.logInfo(
1365
+ `User ${user.id} has required role: ${user.role}`
1366
+ );
1367
+ return Effect.succeed(void 0);
1368
+ }),
1369
+ };
1370
+ },
1371
+ }
1372
+ ) {}
1373
+
1374
+ interface ApiResponse {
1375
+ readonly error?: string;
1376
+ readonly message?: string;
1377
+ readonly data?: User;
1378
+ }
1379
+
1380
+ // Create routes with proper error handling
1381
+ const createRoutes = () =>
1382
+ Effect.gen(function* () {
1383
+ const repo = yield* UserRepository;
1384
+ const errorHandler = yield* ErrorHandlerService;
1385
+
1386
+ yield* Effect.logInfo("=== Processing API request ===");
1387
+
1388
+ // Test different scenarios
1389
+ for (const userId of ["user_123", "user_456", "invalid_id", "user_789"]) {
1390
+ yield* Effect.logInfo(`\n--- Testing user ID: ${userId} ---`);
1391
+
1392
+ const response = yield* repo.getUser(userId).pipe(
1393
+ Effect.map((user) => ({
1394
+ data: {
1395
+ ...user,
1396
+ email: user.role === "admin" ? user.email : "[hidden]",
1397
+ },
1398
+ })),
1399
+ Effect.catchAll((error) => errorHandler.handleApiError(error))
1400
+ );
1401
+
1402
+ yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`);
1403
+ }
1404
+
1405
+ // Test role checking
1406
+ const adminUser = yield* repo.getUser("user_123");
1407
+ const regularUser = yield* repo.getUser("user_456");
1408
+
1409
+ yield* Effect.logInfo("\n=== Testing role checks ===");
1410
+
1411
+ yield* repo.checkRole(adminUser, "admin").pipe(
1412
+ Effect.tap(() => Effect.logInfo("Admin access successful")),
1413
+ Effect.catchAll((error) => errorHandler.handleApiError(error))
1414
+ );
1415
+
1416
+ yield* repo.checkRole(regularUser, "admin").pipe(
1417
+ Effect.tap(() => Effect.logInfo("User admin access successful")),
1418
+ Effect.catchAll((error) => errorHandler.handleApiError(error))
1419
+ );
1420
+
1421
+ return { message: "Tests completed successfully" };
1422
+ });
1423
+
1424
+ // Run the program with all services
1425
+ Effect.runPromise(
1426
+ Effect.provide(
1427
+ Effect.provide(createRoutes(), ErrorHandlerService.Default),
1428
+ UserRepository.Default
1429
+ )
1430
+ );
1431
+ ```
1432
+
1433
+ **Anti-Pattern:**
1434
+
1435
+ The anti-pattern is to handle HTTP-specific error logic inside each route handler using functions like `Effect.catchTag`.
1436
+
1437
+ ```typescript
1438
+ import { Effect, Data } from "effect";
1439
+ import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
1440
+
1441
+ class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
1442
+ id: string;
1443
+ }> {}
1444
+ // ... same getUser function and error classes
1445
+
1446
+ const userRoute = Http.router.get(
1447
+ "/users/:userId",
1448
+ Effect.flatMap(Http.request.ServerRequest, (req) =>
1449
+ getUser(req.params.userId)
1450
+ ).pipe(
1451
+ Effect.map(Http.response.json),
1452
+ // Manually catching errors inside the route logic
1453
+ Effect.catchTag("UserNotFoundError", (e) =>
1454
+ Http.response.text(`User ${e.id} not found`, { status: 404 })
1455
+ ),
1456
+ Effect.catchTag("InvalidIdError", (e) =>
1457
+ Http.response.text(`ID ${e.id} is not a valid format`, { status: 400 })
1458
+ )
1459
+ )
1460
+ );
1461
+
1462
+ const app = Http.router.empty.pipe(Http.router.addRoute(userRoute));
1463
+
1464
+ // No centralized error handling
1465
+ const program = Http.server
1466
+ .serve(app)
1467
+ .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
1468
+
1469
+ NodeRuntime.runMain(program);
1470
+ ```
1471
+
1472
+ This approach is problematic because it pollutes the business logic of the route handler with details about HTTP status codes. It's also highly repetitive; if ten different routes could produce a `UserNotFoundError`, you would need to copy this `catchTag` logic into all ten of them, making the API difficult to maintain.
1473
+
1474
+ **Rationale:**
1475
+
1476
+ Define specific error types for your application logic and use `Http.server.serveOptions` with a custom `unhandledErrorResponse` function to map those errors to appropriate HTTP status codes and responses.
1477
+
1478
+ ---
1479
+
1480
+
1481
+ By default, any unhandled failure in an Effect route handler results in a generic `500 Internal Server Error`. This is a safe default, but it's not helpful for API clients who need to know _why_ their request failed. Was it a client-side error (like a non-existent resource, `404`) or a true server-side problem (`500`)?
1482
+
1483
+ Centralizing error handling at the server level provides a clean separation of concerns:
1484
+
1485
+ 1. **Domain-Focused Logic**: Your business logic can fail with specific, descriptive errors (e.g., `UserNotFoundError`) without needing any knowledge of HTTP status codes.
1486
+ 2. **Centralized Mapping**: You define the mapping from application errors to HTTP responses in a single location. This makes your API's error handling consistent and easy to maintain. If you need to change how an error is reported, you only change it in one place.
1487
+ 3. **Type Safety**: Using `Data.TaggedClass` for your errors allows you to use `Match` to exhaustively handle all known error cases, preventing you from forgetting to map a specific error type.
1488
+ 4. **Clear Client Communication**: It produces a predictable and useful API, allowing clients to programmatically react to different failure scenarios.
1489
+
1490
+ ---
1491
+
1492
+ ---
1493
+
1494
+ ### Compose API Middleware
1495
+
1496
+ **Rule:** Use Effect composition to build a middleware pipeline that processes requests.
1497
+
1498
+ **Good Example:**
1499
+
1500
+ ```typescript
1501
+ import { Effect, Context, Layer, Duration } from "effect"
1502
+ import { HttpServerRequest, HttpServerResponse } from "@effect/platform"
1503
+
1504
+ // ============================================
1505
+ // 1. Define middleware type
1506
+ // ============================================
1507
+
1508
+ type Handler<E, R> = Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>
1509
+
1510
+ type Middleware<E1, R1, E2 = E1, R2 = R1> = <E extends E1, R extends R1>(
1511
+ handler: Handler<E, R>
1512
+ ) => Handler<E | E2, R | R2>
1513
+
1514
+ // ============================================
1515
+ // 2. Logging middleware
1516
+ // ============================================
1517
+
1518
+ const withLogging: Middleware<never, HttpServerRequest.HttpServerRequest> =
1519
+ (handler) =>
1520
+ Effect.gen(function* () {
1521
+ const request = yield* HttpServerRequest.HttpServerRequest
1522
+ const startTime = Date.now()
1523
+
1524
+ yield* Effect.log(`→ ${request.method} ${request.url}`)
1525
+
1526
+ const response = yield* handler
1527
+
1528
+ const duration = Date.now() - startTime
1529
+ yield* Effect.log(`← ${response.status} (${duration}ms)`)
1530
+
1531
+ return response
1532
+ })
1533
+
1534
+ // ============================================
1535
+ // 3. Timing middleware (adds header)
1536
+ // ============================================
1537
+
1538
+ const withTiming: Middleware<never, never> = (handler) =>
1539
+ Effect.gen(function* () {
1540
+ const startTime = Date.now()
1541
+ const response = yield* handler
1542
+ const duration = Date.now() - startTime
1543
+
1544
+ return HttpServerResponse.setHeader(
1545
+ response,
1546
+ "X-Response-Time",
1547
+ `${duration}ms`
1548
+ )
1549
+ })
1550
+
1551
+ // ============================================
1552
+ // 4. Error handling middleware
1553
+ // ============================================
1554
+
1555
+ const withErrorHandling: Middleware<unknown, never, never> = (handler) =>
1556
+ handler.pipe(
1557
+ Effect.catchAll((error) =>
1558
+ Effect.gen(function* () {
1559
+ yield* Effect.logError(`Unhandled error: ${error}`)
1560
+
1561
+ return HttpServerResponse.json(
1562
+ { error: "Internal Server Error" },
1563
+ { status: 500 }
1564
+ )
1565
+ })
1566
+ )
1567
+ )
1568
+
1569
+ // ============================================
1570
+ // 5. Request ID middleware
1571
+ // ============================================
1572
+
1573
+ class RequestId extends Context.Tag("RequestId")<RequestId, string>() {}
1574
+
1575
+ const withRequestId: Middleware<never, never, never, RequestId> = (handler) =>
1576
+ Effect.gen(function* () {
1577
+ const requestId = crypto.randomUUID()
1578
+
1579
+ const response = yield* handler.pipe(
1580
+ Effect.provideService(RequestId, requestId)
1581
+ )
1582
+
1583
+ return HttpServerResponse.setHeader(response, "X-Request-Id", requestId)
1584
+ })
1585
+
1586
+ // ============================================
1587
+ // 6. Timeout middleware
1588
+ // ============================================
1589
+
1590
+ const withTimeout = (duration: Duration.DurationInput): Middleware<never, never> =>
1591
+ (handler) =>
1592
+ handler.pipe(
1593
+ Effect.timeout(duration),
1594
+ Effect.catchTag("TimeoutException", () =>
1595
+ Effect.succeed(
1596
+ HttpServerResponse.json(
1597
+ { error: "Request timeout" },
1598
+ { status: 504 }
1599
+ )
1600
+ )
1601
+ )
1602
+ )
1603
+
1604
+ // ============================================
1605
+ // 7. CORS middleware (see separate pattern)
1606
+ // ============================================
1607
+
1608
+ const withCORS = (origin: string): Middleware<never, never> => (handler) =>
1609
+ Effect.gen(function* () {
1610
+ const response = yield* handler
1611
+
1612
+ return response.pipe(
1613
+ HttpServerResponse.setHeader("Access-Control-Allow-Origin", origin),
1614
+ HttpServerResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"),
1615
+ HttpServerResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization")
1616
+ )
1617
+ })
1618
+
1619
+ // ============================================
1620
+ // 8. Compose middleware
1621
+ // ============================================
1622
+
1623
+ const applyMiddleware = <E, R>(handler: Handler<E, R>) =>
1624
+ handler.pipe(
1625
+ withLogging,
1626
+ withTiming,
1627
+ withRequestId,
1628
+ withTimeout("30 seconds"),
1629
+ withCORS("*"),
1630
+ withErrorHandling
1631
+ )
1632
+
1633
+ // ============================================
1634
+ // 9. Usage
1635
+ // ============================================
1636
+
1637
+ const myHandler = Effect.gen(function* () {
1638
+ const requestId = yield* RequestId
1639
+ yield* Effect.log(`Processing request ${requestId}`)
1640
+
1641
+ return HttpServerResponse.json({ message: "Hello!" })
1642
+ })
1643
+
1644
+ const protectedHandler = applyMiddleware(myHandler)
1645
+ ```
1646
+
1647
+ **Rationale:**
1648
+
1649
+ Build middleware as composable Effect functions that wrap handlers, adding cross-cutting concerns like logging, authentication, and error handling.
1650
+
1651
+ ---
1652
+
1653
+
1654
+ Middleware provides separation of concerns:
1655
+
1656
+ 1. **Reusability** - Write once, apply everywhere
1657
+ 2. **Composability** - Stack multiple middlewares
1658
+ 3. **Testability** - Test each middleware in isolation
1659
+ 4. **Clarity** - Handlers focus on business logic
1660
+
1661
+ ---
1662
+
1663
+ ---
1664
+
1665
+ ### Configure CORS for APIs
1666
+
1667
+ **Rule:** Configure CORS headers to allow legitimate cross-origin requests while blocking unauthorized ones.
1668
+
1669
+ **Good Example:**
1670
+
1671
+ ```typescript
1672
+ import { Effect } from "effect"
1673
+ import { HttpServerRequest, HttpServerResponse } from "@effect/platform"
1674
+
1675
+ // ============================================
1676
+ // 1. CORS configuration
1677
+ // ============================================
1678
+
1679
+ interface CorsConfig {
1680
+ readonly allowedOrigins: ReadonlyArray<string> | "*"
1681
+ readonly allowedMethods: ReadonlyArray<string>
1682
+ readonly allowedHeaders: ReadonlyArray<string>
1683
+ readonly exposedHeaders?: ReadonlyArray<string>
1684
+ readonly credentials?: boolean
1685
+ readonly maxAge?: number
1686
+ }
1687
+
1688
+ const defaultCorsConfig: CorsConfig = {
1689
+ allowedOrigins: "*",
1690
+ allowedMethods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
1691
+ allowedHeaders: ["Content-Type", "Authorization", "X-Request-Id"],
1692
+ exposedHeaders: ["X-Request-Id", "X-Response-Time"],
1693
+ credentials: false,
1694
+ maxAge: 86400, // 24 hours
1695
+ }
1696
+
1697
+ // ============================================
1698
+ // 2. Check if origin is allowed
1699
+ // ============================================
1700
+
1701
+ const isOriginAllowed = (
1702
+ origin: string | undefined,
1703
+ allowedOrigins: ReadonlyArray<string> | "*"
1704
+ ): boolean => {
1705
+ if (!origin) return false
1706
+ if (allowedOrigins === "*") return true
1707
+ return allowedOrigins.includes(origin)
1708
+ }
1709
+
1710
+ // ============================================
1711
+ // 3. Add CORS headers to response
1712
+ // ============================================
1713
+
1714
+ const addCorsHeaders = (
1715
+ response: HttpServerResponse.HttpServerResponse,
1716
+ origin: string | undefined,
1717
+ config: CorsConfig
1718
+ ): HttpServerResponse.HttpServerResponse => {
1719
+ let result = response
1720
+
1721
+ // Set allowed origin
1722
+ if (config.allowedOrigins === "*") {
1723
+ result = HttpServerResponse.setHeader(result, "Access-Control-Allow-Origin", "*")
1724
+ } else if (origin && isOriginAllowed(origin, config.allowedOrigins)) {
1725
+ result = HttpServerResponse.setHeader(result, "Access-Control-Allow-Origin", origin)
1726
+ result = HttpServerResponse.setHeader(result, "Vary", "Origin")
1727
+ }
1728
+
1729
+ // Set allowed methods
1730
+ result = HttpServerResponse.setHeader(
1731
+ result,
1732
+ "Access-Control-Allow-Methods",
1733
+ config.allowedMethods.join(", ")
1734
+ )
1735
+
1736
+ // Set allowed headers
1737
+ result = HttpServerResponse.setHeader(
1738
+ result,
1739
+ "Access-Control-Allow-Headers",
1740
+ config.allowedHeaders.join(", ")
1741
+ )
1742
+
1743
+ // Set exposed headers
1744
+ if (config.exposedHeaders?.length) {
1745
+ result = HttpServerResponse.setHeader(
1746
+ result,
1747
+ "Access-Control-Expose-Headers",
1748
+ config.exposedHeaders.join(", ")
1749
+ )
1750
+ }
1751
+
1752
+ // Set credentials
1753
+ if (config.credentials) {
1754
+ result = HttpServerResponse.setHeader(
1755
+ result,
1756
+ "Access-Control-Allow-Credentials",
1757
+ "true"
1758
+ )
1759
+ }
1760
+
1761
+ // Set max age for preflight cache
1762
+ if (config.maxAge) {
1763
+ result = HttpServerResponse.setHeader(
1764
+ result,
1765
+ "Access-Control-Max-Age",
1766
+ String(config.maxAge)
1767
+ )
1768
+ }
1769
+
1770
+ return result
1771
+ }
1772
+
1773
+ // ============================================
1774
+ // 4. CORS middleware
1775
+ // ============================================
1776
+
1777
+ const withCors = (config: CorsConfig = defaultCorsConfig) =>
1778
+ <E, R>(
1779
+ handler: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>
1780
+ ): Effect.Effect<
1781
+ HttpServerResponse.HttpServerResponse,
1782
+ E,
1783
+ R | HttpServerRequest.HttpServerRequest
1784
+ > =>
1785
+ Effect.gen(function* () {
1786
+ const request = yield* HttpServerRequest.HttpServerRequest
1787
+ const origin = request.headers["origin"]
1788
+
1789
+ // Handle preflight OPTIONS request
1790
+ if (request.method === "OPTIONS") {
1791
+ const preflightResponse = HttpServerResponse.empty({ status: 204 })
1792
+ return addCorsHeaders(preflightResponse, origin, config)
1793
+ }
1794
+
1795
+ // Check if origin is allowed
1796
+ if (
1797
+ origin &&
1798
+ config.allowedOrigins !== "*" &&
1799
+ !isOriginAllowed(origin, config.allowedOrigins)
1800
+ ) {
1801
+ return HttpServerResponse.json(
1802
+ { error: "CORS: Origin not allowed" },
1803
+ { status: 403 }
1804
+ )
1805
+ }
1806
+
1807
+ // Process request and add CORS headers to response
1808
+ const response = yield* handler
1809
+ return addCorsHeaders(response, origin, config)
1810
+ })
1811
+
1812
+ // ============================================
1813
+ // 5. Usage examples
1814
+ // ============================================
1815
+
1816
+ // Allow all origins (development)
1817
+ const devCors = withCors({
1818
+ ...defaultCorsConfig,
1819
+ allowedOrigins: "*",
1820
+ })
1821
+
1822
+ // Specific origins (production)
1823
+ const prodCors = withCors({
1824
+ allowedOrigins: [
1825
+ "https://myapp.com",
1826
+ "https://admin.myapp.com",
1827
+ ],
1828
+ allowedMethods: ["GET", "POST", "PUT", "DELETE"],
1829
+ allowedHeaders: ["Content-Type", "Authorization"],
1830
+ credentials: true,
1831
+ maxAge: 3600,
1832
+ })
1833
+
1834
+ // Apply to handlers
1835
+ const myHandler = Effect.succeed(
1836
+ HttpServerResponse.json({ message: "Hello!" })
1837
+ )
1838
+
1839
+ const corsEnabledHandler = devCors(myHandler)
1840
+ ```
1841
+
1842
+ **Rationale:**
1843
+
1844
+ Implement CORS as middleware that adds appropriate headers and handles preflight OPTIONS requests.
1845
+
1846
+ ---
1847
+
1848
+
1849
+ Browsers block cross-origin requests by default:
1850
+
1851
+ 1. **Security** - Prevents malicious sites from accessing your API
1852
+ 2. **Controlled access** - Allow specific origins only
1853
+ 3. **Credentials** - Control cookie/auth header sharing
1854
+ 4. **Methods** - Limit which HTTP methods are allowed
1855
+
1856
+ ---
1857
+
1858
+ ---
1859
+
1860
+ ### Implement API Authentication
1861
+
1862
+ **Rule:** Use middleware to validate authentication tokens before handling requests.
1863
+
1864
+ **Good Example:**
1865
+
1866
+ ```typescript
1867
+ import { Effect, Context, Layer, Data } from "effect"
1868
+ import { HttpServer, HttpServerRequest, HttpServerResponse } from "@effect/platform"
1869
+
1870
+ // ============================================
1871
+ // 1. Define authentication types
1872
+ // ============================================
1873
+
1874
+ interface User {
1875
+ readonly id: string
1876
+ readonly email: string
1877
+ readonly roles: ReadonlyArray<string>
1878
+ }
1879
+
1880
+ class AuthenticatedUser extends Context.Tag("AuthenticatedUser")<
1881
+ AuthenticatedUser,
1882
+ User
1883
+ >() {}
1884
+
1885
+ class UnauthorizedError extends Data.TaggedError("UnauthorizedError")<{
1886
+ readonly reason: string
1887
+ }> {}
1888
+
1889
+ class ForbiddenError extends Data.TaggedError("ForbiddenError")<{
1890
+ readonly requiredRole: string
1891
+ }> {}
1892
+
1893
+ // ============================================
1894
+ // 2. JWT validation service
1895
+ // ============================================
1896
+
1897
+ interface JwtService {
1898
+ readonly verify: (token: string) => Effect.Effect<User, UnauthorizedError>
1899
+ }
1900
+
1901
+ class Jwt extends Context.Tag("Jwt")<Jwt, JwtService>() {}
1902
+
1903
+ const JwtLive = Layer.succeed(Jwt, {
1904
+ verify: (token) =>
1905
+ Effect.gen(function* () {
1906
+ // In production: use a real JWT library
1907
+ if (!token || token === "invalid") {
1908
+ return yield* Effect.fail(new UnauthorizedError({
1909
+ reason: "Invalid or expired token"
1910
+ }))
1911
+ }
1912
+
1913
+ // Decode token (simplified)
1914
+ if (token.startsWith("user-")) {
1915
+ return {
1916
+ id: token.replace("user-", ""),
1917
+ email: "user@example.com",
1918
+ roles: ["user"],
1919
+ }
1920
+ }
1921
+
1922
+ if (token.startsWith("admin-")) {
1923
+ return {
1924
+ id: token.replace("admin-", ""),
1925
+ email: "admin@example.com",
1926
+ roles: ["user", "admin"],
1927
+ }
1928
+ }
1929
+
1930
+ return yield* Effect.fail(new UnauthorizedError({
1931
+ reason: "Malformed token"
1932
+ }))
1933
+ }),
1934
+ })
1935
+
1936
+ // ============================================
1937
+ // 3. Authentication middleware
1938
+ // ============================================
1939
+
1940
+ const extractBearerToken = (header: string | undefined): string | null => {
1941
+ if (!header?.startsWith("Bearer ")) return null
1942
+ return header.slice(7)
1943
+ }
1944
+
1945
+ const authenticate = <A, E, R>(
1946
+ handler: Effect.Effect<A, E, R | AuthenticatedUser>
1947
+ ): Effect.Effect<A, E | UnauthorizedError, R | Jwt | HttpServerRequest.HttpServerRequest> =>
1948
+ Effect.gen(function* () {
1949
+ const request = yield* HttpServerRequest.HttpServerRequest
1950
+ const jwt = yield* Jwt
1951
+
1952
+ const authHeader = request.headers["authorization"]
1953
+ const token = extractBearerToken(authHeader)
1954
+
1955
+ if (!token) {
1956
+ return yield* Effect.fail(new UnauthorizedError({
1957
+ reason: "Missing Authorization header"
1958
+ }))
1959
+ }
1960
+
1961
+ const user = yield* jwt.verify(token)
1962
+
1963
+ return yield* handler.pipe(
1964
+ Effect.provideService(AuthenticatedUser, user)
1965
+ )
1966
+ })
1967
+
1968
+ // ============================================
1969
+ // 4. Role-based authorization
1970
+ // ============================================
1971
+
1972
+ const requireRole = (role: string) =>
1973
+ <A, E, R>(handler: Effect.Effect<A, E, R | AuthenticatedUser>) =>
1974
+ Effect.gen(function* () {
1975
+ const user = yield* AuthenticatedUser
1976
+
1977
+ if (!user.roles.includes(role)) {
1978
+ return yield* Effect.fail(new ForbiddenError({ requiredRole: role }))
1979
+ }
1980
+
1981
+ return yield* handler
1982
+ })
1983
+
1984
+ // ============================================
1985
+ // 5. Protected routes
1986
+ // ============================================
1987
+
1988
+ const getProfile = authenticate(
1989
+ Effect.gen(function* () {
1990
+ const user = yield* AuthenticatedUser
1991
+ return HttpServerResponse.json({
1992
+ id: user.id,
1993
+ email: user.email,
1994
+ roles: user.roles,
1995
+ })
1996
+ })
1997
+ )
1998
+
1999
+ const adminOnly = authenticate(
2000
+ requireRole("admin")(
2001
+ Effect.gen(function* () {
2002
+ const user = yield* AuthenticatedUser
2003
+ return HttpServerResponse.json({
2004
+ message: `Welcome admin ${user.email}`,
2005
+ users: ["user1", "user2", "user3"],
2006
+ })
2007
+ })
2008
+ )
2009
+ )
2010
+
2011
+ // ============================================
2012
+ // 6. Error handling
2013
+ // ============================================
2014
+
2015
+ const handleAuthErrors = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
2016
+ effect.pipe(
2017
+ Effect.catchTag("UnauthorizedError", (e) =>
2018
+ Effect.succeed(
2019
+ HttpServerResponse.json({ error: e.reason }, { status: 401 })
2020
+ )
2021
+ ),
2022
+ Effect.catchTag("ForbiddenError", (e) =>
2023
+ Effect.succeed(
2024
+ HttpServerResponse.json(
2025
+ { error: `Requires role: ${e.requiredRole}` },
2026
+ { status: 403 }
2027
+ )
2028
+ )
2029
+ )
2030
+ )
2031
+ ```
2032
+
2033
+ **Rationale:**
2034
+
2035
+ Implement authentication as middleware that validates tokens and provides user context to route handlers.
2036
+
2037
+ ---
2038
+
2039
+
2040
+ Authentication protects your API:
2041
+
2042
+ 1. **Identity verification** - Know who's making requests
2043
+ 2. **Access control** - Limit what users can do
2044
+ 3. **Audit trail** - Track who did what
2045
+ 4. **Rate limiting** - Per-user limits
2046
+
2047
+ ---
2048
+
2049
+ ---
2050
+
2051
+ ### Make an Outgoing HTTP Client Request
2052
+
2053
+ **Rule:** Use the Http.client module to make outgoing requests to keep the entire operation within the Effect ecosystem.
2054
+
2055
+ **Good Example:**
2056
+
2057
+ This example creates a proxy endpoint. A request to `/proxy/posts/1` on our server will trigger an outgoing request to the JSONPlaceholder API. The response is then parsed and relayed back to the original client.
2058
+
2059
+ ```typescript
2060
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
2061
+ import * as HttpRouter from "@effect/platform/HttpRouter";
2062
+ import * as HttpServer from "@effect/platform/HttpServer";
2063
+ import * as HttpResponse from "@effect/platform/HttpServerResponse";
2064
+ import { Console, Data, Duration, Effect, Fiber, Layer } from "effect";
2065
+
2066
+ class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
2067
+ id: string;
2068
+ }> {}
2069
+
2070
+ export class Database extends Effect.Service<Database>()("Database", {
2071
+ sync: () => ({
2072
+ getUser: (id: string) =>
2073
+ id === "123"
2074
+ ? Effect.succeed({ name: "Paul" })
2075
+ : Effect.fail(new UserNotFoundError({ id })),
2076
+ }),
2077
+ }) {}
2078
+
2079
+ const userHandler = Effect.flatMap(HttpRouter.params, (p) =>
2080
+ Effect.flatMap(Database, (db) => db.getUser(p["userId"] ?? "")).pipe(
2081
+ Effect.flatMap(HttpResponse.json)
2082
+ )
2083
+ );
2084
+
2085
+ const app = HttpRouter.empty.pipe(
2086
+ HttpRouter.get("/users/:userId", userHandler)
2087
+ );
2088
+
2089
+ const server = NodeHttpServer.layer(() => require("node:http").createServer(), {
2090
+ port: 3457,
2091
+ });
2092
+
2093
+ const serverLayer = HttpServer.serve(app);
2094
+
2095
+ const mainLayer = Layer.merge(Database.Default, server);
2096
+
2097
+ const program = Effect.gen(function* () {
2098
+ yield* Effect.log("Server started on http://localhost:3457");
2099
+ const layer = Layer.provide(serverLayer, mainLayer);
2100
+
2101
+ // Launch server and run for a short duration to demonstrate
2102
+ const serverFiber = yield* Layer.launch(layer).pipe(Effect.fork);
2103
+
2104
+ // Wait a moment for server to start
2105
+ yield* Effect.sleep(Duration.seconds(1));
2106
+
2107
+ // Simulate some server activity
2108
+ yield* Effect.log("Server is running and ready to handle requests");
2109
+ yield* Effect.sleep(Duration.seconds(2));
2110
+
2111
+ // Shutdown gracefully
2112
+ yield* Fiber.interrupt(serverFiber);
2113
+ yield* Effect.log("Server shutdown complete");
2114
+ });
2115
+
2116
+ NodeRuntime.runMain(
2117
+ Effect.provide(
2118
+ program,
2119
+ Layer.provide(serverLayer, Layer.merge(Database.Default, server))
2120
+ ) as Effect.Effect<void, unknown, never>
2121
+ );
2122
+ ```
2123
+
2124
+ **Anti-Pattern:**
2125
+
2126
+ The anti-pattern is to use `fetch` inside a route handler, wrapped in `Effect.tryPromise`. This approach requires manual error handling and loses the benefits of the Effect ecosystem.
2127
+
2128
+ ```typescript
2129
+ import { Effect } from "effect";
2130
+ import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
2131
+
2132
+ const proxyRoute = Http.router.get(
2133
+ "/proxy/posts/:id",
2134
+ Effect.flatMap(Http.request.ServerRequest, (req) =>
2135
+ // Manually wrap fetch in an Effect
2136
+ Effect.tryPromise({
2137
+ try: () =>
2138
+ fetch(`https://jsonplaceholder.typicode.com/posts/${req.params.id}`),
2139
+ catch: () => "FetchError", // Untyped error
2140
+ }).pipe(
2141
+ Effect.flatMap((res) =>
2142
+ // Manually check status and parse JSON, each with its own error case
2143
+ res.ok
2144
+ ? Effect.tryPromise({
2145
+ try: () => res.json(),
2146
+ catch: () => "JsonError",
2147
+ })
2148
+ : Effect.fail("BadStatusError")
2149
+ ),
2150
+ Effect.map(Http.response.json),
2151
+ // A generic catch-all because we can't easily distinguish error types
2152
+ Effect.catchAll(() =>
2153
+ Http.response.text("An unknown error occurred", { status: 500 })
2154
+ )
2155
+ )
2156
+ )
2157
+ );
2158
+
2159
+ const app = Http.router.empty.pipe(Http.router.addRoute(proxyRoute));
2160
+
2161
+ const program = Http.server
2162
+ .serve(app)
2163
+ .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
2164
+
2165
+ NodeRuntime.runMain(program);
2166
+ ```
2167
+
2168
+ This manual approach is significantly more complex and less safe. It forces you to reinvent status and parsing logic, uses untyped string-based errors, and most importantly, the `fetch` call will not be automatically interrupted if the parent request is cancelled.
2169
+
2170
+ **Rationale:**
2171
+
2172
+ To call an external API from within your server, use the `Http.client` module. This creates an `Effect` that represents the outgoing request, keeping it fully integrated with the Effect runtime.
2173
+
2174
+ ---
2175
+
2176
+
2177
+ An API server often needs to communicate with other services. While you could use the native `fetch` API, this breaks out of the Effect ecosystem and forfeits its most powerful features. Using the built-in `Http.client` is superior for several critical reasons:
2178
+
2179
+ 1. **Full Integration**: An `Http.client` request is a first-class `Effect`. This means it seamlessly composes with all other effects. You can add timeouts, retry logic (`Schedule`), or race it with other operations using the standard Effect operators you already know.
2180
+ 2. **Structured Concurrency**: This is a key benefit. If the original incoming request to your server is cancelled or times out, Effect will automatically interrupt the outgoing `Http.client` request. A raw `fetch` call would continue running in the background, wasting resources.
2181
+ 3. **Typed Errors**: The client provides a rich set of typed errors (e.g., `Http.error.RequestError`, `Http.error.ResponseError`). This allows you to write precise error handling logic to distinguish between a network failure and a non-2xx response from the external API.
2182
+ 4. **Testability**: The `Http.client` can be provided via a `Layer`, making it trivial to mock in tests. You can test your route's logic without making actual network calls, leading to faster and more reliable tests.
2183
+
2184
+ ---
2185
+
2186
+ ---
2187
+
2188
+
2189
+ ## 🟠 Advanced Patterns
2190
+
2191
+ ### Generate OpenAPI Documentation
2192
+
2193
+ **Rule:** Use Schema definitions to automatically generate OpenAPI documentation for your API.
2194
+
2195
+ **Good Example:**
2196
+
2197
+ ```typescript
2198
+ import { Effect, Schema } from "effect"
2199
+ import {
2200
+ HttpApi,
2201
+ HttpApiBuilder,
2202
+ HttpApiEndpoint,
2203
+ HttpApiGroup,
2204
+ HttpApiSwagger,
2205
+ OpenApi,
2206
+ } from "@effect/platform"
2207
+
2208
+ // ============================================
2209
+ // 1. Define schemas for request/response
2210
+ // ============================================
2211
+
2212
+ const UserSchema = Schema.Struct({
2213
+ id: Schema.String,
2214
+ email: Schema.String.pipe(Schema.pattern(/@/)),
2215
+ name: Schema.String,
2216
+ createdAt: Schema.DateFromString,
2217
+ })
2218
+
2219
+ const CreateUserSchema = Schema.Struct({
2220
+ email: Schema.String.pipe(Schema.pattern(/@/)),
2221
+ name: Schema.String,
2222
+ })
2223
+
2224
+ const UserListSchema = Schema.Array(UserSchema)
2225
+
2226
+ const ErrorSchema = Schema.Struct({
2227
+ error: Schema.String,
2228
+ code: Schema.String,
2229
+ })
2230
+
2231
+ // ============================================
2232
+ // 2. Define API endpoints with schemas
2233
+ // ============================================
2234
+
2235
+ const usersApi = HttpApiGroup.make("users")
2236
+ .pipe(
2237
+ HttpApiGroup.add(
2238
+ HttpApiEndpoint.get("getUsers", "/users")
2239
+ .pipe(
2240
+ HttpApiEndpoint.setSuccess(UserListSchema),
2241
+ HttpApiEndpoint.addError(ErrorSchema, { status: 500 })
2242
+ )
2243
+ ),
2244
+ HttpApiGroup.add(
2245
+ HttpApiEndpoint.get("getUser", "/users/:id")
2246
+ .pipe(
2247
+ HttpApiEndpoint.setPath(Schema.Struct({
2248
+ id: Schema.String,
2249
+ })),
2250
+ HttpApiEndpoint.setSuccess(UserSchema),
2251
+ HttpApiEndpoint.addError(ErrorSchema, { status: 404 }),
2252
+ HttpApiEndpoint.addError(ErrorSchema, { status: 500 })
2253
+ )
2254
+ ),
2255
+ HttpApiGroup.add(
2256
+ HttpApiEndpoint.post("createUser", "/users")
2257
+ .pipe(
2258
+ HttpApiEndpoint.setPayload(CreateUserSchema),
2259
+ HttpApiEndpoint.setSuccess(UserSchema, { status: 201 }),
2260
+ HttpApiEndpoint.addError(ErrorSchema, { status: 400 }),
2261
+ HttpApiEndpoint.addError(ErrorSchema, { status: 500 })
2262
+ )
2263
+ ),
2264
+ HttpApiGroup.add(
2265
+ HttpApiEndpoint.del("deleteUser", "/users/:id")
2266
+ .pipe(
2267
+ HttpApiEndpoint.setPath(Schema.Struct({
2268
+ id: Schema.String,
2269
+ })),
2270
+ HttpApiEndpoint.setSuccess(Schema.Void, { status: 204 }),
2271
+ HttpApiEndpoint.addError(ErrorSchema, { status: 404 }),
2272
+ HttpApiEndpoint.addError(ErrorSchema, { status: 500 })
2273
+ )
2274
+ )
2275
+ )
2276
+
2277
+ // ============================================
2278
+ // 3. Create the API definition
2279
+ // ============================================
2280
+
2281
+ const api = HttpApi.make("My API")
2282
+ .pipe(
2283
+ HttpApi.addGroup(usersApi),
2284
+ OpenApi.annotate({
2285
+ title: "My Effect API",
2286
+ version: "1.0.0",
2287
+ description: "A sample API built with Effect",
2288
+ })
2289
+ )
2290
+
2291
+ // ============================================
2292
+ // 4. Implement the handlers
2293
+ // ============================================
2294
+
2295
+ const usersHandlers = HttpApiBuilder.group(api, "users", (handlers) =>
2296
+ handlers
2297
+ .pipe(
2298
+ HttpApiBuilder.handle("getUsers", () =>
2299
+ Effect.succeed([
2300
+ {
2301
+ id: "1",
2302
+ email: "alice@example.com",
2303
+ name: "Alice",
2304
+ createdAt: new Date(),
2305
+ },
2306
+ ])
2307
+ ),
2308
+ HttpApiBuilder.handle("getUser", ({ path }) =>
2309
+ Effect.gen(function* () {
2310
+ if (path.id === "not-found") {
2311
+ return yield* Effect.fail({ error: "User not found", code: "NOT_FOUND" })
2312
+ }
2313
+ return {
2314
+ id: path.id,
2315
+ email: "user@example.com",
2316
+ name: "User",
2317
+ createdAt: new Date(),
2318
+ }
2319
+ })
2320
+ ),
2321
+ HttpApiBuilder.handle("createUser", ({ payload }) =>
2322
+ Effect.succeed({
2323
+ id: crypto.randomUUID(),
2324
+ email: payload.email,
2325
+ name: payload.name,
2326
+ createdAt: new Date(),
2327
+ })
2328
+ ),
2329
+ HttpApiBuilder.handle("deleteUser", ({ path }) =>
2330
+ Effect.gen(function* () {
2331
+ if (path.id === "not-found") {
2332
+ return yield* Effect.fail({ error: "User not found", code: "NOT_FOUND" })
2333
+ }
2334
+ yield* Effect.log(`Deleted user ${path.id}`)
2335
+ })
2336
+ )
2337
+ )
2338
+ )
2339
+
2340
+ // ============================================
2341
+ // 5. Build the server with Swagger UI
2342
+ // ============================================
2343
+
2344
+ const MyApiLive = HttpApiBuilder.api(api).pipe(
2345
+ Layer.provide(usersHandlers)
2346
+ )
2347
+
2348
+ const ServerLive = HttpApiBuilder.serve().pipe(
2349
+ // Add Swagger UI at /docs
2350
+ Layer.provide(HttpApiSwagger.layer({ path: "/docs" })),
2351
+ Layer.provide(MyApiLive),
2352
+ Layer.provide(NodeHttpServer.layer({ port: 3000 }))
2353
+ )
2354
+
2355
+ // ============================================
2356
+ // 6. Export OpenAPI spec as JSON
2357
+ // ============================================
2358
+
2359
+ const openApiSpec = OpenApi.fromApi(api)
2360
+
2361
+ // Save to file for external tools
2362
+ import { NodeFileSystem } from "@effect/platform-node"
2363
+
2364
+ const saveSpec = Effect.gen(function* () {
2365
+ const fs = yield* FileSystem.FileSystem
2366
+ yield* fs.writeFileString(
2367
+ "openapi.json",
2368
+ JSON.stringify(openApiSpec, null, 2)
2369
+ )
2370
+ yield* Effect.log("OpenAPI spec saved to openapi.json")
2371
+ })
2372
+ ```
2373
+
2374
+ **Rationale:**
2375
+
2376
+ Define your API using Effect Schema and HttpApi to automatically generate OpenAPI documentation that stays in sync with your implementation.
2377
+
2378
+ ---
2379
+
2380
+
2381
+ OpenAPI documentation provides:
2382
+
2383
+ 1. **Discovery** - Clients know what endpoints exist
2384
+ 2. **Contracts** - Clear request/response shapes
2385
+ 3. **Testing** - Swagger UI for manual testing
2386
+ 4. **Code generation** - Generate client SDKs
2387
+ 5. **Validation** - Schema-first development
2388
+
2389
+ ---
2390
+
2391
+ ---
2392
+
2393
+