@effect/platform 0.53.12 → 0.53.14
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 +989 -142
- package/dist/cjs/Http/Router.js.map +1 -1
- package/dist/dts/Http/App.d.ts +2 -2
- package/dist/dts/Http/Client.d.ts +29 -29
- package/dist/dts/Http/Client.d.ts.map +1 -1
- package/dist/dts/Http/IncomingMessage.d.ts +1 -1
- package/dist/dts/Http/Middleware.d.ts +10 -10
- package/dist/dts/Http/Multipart.d.ts +8 -8
- package/dist/dts/Http/Multiplex.d.ts +19 -19
- package/dist/dts/Http/Router.d.ts +34 -35
- package/dist/dts/Http/Router.d.ts.map +1 -1
- package/dist/dts/Http/Server.d.ts +13 -13
- package/dist/dts/Socket.d.ts +2 -2
- package/dist/dts/WorkerRunner.d.ts +2 -2
- package/dist/dts/internal/http/router.d.ts +1 -1
- package/dist/esm/Http/Router.js.map +1 -1
- package/package.json +3 -3
- package/src/Http/App.ts +3 -3
- package/src/Http/Client.ts +29 -29
- package/src/Http/IncomingMessage.ts +2 -2
- package/src/Http/Middleware.ts +10 -10
- package/src/Http/Multipart.ts +8 -8
- package/src/Http/Multiplex.ts +24 -24
- package/src/Http/Router.ts +34 -39
- package/src/Http/Server.ts +13 -13
- package/src/Socket.ts +4 -4
- package/src/WorkerRunner.ts +2 -2
- package/src/internal/http/client.ts +31 -31
- package/src/internal/http/middleware.ts +7 -7
- package/src/internal/http/multipart.ts +8 -8
- package/src/internal/http/multiplex.ts +28 -28
- package/src/internal/http/router.ts +23 -23
- package/src/internal/http/server.ts +13 -13
- package/src/internal/workerRunner.ts +2 -2
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
|
+
```
|