@effect/platform 0.53.11 → 0.53.13

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.
package/README.md CHANGED
@@ -26,35 +26,35 @@ The `@effect/platform/Terminal` module exports a single `Terminal` tag, which se
26
26
  ## Writing to standard output
27
27
 
28
28
  ```ts
29
- import { Terminal } from "@effect/platform";
30
- import { NodeRuntime, NodeTerminal } from "@effect/platform-node";
31
- import { Effect } from "effect";
29
+ import { Terminal } from "@effect/platform"
30
+ import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
31
+ import { Effect } from "effect"
32
32
 
33
33
  // const displayMessage: Effect.Effect<void, PlatformError, Terminal.Terminal>
34
34
  const displayMessage = Effect.gen(function* (_) {
35
- const terminal = yield* _(Terminal.Terminal);
36
- yield* _(terminal.display("a message\n"));
37
- });
35
+ const terminal = yield* _(Terminal.Terminal)
36
+ yield* _(terminal.display("a message\n"))
37
+ })
38
38
 
39
- NodeRuntime.runMain(displayMessage.pipe(Effect.provide(NodeTerminal.layer)));
39
+ NodeRuntime.runMain(displayMessage.pipe(Effect.provide(NodeTerminal.layer)))
40
40
  // Output: "a message"
41
41
  ```
42
42
 
43
43
  ## Reading from standard input
44
44
 
45
45
  ```ts
46
- import { Terminal } from "@effect/platform";
47
- import { NodeRuntime, NodeTerminal } from "@effect/platform-node";
48
- import { Console, Effect } from "effect";
46
+ import { Terminal } from "@effect/platform"
47
+ import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
48
+ import { Console, Effect } from "effect"
49
49
 
50
50
  // const readLine: Effect.Effect<void, Terminal.QuitException, Terminal.Terminal>
51
51
  const readLine = Effect.gen(function* (_) {
52
- const terminal = yield* _(Terminal.Terminal);
53
- const input = yield* _(terminal.readLine);
54
- yield* _(Console.log(`input: ${input}`));
55
- });
52
+ const terminal = yield* _(Terminal.Terminal)
53
+ const input = yield* _(terminal.readLine)
54
+ yield* _(Console.log(`input: ${input}`))
55
+ })
56
56
 
57
- NodeRuntime.runMain(readLine.pipe(Effect.provide(NodeTerminal.layer)));
57
+ NodeRuntime.runMain(readLine.pipe(Effect.provide(NodeTerminal.layer)))
58
58
  // Input: "hello"
59
59
  // Output: "input: hello"
60
60
  ```
@@ -62,42 +62,42 @@ NodeRuntime.runMain(readLine.pipe(Effect.provide(NodeTerminal.layer)));
62
62
  These simple examples illustrate how to utilize the `Terminal` module for handling standard input and output in your programs. Let's use this knowledge to build a number guessing game:
63
63
 
64
64
  ```ts
65
- import { Terminal } from "@effect/platform";
66
- import type { PlatformError } from "@effect/platform/Error";
67
- import { Effect, Option, Random } from "effect";
65
+ import { Terminal } from "@effect/platform"
66
+ import type { PlatformError } from "@effect/platform/Error"
67
+ import { Effect, Option, Random } from "effect"
68
68
 
69
- export const secret = Random.nextIntBetween(1, 100);
69
+ export const secret = Random.nextIntBetween(1, 100)
70
70
 
71
71
  const parseGuess = (input: string) => {
72
- const n = parseInt(input, 10);
73
- return isNaN(n) || n < 1 || n > 100 ? Option.none() : Option.some(n);
74
- };
72
+ const n = parseInt(input, 10)
73
+ return isNaN(n) || n < 1 || n > 100 ? Option.none() : Option.some(n)
74
+ }
75
75
 
76
76
  const display = (message: string) =>
77
77
  Effect.gen(function* (_) {
78
- const terminal = yield* _(Terminal.Terminal);
79
- yield* _(terminal.display(`${message}\n`));
80
- });
78
+ const terminal = yield* _(Terminal.Terminal)
79
+ yield* _(terminal.display(`${message}\n`))
80
+ })
81
81
 
82
82
  const prompt = Effect.gen(function* (_) {
83
- const terminal = yield* _(Terminal.Terminal);
84
- yield* _(terminal.display("Enter a guess: "));
85
- return yield* _(terminal.readLine);
86
- });
83
+ const terminal = yield* _(Terminal.Terminal)
84
+ yield* _(terminal.display("Enter a guess: "))
85
+ return yield* _(terminal.readLine)
86
+ })
87
87
 
88
88
  const answer: Effect.Effect<
89
89
  number,
90
90
  Terminal.QuitException | PlatformError,
91
91
  Terminal.Terminal
92
92
  > = Effect.gen(function* (_) {
93
- const input = yield* _(prompt);
94
- const guess = parseGuess(input);
93
+ const input = yield* _(prompt)
94
+ const guess = parseGuess(input)
95
95
  if (Option.isNone(guess)) {
96
- yield* _(display("You must enter an integer from 1 to 100"));
97
- return yield* _(answer);
96
+ yield* _(display("You must enter an integer from 1 to 100"))
97
+ return yield* _(answer)
98
98
  }
99
- return guess.value;
100
- });
99
+ return guess.value
100
+ })
101
101
 
102
102
  const check = <A, E, R>(
103
103
  secret: number,
@@ -107,17 +107,17 @@ const check = <A, E, R>(
107
107
  ): Effect.Effect<A, E | PlatformError, R | Terminal.Terminal> =>
108
108
  Effect.gen(function* (_) {
109
109
  if (guess > secret) {
110
- yield* _(display("Too high"));
111
- return yield* _(ko);
110
+ yield* _(display("Too high"))
111
+ return yield* _(ko)
112
112
  } else if (guess < secret) {
113
- yield* _(display("Too low"));
114
- return yield* _(ko);
113
+ yield* _(display("Too low"))
114
+ return yield* _(ko)
115
115
  } else {
116
- return yield* _(ok);
116
+ return yield* _(ok)
117
117
  }
118
- });
118
+ })
119
119
 
120
- const end = display("You guessed it!");
120
+ const end = display("You guessed it!")
121
121
 
122
122
  const loop = (
123
123
  secret: number
@@ -127,7 +127,7 @@ const loop = (
127
127
  Terminal.Terminal
128
128
  > =>
129
129
  Effect.gen(function* (_) {
130
- const guess = yield* _(answer);
130
+ const guess = yield* _(answer)
131
131
  return yield* _(
132
132
  check(
133
133
  secret,
@@ -135,37 +135,37 @@ const loop = (
135
135
  end,
136
136
  Effect.suspend(() => loop(secret))
137
137
  )
138
- );
139
- });
138
+ )
139
+ })
140
140
 
141
141
  export const game = Effect.gen(function* (_) {
142
142
  yield* _(
143
143
  display(
144
144
  "We have selected a random number between 1 and 100. See if you can guess it in 10 turns or fewer. We'll tell you if your guess was too high or too low."
145
145
  )
146
- );
147
- yield* _(loop(yield* _(secret)));
148
- });
146
+ )
147
+ yield* _(loop(yield* _(secret)))
148
+ })
149
149
  ```
150
150
 
151
151
  Let's run the game in Node.js:
152
152
 
