@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,1756 @@
1
+ ---
2
+ name: effect-patterns-making-http-requests
3
+ description: Effect-TS patterns for Making Http Requests. Use when working with making http requests in Effect-TS applications.
4
+ ---
5
+ # Effect-TS Patterns: Making Http Requests
6
+ This skill provides 10 curated Effect-TS patterns for making http requests.
7
+ Use this skill when working on tasks related to:
8
+ - making http requests
9
+ - Best practices in Effect-TS applications
10
+ - Real-world patterns and solutions
11
+
12
+ ---
13
+
14
+ ## 🟢 Beginner Patterns
15
+
16
+ ### Parse JSON Responses Safely
17
+
18
+ **Rule:** Always validate HTTP responses with Schema to catch API changes at runtime.
19
+
20
+ **Good Example:**
21
+
22
+ ```typescript
23
+ import { Effect, Console } from "effect"
24
+ import { Schema } from "effect"
25
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform"
26
+ import { NodeHttpClient, NodeRuntime } from "@effect/platform-node"
27
+
28
+ // ============================================
29
+ // 1. Define response schemas
30
+ // ============================================
31
+
32
+ const PostSchema = Schema.Struct({
33
+ id: Schema.Number,
34
+ title: Schema.String,
35
+ body: Schema.String,
36
+ userId: Schema.Number,
37
+ })
38
+
39
+ type Post = Schema.Schema.Type<typeof PostSchema>
40
+
41
+ const PostArraySchema = Schema.Array(PostSchema)
42
+
43
+ // ============================================
44
+ // 2. Fetch and validate single item
45
+ // ============================================
46
+
47
+ const getPost = (id: number) =>
48
+ Effect.gen(function* () {
49
+ const client = yield* HttpClient.HttpClient
50
+
51
+ const response = yield* client.get(
52
+ `https://jsonplaceholder.typicode.com/posts/${id}`
53
+ )
54
+ const json = yield* HttpClientResponse.json(response)
55
+
56
+ // Validate against schema - fails if data doesn't match
57
+ const post = yield* Schema.decodeUnknown(PostSchema)(json)
58
+
59
+ return post
60
+ })
61
+
62
+ // ============================================
63
+ // 3. Fetch and validate array
64
+ // ============================================
65
+
66
+ const getPosts = Effect.gen(function* () {
67
+ const client = yield* HttpClient.HttpClient
68
+
69
+ const response = yield* client.get(
70
+ "https://jsonplaceholder.typicode.com/posts"
71
+ )
72
+ const json = yield* HttpClientResponse.json(response)
73
+
74
+ // Validate array of posts
75
+ const posts = yield* Schema.decodeUnknown(PostArraySchema)(json)
76
+
77
+ return posts
78
+ })
79
+
80
+ // ============================================
81
+ // 4. Handle validation errors
82
+ // ============================================
83
+
84
+ const safeGetPost = (id: number) =>
85
+ getPost(id).pipe(
86
+ Effect.catchTag("ParseError", (error) =>
87
+ Effect.gen(function* () {
88
+ yield* Console.error(`Invalid response format: ${error.message}`)
89
+ // Return a default or fail differently
90
+ return yield* Effect.fail(new Error(`Post ${id} has invalid format`))
91
+ })
92
+ )
93
+ )
94
+
95
+ // ============================================
96
+ // 5. Schema with optional fields
97
+ // ============================================
98
+
99
+ const UserSchema = Schema.Struct({
100
+ id: Schema.Number,
101
+ name: Schema.String,
102
+ email: Schema.String,
103
+ phone: Schema.optional(Schema.String), // May not exist
104
+ website: Schema.optional(Schema.String),
105
+ company: Schema.optional(
106
+ Schema.Struct({
107
+ name: Schema.String,
108
+ catchPhrase: Schema.optional(Schema.String),
109
+ })
110
+ ),
111
+ })
112
+
113
+ const getUser = (id: number) =>
114
+ Effect.gen(function* () {
115
+ const client = yield* HttpClient.HttpClient
116
+
117
+ const response = yield* client.get(
118
+ `https://jsonplaceholder.typicode.com/users/${id}`
119
+ )
120
+ const json = yield* HttpClientResponse.json(response)
121
+
122
+ return yield* Schema.decodeUnknown(UserSchema)(json)
123
+ })
124
+
125
+ // ============================================
126
+ // 6. Run examples
127
+ // ============================================
128
+
129
+ const program = Effect.gen(function* () {
130
+ yield* Console.log("=== Validated Single Post ===")
131
+ const post = yield* getPost(1)
132
+ yield* Console.log(`Title: ${post.title}`)
133
+
134
+ yield* Console.log("\n=== Validated Posts Array ===")
135
+ const posts = yield* getPosts
136
+ yield* Console.log(`Fetched ${posts.length} posts`)
137
+
138
+ yield* Console.log("\n=== User with Optional Fields ===")
139
+ const user = yield* getUser(1)
140
+ yield* Console.log(`User: ${user.name}`)
141
+ yield* Console.log(`Company: ${user.company?.name ?? "N/A"}`)
142
+ })
143
+
144
+ program.pipe(
145
+ Effect.provide(NodeHttpClient.layer),
146
+ NodeRuntime.runMain
147
+ )
148
+ ```
149
+
150
+ **Rationale:**
151
+
152
+ Use Effect Schema to validate HTTP JSON responses, ensuring the data matches your expected types at runtime.
153
+
154
+ ---
155
+
156
+
157
+ APIs can change without warning:
158
+
159
+ 1. **Fields disappear** - Backend removes a field
160
+ 2. **Types change** - String becomes number
161
+ 3. **Nulls appear** - Required field becomes optional
162
+ 4. **New fields** - Extra data you didn't expect
163
+
164
+ Schema validation catches these issues immediately.
165
+
166
+ ---
167
+
168
+ ---
169
+
170
+ ### Your First HTTP Request
171
+
172
+ **Rule:** Use @effect/platform HttpClient for type-safe HTTP requests with automatic error handling.
173
+
174
+ **Good Example:**
175
+
176
+ ```typescript
177
+ import { Effect, Console } from "effect"
178
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform"
179
+ import { NodeHttpClient, NodeRuntime } from "@effect/platform-node"
180
+
181
+ // ============================================
182
+ // 1. Simple GET request
183
+ // ============================================
184
+
185
+ const simpleGet = Effect.gen(function* () {
186
+ const client = yield* HttpClient.HttpClient
187
+
188
+ // Make a GET request
189
+ const response = yield* client.get("https://jsonplaceholder.typicode.com/posts/1")
190
+
191
+ // Get response as JSON
192
+ const json = yield* HttpClientResponse.json(response)
193
+
194
+ return json
195
+ })
196
+
197
+ // ============================================
198
+ // 2. GET with typed response
199
+ // ============================================
200
+
201
+ interface Post {
202
+ id: number
203
+ title: string
204
+ body: string
205
+ userId: number
206
+ }
207
+
208
+ const getPost = (id: number) =>
209
+ Effect.gen(function* () {
210
+ const client = yield* HttpClient.HttpClient
211
+ const response = yield* client.get(
212
+ `https://jsonplaceholder.typicode.com/posts/${id}`
213
+ )
214
+ const post = yield* HttpClientResponse.json(response) as Effect.Effect<Post>
215
+ return post
216
+ })
217
+
218
+ // ============================================
219
+ // 3. POST with body
220
+ // ============================================
221
+
222
+ const createPost = (title: string, body: string) =>
223
+ Effect.gen(function* () {
224
+ const client = yield* HttpClient.HttpClient
225
+
226
+ const request = HttpClientRequest.post(
227
+ "https://jsonplaceholder.typicode.com/posts"
228
+ ).pipe(
229
+ HttpClientRequest.jsonBody({ title, body, userId: 1 })
230
+ )
231
+
232
+ const response = yield* client.execute(yield* request)
233
+ const created = yield* HttpClientResponse.json(response)
234
+
235
+ return created
236
+ })
237
+
238
+ // ============================================
239
+ // 4. Handle errors
240
+ // ============================================
241
+
242
+ const safeGetPost = (id: number) =>
243
+ getPost(id).pipe(
244
+ Effect.catchAll((error) =>
245
+ Effect.gen(function* () {
246
+ yield* Console.error(`Failed to fetch post ${id}: ${error}`)
247
+ return { id, title: "Unavailable", body: "", userId: 0 }
248
+ })
249
+ )
250
+ )
251
+
252
+ // ============================================
253
+ // 5. Run the program
254
+ // ============================================
255
+
256
+ const program = Effect.gen(function* () {
257
+ yield* Console.log("=== Simple GET ===")
258
+ const data = yield* simpleGet
259
+ yield* Console.log(JSON.stringify(data, null, 2))
260
+
261
+ yield* Console.log("\n=== Typed GET ===")
262
+ const post = yield* getPost(1)
263
+ yield* Console.log(`Post: ${post.title}`)
264
+
265
+ yield* Console.log("\n=== POST Request ===")
266
+ const created = yield* createPost("My New Post", "This is the body")
267
+ yield* Console.log(`Created: ${JSON.stringify(created)}`)
268
+ })
269
+
270
+ // Provide the HTTP client implementation
271
+ program.pipe(
272
+ Effect.provide(NodeHttpClient.layer),
273
+ NodeRuntime.runMain
274
+ )
275
+ ```
276
+
277
+ **Rationale:**
278
+
279
+ Use Effect's `HttpClient` from `@effect/platform` to make HTTP requests with built-in error handling, retries, and type safety.
280
+
281
+ ---
282
+
283
+
284
+ Effect's HttpClient is better than `fetch`:
285
+
286
+ 1. **Type-safe errors** - Network failures are typed, not exceptions
287
+ 2. **Automatic JSON parsing** - No manual `.json()` calls
288
+ 3. **Composable** - Chain requests, add retries, timeouts
289
+ 4. **Testable** - Easy to mock in tests
290
+
291
+ ---
292
+
293
+ ---
294
+
295
+
296
+ ## 🟡 Intermediate Patterns
297
+
298
+ ### Retry HTTP Requests with Backoff
299
+
300
+ **Rule:** Use Schedule to retry failed HTTP requests with configurable backoff strategies.
301
+
302
+ **Good Example:**
303
+
304
+ ```typescript
305
+ import { Effect, Schedule, Duration, Data } from "effect"
306
+ import { HttpClient, HttpClientRequest, HttpClientResponse, HttpClientError } from "@effect/platform"
307
+
308
+ // ============================================
309
+ // 1. Basic retry with exponential backoff
310
+ // ============================================
311
+
312
+ const fetchWithRetry = (url: string) =>
313
+ Effect.gen(function* () {
314
+ const client = yield* HttpClient.HttpClient
315
+
316
+ return yield* client.get(url).pipe(
317
+ Effect.flatMap((response) => HttpClientResponse.json(response)),
318
+ Effect.retry(
319
+ Schedule.exponential("100 millis", 2).pipe(
320
+ Schedule.intersect(Schedule.recurs(5)), // Max 5 retries
321
+ Schedule.jittered // Add randomness
322
+ )
323
+ )
324
+ )
325
+ })
326
+
327
+ // ============================================
328
+ // 2. Retry only specific status codes
329
+ // ============================================
330
+
331
+ class RetryableHttpError extends Data.TaggedError("RetryableHttpError")<{
332
+ readonly status: number
333
+ readonly message: string
334
+ }> {}
335
+
336
+ class NonRetryableHttpError extends Data.TaggedError("NonRetryableHttpError")<{
337
+ readonly status: number
338
+ readonly message: string
339
+ }> {}
340
+
341
+ const isRetryable = (status: number): boolean =>
342
+ status === 429 || // Rate limited
343
+ status === 503 || // Service unavailable
344
+ status === 502 || // Bad gateway
345
+ status === 504 || // Gateway timeout
346
+ status >= 500 // Server errors
347
+
348
+ const fetchWithSelectiveRetry = (url: string) =>
349
+ Effect.gen(function* () {
350
+ const client = yield* HttpClient.HttpClient
351
+
352
+ const response = yield* client.get(url).pipe(
353
+ Effect.flatMap((response) => {
354
+ if (response.status >= 400) {
355
+ if (isRetryable(response.status)) {
356
+ return Effect.fail(new RetryableHttpError({
357
+ status: response.status,
358
+ message: `HTTP ${response.status}`,
359
+ }))
360
+ }
361
+ return Effect.fail(new NonRetryableHttpError({
362
+ status: response.status,
363
+ message: `HTTP ${response.status}`,
364
+ }))
365
+ }
366
+ return Effect.succeed(response)
367
+ }),
368
+ Effect.retry({
369
+ schedule: Schedule.exponential("200 millis").pipe(
370
+ Schedule.intersect(Schedule.recurs(3))
371
+ ),
372
+ while: (error) => error._tag === "RetryableHttpError",
373
+ })
374
+ )
375
+
376
+ return yield* HttpClientResponse.json(response)
377
+ })
378
+
379
+ // ============================================
380
+ // 3. Retry with logging
381
+ // ============================================
382
+
383
+ const fetchWithRetryLogging = (url: string) =>
384
+ Effect.gen(function* () {
385
+ const client = yield* HttpClient.HttpClient
386
+
387
+ return yield* client.get(url).pipe(
388
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
389
+ Effect.retry(
390
+ Schedule.exponential("100 millis").pipe(
391
+ Schedule.intersect(Schedule.recurs(3)),
392
+ Schedule.tapOutput((_, output) =>
393
+ Effect.log(`Retry attempt, waiting ${Duration.toMillis(output)}ms`)
394
+ )
395
+ )
396
+ ),
397
+ Effect.tapError((error) => Effect.log(`Request failed: ${error}`))
398
+ )
399
+ })
400
+
401
+ // ============================================
402
+ // 4. Custom retry policy
403
+ // ============================================
404
+
405
+ const customRetryPolicy = Schedule.exponential("500 millis", 2).pipe(
406
+ Schedule.intersect(Schedule.recurs(5)),
407
+ Schedule.union(Schedule.spaced("30 seconds")), // Also retry after 30s
408
+ Schedule.whileOutput((duration) => Duration.lessThanOrEqualTo(duration, "2 minutes")),
409
+ Schedule.jittered
410
+ )
411
+
412
+ // ============================================
413
+ // 5. Retry respecting Retry-After header
414
+ // ============================================
415
+
416
+ const fetchWithRetryAfter = (url: string) =>
417
+ Effect.gen(function* () {
418
+ const client = yield* HttpClient.HttpClient
419
+
420
+ const makeRequest = client.get(url).pipe(
421
+ Effect.flatMap((response) => {
422
+ if (response.status === 429) {
423
+ const retryAfter = response.headers["retry-after"]
424
+ const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 1000
425
+
426
+ return Effect.fail({
427
+ _tag: "RateLimited" as const,
428
+ delay,
429
+ })
430
+ }
431
+ return Effect.succeed(response)
432
+ })
433
+ )
434
+
435
+ return yield* makeRequest.pipe(
436
+ Effect.retry(
437
+ Schedule.recurWhile<{ _tag: "RateLimited"; delay: number }>(
438
+ (error) => error._tag === "RateLimited"
439
+ ).pipe(
440
+ Schedule.intersect(Schedule.recurs(3)),
441
+ Schedule.delayed((_, error) => Duration.millis(error.delay))
442
+ )
443
+ ),
444
+ Effect.flatMap((r) => HttpClientResponse.json(r))
445
+ )
446
+ })
447
+
448
+ // ============================================
449
+ // 6. Usage
450
+ // ============================================
451
+
452
+ const program = Effect.gen(function* () {
453
+ yield* Effect.log("Fetching with retry...")
454
+
455
+ const data = yield* fetchWithRetry("https://api.example.com/data").pipe(
456
+ Effect.catchAll((error) => {
457
+ return Effect.succeed({ error: "All retries exhausted" })
458
+ })
459
+ )
460
+
461
+ yield* Effect.log(`Result: ${JSON.stringify(data)}`)
462
+ })
463
+ ```
464
+
465
+ **Rationale:**
466
+
467
+ Use Effect's `retry` with `Schedule` to automatically retry failed HTTP requests with exponential backoff and jitter.
468
+
469
+ ---
470
+
471
+
472
+ HTTP requests fail for transient reasons:
473
+
474
+ 1. **Network issues** - Temporary connectivity problems
475
+ 2. **Server overload** - 503 Service Unavailable
476
+ 3. **Rate limits** - 429 Too Many Requests
477
+ 4. **Timeouts** - Slow responses
478
+
479
+ Proper retry logic handles these gracefully.
480
+
481
+ ---
482
+
483
+ ---
484
+
485
+ ### Log HTTP Requests and Responses
486
+
487
+ **Rule:** Use Effect's logging to trace HTTP requests for debugging and monitoring.
488
+
489
+ **Good Example:**
490
+
491
+ ```typescript
492
+ import { Effect, Duration } from "effect"
493
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform"
494
+
495
+ // ============================================
496
+ // 1. Simple request/response logging
497
+ // ============================================
498
+
499
+ const withLogging = <A, E>(
500
+ request: Effect.Effect<A, E, HttpClient.HttpClient>
501
+ ): Effect.Effect<A, E, HttpClient.HttpClient> =>
502
+ Effect.gen(function* () {
503
+ const startTime = Date.now()
504
+ yield* Effect.log("→ HTTP Request starting...")
505
+
506
+ const result = yield* request
507
+
508
+ const duration = Date.now() - startTime
509
+ yield* Effect.log(`← HTTP Response received (${duration}ms)`)
510
+
511
+ return result
512
+ })
513
+
514
+ // ============================================
515
+ // 2. Detailed request logging
516
+ // ============================================
517
+
518
+ interface RequestLog {
519
+ method: string
520
+ url: string
521
+ headers: Record<string, string>
522
+ body?: unknown
523
+ }
524
+
525
+ interface ResponseLog {
526
+ status: number
527
+ headers: Record<string, string>
528
+ duration: number
529
+ size?: number
530
+ }
531
+
532
+ const makeLoggingClient = Effect.gen(function* () {
533
+ const baseClient = yield* HttpClient.HttpClient
534
+
535
+ const logRequest = (method: string, url: string, headers: Record<string, string>) =>
536
+ Effect.log("HTTP Request").pipe(
537
+ Effect.annotateLogs({
538
+ method,
539
+ url,
540
+ headers: JSON.stringify(headers),
541
+ })
542
+ )
543
+
544
+ const logResponse = (status: number, duration: number, headers: Record<string, string>) =>
545
+ Effect.log("HTTP Response").pipe(
546
+ Effect.annotateLogs({
547
+ status: String(status),
548
+ duration: `${duration}ms`,
549
+ headers: JSON.stringify(headers),
550
+ })
551
+ )
552
+
553
+ return {
554
+ get: <T>(url: string, options?: { headers?: Record<string, string> }) =>
555
+ Effect.gen(function* () {
556
+ const headers = options?.headers ?? {}
557
+ yield* logRequest("GET", url, headers)
558
+ const startTime = Date.now()
559
+
560
+ const response = yield* baseClient.get(url)
561
+
562
+ yield* logResponse(
563
+ response.status,
564
+ Date.now() - startTime,
565
+ response.headers
566
+ )
567
+
568
+ return yield* HttpClientResponse.json(response) as Effect.Effect<T>
569
+ }),
570
+
571
+ post: <T>(url: string, body: unknown, options?: { headers?: Record<string, string> }) =>
572
+ Effect.gen(function* () {
573
+ const headers = options?.headers ?? {}
574
+ yield* logRequest("POST", url, headers).pipe(
575
+ Effect.annotateLogs("body", JSON.stringify(body).slice(0, 200))
576
+ )
577
+ const startTime = Date.now()
578
+
579
+ const request = yield* HttpClientRequest.post(url).pipe(
580
+ HttpClientRequest.jsonBody(body)
581
+ )
582
+ const response = yield* baseClient.execute(request)
583
+
584
+ yield* logResponse(
585
+ response.status,
586
+ Date.now() - startTime,
587
+ response.headers
588
+ )
589
+
590
+ return yield* HttpClientResponse.json(response) as Effect.Effect<T>
591
+ }),
592
+ }
593
+ })
594
+
595
+ // ============================================
596
+ // 3. Log with span for timing
597
+ // ============================================
598
+
599
+ const fetchWithSpan = (url: string) =>
600
+ Effect.gen(function* () {
601
+ const client = yield* HttpClient.HttpClient
602
+
603
+ return yield* client.get(url).pipe(
604
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
605
+ Effect.withLogSpan(`HTTP GET ${url}`)
606
+ )
607
+ })
608
+
609
+ // ============================================
610
+ // 4. Conditional logging (debug mode)
611
+ // ============================================
612
+
613
+ const makeConditionalLoggingClient = (debug: boolean) =>
614
+ Effect.gen(function* () {
615
+ const baseClient = yield* HttpClient.HttpClient
616
+
617
+ const maybeLog = (message: string, data?: Record<string, unknown>) =>
618
+ debug
619
+ ? Effect.log(message).pipe(
620
+ data ? Effect.annotateLogs(data) : (e) => e
621
+ )
622
+ : Effect.void
623
+
624
+ return {
625
+ get: <T>(url: string) =>
626
+ Effect.gen(function* () {
627
+ yield* maybeLog("HTTP Request", { method: "GET", url })
628
+ const startTime = Date.now()
629
+
630
+ const response = yield* baseClient.get(url)
631
+
632
+ yield* maybeLog("HTTP Response", {
633
+ status: String(response.status),
634
+ duration: `${Date.now() - startTime}ms`,
635
+ })
636
+
637
+ return yield* HttpClientResponse.json(response) as Effect.Effect<T>
638
+ }),
639
+ }
640
+ })
641
+
642
+ // ============================================
643
+ // 5. Request ID tracking
644
+ // ============================================
645
+
646
+ const makeTrackedClient = Effect.gen(function* () {
647
+ const baseClient = yield* HttpClient.HttpClient
648
+
649
+ return {
650
+ get: <T>(url: string) =>
651
+ Effect.gen(function* () {
652
+ const requestId = crypto.randomUUID().slice(0, 8)
653
+
654
+ yield* Effect.log("HTTP Request").pipe(
655
+ Effect.annotateLogs({
656
+ requestId,
657
+ method: "GET",
658
+ url,
659
+ })
660
+ )
661
+
662
+ const startTime = Date.now()
663
+ const response = yield* baseClient.get(url)
664
+
665
+ yield* Effect.log("HTTP Response").pipe(
666
+ Effect.annotateLogs({
667
+ requestId,
668
+ status: String(response.status),
669
+ duration: `${Date.now() - startTime}ms`,
670
+ })
671
+ )
672
+
673
+ return yield* HttpClientResponse.json(response) as Effect.Effect<T>
674
+ })
675
+ }
676
+ })
677
+
678
+ // ============================================
679
+ // 6. Error logging
680
+ // ============================================
681
+
682
+ const fetchWithErrorLogging = (url: string) =>
683
+ Effect.gen(function* () {
684
+ const client = yield* HttpClient.HttpClient
685
+
686
+ return yield* client.get(url).pipe(
687
+ Effect.flatMap((response) => {
688
+ if (response.status >= 400) {
689
+ return Effect.gen(function* () {
690
+ yield* Effect.logError("HTTP Error").pipe(
691
+ Effect.annotateLogs({
692
+ url,
693
+ status: String(response.status),
694
+ })
695
+ )
696
+ return yield* Effect.fail(new Error(`HTTP ${response.status}`))
697
+ })
698
+ }
699
+ return Effect.succeed(response)
700
+ }),
701
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
702
+ Effect.tapError((error) =>
703
+ Effect.logError("Request failed").pipe(
704
+ Effect.annotateLogs({
705
+ url,
706
+ error: String(error),
707
+ })
708
+ )
709
+ )
710
+ )
711
+ })
712
+
713
+ // ============================================
714
+ // 7. Usage
715
+ // ============================================
716
+
717
+ const program = Effect.gen(function* () {
718
+ const client = yield* makeLoggingClient
719
+
720
+ yield* Effect.log("Starting HTTP operations...")
721
+
722
+ const data = yield* client.get("https://api.example.com/users")
723
+
724
+ yield* Effect.log("Operations complete")
725
+ })
726
+ ```
727
+
728
+ **Rationale:**
729
+
730
+ Wrap HTTP clients with logging middleware to capture request details, response info, and timing for debugging and observability.
731
+
732
+ ---
733
+
734
+
735
+ HTTP logging helps with:
736
+
737
+ 1. **Debugging** - See what's being sent/received
738
+ 2. **Performance** - Track slow requests
739
+ 3. **Auditing** - Record API usage
740
+ 4. **Troubleshooting** - Diagnose production issues
741
+
742
+ ---
743
+
744
+ ---
745
+
746
+ ### Cache HTTP Responses
747
+
748
+ **Rule:** Use an in-memory or persistent cache to store HTTP responses.
749
+
750
+ **Good Example:**
751
+
752
+ ```typescript
753
+ import { Effect, Ref, HashMap, Option, Duration } from "effect"
754
+ import { HttpClient, HttpClientResponse } from "@effect/platform"
755
+
756
+ // ============================================
757
+ // 1. Simple in-memory cache
758
+ // ============================================
759
+
760
+ interface CacheEntry<T> {
761
+ readonly data: T
762
+ readonly timestamp: number
763
+ readonly ttl: number
764
+ }
765
+
766
+ const makeCache = <T>() =>
767
+ Effect.gen(function* () {
768
+ const store = yield* Ref.make(HashMap.empty<string, CacheEntry<T>>())
769
+
770
+ const get = (key: string): Effect.Effect<Option.Option<T>> =>
771
+ Ref.get(store).pipe(
772
+ Effect.map((map) => {
773
+ const entry = HashMap.get(map, key)
774
+ if (entry._tag === "None") return Option.none()
775
+
776
+ const now = Date.now()
777
+ if (now > entry.value.timestamp + entry.value.ttl) {
778
+ return Option.none() // Expired
779
+ }
780
+ return Option.some(entry.value.data)
781
+ })
782
+ )
783
+
784
+ const set = (key: string, data: T, ttl: number): Effect.Effect<void> =>
785
+ Ref.update(store, (map) =>
786
+ HashMap.set(map, key, {
787
+ data,
788
+ timestamp: Date.now(),
789
+ ttl,
790
+ })
791
+ )
792
+
793
+ const invalidate = (key: string): Effect.Effect<void> =>
794
+ Ref.update(store, (map) => HashMap.remove(map, key))
795
+
796
+ const clear = (): Effect.Effect<void> =>
797
+ Ref.set(store, HashMap.empty())
798
+
799
+ return { get, set, invalidate, clear }
800
+ })
801
+
802
+ // ============================================
803
+ // 2. Cached HTTP client
804
+ // ============================================
805
+
806
+ interface CachedHttpClient {
807
+ readonly get: <T>(
808
+ url: string,
809
+ options?: { ttl?: Duration.DurationInput }
810
+ ) => Effect.Effect<T, Error>
811
+ readonly invalidate: (url: string) => Effect.Effect<void>
812
+ }
813
+
814
+ const makeCachedHttpClient = Effect.gen(function* () {
815
+ const httpClient = yield* HttpClient.HttpClient
816
+ const cache = yield* makeCache<unknown>()
817
+
818
+ const client: CachedHttpClient = {
819
+ get: <T>(url: string, options?: { ttl?: Duration.DurationInput }) => {
820
+ const ttl = options?.ttl ? Duration.toMillis(Duration.decode(options.ttl)) : 60000
821
+
822
+ return Effect.gen(function* () {
823
+ // Check cache first
824
+ const cached = yield* cache.get(url)
825
+ if (Option.isSome(cached)) {
826
+ yield* Effect.log(`Cache hit: ${url}`)
827
+ return cached.value as T
828
+ }
829
+
830
+ yield* Effect.log(`Cache miss: ${url}`)
831
+
832
+ // Fetch from network
833
+ const response = yield* httpClient.get(url)
834
+ const data = yield* HttpClientResponse.json(response) as Effect.Effect<T>
835
+
836
+ // Store in cache
837
+ yield* cache.set(url, data, ttl)
838
+
839
+ return data
840
+ })
841
+ },
842
+
843
+ invalidate: (url) => cache.invalidate(url),
844
+ }
845
+
846
+ return client
847
+ })
848
+
849
+ // ============================================
850
+ // 3. Stale-while-revalidate pattern
851
+ // ============================================
852
+
853
+ interface SWRCache<T> {
854
+ readonly data: T
855
+ readonly timestamp: number
856
+ readonly staleAfter: number
857
+ readonly expireAfter: number
858
+ }
859
+
860
+ const makeSWRClient = Effect.gen(function* () {
861
+ const httpClient = yield* HttpClient.HttpClient
862
+ const cache = yield* Ref.make(HashMap.empty<string, SWRCache<unknown>>())
863
+
864
+ return {
865
+ get: <T>(
866
+ url: string,
867
+ options: {
868
+ staleAfter: Duration.DurationInput
869
+ expireAfter: Duration.DurationInput
870
+ }
871
+ ) =>
872
+ Effect.gen(function* () {
873
+ const now = Date.now()
874
+ const staleMs = Duration.toMillis(Duration.decode(options.staleAfter))
875
+ const expireMs = Duration.toMillis(Duration.decode(options.expireAfter))
876
+
877
+ const cached = yield* Ref.get(cache).pipe(
878
+ Effect.map((map) => HashMap.get(map, url))
879
+ )
880
+
881
+ if (cached._tag === "Some") {
882
+ const entry = cached.value
883
+ const age = now - entry.timestamp
884
+
885
+ if (age < staleMs) {
886
+ // Fresh - return immediately
887
+ return entry.data as T
888
+ }
889
+
890
+ if (age < expireMs) {
891
+ // Stale - return cached, revalidate in background
892
+ yield* Effect.fork(
893
+ httpClient.get(url).pipe(
894
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
895
+ Effect.flatMap((data) =>
896
+ Ref.update(cache, (map) =>
897
+ HashMap.set(map, url, {
898
+ data,
899
+ timestamp: Date.now(),
900
+ staleAfter: staleMs,
901
+ expireAfter: expireMs,
902
+ })
903
+ )
904
+ ),
905
+ Effect.catchAll(() => Effect.void) // Ignore errors
906
+ )
907
+ )
908
+ return entry.data as T
909
+ }
910
+ }
911
+
912
+ // Expired or missing - fetch fresh
913
+ const response = yield* httpClient.get(url)
914
+ const data = yield* HttpClientResponse.json(response) as Effect.Effect<T>
915
+
916
+ yield* Ref.update(cache, (map) =>
917
+ HashMap.set(map, url, {
918
+ data,
919
+ timestamp: now,
920
+ staleAfter: staleMs,
921
+ expireAfter: expireMs,
922
+ })
923
+ )
924
+
925
+ return data
926
+ }),
927
+ }
928
+ })
929
+
930
+ // ============================================
931
+ // 4. Cache with request deduplication
932
+ // ============================================
933
+
934
+ const makeDeduplicatedClient = Effect.gen(function* () {
935
+ const httpClient = yield* HttpClient.HttpClient
936
+ const inFlight = yield* Ref.make(HashMap.empty<string, Effect.Effect<unknown>>())
937
+ const cache = yield* makeCache<unknown>()
938
+
939
+ return {
940
+ get: <T>(url: string, ttl: number = 60000) =>
941
+ Effect.gen(function* () {
942
+ // Check cache
943
+ const cached = yield* cache.get(url)
944
+ if (Option.isSome(cached)) {
945
+ return cached.value as T
946
+ }
947
+
948
+ // Check if request already in flight
949
+ const pending = yield* Ref.get(inFlight).pipe(
950
+ Effect.map((map) => HashMap.get(map, url))
951
+ )
952
+
953
+ if (pending._tag === "Some") {
954
+ yield* Effect.log(`Deduplicating request: ${url}`)
955
+ return (yield* pending.value) as T
956
+ }
957
+
958
+ // Make the request
959
+ const request = httpClient.get(url).pipe(
960
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
961
+ Effect.tap((data) => cache.set(url, data, ttl)),
962
+ Effect.ensuring(
963
+ Ref.update(inFlight, (map) => HashMap.remove(map, url))
964
+ )
965
+ )
966
+
967
+ // Store in-flight request
968
+ yield* Ref.update(inFlight, (map) => HashMap.set(map, url, request))
969
+
970
+ return (yield* request) as T
971
+ }),
972
+ }
973
+ })
974
+
975
+ // ============================================
976
+ // 5. Usage
977
+ // ============================================
978
+
979
+ const program = Effect.gen(function* () {
980
+ const client = yield* makeCachedHttpClient
981
+
982
+ // First call - cache miss
983
+ yield* client.get("https://api.example.com/users/1", { ttl: "5 minutes" })
984
+
985
+ // Second call - cache hit
986
+ yield* client.get("https://api.example.com/users/1")
987
+
988
+ // Invalidate when data changes
989
+ yield* client.invalidate("https://api.example.com/users/1")
990
+ })
991
+ ```
992
+
993
+ **Rationale:**
994
+
995
+ Cache HTTP responses to reduce network calls, improve latency, and handle offline scenarios.
996
+
997
+ ---
998
+
999
+
1000
+ Caching provides:
1001
+
1002
+ 1. **Performance** - Avoid redundant network calls
1003
+ 2. **Cost reduction** - Fewer API calls
1004
+ 3. **Resilience** - Serve stale data when API is down
1005
+ 4. **Rate limit safety** - Stay under quotas
1006
+
1007
+ ---
1008
+
1009
+ ---
1010
+
1011
+ ### Add Timeouts to HTTP Requests
1012
+
1013
+ **Rule:** Always set timeouts on HTTP requests to ensure your application doesn't hang.
1014
+
1015
+ **Good Example:**
1016
+
1017
+ ```typescript
1018
+ import { Effect, Duration, Data } from "effect"
1019
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform"
1020
+
1021
+ // ============================================
1022
+ // 1. Basic request timeout
1023
+ // ============================================
1024
+
1025
+ const fetchWithTimeout = (url: string, timeout: Duration.DurationInput) =>
1026
+ Effect.gen(function* () {
1027
+ const client = yield* HttpClient.HttpClient
1028
+
1029
+ return yield* client.get(url).pipe(
1030
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
1031
+ Effect.timeout(timeout)
1032
+ )
1033
+ // Returns Option<A> - None if timed out
1034
+ })
1035
+
1036
+ // ============================================
1037
+ // 2. Timeout with custom error
1038
+ // ============================================
1039
+
1040
+ class RequestTimeoutError extends Data.TaggedError("RequestTimeoutError")<{
1041
+ readonly url: string
1042
+ readonly timeout: Duration.Duration
1043
+ }> {}
1044
+
1045
+ const fetchWithTimeoutError = (url: string, timeout: Duration.DurationInput) =>
1046
+ Effect.gen(function* () {
1047
+ const client = yield* HttpClient.HttpClient
1048
+
1049
+ return yield* client.get(url).pipe(
1050
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
1051
+ Effect.timeoutFail({
1052
+ duration: timeout,
1053
+ onTimeout: () => new RequestTimeoutError({
1054
+ url,
1055
+ timeout: Duration.decode(timeout),
1056
+ }),
1057
+ })
1058
+ )
1059
+ })
1060
+
1061
+ // ============================================
1062
+ // 3. Different timeouts for different phases
1063
+ // ============================================
1064
+
1065
+ const fetchWithPhasedTimeouts = (url: string) =>
1066
+ Effect.gen(function* () {
1067
+ const client = yield* HttpClient.HttpClient
1068
+
1069
+ // Connection timeout (initial)
1070
+ const response = yield* client.get(url).pipe(
1071
+ Effect.timeout("5 seconds"),
1072
+ Effect.flatten,
1073
+ Effect.mapError(() => new Error("Connection timeout"))
1074
+ )
1075
+
1076
+ // Read timeout (body)
1077
+ const body = yield* HttpClientResponse.text(response).pipe(
1078
+ Effect.timeout("30 seconds"),
1079
+ Effect.flatten,
1080
+ Effect.mapError(() => new Error("Read timeout"))
1081
+ )
1082
+
1083
+ return body
1084
+ })
1085
+
1086
+ // ============================================
1087
+ // 4. Timeout with fallback
1088
+ // ============================================
1089
+
1090
+ interface ApiResponse {
1091
+ data: unknown
1092
+ cached: boolean
1093
+ }
1094
+
1095
+ const fetchWithFallback = (url: string): Effect.Effect<ApiResponse> =>
1096
+ Effect.gen(function* () {
1097
+ const client = yield* HttpClient.HttpClient
1098
+
1099
+ return yield* client.get(url).pipe(
1100
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
1101
+ Effect.map((data) => ({ data, cached: false })),
1102
+ Effect.timeout("5 seconds"),
1103
+ Effect.flatMap((result) =>
1104
+ result._tag === "Some"
1105
+ ? Effect.succeed(result.value)
1106
+ : Effect.succeed({ data: null, cached: true }) // Fallback
1107
+ )
1108
+ )
1109
+ })
1110
+
1111
+ // ============================================
1112
+ // 5. Timeout with interrupt
1113
+ // ============================================
1114
+
1115
+ const fetchWithInterrupt = (url: string) =>
1116
+ Effect.gen(function* () {
1117
+ const client = yield* HttpClient.HttpClient
1118
+
1119
+ return yield* client.get(url).pipe(
1120
+ Effect.flatMap((r) => HttpClientResponse.json(r)),
1121
+ Effect.interruptible,
1122
+ Effect.timeout("10 seconds")
1123
+ )
1124
+ // Fiber is interrupted if timeout, freeing resources
1125
+ })
1126
+
1127
+ // ============================================
1128
+ // 6. Configurable timeout wrapper
1129
+ // ============================================
1130
+
1131
+ interface TimeoutConfig {
1132
+ readonly connect: Duration.DurationInput
1133
+ readonly read: Duration.DurationInput
1134
+ readonly total: Duration.DurationInput
1135
+ }
1136
+
1137
+ const defaultTimeouts: TimeoutConfig = {
1138
+ connect: "5 seconds",
1139
+ read: "30 seconds",
1140
+ total: "60 seconds",
1141
+ }
1142
+
1143
+ const createHttpClient = (config: TimeoutConfig = defaultTimeouts) =>
1144
+ Effect.gen(function* () {
1145
+ const baseClient = yield* HttpClient.HttpClient
1146
+
1147
+ return {
1148
+ get: (url: string) =>
1149
+ baseClient.get(url).pipe(
1150
+ Effect.timeout(config.connect),
1151
+ Effect.flatten,
1152
+ Effect.flatMap((r) =>
1153
+ HttpClientResponse.json(r).pipe(
1154
+ Effect.timeout(config.read),
1155
+ Effect.flatten
1156
+ )
1157
+ ),
1158
+ Effect.timeout(config.total),
1159
+ Effect.flatten
1160
+ ),
1161
+ }
1162
+ })
1163
+
1164
+ // ============================================
1165
+ // 7. Usage
1166
+ // ============================================
1167
+
1168
+ const program = Effect.gen(function* () {
1169
+ yield* Effect.log("Fetching with timeout...")
1170
+
1171
+ const result = yield* fetchWithTimeoutError(
1172
+ "https://api.example.com/slow",
1173
+ "5 seconds"
1174
+ ).pipe(
1175
+ Effect.catchTag("RequestTimeoutError", (error) =>
1176
+ Effect.gen(function* () {
1177
+ yield* Effect.log(`Request to ${error.url} timed out`)
1178
+ return { error: "timeout" }
1179
+ })
1180
+ )
1181
+ )
1182
+
1183
+ yield* Effect.log(`Result: ${JSON.stringify(result)}`)
1184
+ })
1185
+ ```
1186
+
1187
+ **Rationale:**
1188
+
1189
+ Use Effect's timeout functions to set limits on HTTP request duration, with appropriate fallback handling.
1190
+
1191
+ ---
1192
+
1193
+
1194
+ HTTP requests can hang indefinitely:
1195
+
1196
+ 1. **Server issues** - Unresponsive servers
1197
+ 2. **Network problems** - Packets lost
1198
+ 3. **Slow responses** - Large payloads
1199
+ 4. **Resource leaks** - Connections never closed
1200
+
1201
+ Timeouts prevent these from blocking your application.
1202
+
1203
+ ---
1204
+
1205
+ ---
1206
+
1207
+ ### Model Dependencies as Services
1208
+
1209
+ **Rule:** Model dependencies as services.
1210
+
1211
+ **Good Example:**
1212
+
1213
+ ```typescript
1214
+ import { Effect } from "effect";
1215
+
1216
+ // Define Random service with production implementation as default
1217
+ export class Random extends Effect.Service<Random>()("Random", {
1218
+ // Default production implementation
1219
+ sync: () => ({
1220
+ next: Effect.sync(() => Math.random()),
1221
+ }),
1222
+ }) {}
1223
+
1224
+ // Example usage
1225
+ const program = Effect.gen(function* () {
1226
+ const random = yield* Random;
1227
+ const value = yield* random.next;
1228
+ return value;
1229
+ });
1230
+
1231
+ // Run with default implementation
1232
+ const programWithLogging = Effect.gen(function* () {
1233
+ const value = yield* Effect.provide(program, Random.Default);
1234
+ yield* Effect.log(`Random value: ${value}`);
1235
+ return value;
1236
+ });
1237
+
1238
+ Effect.runPromise(programWithLogging);
1239
+ ```
1240
+
1241
+ **Explanation:**
1242
+ By modeling dependencies as services, you can easily substitute mocked or deterministic implementations for testing, leading to more reliable and predictable tests.
1243
+
1244
+ **Anti-Pattern:**
1245
+
1246
+ Directly calling external APIs like `fetch` or using impure functions like `Math.random()` within your business logic. This tightly couples your logic to a specific implementation and makes it difficult to test.
1247
+
1248
+ **Rationale:**
1249
+
1250
+ Represent any external dependency or distinct capability—from a database client to a simple UUID generator—as a service.
1251
+
1252
+
1253
+ This pattern is the key to testability. It allows you to provide a `Live` implementation in production and a `Test` implementation (returning mock data) in your tests, making your code decoupled and reliable.
1254
+
1255
+ ---
1256
+
1257
+ ### Create a Testable HTTP Client Service
1258
+
1259
+ **Rule:** Define an HttpClient service with distinct Live and Test layers to enable testable API interactions.
1260
+
1261
+ **Good Example:**
1262
+
1263
+ ### 1. Define the Service
1264
+
1265
+ ```typescript
1266
+ import { Effect, Data, Layer } from "effect";
1267
+
1268
+ interface HttpErrorType {
1269
+ readonly _tag: "HttpError";
1270
+ readonly error: unknown;
1271
+ }
1272
+
1273
+ const HttpError = Data.tagged<HttpErrorType>("HttpError");
1274
+
1275
+ interface HttpClientType {
1276
+ readonly get: <T>(url: string) => Effect.Effect<T, HttpErrorType>;
1277
+ }
1278
+
1279
+ class HttpClient extends Effect.Service<HttpClientType>()("HttpClient", {
1280
+ sync: () => ({
1281
+ get: <T>(url: string): Effect.Effect<T, HttpErrorType> =>
1282
+ Effect.tryPromise<T>(() =>
1283
+ fetch(url).then((res) => res.json() as T)
1284
+ ).pipe(Effect.catchAll((error) => Effect.fail(HttpError({ error })))),
1285
+ }),
1286
+ }) {}
1287
+
1288
+ // Test implementation
1289
+ const TestLayer = Layer.succeed(
1290
+ HttpClient,
1291
+ HttpClient.of({
1292
+ get: <T>(_url: string) => Effect.succeed({ title: "Mock Data" } as T),
1293
+ })
1294
+ );
1295
+
1296
+ // Example usage
1297
+ const program = Effect.gen(function* () {
1298
+ const client = yield* HttpClient;
1299
+ yield* Effect.logInfo("Fetching data...");
1300
+ const data = yield* client.get<{ title: string }>(
1301
+ "https://api.example.com/data"
1302
+ );
1303
+ yield* Effect.logInfo(`Received data: ${JSON.stringify(data)}`);
1304
+ });
1305
+
1306
+ // Run with test implementation
1307
+ Effect.runPromise(Effect.provide(program, TestLayer));
1308
+ ```
1309
+
1310
+ ### 2. Create the Live Implementation
1311
+
1312
+ ```typescript
1313
+ import { Effect, Data, Layer } from "effect";
1314
+
1315
+ interface HttpErrorType {
1316
+ readonly _tag: "HttpError";
1317
+ readonly error: unknown;
1318
+ }
1319
+
1320
+ const HttpError = Data.tagged<HttpErrorType>("HttpError");
1321
+
1322
+ interface HttpClientType {
1323
+ readonly get: <T>(url: string) => Effect.Effect<T, HttpErrorType>;
1324
+ }
1325
+
1326
+ class HttpClient extends Effect.Service<HttpClientType>()("HttpClient", {
1327
+ sync: () => ({
1328
+ get: <T>(url: string): Effect.Effect<T, HttpErrorType> =>
1329
+ Effect.tryPromise({
1330
+ try: () => fetch(url).then((res) => res.json()),
1331
+ catch: (error) => HttpError({ error }),
1332
+ }),
1333
+ }),
1334
+ }) {}
1335
+
1336
+ // Test implementation
1337
+ const TestLayer = Layer.succeed(
1338
+ HttpClient,
1339
+ HttpClient.of({
1340
+ get: <T>(_url: string) => Effect.succeed({ title: "Mock Data" } as T),
1341
+ })
1342
+ );
1343
+
1344
+ // Example usage
1345
+ const program = Effect.gen(function* () {
1346
+ const client = yield* HttpClient;
1347
+ yield* Effect.logInfo("Fetching data...");
1348
+ const data = yield* client.get<{ title: string }>(
1349
+ "https://api.example.com/data"
1350
+ );
1351
+ yield* Effect.logInfo(`Received data: ${JSON.stringify(data)}`);
1352
+ });
1353
+
1354
+ // Run with test implementation
1355
+ Effect.runPromise(Effect.provide(program, TestLayer));
1356
+ ```
1357
+
1358
+ ### 3. Create the Test Implementation
1359
+
1360
+ ```typescript
1361
+ // src/services/HttpClientTest.ts
1362
+ import { Effect, Layer } from "effect";
1363
+ import { HttpClient } from "./HttpClient";
1364
+
1365
+ export const HttpClientTest = Layer.succeed(
1366
+ HttpClient,
1367
+ HttpClient.of({
1368
+ get: (url) => Effect.succeed({ mock: "data", url }),
1369
+ })
1370
+ );
1371
+ ```
1372
+
1373
+ ### 4. Usage in Business Logic
1374
+
1375
+ Your business logic is now clean and only depends on the abstract `HttpClient`.
1376
+
1377
+ ```typescript
1378
+ // src/features/User/UserService.ts
1379
+ import { Effect } from "effect";
1380
+ import { HttpClient } from "../../services/HttpClient";
1381
+
1382
+ export const getUserFromApi = (id: number) =>
1383
+ Effect.gen(function* () {
1384
+ const client = yield* HttpClient;
1385
+ const data = yield* client.get(`https://api.example.com/users/${id}`);
1386
+ // ... logic to parse and return user
1387
+ return data;
1388
+ });
1389
+ ```
1390
+
1391
+ ---
1392
+
1393
+ **Anti-Pattern:**
1394
+
1395
+ Calling `fetch` directly from within your business logic functions. This creates a hard dependency on the global `fetch` API, making the function difficult to test and reuse.
1396
+
1397
+ ```typescript
1398
+ import { Effect } from "effect";
1399
+
1400
+ // ❌ WRONG: This function is not easily testable.
1401
+ export const getUserDirectly = (id: number) =>
1402
+ Effect.tryPromise({
1403
+ try: () =>
1404
+ fetch(`https://api.example.com/users/${id}`).then((res) => res.json()),
1405
+ catch: () => "ApiError" as const,
1406
+ });
1407
+ ```
1408
+
1409
+ **Rationale:**
1410
+
1411
+ To interact with external APIs, define an `HttpClient` service. Create two separate `Layer` implementations for this service:
1412
+
1413
+ 1. **`HttpClientLive`**: The production implementation that uses a real HTTP client (like `fetch`) to make network requests.
1414
+ 2. **`HttpClientTest`**: A test implementation that returns mock data, allowing you to test your business logic without making actual network calls.
1415
+
1416
+ ---
1417
+
1418
+
1419
+ Directly using `fetch` in your business logic makes it nearly impossible to test. Your tests would become slow, flaky (dependent on network conditions), and could have unintended side effects.
1420
+
1421
+ By abstracting the HTTP client into a service, you decouple your application's logic from the specific implementation of how HTTP requests are made. Your business logic depends only on the abstract `HttpClient` interface. In production, you provide the `Live` layer. In tests, you provide the `Test` layer. This makes your tests fast, deterministic, and reliable.
1422
+
1423
+ ---
1424
+
1425
+ ---
1426
+
1427
+ ### Handle Rate Limiting Responses
1428
+
1429
+ **Rule:** Detect 429 responses and automatically retry after the Retry-After period.
1430
+
1431
+ **Good Example:**
1432
+
1433
+ ```typescript
1434
+ import { Effect, Schedule, Duration, Data, Ref } from "effect"
1435
+ import { HttpClient, HttpClientResponse } from "@effect/platform"
1436
+
1437
+ // ============================================
1438
+ // 1. Rate limit error type
1439
+ // ============================================
1440
+
1441
+ class RateLimitedError extends Data.TaggedError("RateLimitedError")<{
1442
+ readonly retryAfter: number
1443
+ readonly limit: number | undefined
1444
+ readonly remaining: number | undefined
1445
+ readonly reset: number | undefined
1446
+ }> {}
1447
+
1448
+ // ============================================
1449
+ // 2. Parse rate limit headers
1450
+ // ============================================
1451
+
1452
+ interface RateLimitInfo {
1453
+ readonly retryAfter: number
1454
+ readonly limit?: number
1455
+ readonly remaining?: number
1456
+ readonly reset?: number
1457
+ }
1458
+
1459
+ const parseRateLimitHeaders = (headers: Record<string, string>): RateLimitInfo => {
1460
+ // Parse Retry-After (seconds or date)
1461
+ const retryAfterHeader = headers["retry-after"]
1462
+ let retryAfter = 60 // Default 60 seconds
1463
+
1464
+ if (retryAfterHeader) {
1465
+ const parsed = parseInt(retryAfterHeader, 10)
1466
+ if (!isNaN(parsed)) {
1467
+ retryAfter = parsed
1468
+ } else {
1469
+ // Try parsing as date
1470
+ const date = Date.parse(retryAfterHeader)
1471
+ if (!isNaN(date)) {
1472
+ retryAfter = Math.max(0, Math.ceil((date - Date.now()) / 1000))
1473
+ }
1474
+ }
1475
+ }
1476
+
1477
+ return {
1478
+ retryAfter,
1479
+ limit: headers["x-ratelimit-limit"] ? parseInt(headers["x-ratelimit-limit"], 10) : undefined,
1480
+ remaining: headers["x-ratelimit-remaining"] ? parseInt(headers["x-ratelimit-remaining"], 10) : undefined,
1481
+ reset: headers["x-ratelimit-reset"] ? parseInt(headers["x-ratelimit-reset"], 10) : undefined,
1482
+ }
1483
+ }
1484
+
1485
+ // ============================================
1486
+ // 3. HTTP client with rate limit handling
1487
+ // ============================================
1488
+
1489
+ const makeRateLimitAwareClient = Effect.gen(function* () {
1490
+ const httpClient = yield* HttpClient.HttpClient
1491
+
1492
+ return {
1493
+ get: <T>(url: string) =>
1494
+ Effect.gen(function* () {
1495
+ const response = yield* httpClient.get(url)
1496
+
1497
+ if (response.status === 429) {
1498
+ const rateLimitInfo = parseRateLimitHeaders(response.headers)
1499
+
1500
+ yield* Effect.log(
1501
+ `Rate limited. Retry after ${rateLimitInfo.retryAfter}s`
1502
+ )
1503
+
1504
+ return yield* Effect.fail(new RateLimitedError({
1505
+ retryAfter: rateLimitInfo.retryAfter,
1506
+ limit: rateLimitInfo.limit,
1507
+ remaining: rateLimitInfo.remaining,
1508
+ reset: rateLimitInfo.reset,
1509
+ }))
1510
+ }
1511
+
1512
+ return yield* HttpClientResponse.json(response) as Effect.Effect<T>
1513
+ }).pipe(
1514
+ Effect.retry({
1515
+ schedule: Schedule.recurWhile<RateLimitedError>(
1516
+ (e) => e._tag === "RateLimitedError"
1517
+ ).pipe(
1518
+ Schedule.intersect(Schedule.recurs(3)),
1519
+ Schedule.delayed((_, error) =>
1520
+ Duration.seconds(error.retryAfter + 1) // Add 1s buffer
1521
+ )
1522
+ ),
1523
+ while: (error) => error._tag === "RateLimitedError",
1524
+ })
1525
+ ),
1526
+ }
1527
+ })
1528
+
1529
+ // ============================================
1530
+ // 4. Proactive rate limiting (client-side)
1531
+ // ============================================
1532
+
1533
+ interface RateLimiter {
1534
+ readonly acquire: () => Effect.Effect<void>
1535
+ readonly release: () => Effect.Effect<void>
1536
+ }
1537
+
1538
+ const makeClientRateLimiter = (requestsPerSecond: number) =>
1539
+ Effect.gen(function* () {
1540
+ const tokens = yield* Ref.make(requestsPerSecond)
1541
+ const interval = 1000 / requestsPerSecond
1542
+
1543
+ // Refill tokens periodically
1544
+ yield* Effect.fork(
1545
+ Effect.forever(
1546
+ Effect.gen(function* () {
1547
+ yield* Effect.sleep(Duration.millis(interval))
1548
+ yield* Ref.update(tokens, (n) => Math.min(n + 1, requestsPerSecond))
1549
+ })
1550
+ )
1551
+ )
1552
+
1553
+ const limiter: RateLimiter = {
1554
+ acquire: () =>
1555
+ Effect.gen(function* () {
1556
+ let acquired = false
1557
+ while (!acquired) {
1558
+ const current = yield* Ref.get(tokens)
1559
+ if (current > 0) {
1560
+ yield* Ref.update(tokens, (n) => n - 1)
1561
+ acquired = true
1562
+ } else {
1563
+ yield* Effect.sleep(Duration.millis(interval))
1564
+ }
1565
+ }
1566
+ }),
1567
+
1568
+ release: () => Ref.update(tokens, (n) => Math.min(n + 1, requestsPerSecond)),
1569
+ }
1570
+
1571
+ return limiter
1572
+ })
1573
+
1574
+ // ============================================
1575
+ // 5. Combined client
1576
+ // ============================================
1577
+
1578
+ const makeRobustHttpClient = (requestsPerSecond: number) =>
1579
+ Effect.gen(function* () {
1580
+ const httpClient = yield* HttpClient.HttpClient
1581
+ const rateLimiter = yield* makeClientRateLimiter(requestsPerSecond)
1582
+
1583
+ return {
1584
+ get: <T>(url: string) =>
1585
+ Effect.gen(function* () {
1586
+ // Wait for rate limiter token
1587
+ yield* rateLimiter.acquire()
1588
+
1589
+ const response = yield* httpClient.get(url)
1590
+
1591
+ if (response.status === 429) {
1592
+ const info = parseRateLimitHeaders(response.headers)
1593
+ yield* Effect.log(`Server rate limit hit, waiting ${info.retryAfter}s`)
1594
+ yield* Effect.sleep(Duration.seconds(info.retryAfter))
1595
+ return yield* Effect.fail(new Error("Rate limited"))
1596
+ }
1597
+
1598
+ return yield* HttpClientResponse.json(response) as Effect.Effect<T>
1599
+ }).pipe(
1600
+ Effect.retry(
1601
+ Schedule.exponential("1 second").pipe(
1602
+ Schedule.intersect(Schedule.recurs(3))
1603
+ )
1604
+ )
1605
+ ),
1606
+ }
1607
+ })
1608
+
1609
+ // ============================================
1610
+ // 6. Batch requests to stay under limits
1611
+ // ============================================
1612
+
1613
+ const batchRequests = <T>(
1614
+ urls: string[],
1615
+ requestsPerSecond: number
1616
+ ) =>
1617
+ Effect.gen(function* () {
1618
+ const httpClient = yield* HttpClient.HttpClient
1619
+ const results: T[] = []
1620
+ const interval = 1000 / requestsPerSecond
1621
+
1622
+ for (const url of urls) {
1623
+ const response = yield* httpClient.get(url)
1624
+ const data = yield* HttpClientResponse.json(response) as Effect.Effect<T>
1625
+ results.push(data)
1626
+
1627
+ // Wait between requests
1628
+ if (urls.indexOf(url) < urls.length - 1) {
1629
+ yield* Effect.sleep(Duration.millis(interval))
1630
+ }
1631
+ }
1632
+
1633
+ return results
1634
+ })
1635
+
1636
+ // ============================================
1637
+ // 7. Usage
1638
+ // ============================================
1639
+
1640
+ const program = Effect.gen(function* () {
1641
+ const client = yield* makeRateLimitAwareClient
1642
+
1643
+ yield* Effect.log("Making rate-limited request...")
1644
+
1645
+ const data = yield* client.get("https://api.example.com/data").pipe(
1646
+ Effect.catchTag("RateLimitedError", (error) =>
1647
+ Effect.gen(function* () {
1648
+ yield* Effect.log(`Gave up after rate limiting. Limit: ${error.limit}`)
1649
+ return { error: "rate_limited" }
1650
+ })
1651
+ )
1652
+ )
1653
+
1654
+ yield* Effect.log(`Result: ${JSON.stringify(data)}`)
1655
+ })
1656
+ ```
1657
+
1658
+ **Rationale:**
1659
+
1660
+ Handle HTTP 429 (Too Many Requests) responses by reading the `Retry-After` header and waiting before retrying.
1661
+
1662
+ ---
1663
+
1664
+
1665
+ Rate limits protect APIs:
1666
+
1667
+ 1. **Fair usage** - Share resources among clients
1668
+ 2. **Stability** - Prevent overload
1669
+ 3. **Quotas** - Enforce billing tiers
1670
+
1671
+ Respecting limits prevents bans and ensures reliable access.
1672
+
1673
+ ---
1674
+
1675
+ ---
1676
+
1677
+
1678
+ ## 🟠 Advanced Patterns
1679
+
1680
+ ### Build a Basic HTTP Server
1681
+
1682
+ **Rule:** Use a managed Runtime created from a Layer to handle requests in a Node.js HTTP server.
1683
+
1684
+ **Good Example:**
1685
+
1686
+ This example creates a simple server with a `Greeter` service. The server starts, creates a runtime containing the `Greeter`, and then uses that runtime to handle requests.
1687
+
1688
+ ```typescript
1689
+ import { HttpServer, HttpServerResponse } from "@effect/platform";
1690
+ import { NodeHttpServer } from "@effect/platform-node";
1691
+ import { Duration, Effect, Fiber, Layer } from "effect";
1692
+ import { createServer } from "node:http";
1693
+
1694
+ // Create a server layer using Node's built-in HTTP server
1695
+ const ServerLive = NodeHttpServer.layer(() => createServer(), { port: 3001 });
1696
+
1697
+ // Define your HTTP app (here responding "Hello World" to every request)
1698
+ const app = Effect.gen(function* () {
1699
+ yield* Effect.logInfo("Received HTTP request");
1700
+ return yield* HttpServerResponse.text("Hello World");
1701
+ });
1702
+
1703
+ const serverLayer = HttpServer.serve(app).pipe(Layer.provide(ServerLive));
1704
+
1705
+ const program = Effect.gen(function* () {
1706
+ yield* Effect.logInfo("Server starting on http://localhost:3001");
1707
+ const fiber = yield* Layer.launch(serverLayer).pipe(Effect.fork);
1708
+ yield* Effect.sleep(Duration.seconds(2));
1709
+ yield* Fiber.interrupt(fiber);
1710
+ yield* Effect.logInfo("Server shutdown complete");
1711
+ });
1712
+
1713
+ Effect.runPromise(program as unknown as Effect.Effect<void, unknown, never>);
1714
+ ```
1715
+
1716
+ ---
1717
+
1718
+ **Anti-Pattern:**
1719
+
1720
+ Creating a new runtime or rebuilding layers for every single incoming request. This is extremely inefficient and defeats the purpose of Effect's `Layer` system.
1721
+
1722
+ ```typescript
1723
+ import * as http from "http";
1724
+ import { Effect, Layer } from "effect";
1725
+ import { GreeterLive } from "./somewhere";
1726
+
1727
+ // ❌ WRONG: This rebuilds the GreeterLive layer on every request.
1728
+ const server = http.createServer((_req, res) => {
1729
+ const requestEffect = Effect.succeed("Hello!").pipe(
1730
+ Effect.provide(GreeterLive) // Providing the layer here is inefficient
1731
+ );
1732
+ Effect.runPromise(requestEffect).then((msg) => res.end(msg));
1733
+ });
1734
+ ```
1735
+
1736
+ **Rationale:**
1737
+
1738
+ To build an HTTP server, create a main `AppLayer` that provides all your application's services. Compile this layer into a managed `Runtime` at startup. Use this runtime to execute an `Effect` for each incoming HTTP request, ensuring all logic is handled within the Effect system.
1739
+
1740
+ ---
1741
+
1742
+
1743
+ This pattern demonstrates the complete lifecycle of a long-running Effect application.
1744
+
1745
+ 1. **Setup Phase:** You define all your application's dependencies (database connections, clients, config) in `Layer`s and compose them into a single `AppLayer`.
1746
+ 2. **Runtime Creation:** You use `Layer.toRuntime(AppLayer)` to create a highly-optimized `Runtime` object. This is done _once_ when the server starts.
1747
+ 3. **Request Handling:** For each incoming request, you create an `Effect` that describes the work to be done (e.g., parse request, call services, create response).
1748
+ 4. **Execution:** You use the `Runtime` you created in the setup phase to execute the request-handling `Effect` using `Runtime.runPromise`.
1749
+
1750
+ This architecture ensures that your request handling logic is fully testable, benefits from structured concurrency, and is completely decoupled from the server's setup and infrastructure.
1751
+
1752
+ ---
1753
+
1754
+ ---
1755
+
1756
+