@effect/platform 0.71.7 → 0.72.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +769 -260
- package/dist/cjs/HttpClient.js.map +1 -1
- package/dist/cjs/internal/httpClient.js.map +1 -1
- package/dist/dts/HttpClient.d.ts +59 -53
- package/dist/dts/HttpClient.d.ts.map +1 -1
- package/dist/dts/internal/httpClient.d.ts +8 -8
- package/dist/dts/internal/httpClient.d.ts.map +1 -1
- package/dist/esm/HttpClient.js.map +1 -1
- package/dist/esm/internal/httpClient.js.map +1 -1
- package/package.json +2 -2
- package/src/HttpClient.ts +97 -87
- package/src/internal/httpClient.ts +80 -80
package/README.md
CHANGED
|
@@ -9,26 +9,185 @@ Welcome to the documentation for `@effect/platform`, a library designed for crea
|
|
|
9
9
|
|
|
10
10
|
## Overview
|
|
11
11
|
|
|
12
|
-
The `HttpApi`
|
|
13
|
-
You can create an API by combining multiple endpoints, each with its own set of
|
|
14
|
-
schemas that define the request and response types.
|
|
12
|
+
The `HttpApi` modules offer a flexible and declarative way to define HTTP APIs. You build an API by combining endpoints, each describing its path and the request/response schemas. Once defined, the same API definition can be used to:
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
- Spin up a server
|
|
15
|
+
- Provide a Swagger documentation page
|
|
16
|
+
- Derive a fully-typed client
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
This separation helps avoid duplication, keeps everything up to date, and simplifies maintenance when your API evolves. It also makes it straightforward to add new functionality or reconfigure existing endpoints without changing the entire stack.
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
defined by a path, a method, and a set of schemas that define the request and
|
|
23
|
-
response types.
|
|
20
|
+
## Hello World
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
other groups to create a complete API.
|
|
22
|
+
Here is a simple example of defining an API with a single endpoint that returns a string:
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
**Example** (Defining an API)
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
HttpApi,
|
|
29
|
+
HttpApiBuilder,
|
|
30
|
+
HttpApiEndpoint,
|
|
31
|
+
HttpApiGroup
|
|
32
|
+
} from "@effect/platform"
|
|
33
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
34
|
+
import { Effect, Layer, Schema } from "effect"
|
|
35
|
+
import { createServer } from "node:http"
|
|
36
|
+
|
|
37
|
+
// Define our API with one group named "Greetings" and one endpoint called "hello-world"
|
|
38
|
+
const MyApi = HttpApi.make("MyApi").add(
|
|
39
|
+
HttpApiGroup.make("Greetings").add(
|
|
40
|
+
HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// Implement the "Greetings" group
|
|
45
|
+
const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
|
|
46
|
+
handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Provide the implementation for the API
|
|
50
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))
|
|
51
|
+
|
|
52
|
+
// Set up the server using NodeHttpServer on port 3000
|
|
53
|
+
const ServerLive = HttpApiBuilder.serve().pipe(
|
|
54
|
+
Layer.provide(MyApiLive),
|
|
55
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Run the server
|
|
59
|
+
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Navigate to `http://localhost:3000` in your browser to see the response "Hello, World!".
|
|
63
|
+
|
|
64
|
+
### Serving The Auto Generated Swagger Documentation
|
|
65
|
+
|
|
66
|
+
You can add Swagger documentation to your API by including the `HttpApiSwagger` module. Provide the `HttpApiSwagger.layer` in your server setup, as shown here:
|
|
67
|
+
|
|
68
|
+
**Example** (Serving Swagger Documentation)
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
HttpApi,
|
|
73
|
+
HttpApiBuilder,
|
|
74
|
+
HttpApiEndpoint,
|
|
75
|
+
HttpApiGroup,
|
|
76
|
+
HttpApiSwagger
|
|
77
|
+
} from "@effect/platform"
|
|
78
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
79
|
+
import { Effect, Layer, Schema } from "effect"
|
|
80
|
+
import { createServer } from "node:http"
|
|
81
|
+
|
|
82
|
+
const MyApi = HttpApi.make("MyApi").add(
|
|
83
|
+
HttpApiGroup.make("Greetings").add(
|
|
84
|
+
HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
|
|
89
|
+
handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))
|
|
93
|
+
|
|
94
|
+
const ServerLive = HttpApiBuilder.serve().pipe(
|
|
95
|
+
// Provide the Swagger layer so clients can access auto-generated docs
|
|
96
|
+
Layer.provide(HttpApiSwagger.layer()),
|
|
97
|
+
Layer.provide(MyApiLive),
|
|
98
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Navigate to `http://localhost:3000/docs` in your browser to see the Swagger documentation:
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
108
|
+
### Deriving a Client
|
|
109
|
+
|
|
110
|
+
After you define your API, you can generate a client to interact with the server. The `HttpApiClient` module provides the needed tools:
|
|
111
|
+
|
|
112
|
+
**Example** (Deriving a Client)
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import {
|
|
116
|
+
FetchHttpClient,
|
|
117
|
+
HttpApi,
|
|
118
|
+
HttpApiBuilder,
|
|
119
|
+
HttpApiClient,
|
|
120
|
+
HttpApiEndpoint,
|
|
121
|
+
HttpApiGroup,
|
|
122
|
+
HttpApiSwagger
|
|
123
|
+
} from "@effect/platform"
|
|
124
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
125
|
+
import { Effect, Layer, Schema } from "effect"
|
|
126
|
+
import { createServer } from "node:http"
|
|
127
|
+
|
|
128
|
+
const MyApi = HttpApi.make("MyApi").add(
|
|
129
|
+
HttpApiGroup.make("Greetings").add(
|
|
130
|
+
HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
|
|
135
|
+
handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))
|
|
139
|
+
|
|
140
|
+
const ServerLive = HttpApiBuilder.serve().pipe(
|
|
141
|
+
Layer.provide(HttpApiSwagger.layer()),
|
|
142
|
+
Layer.provide(MyApiLive),
|
|
143
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
147
|
+
|
|
148
|
+
// Create a program that derives and uses the client
|
|
149
|
+
const program = Effect.gen(function* () {
|
|
150
|
+
// Derive the client
|
|
151
|
+
const client = yield* HttpApiClient.make(MyApi, {
|
|
152
|
+
baseUrl: "http://localhost:3000"
|
|
153
|
+
})
|
|
154
|
+
// Call the "hello-world" endpoint
|
|
155
|
+
const hello = yield* client.Greetings["hello-world"]()
|
|
156
|
+
console.log(hello)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// Provide a Fetch-based HTTP client and run the program
|
|
160
|
+
Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
|
|
161
|
+
// Output: Hello, World!
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Basic Usage
|
|
165
|
+
|
|
166
|
+
To define an API, create a set of endpoints. Each endpoint is described by a path, a method, and schemas for the request and response.
|
|
167
|
+
|
|
168
|
+
Collections of endpoints are grouped in an `HttpApiGroup`, and multiple groups can be merged into a complete API.
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
API
|
|
172
|
+
├── Group
|
|
173
|
+
│ ├── Endpoint
|
|
174
|
+
│ └── Endpoint
|
|
175
|
+
└── Group
|
|
176
|
+
├── Endpoint
|
|
177
|
+
├── Endpoint
|
|
178
|
+
└── Endpoint
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Defining a HttpApiGroup
|
|
182
|
+
|
|
183
|
+
Below is a simple CRUD API for user management. We have an `HttpApiGroup` with the following endpoints:
|
|
184
|
+
|
|
185
|
+
- `GET /users/:userId` - Find a user by id
|
|
186
|
+
- `POST /users` - Create a new user
|
|
187
|
+
- `DELETE /users/:userId` - Delete a user by id
|
|
188
|
+
- `PATCH /users/:userId` - Update a user by id
|
|
189
|
+
|
|
190
|
+
**Example** (Defining a Group)
|
|
32
191
|
|
|
33
192
|
```ts
|
|
34
193
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
|
|
@@ -46,28 +205,26 @@ const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
|
46
205
|
|
|
47
206
|
const usersApi = HttpApiGroup.make("users")
|
|
48
207
|
.add(
|
|
49
|
-
//
|
|
50
|
-
// You can use a template string to define path
|
|
208
|
+
// Each endpoint has a name and a path.
|
|
209
|
+
// You can use a template string to define path parameters...
|
|
51
210
|
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`
|
|
52
|
-
//
|
|
211
|
+
// Add a Schema for a successful response.
|
|
53
212
|
.addSuccess(User)
|
|
54
213
|
)
|
|
55
214
|
.add(
|
|
56
|
-
// you can
|
|
57
|
-
// path parameter schema
|
|
215
|
+
// ..or you can pass the path as a string and use `.setPath` to define path parameters.
|
|
58
216
|
HttpApiEndpoint.post("create", "/users")
|
|
59
217
|
.addSuccess(User)
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// but for a GET request, the payload would be in the URL search params
|
|
218
|
+
// Define a Schema for the request body.
|
|
219
|
+
// Since this is a POST, data is in the body.
|
|
220
|
+
// For GET requests, data could be in the URL search parameters.
|
|
64
221
|
.setPayload(
|
|
65
222
|
Schema.Struct({
|
|
66
223
|
name: Schema.String
|
|
67
224
|
})
|
|
68
225
|
)
|
|
69
226
|
)
|
|
70
|
-
//
|
|
227
|
+
// By default, this endpoint responds with 204 No Content.
|
|
71
228
|
.add(HttpApiEndpoint.del("delete")`/users/${UserIdParam}`)
|
|
72
229
|
.add(
|
|
73
230
|
HttpApiEndpoint.patch("update")`/users/${UserIdParam}`
|
|
@@ -80,84 +237,83 @@ const usersApi = HttpApiGroup.make("users")
|
|
|
80
237
|
)
|
|
81
238
|
```
|
|
82
239
|
|
|
83
|
-
You can also extend
|
|
84
|
-
|
|
240
|
+
You can also extend `HttpApiGroup` with a class to create an opaque type:
|
|
241
|
+
|
|
242
|
+
**Example** (Defining a Group with an Opaque Type)
|
|
85
243
|
|
|
86
244
|
```ts
|
|
87
245
|
class UsersApi extends HttpApiGroup.make("users").add(
|
|
88
246
|
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`
|
|
89
|
-
// ...
|
|
247
|
+
// ... etc
|
|
90
248
|
) {}
|
|
91
249
|
```
|
|
92
250
|
|
|
93
|
-
### Creating the
|
|
251
|
+
### Creating the Top-Level HttpApi
|
|
94
252
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import { HttpApi } from "@effect/platform"
|
|
99
|
-
|
|
100
|
-
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
101
|
-
```
|
|
253
|
+
After defining your groups, you can combine them into a single `HttpApi` to represent the full set of endpoints for your application.
|
|
102
254
|
|
|
103
|
-
|
|
255
|
+
**Example** (Combining Groups into a Top-Level API)
|
|
104
256
|
|
|
105
257
|
```ts
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
258
|
+
import {
|
|
259
|
+
HttpApi,
|
|
260
|
+
HttpApiEndpoint,
|
|
261
|
+
HttpApiGroup,
|
|
262
|
+
HttpApiSchema
|
|
263
|
+
} from "@effect/platform"
|
|
264
|
+
import { Schema } from "effect"
|
|
112
265
|
|
|
113
|
-
|
|
266
|
+
class User extends Schema.Class<User>("User")({
|
|
267
|
+
id: Schema.Number,
|
|
268
|
+
name: Schema.String,
|
|
269
|
+
createdAt: Schema.DateTimeUtc
|
|
270
|
+
}) {}
|
|
114
271
|
|
|
115
|
-
|
|
116
|
-
import { OpenApi } from "@effect/platform"
|
|
272
|
+
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
117
273
|
|
|
118
274
|
class UsersApi extends HttpApiGroup.make("users")
|
|
275
|
+
.add(HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User))
|
|
119
276
|
.add(
|
|
120
|
-
HttpApiEndpoint.
|
|
121
|
-
|
|
277
|
+
HttpApiEndpoint.post("create", "/users")
|
|
278
|
+
.addSuccess(User)
|
|
279
|
+
.setPayload(
|
|
280
|
+
Schema.Struct({
|
|
281
|
+
name: Schema.String
|
|
282
|
+
})
|
|
283
|
+
)
|
|
122
284
|
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
285
|
+
.add(HttpApiEndpoint.del("delete")`/users/${UserIdParam}`)
|
|
286
|
+
.add(
|
|
287
|
+
HttpApiEndpoint.patch("update")`/users/${UserIdParam}`
|
|
288
|
+
.addSuccess(User)
|
|
289
|
+
.setPayload(
|
|
290
|
+
Schema.Struct({
|
|
291
|
+
name: Schema.String
|
|
292
|
+
})
|
|
293
|
+
)
|
|
132
294
|
) {}
|
|
133
|
-
```
|
|
134
295
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
You can also add OpenApi annotations to the top-level `HttpApi`:
|
|
296
|
+
// Combine the groups into a top-level API with an opaque style
|
|
297
|
+
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
139
298
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.add(UsersApi)
|
|
143
|
-
.annotate(OpenApi.Title, "My API") {}
|
|
299
|
+
// Alternatively, use a non-opaque style
|
|
300
|
+
const api = HttpApi.make("myApi").add(UsersApi)
|
|
144
301
|
```
|
|
145
302
|
|
|
146
303
|
### Adding errors
|
|
147
304
|
|
|
148
|
-
|
|
305
|
+
Error responses can be added to your endpoints to handle various scenarios. These responses can be specific to an endpoint, a group of endpoints, or the entire API.
|
|
149
306
|
|
|
150
|
-
- `HttpApiEndpoint.addError`
|
|
151
|
-
- `HttpApiGroup.addError`
|
|
152
|
-
- `HttpApi.addError`
|
|
307
|
+
- Use `HttpApiEndpoint.addError` to add an error response to a specific endpoint.
|
|
308
|
+
- Use `HttpApiGroup.addError` to add an error response to all endpoints in a group.
|
|
309
|
+
- Use `HttpApi.addError` to add an error response to all endpoints in the API.
|
|
153
310
|
|
|
154
|
-
|
|
155
|
-
can be used in middleware.
|
|
311
|
+
Group-level and API-level errors are particularly useful for handling common error scenarios, such as authentication failures, that might be managed through middleware.
|
|
156
312
|
|
|
157
|
-
|
|
313
|
+
**Example** (Adding Errors to Endpoints and Groups)
|
|
158
314
|
|
|
159
315
|
```ts
|
|
160
|
-
//
|
|
316
|
+
// Define error schemas
|
|
161
317
|
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
162
318
|
"UserNotFound",
|
|
163
319
|
{}
|
|
@@ -171,33 +327,39 @@ class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
|
171
327
|
class UsersApi extends HttpApiGroup.make("users")
|
|
172
328
|
.add(
|
|
173
329
|
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`
|
|
174
|
-
// here we are adding our error response
|
|
175
|
-
.addError(UserNotFound, { status: 404 })
|
|
176
330
|
.addSuccess(User)
|
|
331
|
+
// Add a 404 error response for this endpoint
|
|
332
|
+
.addError(UserNotFound, { status: 404 })
|
|
177
333
|
)
|
|
178
|
-
//
|
|
179
|
-
.addError(Unauthorized, { status: 401 }) {
|
|
334
|
+
// Add a 401 error response to the entire group
|
|
335
|
+
.addError(Unauthorized, { status: 401 }) {
|
|
336
|
+
// ...etc
|
|
337
|
+
}
|
|
180
338
|
```
|
|
181
339
|
|
|
182
|
-
|
|
183
|
-
just by calling `HttpApiEndpoint.addError` multiple times.
|
|
340
|
+
You can add multiple error responses to a single endpoint by calling `HttpApiEndpoint.addError` multiple times. This allows you to handle different types of errors with specific status codes and descriptions, ensuring that the API behaves as expected in various scenarios.
|
|
184
341
|
|
|
185
|
-
### Multipart
|
|
342
|
+
### Multipart Requests
|
|
186
343
|
|
|
187
|
-
|
|
188
|
-
api to flag a `HttpApiEndpoint` payload schema as a multipart request.
|
|
344
|
+
To handle file uploads, you can use the `HttpApiSchema.Multipart` API to designate an endpoint's payload schema as a multipart request. This allows you to specify the structure of the expected multipart data, including file uploads, using the `Multipart` module.
|
|
189
345
|
|
|
190
|
-
|
|
191
|
-
shape of the multipart request.
|
|
346
|
+
**Example** (Handling File Uploads)
|
|
192
347
|
|
|
193
348
|
```ts
|
|
194
|
-
import {
|
|
349
|
+
import {
|
|
350
|
+
HttpApiEndpoint,
|
|
351
|
+
HttpApiGroup,
|
|
352
|
+
HttpApiSchema,
|
|
353
|
+
Multipart
|
|
354
|
+
} from "@effect/platform"
|
|
355
|
+
import { Schema } from "effect"
|
|
195
356
|
|
|
196
357
|
class UsersApi extends HttpApiGroup.make("users").add(
|
|
197
358
|
HttpApiEndpoint.post("upload")`/users/upload`.setPayload(
|
|
359
|
+
// Mark the payload as a multipart request
|
|
198
360
|
HttpApiSchema.Multipart(
|
|
199
361
|
Schema.Struct({
|
|
200
|
-
//
|
|
362
|
+
// Define a "files" field for the uploaded files
|
|
201
363
|
files: Multipart.FilesSchema
|
|
202
364
|
})
|
|
203
365
|
)
|
|
@@ -205,27 +367,34 @@ class UsersApi extends HttpApiGroup.make("users").add(
|
|
|
205
367
|
) {}
|
|
206
368
|
```
|
|
207
369
|
|
|
370
|
+
This setup makes it clear that the endpoint expects a multipart request with a `files` field. The `Multipart.FilesSchema` automatically handles file data, making it easier to work with uploads in your application.
|
|
371
|
+
|
|
208
372
|
### Changing the response encoding
|
|
209
373
|
|
|
210
|
-
By default,
|
|
211
|
-
the `HttpApiSchema.withEncoding` api.
|
|
374
|
+
By default, responses are encoded as JSON. If you need a different format, you can modify the encoding using the `HttpApiSchema.withEncoding` API. This allows you to specify both the type and content of the response.
|
|
212
375
|
|
|
213
|
-
|
|
376
|
+
**Example** (Changing Response Encoding to `text/csv`)
|
|
214
377
|
|
|
215
378
|
```ts
|
|
379
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
|
|
380
|
+
import { Schema } from "effect"
|
|
381
|
+
|
|
382
|
+
// Define the UsersApi group with an endpoint that returns CSV data
|
|
216
383
|
class UsersApi extends HttpApiGroup.make("users").add(
|
|
217
|
-
HttpApiEndpoint.get("csv")`/users/csv
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
384
|
+
HttpApiEndpoint.get("csv")`/users/csv`
|
|
385
|
+
// Define the success response as a string and set the encoding to CSV
|
|
386
|
+
.addSuccess(
|
|
387
|
+
Schema.String.pipe(
|
|
388
|
+
HttpApiSchema.withEncoding({
|
|
389
|
+
kind: "Text",
|
|
390
|
+
contentType: "text/csv"
|
|
391
|
+
})
|
|
392
|
+
)
|
|
223
393
|
)
|
|
224
|
-
)
|
|
225
394
|
) {}
|
|
226
395
|
```
|
|
227
396
|
|
|
228
|
-
## Implementing a
|
|
397
|
+
## Implementing a Server
|
|
229
398
|
|
|
230
399
|
Now that you have defined your API, you can implement a server that serves the
|
|
231
400
|
endpoints.
|
|
@@ -233,14 +402,47 @@ endpoints.
|
|
|
233
402
|
The `HttpApiBuilder` module provides all the apis you need to implement your
|
|
234
403
|
server.
|
|
235
404
|
|
|
236
|
-
|
|
405
|
+
For semplicity we will use a `UsersApi` group with a single `findById` endpoint.
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import {
|
|
409
|
+
HttpApi,
|
|
410
|
+
HttpApiEndpoint,
|
|
411
|
+
HttpApiGroup,
|
|
412
|
+
HttpApiSchema
|
|
413
|
+
} from "@effect/platform"
|
|
414
|
+
import { Schema } from "effect"
|
|
415
|
+
|
|
416
|
+
class User extends Schema.Class<User>("User")({
|
|
417
|
+
id: Schema.Number,
|
|
418
|
+
name: Schema.String,
|
|
419
|
+
createdAt: Schema.DateTimeUtc
|
|
420
|
+
}) {}
|
|
421
|
+
|
|
422
|
+
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
423
|
+
|
|
424
|
+
class UsersApi extends HttpApiGroup.make("users").add(
|
|
425
|
+
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
426
|
+
) {}
|
|
427
|
+
|
|
428
|
+
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Implementing a HttpApiGroup
|
|
432
|
+
|
|
433
|
+
The `HttpApiBuilder.group` API is used to implement a specific group of endpoints within an `HttpApi` definition. It requires the following inputs:
|
|
434
|
+
|
|
435
|
+
| Input | Description |
|
|
436
|
+
| --------------------------------- | ----------------------------------------------------------------------- |
|
|
437
|
+
| The complete `HttpApi` definition | The overall API structure that includes the group you are implementing. |
|
|
438
|
+
| The name of the group | The specific group you are focusing on within the API. |
|
|
439
|
+
| A function to add handlers | A function that defines how each endpoint in the group is handled. |
|
|
237
440
|
|
|
238
|
-
|
|
441
|
+
Each endpoint in the group is connected to its logic using the `HttpApiBuilder.handle` method, which maps the endpoint's definition to its corresponding implementation.
|
|
239
442
|
|
|
240
|
-
The `HttpApiBuilder.group`
|
|
241
|
-
and a function that adds the handlers required for the group.
|
|
443
|
+
The `HttpApiBuilder.group` API produces a `Layer` that can later be provided to the server implementation.
|
|
242
444
|
|
|
243
|
-
|
|
445
|
+
**Example** (Implementing an API Group)
|
|
244
446
|
|
|
245
447
|
```ts
|
|
246
448
|
import {
|
|
@@ -250,16 +452,14 @@ import {
|
|
|
250
452
|
HttpApiGroup,
|
|
251
453
|
HttpApiSchema
|
|
252
454
|
} from "@effect/platform"
|
|
253
|
-
import { DateTime, Effect,
|
|
455
|
+
import { DateTime, Effect, Schema } from "effect"
|
|
254
456
|
|
|
255
|
-
// here is our api definition
|
|
256
457
|
class User extends Schema.Class<User>("User")({
|
|
257
458
|
id: Schema.Number,
|
|
258
459
|
name: Schema.String,
|
|
259
460
|
createdAt: Schema.DateTimeUtc
|
|
260
461
|
}) {}
|
|
261
462
|
|
|
262
|
-
// Our user id path parameter schema
|
|
263
463
|
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
264
464
|
|
|
265
465
|
class UsersApi extends HttpApiGroup.make("users").add(
|
|
@@ -272,29 +472,65 @@ class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
|
272
472
|
// Implementation
|
|
273
473
|
// --------------------------------------------
|
|
274
474
|
|
|
275
|
-
//
|
|
276
|
-
|
|
475
|
+
// ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">>
|
|
476
|
+
// ▼
|
|
477
|
+
const UsersApiLive =
|
|
478
|
+
// ┌─── The Whole API
|
|
479
|
+
// │ ┌─── The Group you are implementing
|
|
480
|
+
// ▼ ▼
|
|
277
481
|
HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
278
|
-
handlers
|
|
279
|
-
//
|
|
280
|
-
|
|
482
|
+
handlers.handle(
|
|
483
|
+
// ┌─── The Endpoint you are implementing
|
|
484
|
+
// ▼
|
|
485
|
+
"findById",
|
|
486
|
+
// Provide the handler logic for the endpoint.
|
|
487
|
+
// The parameters & payload are passed to the handler function.
|
|
488
|
+
({ path: { userId } }) =>
|
|
281
489
|
Effect.succeed(
|
|
490
|
+
// Return a mock user object with the provided ID
|
|
282
491
|
new User({
|
|
283
492
|
id: userId,
|
|
284
493
|
name: "John Doe",
|
|
285
494
|
createdAt: DateTime.unsafeNow()
|
|
286
495
|
})
|
|
287
496
|
)
|
|
288
|
-
|
|
497
|
+
)
|
|
289
498
|
)
|
|
290
499
|
```
|
|
291
500
|
|
|
292
|
-
|
|
501
|
+
Using `HttpApiBuilder.group`, you connect the structure of your API to its logic, enabling you to focus on each endpoint's functionality in isolation. Each handler receives the parameters and payload for the request, making it easy to process input and generate a response.
|
|
293
502
|
|
|
294
|
-
|
|
295
|
-
|
|
503
|
+
### Using Services Inside a HttpApiGroup
|
|
504
|
+
|
|
505
|
+
If your handlers need to use services, you can easily integrate them because the `HttpApiBuilder.group` API allows you to return an `Effect`. This ensures that external services can be accessed and utilized directly within your handlers.
|
|
506
|
+
|
|
507
|
+
**Example** (Using Services in a Group Implementation)
|
|
296
508
|
|
|
297
509
|
```ts
|
|
510
|
+
import {
|
|
511
|
+
HttpApi,
|
|
512
|
+
HttpApiBuilder,
|
|
513
|
+
HttpApiEndpoint,
|
|
514
|
+
HttpApiGroup,
|
|
515
|
+
HttpApiSchema
|
|
516
|
+
} from "@effect/platform"
|
|
517
|
+
import { Context, Effect, Schema } from "effect"
|
|
518
|
+
|
|
519
|
+
class User extends Schema.Class<User>("User")({
|
|
520
|
+
id: Schema.Number,
|
|
521
|
+
name: Schema.String,
|
|
522
|
+
createdAt: Schema.DateTimeUtc
|
|
523
|
+
}) {}
|
|
524
|
+
|
|
525
|
+
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
526
|
+
|
|
527
|
+
class UsersApi extends HttpApiGroup.make("users").add(
|
|
528
|
+
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
529
|
+
) {}
|
|
530
|
+
|
|
531
|
+
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
532
|
+
|
|
533
|
+
// Define the UsersRepository service
|
|
298
534
|
class UsersRepository extends Context.Tag("UsersRepository")<
|
|
299
535
|
UsersRepository,
|
|
300
536
|
{
|
|
@@ -302,14 +538,11 @@ class UsersRepository extends Context.Tag("UsersRepository")<
|
|
|
302
538
|
}
|
|
303
539
|
>() {}
|
|
304
540
|
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
never,
|
|
309
|
-
UsersRepository
|
|
310
|
-
> = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
311
|
-
// we can return an Effect that creates our handlers
|
|
541
|
+
// ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">, never, UsersRepository>
|
|
542
|
+
// ▼
|
|
543
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
312
544
|
Effect.gen(function* () {
|
|
545
|
+
// Access the UsersRepository service
|
|
313
546
|
const repository = yield* UsersRepository
|
|
314
547
|
return handlers.handle("findById", ({ path: { userId } }) =>
|
|
315
548
|
repository.findById(userId)
|
|
@@ -318,90 +551,142 @@ const UsersApiLive: Layer.Layer<
|
|
|
318
551
|
)
|
|
319
552
|
```
|
|
320
553
|
|
|
321
|
-
### Implementing a
|
|
554
|
+
### Implementing a HttpApi
|
|
322
555
|
|
|
323
|
-
Once all your groups are implemented, you can
|
|
556
|
+
Once all your groups are implemented, you can create a top-level implementation to combine them into a unified API. This is done using the `HttpApiBuilder.api` API, which generates a `Layer`. You then use `Layer.provide` to include the implementations of all the groups into the top-level `HttpApi`.
|
|
324
557
|
|
|
325
|
-
|
|
326
|
-
to add all the group implementations.
|
|
558
|
+
**Example** (Combining Group Implementations into a Top-Level API)
|
|
327
559
|
|
|
328
560
|
```ts
|
|
329
|
-
|
|
330
|
-
|
|
561
|
+
import {
|
|
562
|
+
HttpApi,
|
|
563
|
+
HttpApiBuilder,
|
|
564
|
+
HttpApiEndpoint,
|
|
565
|
+
HttpApiGroup,
|
|
566
|
+
HttpApiSchema
|
|
567
|
+
} from "@effect/platform"
|
|
568
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
569
|
+
|
|
570
|
+
class User extends Schema.Class<User>("User")({
|
|
571
|
+
id: Schema.Number,
|
|
572
|
+
name: Schema.String,
|
|
573
|
+
createdAt: Schema.DateTimeUtc
|
|
574
|
+
}) {}
|
|
575
|
+
|
|
576
|
+
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
577
|
+
|
|
578
|
+
class UsersApi extends HttpApiGroup.make("users").add(
|
|
579
|
+
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
580
|
+
) {}
|
|
581
|
+
|
|
582
|
+
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
583
|
+
|
|
584
|
+
// --------------------------------------------
|
|
585
|
+
// Implementation
|
|
586
|
+
// --------------------------------------------
|
|
587
|
+
|
|
588
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
589
|
+
handlers.handle("findById", ({ path: { userId } }) =>
|
|
590
|
+
Effect.succeed(
|
|
591
|
+
// Return a mock user object with the provided ID
|
|
592
|
+
new User({
|
|
593
|
+
id: userId,
|
|
594
|
+
name: "John Doe",
|
|
595
|
+
createdAt: DateTime.unsafeNow()
|
|
596
|
+
})
|
|
597
|
+
)
|
|
598
|
+
)
|
|
331
599
|
)
|
|
600
|
+
|
|
601
|
+
// Combine all group implementations into the top-level API
|
|
602
|
+
//
|
|
603
|
+
// ┌─── Layer<HttpApi.Api, never, never>
|
|
604
|
+
// ▼
|
|
605
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(UsersApiLive))
|
|
332
606
|
```
|
|
333
607
|
|
|
334
608
|
### Serving the API
|
|
335
609
|
|
|
336
|
-
|
|
610
|
+
You can serve your API using the `HttpApiBuilder.serve` API. This function builds an `HttpApp` from an `HttpApi` instance and serves it using an `HttpServer`.
|
|
611
|
+
|
|
612
|
+
Optionally, you can provide middleware to enhance the `HttpApp` before serving it.
|
|
337
613
|
|
|
338
|
-
|
|
339
|
-
use some of the middleware Layer's from the `HttpApiBuilder` module.
|
|
614
|
+
**Example** (Serving an API with Middleware)
|
|
340
615
|
|
|
341
616
|
```ts
|
|
342
|
-
import {
|
|
617
|
+
import {
|
|
618
|
+
HttpApi,
|
|
619
|
+
HttpApiBuilder,
|
|
620
|
+
HttpApiEndpoint,
|
|
621
|
+
HttpApiGroup,
|
|
622
|
+
HttpApiSchema,
|
|
623
|
+
HttpMiddleware,
|
|
624
|
+
HttpServer
|
|
625
|
+
} from "@effect/platform"
|
|
343
626
|
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
627
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
344
628
|
import { createServer } from "node:http"
|
|
345
629
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// Provide the API implementation
|
|
352
|
-
Layer.provide(MyApiLive),
|
|
353
|
-
// Log the address the server is listening on
|
|
354
|
-
HttpServer.withLogAddress,
|
|
355
|
-
// Provide the HTTP server implementation
|
|
356
|
-
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
357
|
-
)
|
|
630
|
+
class User extends Schema.Class<User>("User")({
|
|
631
|
+
id: Schema.Number,
|
|
632
|
+
name: Schema.String,
|
|
633
|
+
createdAt: Schema.DateTimeUtc
|
|
634
|
+
}) {}
|
|
358
635
|
|
|
359
|
-
|
|
360
|
-
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
361
|
-
```
|
|
636
|
+
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
362
637
|
|
|
363
|
-
|
|
638
|
+
class UsersApi extends HttpApiGroup.make("users").add(
|
|
639
|
+
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
640
|
+
) {}
|
|
364
641
|
|
|
365
|
-
|
|
642
|
+
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
366
643
|
|
|
367
|
-
|
|
368
|
-
|
|
644
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
645
|
+
handlers.handle("findById", ({ path: { userId } }) =>
|
|
646
|
+
Effect.succeed(
|
|
647
|
+
new User({
|
|
648
|
+
id: userId,
|
|
649
|
+
name: "John Doe",
|
|
650
|
+
createdAt: DateTime.unsafeNow()
|
|
651
|
+
})
|
|
652
|
+
)
|
|
653
|
+
)
|
|
654
|
+
)
|
|
369
655
|
|
|
370
|
-
|
|
371
|
-
import { HttpApiSwagger } from "@effect/platform"
|
|
656
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(UsersApiLive))
|
|
372
657
|
|
|
658
|
+
// Use the `HttpApiBuilder.serve` function to serve the API
|
|
373
659
|
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
374
|
-
//
|
|
375
|
-
Layer.provide(
|
|
376
|
-
HttpApiSwagger.layer({
|
|
377
|
-
// "/docs" is the default path for the swagger documentation
|
|
378
|
-
path: "/docs"
|
|
379
|
-
})
|
|
380
|
-
),
|
|
660
|
+
// Add middleware for Cross-Origin Resource Sharing (CORS)
|
|
381
661
|
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
662
|
+
// Provide the API implementation
|
|
382
663
|
Layer.provide(MyApiLive),
|
|
664
|
+
// Log the server's listening address
|
|
665
|
+
HttpServer.withLogAddress,
|
|
666
|
+
// Provide the HTTP server implementation
|
|
383
667
|
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
384
668
|
)
|
|
669
|
+
|
|
670
|
+
// run the server
|
|
671
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
385
672
|
```
|
|
386
673
|
|
|
387
|
-
##
|
|
674
|
+
## Middlewares
|
|
388
675
|
|
|
389
|
-
### Defining
|
|
676
|
+
### Defining Middleware
|
|
390
677
|
|
|
391
|
-
The `HttpApiMiddleware` module
|
|
678
|
+
The `HttpApiMiddleware` module allows you to add middleware to your API. Middleware can enhance your API by introducing features like logging, authentication, or additional error handling.
|
|
392
679
|
|
|
393
|
-
You can
|
|
394
|
-
allows you to set:
|
|
680
|
+
You can define middleware using the `HttpApiMiddleware.Tag` class, which lets you specify:
|
|
395
681
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
`provides` & `failure` options will not affect the handlers or final error type.
|
|
682
|
+
| Option | Description |
|
|
683
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
684
|
+
| `failure` | A schema that describes any errors the middleware might return. |
|
|
685
|
+
| `provides` | A `Context.Tag` representing the resource or data the middleware will provide to subsequent handlers. |
|
|
686
|
+
| `security` | Definitions from `HttpApiSecurity` that the middleware will implement, such as authentication mechanisms. |
|
|
687
|
+
| `optional` | A boolean indicating whether the request should continue if the middleware fails with an expected error. When `optional` is set to `true`, the `provides` and `failure` options do not affect the final error type or handlers. |
|
|
403
688
|
|
|
404
|
-
|
|
689
|
+
**Example** (Defining a Logger Middleware)
|
|
405
690
|
|
|
406
691
|
```ts
|
|
407
692
|
import {
|
|
@@ -411,42 +696,126 @@ import {
|
|
|
411
696
|
} from "@effect/platform"
|
|
412
697
|
import { Schema } from "effect"
|
|
413
698
|
|
|
699
|
+
// Define a schema for errors returned by the logger middleware
|
|
414
700
|
class LoggerError extends Schema.TaggedError<LoggerError>()(
|
|
415
701
|
"LoggerError",
|
|
416
702
|
{}
|
|
417
703
|
) {}
|
|
418
704
|
|
|
419
|
-
//
|
|
705
|
+
// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag
|
|
420
706
|
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
|
|
421
|
-
//
|
|
707
|
+
// Optionally define the error schema for the middleware
|
|
422
708
|
failure: LoggerError
|
|
423
709
|
}) {}
|
|
424
710
|
|
|
425
|
-
// apply the middleware to an `HttpApiGroup`
|
|
426
711
|
class UsersApi extends HttpApiGroup.make("users")
|
|
427
712
|
.add(
|
|
428
713
|
HttpApiEndpoint.get("findById")`/${Schema.NumberFromString}`
|
|
429
|
-
//
|
|
714
|
+
// Apply the middleware to a single endpoint
|
|
430
715
|
.middleware(Logger)
|
|
431
716
|
)
|
|
432
|
-
//
|
|
717
|
+
// Or apply the middleware to the entire group
|
|
433
718
|
.middleware(Logger) {}
|
|
434
719
|
```
|
|
435
720
|
|
|
721
|
+
### Implementing HttpApiMiddleware
|
|
722
|
+
|
|
723
|
+
Once you have defined your `HttpApiMiddleware`, you can implement it as a `Layer`. This allows the middleware to be applied to specific API groups or endpoints, enabling modular and reusable behavior.
|
|
724
|
+
|
|
725
|
+
**Example** (Implementing and Using Logger Middleware)
|
|
726
|
+
|
|
727
|
+
```ts
|
|
728
|
+
import { HttpApiMiddleware, HttpServerRequest } from "@effect/platform"
|
|
729
|
+
import { Effect, Layer } from "effect"
|
|
730
|
+
|
|
731
|
+
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger") {}
|
|
732
|
+
|
|
733
|
+
const LoggerLive = Layer.effect(
|
|
734
|
+
Logger,
|
|
735
|
+
Effect.gen(function* () {
|
|
736
|
+
yield* Effect.log("creating Logger middleware")
|
|
737
|
+
|
|
738
|
+
// Middleware implementation as an Effect
|
|
739
|
+
// that can access the `HttpServerRequest` context.
|
|
740
|
+
return Effect.gen(function* () {
|
|
741
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
742
|
+
yield* Effect.log(`Request: ${request.method} ${request.url}`)
|
|
743
|
+
})
|
|
744
|
+
})
|
|
745
|
+
)
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
After implementing the middleware, you can attach it to your API groups or specific endpoints using the `Layer` APIs.
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
import {
|
|
752
|
+
HttpApi,
|
|
753
|
+
HttpApiBuilder,
|
|
754
|
+
HttpApiEndpoint,
|
|
755
|
+
HttpApiGroup,
|
|
756
|
+
HttpApiMiddleware,
|
|
757
|
+
HttpServerRequest
|
|
758
|
+
} from "@effect/platform"
|
|
759
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
760
|
+
|
|
761
|
+
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger") {}
|
|
762
|
+
|
|
763
|
+
const LoggerLive = Layer.effect(
|
|
764
|
+
Logger,
|
|
765
|
+
Effect.gen(function* () {
|
|
766
|
+
yield* Effect.log("creating Logger middleware")
|
|
767
|
+
return Effect.gen(function* () {
|
|
768
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
769
|
+
yield* Effect.log(`Request: ${request.method} ${request.url}`)
|
|
770
|
+
})
|
|
771
|
+
})
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
class UsersApi extends HttpApiGroup.make("users").add(
|
|
775
|
+
HttpApiEndpoint.get("findById")`/${Schema.NumberFromString}`.middleware(
|
|
776
|
+
Logger
|
|
777
|
+
)
|
|
778
|
+
) {}
|
|
779
|
+
|
|
780
|
+
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
781
|
+
|
|
782
|
+
class User extends Schema.Class<User>("User")({
|
|
783
|
+
id: Schema.Number,
|
|
784
|
+
name: Schema.String,
|
|
785
|
+
createdAt: Schema.DateTimeUtc
|
|
786
|
+
}) {}
|
|
787
|
+
|
|
788
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
789
|
+
handlers.handle("findById", (req) =>
|
|
790
|
+
Effect.succeed(
|
|
791
|
+
new User({
|
|
792
|
+
id: req.path[0],
|
|
793
|
+
name: "John Doe",
|
|
794
|
+
createdAt: DateTime.unsafeNow()
|
|
795
|
+
})
|
|
796
|
+
)
|
|
797
|
+
)
|
|
798
|
+
).pipe(
|
|
799
|
+
// Provide the Logger middleware to the group
|
|
800
|
+
Layer.provide(LoggerLive)
|
|
801
|
+
)
|
|
802
|
+
```
|
|
803
|
+
|
|
436
804
|
### Defining security middleware
|
|
437
805
|
|
|
438
|
-
The `HttpApiSecurity` module
|
|
439
|
-
|
|
806
|
+
The `HttpApiSecurity` module enables you to add security annotations to your API. These annotations specify the type of authorization required to access specific endpoints.
|
|
807
|
+
|
|
808
|
+
Supported authorization types include:
|
|
440
809
|
|
|
441
|
-
|
|
810
|
+
| Authorization Type | Description |
|
|
811
|
+
| ------------------------ | ---------------------------------------------------------------- |
|
|
812
|
+
| `HttpApiSecurity.apiKey` | API key authorization via headers, query parameters, or cookies. |
|
|
813
|
+
| `HttpApiSecurity.basic` | HTTP Basic authentication. |
|
|
814
|
+
| `HttpApiSecurity.bearer` | Bearer token authentication. |
|
|
442
815
|
|
|
443
|
-
|
|
444
|
-
parameters, or cookies.
|
|
445
|
-
- `HttpApiSecurity.basicAuth` - HTTP Basic authentication.
|
|
446
|
-
- `HttpApiSecurity.bearerAuth` - Bearer token authentication.
|
|
816
|
+
These security annotations can be used alongside `HttpApiMiddleware` to create middleware that protects your API endpoints.
|
|
447
817
|
|
|
448
|
-
|
|
449
|
-
to define middleware that will protect your endpoints.
|
|
818
|
+
**Example** (Defining Security Middleware)
|
|
450
819
|
|
|
451
820
|
```ts
|
|
452
821
|
import {
|
|
@@ -458,92 +827,54 @@ import {
|
|
|
458
827
|
} from "@effect/platform"
|
|
459
828
|
import { Context, Schema } from "effect"
|
|
460
829
|
|
|
830
|
+
// Define a schema for the "User"
|
|
461
831
|
class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}
|
|
462
832
|
|
|
833
|
+
// Define a schema for the "Unauthorized" error
|
|
463
834
|
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
464
835
|
"Unauthorized",
|
|
465
836
|
{},
|
|
837
|
+
// Specify the HTTP status code for unauthorized errors
|
|
466
838
|
HttpApiSchema.annotations({ status: 401 })
|
|
467
839
|
) {}
|
|
468
840
|
|
|
841
|
+
// Define a Context.Tag for the authenticated user
|
|
469
842
|
class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
|
|
470
843
|
|
|
471
|
-
//
|
|
844
|
+
// Create the Authorization middleware
|
|
472
845
|
class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
473
846
|
"Authorization",
|
|
474
847
|
{
|
|
475
|
-
//
|
|
848
|
+
// Define the error schema for unauthorized access
|
|
476
849
|
failure: Unauthorized,
|
|
477
|
-
//
|
|
850
|
+
// Specify the resource this middleware will provide
|
|
478
851
|
provides: CurrentUser,
|
|
479
|
-
//
|
|
852
|
+
// Add security definitions
|
|
480
853
|
security: {
|
|
481
|
-
//
|
|
854
|
+
// ┌─── Custom name for the security definition
|
|
855
|
+
// ▼
|
|
482
856
|
myBearer: HttpApiSecurity.bearer
|
|
483
|
-
//
|
|
484
|
-
// They will attempt to be resolved in the order they are defined
|
|
857
|
+
// Additional security definitions can be added here.
|
|
858
|
+
// They will attempt to be resolved in the order they are defined.
|
|
485
859
|
}
|
|
486
860
|
}
|
|
487
861
|
) {}
|
|
488
862
|
|
|
489
|
-
// apply the middleware to an `HttpApiGroup`
|
|
490
863
|
class UsersApi extends HttpApiGroup.make("users")
|
|
491
864
|
.add(
|
|
492
865
|
HttpApiEndpoint.get("findById")`/${Schema.NumberFromString}`
|
|
493
|
-
//
|
|
866
|
+
// Apply the middleware to a single endpoint
|
|
494
867
|
.middleware(Authorization)
|
|
495
868
|
)
|
|
496
|
-
//
|
|
869
|
+
// Or apply the middleware to the entire group
|
|
497
870
|
.middleware(Authorization) {}
|
|
498
871
|
```
|
|
499
872
|
|
|
500
|
-
### Implementing
|
|
501
|
-
|
|
502
|
-
Once your `HttpApiMiddleware` is defined, you can use the
|
|
503
|
-
`HttpApiMiddleware.Tag` definition to implement your middleware.
|
|
504
|
-
|
|
505
|
-
By using the `Layer` apis, you can create a Layer that implements your
|
|
506
|
-
middleware.
|
|
507
|
-
|
|
508
|
-
Here is an example:
|
|
509
|
-
|
|
510
|
-
```ts
|
|
511
|
-
import { HttpApiMiddleware, HttpServerRequest } from "@effect/platform"
|
|
512
|
-
import { Effect, Layer } from "effect"
|
|
513
|
-
|
|
514
|
-
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger") {}
|
|
515
|
-
|
|
516
|
-
const LoggerLive = Layer.effect(
|
|
517
|
-
Logger,
|
|
518
|
-
Effect.gen(function* () {
|
|
519
|
-
yield* Effect.log("creating Logger middleware")
|
|
520
|
-
|
|
521
|
-
// standard middleware is just an Effect, that can access the `HttpRouter`
|
|
522
|
-
// context.
|
|
523
|
-
return Logger.of(
|
|
524
|
-
Effect.gen(function* () {
|
|
525
|
-
const request = yield* HttpServerRequest.HttpServerRequest
|
|
526
|
-
yield* Effect.log(`Request: ${request.method} ${request.url}`)
|
|
527
|
-
})
|
|
528
|
-
)
|
|
529
|
-
})
|
|
530
|
-
)
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
When the `Layer` is created, you can then provide it to your group layers:
|
|
534
|
-
|
|
535
|
-
```ts
|
|
536
|
-
const UsersApiLive = HttpApiBuilder.group(...).pipe(
|
|
537
|
-
Layer.provide(LoggerLive)
|
|
538
|
-
)
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
### Implementing `HttpApiSecurity` middleware
|
|
873
|
+
### Implementing HttpApiSecurity middleware
|
|
542
874
|
|
|
543
|
-
|
|
544
|
-
looks a bit different.
|
|
875
|
+
When using `HttpApiSecurity` in your middleware, the implementation involves creating a `Layer` with security handlers tailored to your requirements. Below is an example demonstrating how to implement middleware for `HttpApiSecurity.bearer` authentication.
|
|
545
876
|
|
|
546
|
-
|
|
877
|
+
**Example** (Implementing Bearer Token Authentication Middleware)
|
|
547
878
|
|
|
548
879
|
```ts
|
|
549
880
|
import {
|
|
@@ -577,62 +908,240 @@ const AuthorizationLive = Layer.effect(
|
|
|
577
908
|
Effect.gen(function* () {
|
|
578
909
|
yield* Effect.log("creating Authorization middleware")
|
|
579
910
|
|
|
580
|
-
//
|
|
581
|
-
return
|
|
911
|
+
// Return the security handlers for the middleware
|
|
912
|
+
return {
|
|
913
|
+
// Define the handler for the Bearer token
|
|
914
|
+
// The Bearer token is redacted for security
|
|
582
915
|
myBearer: (bearerToken) =>
|
|
583
916
|
Effect.gen(function* () {
|
|
584
917
|
yield* Effect.log(
|
|
585
918
|
"checking bearer token",
|
|
586
919
|
Redacted.value(bearerToken)
|
|
587
920
|
)
|
|
588
|
-
//
|
|
921
|
+
// Return a mock User object as the CurrentUser
|
|
589
922
|
return new User({ id: 1 })
|
|
590
923
|
})
|
|
591
|
-
}
|
|
924
|
+
}
|
|
592
925
|
})
|
|
593
926
|
)
|
|
594
927
|
```
|
|
595
928
|
|
|
596
|
-
### Setting
|
|
929
|
+
### Setting HttpApiSecurity cookies
|
|
597
930
|
|
|
598
|
-
|
|
599
|
-
`HttpApiBuilder.securitySetCookie` api.
|
|
931
|
+
To set a security cookie from within a handler, you can use the `HttpApiBuilder.securitySetCookie` API. This method sets a cookie with default properties, including the `HttpOnly` and `Secure` flags, ensuring the cookie is not accessible via JavaScript and is transmitted over secure connections.
|
|
600
932
|
|
|
601
|
-
|
|
933
|
+
**Example** (Setting a Security Cookie in a Login Handler)
|
|
602
934
|
|
|
603
935
|
```ts
|
|
936
|
+
// Define the security configuration for an API key stored in a cookie
|
|
604
937
|
const security = HttpApiSecurity.apiKey({
|
|
605
|
-
|
|
938
|
+
// Specify that the API key is stored in a cookie
|
|
939
|
+
in: "cookie"
|
|
940
|
+
// Define the cookie name,
|
|
606
941
|
key: "token"
|
|
607
942
|
})
|
|
608
943
|
|
|
609
944
|
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
610
945
|
handlers.handle("login", () =>
|
|
611
|
-
//
|
|
946
|
+
// Set the security cookie with a redacted value
|
|
612
947
|
HttpApiBuilder.securitySetCookie(security, Redacted.make("keep me secret"))
|
|
613
948
|
)
|
|
614
949
|
)
|
|
615
950
|
```
|
|
616
951
|
|
|
617
|
-
##
|
|
952
|
+
## Serving Swagger documentation
|
|
618
953
|
|
|
619
|
-
|
|
620
|
-
the server.
|
|
954
|
+
You can add Swagger documentation to your API using the `HttpApiSwagger` module. This integration provides an interactive interface for developers to explore and test your API. To enable Swagger, you simply provide the `HttpApiSwagger.layer` to your server implementation.
|
|
621
955
|
|
|
622
|
-
|
|
956
|
+
**Example** (Adding Swagger Documentation to an API)
|
|
623
957
|
|
|
624
958
|
```ts
|
|
625
|
-
import {
|
|
959
|
+
import {
|
|
960
|
+
HttpApi,
|
|
961
|
+
HttpApiBuilder,
|
|
962
|
+
HttpApiEndpoint,
|
|
963
|
+
HttpApiGroup,
|
|
964
|
+
HttpApiSchema,
|
|
965
|
+
HttpApiSwagger,
|
|
966
|
+
HttpMiddleware,
|
|
967
|
+
HttpServer
|
|
968
|
+
} from "@effect/platform"
|
|
969
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
970
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
971
|
+
import { createServer } from "node:http"
|
|
626
972
|
|
|
627
|
-
|
|
973
|
+
class User extends Schema.Class<User>("User")({
|
|
974
|
+
id: Schema.Number,
|
|
975
|
+
name: Schema.String,
|
|
976
|
+
createdAt: Schema.DateTimeUtc
|
|
977
|
+
}) {}
|
|
978
|
+
|
|
979
|
+
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
980
|
+
|
|
981
|
+
class UsersApi extends HttpApiGroup.make("users").add(
|
|
982
|
+
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
983
|
+
) {}
|
|
984
|
+
|
|
985
|
+
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
986
|
+
|
|
987
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
988
|
+
handlers.handle("findById", ({ path: { userId } }) =>
|
|
989
|
+
Effect.succeed(
|
|
990
|
+
new User({
|
|
991
|
+
id: userId,
|
|
992
|
+
name: "John Doe",
|
|
993
|
+
createdAt: DateTime.unsafeNow()
|
|
994
|
+
})
|
|
995
|
+
)
|
|
996
|
+
)
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(UsersApiLive))
|
|
1000
|
+
|
|
1001
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1002
|
+
// Add the Swagger documentation layer
|
|
1003
|
+
Layer.provide(
|
|
1004
|
+
HttpApiSwagger.layer({
|
|
1005
|
+
// Specify the Swagger documentation path.
|
|
1006
|
+
// "/docs" is the default path.
|
|
1007
|
+
path: "/docs"
|
|
1008
|
+
})
|
|
1009
|
+
),
|
|
1010
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1011
|
+
Layer.provide(MyApiLive),
|
|
1012
|
+
HttpServer.withLogAddress,
|
|
1013
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+

|
|
1020
|
+
|
|
1021
|
+
### Adding OpenAPI Annotations
|
|
1022
|
+
|
|
1023
|
+
You can enhance your API documentation by adding OpenAPI annotations using the `OpenApi` module. These annotations allow you to include metadata such as titles, descriptions, and other details, making your API documentation more informative and easier to use.
|
|
1024
|
+
|
|
1025
|
+
**Example** (Adding OpenAPI Annotations to a Group)
|
|
1026
|
+
|
|
1027
|
+
In this example:
|
|
1028
|
+
|
|
1029
|
+
- A title ("Users API") and description ("API for managing users") are added to the `UsersApi` group.
|
|
1030
|
+
- These annotations will appear in the generated OpenAPI documentation.
|
|
1031
|
+
|
|
1032
|
+
```ts
|
|
1033
|
+
import { OpenApi } from "@effect/platform"
|
|
1034
|
+
|
|
1035
|
+
class UsersApi extends HttpApiGroup.make("users").add(
|
|
1036
|
+
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`
|
|
1037
|
+
.addSuccess(User)
|
|
1038
|
+
// You can set one attribute at a time
|
|
1039
|
+
.annotate(OpenApi.Title, "Users API")
|
|
1040
|
+
// or multiple at once
|
|
1041
|
+
.annotateContext(
|
|
1042
|
+
OpenApi.annotations({
|
|
1043
|
+
title: "Users API",
|
|
1044
|
+
description: "API for managing users"
|
|
1045
|
+
})
|
|
1046
|
+
)
|
|
1047
|
+
) {}
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
Annotations can also be applied to the entire API. In this example, a title ("My API") is added to the top-level `HttpApi`.
|
|
1051
|
+
|
|
1052
|
+
**Example** (Adding OpenAPI Annotations to the Top-Level API)
|
|
1053
|
+
|
|
1054
|
+
```ts
|
|
1055
|
+
class MyApi extends HttpApi.make("myApi")
|
|
1056
|
+
.add(UsersApi)
|
|
1057
|
+
// Add a title for the top-level API
|
|
1058
|
+
.annotate(OpenApi.Title, "My API") {}
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
## Deriving a Client
|
|
1062
|
+
|
|
1063
|
+
After defining your API, you can derive a client that interacts with the server. The `HttpApiClient` module simplifies the process by providing tools to generate a client based on your API definition.
|
|
1064
|
+
|
|
1065
|
+
**Example** (Deriving and Using a Client)
|
|
1066
|
+
|
|
1067
|
+
This example demonstrates how to create a client for an API and use it to call an endpoint.
|
|
1068
|
+
|
|
1069
|
+
```ts
|
|
1070
|
+
import {
|
|
1071
|
+
FetchHttpClient,
|
|
1072
|
+
HttpApi,
|
|
1073
|
+
HttpApiBuilder,
|
|
1074
|
+
HttpApiClient,
|
|
1075
|
+
HttpApiEndpoint,
|
|
1076
|
+
HttpApiGroup,
|
|
1077
|
+
HttpApiSchema,
|
|
1078
|
+
HttpApiSwagger,
|
|
1079
|
+
HttpMiddleware,
|
|
1080
|
+
HttpServer
|
|
1081
|
+
} from "@effect/platform"
|
|
1082
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1083
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
1084
|
+
import { createServer } from "node:http"
|
|
1085
|
+
|
|
1086
|
+
class User extends Schema.Class<User>("User")({
|
|
1087
|
+
id: Schema.Number,
|
|
1088
|
+
name: Schema.String,
|
|
1089
|
+
createdAt: Schema.DateTimeUtc
|
|
1090
|
+
}) {}
|
|
1091
|
+
|
|
1092
|
+
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
1093
|
+
|
|
1094
|
+
class UsersApi extends HttpApiGroup.make("users").add(
|
|
1095
|
+
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
1096
|
+
) {}
|
|
1097
|
+
|
|
1098
|
+
class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
1099
|
+
|
|
1100
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
1101
|
+
handlers.handle("findById", ({ path: { userId } }) =>
|
|
1102
|
+
Effect.succeed(
|
|
1103
|
+
new User({
|
|
1104
|
+
id: userId,
|
|
1105
|
+
name: "John Doe",
|
|
1106
|
+
createdAt: DateTime.unsafeNow()
|
|
1107
|
+
})
|
|
1108
|
+
)
|
|
1109
|
+
)
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(UsersApiLive))
|
|
1113
|
+
|
|
1114
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1115
|
+
Layer.provide(HttpApiSwagger.layer()),
|
|
1116
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1117
|
+
Layer.provide(MyApiLive),
|
|
1118
|
+
HttpServer.withLogAddress,
|
|
1119
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1123
|
+
|
|
1124
|
+
// Create a program that derives and uses the client
|
|
1125
|
+
const program = Effect.gen(function* () {
|
|
1126
|
+
// Derive the client
|
|
628
1127
|
const client = yield* HttpApiClient.make(MyApi, {
|
|
629
1128
|
baseUrl: "http://localhost:3000"
|
|
630
|
-
// You can transform the HttpClient to add things like authentication
|
|
631
|
-
// transformClient: ....
|
|
632
1129
|
})
|
|
1130
|
+
// Call the `findById` endpoint
|
|
633
1131
|
const user = yield* client.users.findById({ path: { userId: 1 } })
|
|
634
|
-
|
|
1132
|
+
console.log(user)
|
|
635
1133
|
})
|
|
1134
|
+
|
|
1135
|
+
// Provide a Fetch-based HTTP client and run the program
|
|
1136
|
+
Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
|
|
1137
|
+
/*
|
|
1138
|
+
Example Output:
|
|
1139
|
+
User {
|
|
1140
|
+
id: 1,
|
|
1141
|
+
name: 'John Doe',
|
|
1142
|
+
createdAt: DateTime.Utc(2025-01-04T15:14:49.562Z)
|
|
1143
|
+
}
|
|
1144
|
+
*/
|
|
636
1145
|
```
|
|
637
1146
|
|
|
638
1147
|
# HTTP Client
|