153
153
  ```ts
154
- import { NodeRuntime, NodeTerminal } from "@effect/platform-node";
155
- import * as Effect from "effect/Effect";
156
- import { game } from "./game.js";
154
+ import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
155
+ import * as Effect from "effect/Effect"
156
+ import { game } from "./game.js"
157
157
 
158
- NodeRuntime.runMain(game.pipe(Effect.provide(NodeTerminal.layer)));
158
+ NodeRuntime.runMain(game.pipe(Effect.provide(NodeTerminal.layer)))
159
159
  ```
160
160
 
161
161
  Let's run the game in Bun:
162
162
 
163
163
  ```ts
164
- import { BunRuntime, BunTerminal } from "@effect/platform-bun";
165
- import * as Effect from "effect/Effect";
166
- import { game } from "./game.js";
164
+ import { BunRuntime, BunTerminal } from "@effect/platform-bun"
165
+ import * as Effect from "effect/Effect"
166
+ import { game } from "./game.js"
167
167
 
168
- BunRuntime.runMain(game.pipe(Effect.provide(BunTerminal.layer)));
168
+ BunRuntime.runMain(game.pipe(Effect.provide(BunTerminal.layer)))
169
169
  ```
170
170
 
171
171
  # Command
@@ -173,27 +173,27 @@ BunRuntime.runMain(game.pipe(Effect.provide(BunTerminal.layer)));
173
173
  As an example of using the `@effect/platform/Command` module, let's see how to run the TypeScript compiler `tsc`:
174
174
 
175
175
  ```ts
176
- import { Command, CommandExecutor } from "@effect/platform";
176
+ import { Command, CommandExecutor } from "@effect/platform"
177
177
  import {
178
178
  NodeCommandExecutor,
179
179
  NodeFileSystem,
180
- NodeRuntime,
181
- } from "@effect/platform-node";
182
- import { Effect } from "effect";
180
+ NodeRuntime
181
+ } from "@effect/platform-node"
182
+ import { Effect } from "effect"
183
183
 
184
184
  // const program: Effect.Effect<string, PlatformError, CommandExecutor.CommandExecutor>
185
185
  const program = Effect.gen(function* (_) {
186
- const executor = yield* _(CommandExecutor.CommandExecutor);
186
+ const executor = yield* _(CommandExecutor.CommandExecutor)
187
187
 
188
188
  // Creating a command to run the TypeScript compiler
189
- const command = Command.make("tsc", "--noEmit");
190
- console.log("Running tsc...");
189
+ const command = Command.make("tsc", "--noEmit")
190
+ console.log("Running tsc...")
191
191
 
192
192
  // Executing the command and capturing the output
193
- const output = yield* _(executor.string(command));
194
- console.log(output);
195
- return output;
196
- });
193
+ const output = yield* _(executor.string(command))
194
+ console.log(output)
195
+ return output
196
+ })
197
197
 
198
198
  // Running the program with the necessary runtime and executor layers
199
199
  NodeRuntime.runMain(
@@ -201,7 +201,7 @@ NodeRuntime.runMain(
201
201
  Effect.provide(NodeCommandExecutor.layer),
202
202
  Effect.provide(NodeFileSystem.layer)
203
203
  )
204
- );
204
+ )
205
205
  ```
206
206
 
207
207
  ## Obtaining Information About the Running Process
@@ -209,23 +209,23 @@ NodeRuntime.runMain(
209
209
  Here, we'll explore how to retrieve information about a running process.
210
210
 
211
211
  ```ts
212
- import { Command, CommandExecutor } from "@effect/platform";
212
+ import { Command, CommandExecutor } from "@effect/platform"
213
213
  import {
214
214
  NodeCommandExecutor,
215
215
  NodeFileSystem,
216
- NodeRuntime,
217
- } from "@effect/platform-node";
218
- import { Effect, Stream, String } from "effect";
216
+ NodeRuntime
217
+ } from "@effect/platform-node"
218
+ import { Effect, Stream, String } from "effect"
219
219
 
220
220
  const runString = <E, R>(
221
221
  stream: Stream.Stream<Uint8Array, E, R>
222
222
  ): Effect.Effect<string, E, R> =>
223
- stream.pipe(Stream.decodeText(), Stream.runFold(String.empty, String.concat));
223
+ stream.pipe(Stream.decodeText(), Stream.runFold(String.empty, String.concat))
224
224
 
225
225
  const program = Effect.gen(function* (_) {
226
- const executor = yield* _(CommandExecutor.CommandExecutor);
226
+ const executor = yield* _(CommandExecutor.CommandExecutor)
227
227
 
228
- const command = Command.make("ls");
228
+ const command = Command.make("ls")
229
229
 
230
230
  const [exitCode, stdout, stderr] = yield* _(
231
231
  // Start running the command and return a handle to the running process.
@@ -238,21 +238,21 @@ const program = Effect.gen(function* (_) {
238
238
  // The standard output stream of the process.
239
239
  runString(process.stdout),
240
240
  // The standard error stream of the process.
241
- runString(process.stderr),
241
+ runString(process.stderr)
242
242
  ],
243
243
  { concurrency: 3 }
244
244
  )
245
245
  )
246
- );
247
- console.log({ exitCode, stdout, stderr });
248
- });
246
+ )
247
+ console.log({ exitCode, stdout, stderr })
248
+ })
249
249
 
250
250
  NodeRuntime.runMain(
251
251
  Effect.scoped(program).pipe(
252
252
  Effect.provide(NodeCommandExecutor.layer),
253
253
  Effect.provide(NodeFileSystem.layer)
254
254
  )
255
- );
255
+ )
256
256
  ```
257
257
 
258
258
  # FileSystem
@@ -294,20 +294,20 @@ Here's a list of operations that can be performed using the `FileSystem` tag:
294
294
  Let's explore a simple example using `readFileString`:
295
295
 
296
296
  ```ts
297
- import { FileSystem } from "@effect/platform";
298
- import { NodeFileSystem, NodeRuntime } from "@effect/platform-node";
299
- import { Effect } from "effect";
297
+ import { FileSystem } from "@effect/platform"
298
+ import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
299
+ import { Effect } from "effect"
300
300
 
301
301
  // const readFileString: Effect.Effect<void, PlatformError, FileSystem.FileSystem>
302
302
  const readFileString = Effect.gen(function* (_) {
303
- const fs = yield* _(FileSystem.FileSystem);
303
+ const fs = yield* _(FileSystem.FileSystem)
304
304
 
305
305
  // Reading the content of the same file where this code is written
306
- const content = yield* _(fs.readFileString("./index.ts", "utf8"));
307
- console.log(content);
308
- });
306
+ const content = yield* _(fs.readFileString("./index.ts", "utf8"))
307
+ console.log(content)
308
+ })
309
309
 
310
- NodeRuntime.runMain(readFileString.pipe(Effect.provide(NodeFileSystem.layer)));
310
+ NodeRuntime.runMain(readFileString.pipe(Effect.provide(NodeFileSystem.layer)))
311
311
  ```
312
312
 
313
313
  # HTTP Client
@@ -317,17 +317,17 @@ NodeRuntime.runMain(readFileString.pipe(Effect.provide(NodeFileSystem.layer)));
317
317
  In this section, we'll explore how to retrieve data using the `HttpClient` module from `@effect/platform`.
318
318
 
319
319
  ```ts
320
- import { NodeRuntime } from "@effect/platform-node";
321
- import * as Http from "@effect/platform/HttpClient";
322
- import { Console, Effect } from "effect";
320
+ import { NodeRuntime } from "@effect/platform-node"
321
+ import * as Http from "@effect/platform/HttpClient"
322
+ import { Console, Effect } from "effect"
323
323
 
324
324
  const getPostAsJson = Http.request
325
325
  .get("https://jsonplaceholder.typicode.com/posts/1")
326
- .pipe(Http.client.fetch, Http.response.json);
326
+ .pipe(Http.client.fetch, Http.response.json)
327
327
 
328
328
  NodeRuntime.runMain(
329
329
  getPostAsJson.pipe(Effect.andThen((post) => Console.log(typeof post, post)))
330
- );
330
+ )
331
331
  /*
332
332
  Output:
333
333
  object {
@@ -347,17 +347,17 @@ If you want a response in a different format other than JSON, you can utilize ot
347
347
  In the following example, we fetch the post as text:
348
348
 
349
349
  ```ts
350
- import { NodeRuntime } from "@effect/platform-node";
351
- import * as Http from "@effect/platform/HttpClient";
352
- import { Console, Effect } from "effect";
350
+ import { NodeRuntime } from "@effect/platform-node"
351
+ import * as Http from "@effect/platform/HttpClient"
352
+ import { Console, Effect } from "effect"
353
353
 
354
354
  const getPostAsText = Http.request
355
355
  .get("https://jsonplaceholder.typicode.com/posts/1")
356
- .pipe(Http.client.fetch, Http.response.text);
356
+ .pipe(Http.client.fetch, Http.response.text)
357
357
 
358
358
  NodeRuntime.runMain(
359
359
  getPostAsText.pipe(Effect.andThen((post) => Console.log(typeof post, post)))
360
- );
360
+ )
361
361
  /*
362
362
  Output:
363
363
  string {
@@ -388,7 +388,7 @@ Here are some APIs you can use to convert the response:
388
388
  When making HTTP requests, sometimes you need to include additional information in the request headers. You can set headers using the `setHeader` function for a single header or `setHeaders` for multiple headers simultaneously.
389
389
 
390
390
  ```ts
391
- import * as Http from "@effect/platform/HttpClient";
391
+ import * as Http from "@effect/platform/HttpClient"
392
392
 
393
393
  const getPost = Http.request
394
394
  .get("https://jsonplaceholder.typicode.com/posts/1")
@@ -398,10 +398,10 @@ const getPost = Http.request
398
398
  // Setting multiple headers
399
399
  Http.request.setHeaders({
400
400
  "Content-type": "application/json; charset=UTF-8",
401
- Foo: "Bar",
401
+ Foo: "Bar"
402
402
  }),
403
403
  Http.client.fetch
404
- );
404
+ )
405
405
  ```
406
406
 
407
407
  ### Decoding Data with Schemas
@@ -409,15 +409,15 @@ const getPost = Http.request
409
409
  A common use case when fetching data is to validate the received format. For this purpose, the `HttpClient` module is integrated with `@effect/schema`.
410
410
 
411
411
  ```ts
412
- import { NodeRuntime } from "@effect/platform-node";
413
- import * as Http from "@effect/platform/HttpClient";
414
- import { Schema } from "@effect/schema";
415
- import { Console, Effect } from "effect";
412
+ import { NodeRuntime } from "@effect/platform-node"
413
+ import * as Http from "@effect/platform/HttpClient"
414
+ import { Schema } from "@effect/schema"
415
+ import { Console, Effect } from "effect"
416
416
 
417
417
  const Post = Schema.struct({
418
418
  id: Schema.number,
419
- title: Schema.string,
420
- });
419
+ title: Schema.string
420
+ })
421
421
 
422
422
  /*
423
423
  const getPostAndValidate: Effect.Effect<{
@@ -431,9 +431,9 @@ const getPostAndValidate = Http.request
431
431
  Http.client.fetch,
432
432
  Effect.andThen(Http.response.schemaBodyJson(Post)),
433
433
  Effect.scoped
434
- );
434
+ )
435
435
 
436
- NodeRuntime.runMain(getPostAndValidate.pipe(Effect.andThen(Console.log)));
436
+ NodeRuntime.runMain(getPostAndValidate.pipe(Effect.andThen(Console.log)))
437
437
  /*
438
438
  Output:
439
439
  {
@@ -456,15 +456,15 @@ You can use `Http.client.filterStatusOk`, or `Http.client.fetchOk` to ensure onl
456
456
  In this example, we attempt to fetch a non-existent page and don't receive any error:
457
457
 
458
458
  ```ts
459
- import { NodeRuntime } from "@effect/platform-node";
460
- import * as Http from "@effect/platform/HttpClient";
461
- import { Console, Effect } from "effect";
459
+ import { NodeRuntime } from "@effect/platform-node"
460
+ import * as Http from "@effect/platform/HttpClient"
461
+ import { Console, Effect } from "effect"
462
462
 
463
463
  const getText = Http.request
464
464
  .get("https://jsonplaceholder.typicode.com/non-existing-page")
465
- .pipe(Http.client.fetch, Http.response.text);
465
+ .pipe(Http.client.fetch, Http.response.text)
466
466
 
467
- NodeRuntime.runMain(getText.pipe(Effect.andThen(Console.log)));
467
+ NodeRuntime.runMain(getText.pipe(Effect.andThen(Console.log)))
468
468
  /*
469
469
  Output:
470
470
  {}
@@ -474,15 +474,15 @@ Output:
474
474
  However, if we use `Http.client.filterStatusOk`, an error is logged:
475
475
 
476
476
  ```ts
477
- import { NodeRuntime } from "@effect/platform-node";
478
- import * as Http from "@effect/platform/HttpClient";
479
- import { Console, Effect } from "effect";
477
+ import { NodeRuntime } from "@effect/platform-node"
478
+ import * as Http from "@effect/platform/HttpClient"
479
+ import { Console, Effect } from "effect"
480
480
 
481
481
  const getText = Http.request
482
482
  .get("https://jsonplaceholder.typicode.com/non-existing-page")
483
- .pipe(Http.client.filterStatusOk(Http.client.fetch), Http.response.text);
483
+ .pipe(Http.client.filterStatusOk(Http.client.fetch), Http.response.text)
484
484
 
485
- NodeRuntime.runMain(getText.pipe(Effect.andThen(Console.log)));
485
+ NodeRuntime.runMain(getText.pipe(Effect.andThen(Console.log)))
486
486
  /*
487
487
  Output:
488
488
  timestamp=2024-03-25T10:21:16.972Z level=ERROR fiber=#0 cause="ResponseError: StatusCode error (404 GET https://jsonplaceholder.typicode.com/non-existing-page): non 2xx status code
@@ -494,7 +494,7 @@ Note that you can use `Http.client.fetchOk` as a shortcut for `Http.client.filte
494
494
  ```ts
495
495
  const getText = Http.request
496
496
  .get("https://jsonplaceholder.typicode.com/non-existing-page")
497
- .pipe(Http.client.fetchOk, Http.response.text);
497
+ .pipe(Http.client.fetchOk, Http.response.text)
498
498
  ```
499
499
 
500
500
  You can also create your own status-based filters. In fact, `Http.client.filterStatusOk` is just a shortcut for the following filter:
@@ -508,7 +508,7 @@ const getText = Http.request
508
508
  (status) => status >= 200 && status < 300
509
509
  ),
510
510
  Http.response.text
511
- );
511
+ )
512
512
  ```
513
513
 
514
514
  ## POST
@@ -516,9 +516,9 @@ const getText = Http.request
516
516
  To make a POST request, you can use the `Http.request.post` function provided by the `HttpClient` module. Here's an example of how to create and send a POST request:
517
517
 
518
518
  ```ts
519
- import { NodeRuntime } from "@effect/platform-node";
520
- import * as Http from "@effect/platform/HttpClient";
521
- import { Console, Effect } from "effect";
519
+ import { NodeRuntime } from "@effect/platform-node"
520
+ import * as Http from "@effect/platform/HttpClient"
521
+ import { Console, Effect } from "effect"
522
522
 
523
523
  const addPost = Http.request
524
524
  .post("https://jsonplaceholder.typicode.com/posts")
@@ -526,13 +526,13 @@ const addPost = Http.request
526
526
  Http.request.jsonBody({
527
527
  title: "foo",
528
528
  body: "bar",
529
- userId: 1,
529
+ userId: 1
530
530
  }),
531
531
  Effect.andThen(Http.client.fetch),
532
532
  Http.response.json
533
- );
533
+ )
534
534
 
535
- NodeRuntime.runMain(addPost.pipe(Effect.andThen(Console.log)));
535
+ NodeRuntime.runMain(addPost.pipe(Effect.andThen(Console.log)))
536
536
  /*
537
537
  Output:
538
538
  { title: 'foo', body: 'bar', userId: 1, id: 101 }
@@ -544,9 +544,9 @@ If you need to send data in a format other than JSON, such as plain text, you ca
544
544
  In the following example, we send the data as text:
545
545
 
546
546
  ```ts
547
- import { NodeRuntime } from "@effect/platform-node";
548
- import * as Http from "@effect/platform/HttpClient";
549
- import { Console, Effect } from "effect";
547
+ import { NodeRuntime } from "@effect/platform-node"
548
+ import * as Http from "@effect/platform/HttpClient"
549
+ import { Console, Effect } from "effect"
550
550
 
551
551
  const addPost = Http.request
552
552
  .post("https://jsonplaceholder.typicode.com/posts")
@@ -555,15 +555,15 @@ const addPost = Http.request
555
555
  JSON.stringify({
556
556
  title: "foo",
557
557
  body: "bar",
558
- userId: 1,
558
+ userId: 1
559
559
  }),
560
560
  "application/json; charset=UTF-8"
561
561
  ),
562
562
  Http.client.fetch,
563
563
  Http.response.json
564
- );
564
+ )
565
565
 
566
- NodeRuntime.runMain(Effect.andThen(addPost, Console.log));
566
+ NodeRuntime.runMain(Effect.andThen(addPost, Console.log))
567
567
  /*
568
568
  Output:
569
569
  { title: 'foo', body: 'bar', userId: 1, id: 101 }
@@ -575,15 +575,15 @@ Output:
575
575
  A common use case when fetching data is to validate the received format. For this purpose, the `HttpClient` module is integrated with `@effect/schema`.
576
576
 
577
577
  ```ts
578
- import { NodeRuntime } from "@effect/platform-node";
579
- import * as Http from "@effect/platform/HttpClient";
580
- import { Schema } from "@effect/schema";
581
- import { Console, Effect } from "effect";
578
+ import { NodeRuntime } from "@effect/platform-node"
579
+ import * as Http from "@effect/platform/HttpClient"
580
+ import { Schema } from "@effect/schema"
581
+ import { Console, Effect } from "effect"
582
582
 
583
583
  const Post = Schema.struct({
584
584
  id: Schema.number,
585
- title: Schema.string,
586
- });
585
+ title: Schema.string
586
+ })
587
587
 
588
588
  const addPost = Http.request
589
589
  .post("https://jsonplaceholder.typicode.com/posts")
@@ -591,16 +591,863 @@ const addPost = Http.request
591
591
  Http.request.jsonBody({
592
592
  title: "foo",
593
593
  body: "bar",
594
- userId: 1,
594
+ userId: 1
595
595
  }),
596
596
  Effect.andThen(Http.client.fetch),
597
597
  Effect.andThen(Http.response.schemaBodyJson(Post)),
598
598
  Effect.scoped
599
- );
599
+ )
600
600
 
601
- NodeRuntime.runMain(addPost.pipe(Effect.andThen(Console.log)));
601
+ NodeRuntime.runMain(addPost.pipe(Effect.andThen(Console.log)))
602
602
  /*
603
603
  Output:
604
604
  { id: 101, title: 'foo' }
605
605
  */
606
606
  ```
607
+
608
+ # HTTP Server
609
+
610
+ ## Overview
611
+
612
+ This section provides a simplified explanation of key concepts within the `@effect/platform` TypeScript library, focusing on components used to build HTTP servers. Understanding these terms and their relationships helps in structuring and managing server applications effectively.
613
+
614
+ ### Core Concepts
615
+
616
+ - **HttpApp**: This is an `Effect` which results in a value `A`. It can utilize `ServerRequest` to produce the outcome `A`. Essentially, an `HttpApp` represents an application component that handles HTTP requests and generates responses based on those requests.
617
+
618
+ - **Default** (HttpApp): A special type of `HttpApp` that specifically produces a `ServerResponse` as its output `A`. This is the most common form of application where each interaction is expected to result in an HTTP response.
619
+
620
+ - **Server**: A construct that takes a `Default` app and converts it into an `Effect`. This serves as the execution layer where the `Default` app is operated, handling incoming requests and serving responses.
621
+
622
+ - **Router**: A type of `Default` app where the possible error outcome is `RouteNotFound`. Routers are used to direct incoming requests to appropriate handlers based on the request path and method.
623
+
624
+ - **Handler**: Another form of `Default` app, which has access to both `RouteContext` and `ServerRequest.ParsedSearchParams`. Handlers are specific functions designed to process requests and generate responses.
625
+
626
+ - **Middleware**: Functions that transform a `Default` app into another `Default` app. Middleware can be used to modify requests, responses, or handle tasks like logging, authentication, and more. Middleware can be applied in two ways:
627
+ - On a `Router` using `router.use: Handler -> Default` which applies the middleware to specific routes.
628
+ - On a `Server` using `server.serve: () -> Layer | Middleware -> Layer` which applies the middleware globally to all routes handled by the server.
629
+
630
+ ### Applying Concepts
631
+
632
+ These components are designed to work together in a modular and flexible way, allowing developers to build complex server applications with reusable components. Here’s how you might typically use these components in a project:
633
+
634
+ 1. **Create Handlers**: Define functions that process specific types of requests (e.g., GET, POST) and return responses.
635
+
636
+ 2. **Set Up Routers**: Organize handlers into routers, where each router manages a subset of application routes.
637
+
638
+ 3. **Apply Middleware**: Enhance routers or entire servers with middleware to add extra functionality like error handling or request logging.
639
+
640
+ 4. **Initialize the Server**: Wrap the main router with server functionality, applying any server-wide middleware, and start listening for requests.
641
+
642
+ ## Getting Started
643
+
644
+ ### Hello world example
645
+
646
+ In this example, we will create a simple HTTP server that listens on port `3000`. The server will respond with "Hello World!" when a request is made to the root URL (/) and return a `500` error for all other paths.
647
+
648
+ Node.js Example
649
+
650
+ ```ts
651
+ import { HttpServer } from "@effect/platform"
652
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
653
+ import { Layer } from "effect"
654
+ import { createServer } from "node:http"
655
+
656
+ // Define the router with a single route for the root URL
657
+ const router = HttpServer.router.empty.pipe(
658
+ HttpServer.router.get("/", HttpServer.response.text("Hello World"))
659
+ )
660
+
661
+ // Set up the application server with logging
662
+ const app = router.pipe(
663
+ HttpServer.server.serve(),
664
+ HttpServer.server.withLogAddress
665
+ )
666
+
667
+ // Specify the port
668
+ const port = 3000
669
+
670
+ // Create a server layer with the specified port
671
+ const ServerLive = NodeHttpServer.server.layer(() => createServer(), { port })
672
+
673
+ // Run the application
674
+ NodeRuntime.runMain(Layer.launch(Layer.provide(app, ServerLive)))
675
+
676
+ /*
677
+ Output:
678
+ timestamp=... level=INFO fiber=#0 message="Listening on http://localhost:3000"
679
+ */
680
+ ```
681
+
682
+ Bun Example
683
+
684
+ ```ts
685
+ import { HttpServer } from "@effect/platform"
686
+ import { BunHttpServer, BunRuntime } from "@effect/platform-bun"
687
+ import { Layer } from "effect"
688
+
689
+ // Define the router with a single route for the root URL
690
+ const router = HttpServer.router.empty.pipe(
691
+ HttpServer.router.get("/", HttpServer.response.text("Hello World"))
692
+ )
693
+
694
+ // Set up the application server with logging
695
+ const app = router.pipe(
696
+ HttpServer.server.serve(),
697
+ HttpServer.server.withLogAddress
698
+ )
699
+
700
+ // Specify the port
701
+ const port = 3000
702
+
703
+ // Create a server layer with the specified port
704
+ const ServerLive = BunHttpServer.server.layer({ port })
705
+
706
+ // Run the application
707
+ BunRuntime.runMain(Layer.launch(Layer.provide(app, ServerLive)))
708
+
709
+ /*
710
+ Output:
711
+ timestamp=... level=INFO fiber=#0 message="Listening on http://localhost:3000"
712
+ */
713
+ ```
714
+
715
+ To avoid boilerplate code for the final server setup, we'll use a helper function from the `listen.ts` file:
716
+
717
+ ```ts
718
+ import type { HttpServer } from "@effect/platform"
719
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
720
+ import { Layer } from "effect"
721
+ import { createServer } from "node:http"
722
+
723
+ export const listen = (
724
+ app: Layer.Layer<
725
+ never,
726
+ never,
727
+ HttpServer.platform.Platform | HttpServer.server.Server
728
+ >,
729
+ port: number
730
+ ) =>
731
+ NodeRuntime.runMain(
732
+ Layer.launch(
733
+ Layer.provide(
734
+ app,
735
+ NodeHttpServer.server.layer(() => createServer(), { port })
736
+ )
737
+ )
738
+ )
739
+ ```
740
+
741
+ ### Basic routing
742
+
743
+ Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, and so on).
744
+
745
+ Route definition takes the following structure:
746
+
747
+ ```
748
+ router.pipe(HttpServer.router.METHOD(PATH, HANDLER))
749
+ ```
750
+
751
+ Where:
752
+
753
+ - **router** is an instance of `Router` (`import type { Router } from "@effect/platform/Http/Router"`).
754
+ - **METHOD** is an HTTP request method, in lowercase (e.g., get, post, put, del).
755
+ - **PATH** is the path on the server (e.g., "/", "/user").
756
+ - **HANDLER** is the action that gets executed when the route is matched.
757
+
758
+ The following examples illustrate defining simple routes.
759
+
760
+ Respond with `"Hello World!"` on the homepage:
761
+
762
+ ```ts
763
+ router.pipe(HttpServer.router.get("/", HttpServer.response.text("Hello World")))
764
+ ```
765
+
766
+ Respond to POST request on the root route (/), the application's home page:
767
+
768
+ ```ts
769
+ router.pipe(
770
+ HttpServer.router.post("/", HttpServer.response.text("Got a POST request"))
771
+ )
772
+ ```
773
+
774
+ Respond to a PUT request to the `/user` route:
775
+
776
+ ```ts
777
+ router.pipe(
778
+ HttpServer.router.put(
779
+ "/user",
780
+ HttpServer.response.text("Got a PUT request at /user")
781
+ )
782
+ )
783
+ ```
784
+
785
+ Respond to a DELETE request to the `/user` route:
786
+
787
+ ```ts
788
+ router.pipe(
789
+ HttpServer.router.del(
790
+ "/user",
791
+ HttpServer.response.text("Got a DELETE request at /user")
792
+ )
793
+ )
794
+ ```
795
+
796
+ ### Serving static files
797
+
798
+ To serve static files such as images, CSS files, and JavaScript files, use the `HttpServer.response.file` built-in action.
799
+
800
+ ```ts
801
+ import { HttpServer } from "@effect/platform"
802
+ import { listen } from "./listen.js"
803
+
804
+ const router = HttpServer.router.empty.pipe(
805
+ HttpServer.router.get("/", HttpServer.response.file("index.html"))
806
+ )
807
+
808
+ const app = router.pipe(
809
+ HttpServer.server.serve(),
810
+ HttpServer.server.withLogAddress
811
+ )
812
+
813
+ listen(app, 3000)
814
+ ```
815
+
816
+ Create an `index.html` file in your project directory:
817
+
818
+ ```html filename="index.html"
819
+ <!doctype html>
820
+ <html>
821
+ <head>
822
+ <meta charset="utf-8" />
823
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
824
+ <title>index.html</title>
825
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
826
+ </head>
827
+ <body>
828
+ index.html
829
+ </body>
830
+ </html>
831
+ ```
832
+
833
+ ## Routing
834
+
835
+ Routing refers to how an application's endpoints (URIs) respond to client requests.
836
+
837
+ You define routing using methods of the `HttpServer.router` object that correspond to HTTP methods; for example, `HttpServer.router.get()` to handle GET requests and `HttpServer.router.post` to handle POST requests. You can also use `HttpServer.router.all()` to handle all HTTP methods.
838
+
839
+ These routing methods specify a `Route.Handler` called when the application receives a request to the specified route (endpoint) and HTTP method. In other words, the application “listens” for requests that match the specified route(s) and method(s), and when it detects a match, it calls the specified handler.
840
+
841
+ The following code is an example of a very basic route.
842
+
843
+ ```ts
844
+ // respond with "hello world" when a GET request is made to the homepage
845
+ HttpServer.router.get("/", HttpServer.response.text("Hello World"))
846
+ ```
847
+
848
+ ### Route methods
849
+
850
+ A route method is derived from one of the HTTP methods, and is attached to an instance of the `HttpServer.router` object.
851
+
852
+ The following code is an example of routes that are defined for the GET and the POST methods to the root of the app.
853
+
854
+ ```ts
855
+ // GET method route
856
+ HttpServer.router.get(
857
+ "/",
858
+ HttpServer.response.text("GET request to the homepage")
859
+ )
860
+
861
+ // POST method route
862
+ HttpServer.router.post(
863
+ "/",
864
+ HttpServer.response.text("POST request to the homepage")
865
+ )
866
+ ```
867
+
868
+ `HttpServer.router` supports methods that correspond to all HTTP request methods: `get`, `post`, and so on.
869
+
870
+ There is a special routing method, `HttpServer.router.all()`, used to load middleware functions at a path for **all** HTTP request methods. For example, the following handler is executed for requests to the route “/secret” whether using GET, POST, PUT, DELETE.
871
+
872
+ ```ts
873
+ HttpServer.router.all(
874
+ "/secret",
875
+ HttpServer.response
876
+ .empty()
877
+ .pipe(Effect.tap(Console.log("Accessing the secret section ...")))
878
+ )
879
+ ```
880
+
881
+ ### Route paths
882
+
883
+ Route paths, when combined with a request method, define the endpoints where requests can be made. Route paths can be specified as strings according to the following type:
884
+
885
+ ```ts
886
+ type PathInput = `/${string}` | "*"
887
+ ```
888
+
889
+ > [!NOTE]
890
+ > Query strings are not part of the route path.
891
+
892
+ Here are some examples of route paths based on strings.
893
+
894
+ This route path will match requests to the root route, /.
895
+
896
+ ```ts
897
+ HttpServer.router.get("/", HttpServer.response.text("root"))
898
+ ```
899
+
900
+ This route path will match requests to `/user`.
901
+
902
+ ```ts
903
+ HttpServer.router.get("/user", HttpServer.response.text("user"))
904
+ ```
905
+
906
+ This route path matches requests to any path starting with `/user` (e.g., `/user`, `/users`, etc.)
907
+
908
+ ```ts
909
+ HttpServer.router.get(
910
+ "/user*",
911
+ Effect.map(HttpServer.request.ServerRequest, (req) =>
912
+ HttpServer.response.text(req.url)
913
+ )
914
+ )
915
+ ```
916
+
917
+ ### Route parameters
918
+
919
+ Route parameters are named URL segments that are used to capture the values specified at their position in the URL. By using a schema the captured values are populated in an object, with the name of the route parameter specified in the path as their respective keys.
920
+
921
+ Route parameters are named segments in a URL that capture the values specified at those positions. These captured values are stored in an object, with the parameter names used as keys.
922
+
923
+ For example:
924
+
925
+ ```
926
+ Route path: /users/:userId/books/:bookId
927
+ Request URL: http://localhost:3000/users/34/books/8989
928
+ params: { "userId": "34", "bookId": "8989" }
929
+ ```
930
+
931
+ To define routes with parameters, include the parameter names in the path and use a schema to validate and parse these parameters, as shown below.
932
+
933
+ ```ts
934
+ import { HttpServer } from "@effect/platform"
935
+ import { Schema } from "@effect/schema"
936
+ import { Effect } from "effect"
937
+ import { listen } from "./listen.js"
938
+
939
+ // Define the schema for route parameters
940
+ const Params = Schema.Struct({
941
+ userId: Schema.String,
942
+ bookId: Schema.String
943
+ })
944
+
945
+ // Create a router with a route that captures parameters
946
+ const router = HttpServer.router.empty.pipe(
947
+ HttpServer.router.get(
948
+ "/users/:userId/books/:bookId",
949
+ HttpServer.router
950
+ .schemaPathParams(Params)
951
+ .pipe(Effect.flatMap((params) => HttpServer.response.json(params)))
952
+ )
953
+ )
954
+
955
+ const app = router.pipe(
956
+ HttpServer.server.serve(),
957
+ HttpServer.server.withLogAddress
958
+ )
959
+
960
+ listen(app, 3000)
961
+ ```
962
+
963
+ ### Response methods
964
+
965
+ The methods on `HttpServer.response` object in the following table can send a response to the client, and terminate the request-response cycle. If none of these methods are called from a route handler, the client request will be left hanging.
966
+
967
+ | Method | Description |
968
+ | ------------ | ------------------------------ |
969
+ | **empty** | Sends an empty response. |
970
+ | **formData** | Sends form data. |
971
+ | **html** | Sends an HTML response. |
972
+ | **raw** | Sends a raw response. |
973
+ | **setBody** | Sets the body of the response. |
974
+ | **stream** | Sends a streaming response. |
975
+ | **text** | Sends a plain text response. |
976
+
977
+ ### Router
978
+
979
+ Use the `HttpServer.router` object to create modular, mountable route handlers. A `Router` instance is a complete middleware and routing system, often referred to as a "mini-app."
980
+
981
+ The following example shows how to create a router as a module, define some routes, and mount the router module on a path in the main app.
982
+
983
+ Create a file named `birds.ts` in your app directory with the following content:
984
+
985
+ ```ts
986
+ import { HttpServer } from "@effect/platform"
987
+
988
+ export const birds = HttpServer.router.empty.pipe(
989
+ HttpServer.router.get("/", HttpServer.response.text("Birds home page")),
990
+ HttpServer.router.get("/about", HttpServer.response.text("About birds"))
991
+ )
992
+ ```
993
+
994
+ In your main application file, load the router module and mount it.
995
+
996
+ ```ts
997
+ import { HttpServer } from "@effect/platform"
998
+ import { birds } from "./birds.js"
999
+ import { listen } from "./listen.js"
1000
+
1001
+ // Create the main router and mount the birds router
1002
+ const router = HttpServer.router.empty.pipe(
1003
+ HttpServer.router.mount("/birds", birds)
1004
+ )
1005
+
1006
+ const app = router.pipe(
1007
+ HttpServer.server.serve(),
1008
+ HttpServer.server.withLogAddress
1009
+ )
1010
+
1011
+ listen(app, 3000)
1012
+ ```
1013
+
1014
+ When you run this code, your application will be able to handle requests to `/birds` and `/birds/about`, serving the respective responses defined in the `birds` router module.
1015
+
1016
+ ## Writing Middleware
1017
+
1018
+ In this section, we'll build a simple "Hello World" application and demonstrate how to add three middleware functions: `myLogger` for logging, `requestTime` for displaying request timestamps, and `validateCookies` for validating incoming cookies.
1019
+
1020
+ ### Example Application
1021
+
1022
+ Here is an example of a basic "Hello World" application with middleware.
1023
+
1024
+ ### Middleware `myLogger`
1025
+
1026
+ This middleware logs "LOGGED" whenever a request passes through it.
1027
+
1028
+ ```ts
1029
+ const myLogger = HttpServer.middleware.make((app) =>
1030
+ Effect.gen(function* () {
1031
+ console.log("LOGGED")
1032
+ return yield* app
1033
+ })
1034
+ )
1035
+ ```
1036
+
1037
+ To use the middleware, add it to the router using `HttpServer.router.use()`:
1038
+
1039
+ ```ts
1040
+ import { HttpServer } from "@effect/platform"
1041
+ import { Effect } from "effect"
1042
+ import { listen } from "./listen.js"
1043
+
1044
+ const myLogger = HttpServer.middleware.make((app) =>
1045
+ Effect.gen(function* () {
1046
+ console.log("LOGGED")
1047
+ return yield* app
1048
+ })
1049
+ )
1050
+
1051
+ const router = HttpServer.router.empty.pipe(
1052
+ HttpServer.router.get("/", HttpServer.response.text("Hello World"))
1053
+ )
1054
+
1055
+ const app = router.pipe(
1056
+ HttpServer.router.use(myLogger),
1057
+ HttpServer.server.serve(),
1058
+ HttpServer.server.withLogAddress
1059
+ )
1060
+
1061
+ listen(app, 3000)
1062
+ ```
1063
+
1064
+ With this setup, every request to the app will log "LOGGED" to the terminal. Middleware execute in the order they are loaded.
1065
+
1066
+ ### Middleware `requestTime`
1067
+
1068
+ Next, we'll create a middleware that records the timestamp of each HTTP request and provides it via a service called `RequestTime`.
1069
+
1070
+ ```ts
1071
+ class RequestTime extends Context.Tag("RequestTime")<RequestTime, number>() {}
1072
+
1073
+ const requestTime = HttpServer.middleware.make((app) =>
1074
+ Effect.gen(function* () {
1075
+ return yield* app.pipe(Effect.provideService(RequestTime, Date.now()))
1076
+ })
1077
+ )
1078
+ ```
1079
+
1080
+ Update the app to use this middleware and display the timestamp in the response:
1081
+
1082
+ ```ts
1083
+ import { HttpServer } from "@effect/platform"
1084
+ import { Context, Effect } from "effect"
1085
+ import { listen } from "./listen.js"
1086
+
1087
+ class RequestTime extends Context.Tag("RequestTime")<RequestTime, number>() {}
1088
+
1089
+ const requestTime = HttpServer.middleware.make((app) =>
1090
+ Effect.gen(function* () {
1091
+ return yield* app.pipe(Effect.provideService(RequestTime, Date.now()))
1092
+ })
1093
+ )
1094
+
1095
+ const router = HttpServer.router.empty.pipe(
1096
+ HttpServer.router.get(
1097
+ "/",
1098
+ Effect.gen(function* () {
1099
+ const requestTime = yield* RequestTime
1100
+ const responseText = `Hello World<br/><small>Requested at: ${requestTime}</small>`
1101
+ return yield* HttpServer.response.html(responseText)
1102
+ })
1103
+ )
1104
+ )
1105
+
1106
+ const app = router.pipe(
1107
+ HttpServer.router.use(requestTime),
1108
+ HttpServer.server.serve(),
1109
+ HttpServer.server.withLogAddress
1110
+ )
1111
+
1112
+ listen(app, 3000)
1113
+ ```
1114
+
1115
+ Now, when you make a request to the root path, the response will include the timestamp of the request.
1116
+
1117
+ ### Middleware `validateCookies`
1118
+
1119
+ Finally, we'll create a middleware that validates incoming cookies. If the cookies are invalid, it sends a 400 response.
1120
+
1121
+ Here's an example that validates cookies using an external service:
1122
+
1123
+ ```ts
1124
+ class CookieError {
1125
+ readonly _tag = "CookieError"
1126
+ }
1127
+
1128
+ const externallyValidateCookie = (testCookie: string | undefined) =>
1129
+ testCookie && testCookie.length > 0
1130
+ ? Effect.succeed(testCookie)
1131
+ : Effect.fail(new CookieError())
1132
+
1133
+ const cookieValidator = HttpServer.middleware.make((app) =>
1134
+ Effect.gen(function* () {
1135
+ const req = yield* HttpServer.request.ServerRequest
1136
+ yield* externallyValidateCookie(req.cookies.testCookie)
1137
+ return yield* app
1138
+ }).pipe(
1139
+ Effect.catchTag("CookieError", () =>
1140
+ HttpServer.response.text("Invalid cookie")
1141
+ )
1142
+ )
1143
+ )
1144
+ ```
1145
+
1146
+ Update the app to use the `cookieValidator` middleware:
1147
+
1148
+ ```ts
1149
+ import { HttpServer } from "@effect/platform"
1150
+ import { Effect } from "effect"
1151
+ import { listen } from "./listen.js"
1152
+
1153
+ class CookieError {
1154
+ readonly _tag = "CookieError"
1155
+ }
1156
+
1157
+ const externallyValidateCookie = (testCookie: string | undefined) =>
1158
+ testCookie && testCookie.length > 0
1159
+ ? Effect.succeed(testCookie)
1160
+ : Effect.fail(new CookieError())
1161
+
1162
+ const cookieValidator = HttpServer.middleware.make((app) =>
1163
+ Effect.gen(function* () {
1164
+ const req = yield* HttpServer.request.ServerRequest
1165
+ yield* externallyValidateCookie(req.cookies.testCookie)
1166
+ return yield* app
1167
+ }).pipe(
1168
+ Effect.catchTag("CookieError", () =>
1169
+ HttpServer.response.text("Invalid cookie")
1170
+ )
1171
+ )
1172
+ )
1173
+
1174
+ const router = HttpServer.router.empty.pipe(
1175
+ HttpServer.router.get("/", HttpServer.response.text("Hello World"))
1176
+ )
1177
+
1178
+ const app = router.pipe(
1179
+ HttpServer.router.use(cookieValidator),
1180
+ HttpServer.server.serve(),
1181
+ HttpServer.server.withLogAddress
1182
+ )
1183
+
1184
+ listen(app, 3000)
1185
+ ```
1186
+
1187
+ Test the middleware with the following commands:
1188
+
1189
+ ```sh
1190
+ curl -i http://localhost:3000
1191
+ curl -i http://localhost:3000 --cookie "testCookie=myvalue"
1192
+ curl -i http://localhost:3000 --cookie "testCookie="
1193
+ ```
1194
+
1195
+ This setup validates the `testCookie` and returns "Invalid cookie" if the validation fails, or "Hello World" if it passes.
1196
+
1197
+ ## Built-in middleware
1198
+
1199
+ ### Middleware Summary
1200
+
1201
+ | Middleware | Description |
1202
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
1203
+ | **Logger** | Provides detailed logging of all requests and responses, aiding in debugging and monitoring application activities. |
1204
+ | **xForwardedHeaders** | Manages `X-Forwarded-*` headers to accurately maintain client information such as IP addresses and host names in proxy scenarios. |
1205
+
1206
+ ### logger
1207
+
1208
+ The `HttpServer.middleware.logger` middleware enables logging for your entire application, providing insights into each request and response. Here’s how to set it up:
1209
+
1210
+ ```ts
1211
+ import { HttpServer } from "@effect/platform"
1212
+ import { listen } from "./listen.js"
1213
+
1214
+ const router = HttpServer.router.empty.pipe(
1215
+ HttpServer.router.get("/", HttpServer.response.text("Hello World"))
1216
+ )
1217
+
1218
+ // Apply the logger middleware globally
1219
+ const app = router.pipe(
1220
+ HttpServer.server.serve(HttpServer.middleware.logger),
1221
+ HttpServer.server.withLogAddress
1222
+ )
1223
+
1224
+ listen(app, 3000)
1225
+ /*
1226
+ curl -i http://localhost:3000
1227
+ timestamp=... level=INFO fiber=#0 message="Listening on http://0.0.0.0:3000"
1228
+ timestamp=... level=INFO fiber=#19 message="Sent HTTP response" http.span.1=8ms http.status=200 http.method=GET http.url=/
1229
+ timestamp=... level=INFO fiber=#20 cause="RouteNotFound: GET /favicon.ico not found
1230
+ at ...
1231
+ at http.server GET" http.span.2=4ms http.status=500 http.method=GET http.url=/favicon.ico
1232
+ */
1233
+ ```
1234
+
1235
+ To disable the logger for specific routes, you can use `HttpServer.middleware.withLoggerDisabled`:
1236
+
1237
+ ```ts
1238
+ import { HttpServer } from "@effect/platform"
1239
+ import { listen } from "./listen.js"
1240
+
1241
+ // Create the router with routes that will and will not have logging
1242
+ const router = HttpServer.router.empty.pipe(
1243
+ HttpServer.router.get("/", HttpServer.response.text("Hello World")),
1244
+ HttpServer.router.get(
1245
+ "/no-logger",
1246
+ HttpServer.response
1247
+ .text("no-logger")
1248
+ .pipe(HttpServer.middleware.withLoggerDisabled)
1249
+ )
1250
+ )
1251
+
1252
+ // Apply the logger middleware globally
1253
+ const app = router.pipe(
1254
+ HttpServer.server.serve(HttpServer.middleware.logger),
1255
+ HttpServer.server.withLogAddress
1256
+ )
1257
+
1258
+ listen(app, 3000)
1259
+ /*
1260
+ curl -i http://localhost:3000/no-logger
1261
+ timestamp=2024-05-19T09:53:29.877Z level=INFO fiber=#0 message="Listening on http://0.0.0.0:3000"
1262
+ */
1263
+ ```
1264
+
1265
+ ### xForwardedHeaders
1266
+
1267
+ This middleware handles `X-Forwarded-*` headers, useful when your app is behind a reverse proxy or load balancer and you need to retrieve the original client's IP and host information.
1268
+
1269
+ ```ts
1270
+ import { HttpServer } from "@effect/platform"
1271
+ import { Effect } from "effect"
1272
+ import { listen } from "./listen.js"
1273
+
1274
+ // Create a router and a route that logs request headers and remote address
1275
+ const router = HttpServer.router.empty.pipe(
1276
+ HttpServer.router.get(
1277
+ "/",
1278
+ Effect.gen(function* () {
1279
+ const req = yield* HttpServer.request.ServerRequest
1280
+ console.log(req.headers)
1281
+ console.log(req.remoteAddress)
1282
+ return yield* HttpServer.response.text("Hello World")
1283
+ })
1284
+ )
1285
+ )
1286
+
1287
+ // Set up the server with xForwardedHeaders middleware
1288
+ const app = router.pipe(
1289
+ HttpServer.server.serve(HttpServer.middleware.xForwardedHeaders),
1290
+ HttpServer.server.withLogAddress
1291
+ )
1292
+
1293
+ listen(app, 3000)
1294
+ /*
1295
+ curl -H "X-Forwarded-Host: 192.168.1.1" -H "X-Forwarded-For: 192.168.1.1" http://localhost:3000
1296
+ timestamp=... level=INFO fiber=#0 message="Listening on http://0.0.0.0:3000"
1297
+ {
1298
+ host: '192.168.1.1',
1299
+ 'user-agent': 'curl/8.6.0',
1300
+ accept: '*\/*',
1301
+ 'x-forwarded-host': '192.168.1.1',
1302
+ 'x-forwarded-for': '192.168.1.1'
1303
+ }
1304
+ { _id: 'Option', _tag: 'Some', value: '192.168.1.1' }
1305
+ */
1306
+ ```
1307
+
1308
+ ## Error Handling
1309
+
1310
+ ### Catching Errors
1311
+
1312
+ Below is an example illustrating how to catch and manage errors that occur during the execution of route handlers:
1313
+
1314
+ ```ts
1315
+ import { HttpServer } from "@effect/platform"
1316
+ import { Effect } from "effect"
1317
+ import { listen } from "./listen.js"
1318
+
1319
+ // Define routes that might throw errors or fail
1320
+ const router = HttpServer.router.empty.pipe(
1321
+ HttpServer.router.get(
1322
+ "/throw",
1323
+ Effect.sync(() => {
1324
+ throw new Error("BROKEN") // This will intentionally throw an error
1325
+ })
1326
+ ),
1327
+ HttpServer.router.get("/fail", Effect.fail("Uh oh!")) // This will intentionally fail
1328
+ )
1329
+
1330
+ // Configure the application to handle different types of errors
1331
+ const app = router.pipe(
1332
+ Effect.catchTags({
1333
+ RouteNotFound: () =>
1334
+ HttpServer.response.text("Route Not Found", { status: 404 })
1335
+ }),
1336
+ Effect.catchAllCause((cause) =>
1337
+ HttpServer.response.text(cause.toString(), { status: 500 })
1338
+ ),
1339
+ HttpServer.server.serve(),
1340
+ HttpServer.server.withLogAddress
1341
+ )
1342
+
1343
+ listen(app, 3000)
1344
+ ```
1345
+
1346
+ You can test the error handling setup with `curl` commands by trying to access routes that trigger errors:
1347
+
1348
+ ```sh
1349
+ # Accessing a route that does not exist
1350
+ curl -i http://localhost:3000/nonexistent
1351
+
1352
+ # Accessing the route that throws an error
1353
+ curl -i http://localhost:3000/throw
1354
+
1355
+ # Accessing the route that fails
1356
+ curl -i http://localhost:3000/fail
1357
+ ```
1358
+
1359
+ ## Validations
1360
+
1361
+ Validation is a critical aspect of handling HTTP requests to ensure that the data your server receives is as expected. We'll explore how to validate headers and cookies using the `@effect/platform` and `@effect/schema` libraries, which provide structured and robust methods for these tasks.
1362
+
1363
+ ### Headers
1364
+
1365
+ Headers often contain important information needed by your application, such as content types, authentication tokens, or session data. Validating these headers ensures that your application can trust and correctly process the information it receives.
1366
+
1367
+ ```ts
1368
+ import { HttpServer } from "@effect/platform"
1369
+ import { Schema } from "@effect/schema"
1370
+ import { Effect } from "effect"
1371
+ import { listen } from "./listen.js"
1372
+
1373
+ const router = HttpServer.router.empty.pipe(
1374
+ HttpServer.router.get(
1375
+ "/",
1376
+ Effect.gen(function* () {
1377
+ // Define the schema for expected headers and validate them
1378
+ const headers = yield* HttpServer.request.schemaHeaders(
1379
+ Schema.Struct({ test: Schema.String })
1380
+ )
1381
+ return yield* HttpServer.response.text("header: " + headers.test)
1382
+ }).pipe(
1383
+ // Handle parsing errors
1384
+ Effect.catchTag("ParseError", (e) =>
1385
+ HttpServer.response.text(`Invalid header: ${e.message}`)
1386
+ )
1387
+ )
1388
+ )
1389
+ )
1390
+
1391
+ const app = router.pipe(
1392
+ HttpServer.server.serve(),
1393
+ HttpServer.server.withLogAddress
1394
+ )
1395
+
1396
+ listen(app, 3000)
1397
+ ```
1398
+
1399
+ You can test header validation using the following `curl` commands:
1400
+
1401
+ ```sh
1402
+ # Request without the required header
1403
+ curl -i http://localhost:3000
1404
+
1405
+ # Request with the valid header
1406
+ curl -i -H "test: myvalue" http://localhost:3000
1407
+ ```
1408
+
1409
+ ### Cookies
1410
+
1411
+ Cookies are commonly used to maintain session state or user preferences. Validating cookies ensures that the data they carry is intact and as expected, enhancing security and application integrity.
1412
+
1413
+ Here’s how you can validate cookies received in HTTP requests:
1414
+
1415
+ ```ts
1416
+ import { HttpServer } from "@effect/platform"
1417
+ import { Schema } from "@effect/schema"
1418
+ import { Effect } from "effect"
1419
+ import { listen } from "./listen.js"
1420
+
1421
+ const router = HttpServer.router.empty.pipe(
1422
+ HttpServer.router.get(
1423
+ "/",
1424
+ Effect.gen(function* () {
1425
+ const cookies = yield* HttpServer.request.schemaCookies(
1426
+ Schema.Struct({ test: Schema.String })
1427
+ )
1428
+ return yield* HttpServer.response.text("cookie: " + cookies.test)
1429
+ }).pipe(
1430
+ Effect.catchTag("ParseError", (e) =>
1431
+ HttpServer.response.text(`Invalid cookie: ${e.message}`)
1432
+ )
1433
+ )
1434
+ )
1435
+ )
1436
+
1437
+ const app = router.pipe(
1438
+ HttpServer.server.serve(),
1439
+ HttpServer.server.withLogAddress
1440
+ )
1441
+
1442
+ listen(app, 3000)
1443
+ ```
1444
+
1445
+ Validate the cookie handling with the following `curl` commands:
1446
+
1447
+ ```sh
1448
+ # Request without any cookies
1449
+ curl -i http://localhost:3000
1450
+
1451
+ # Request with the valid cookie
1452
+ curl -i http://localhost:3000 --cookie "test=myvalue"
1453
+ ```