@effect/platform 0.72.1 → 0.72.2
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 +1498 -332
- package/dist/cjs/HttpApi.js +22 -18
- package/dist/cjs/HttpApi.js.map +1 -1
- package/dist/cjs/HttpApiSchema.js +33 -4
- package/dist/cjs/HttpApiSchema.js.map +1 -1
- package/dist/cjs/HttpApiSecurity.js +2 -0
- package/dist/cjs/HttpApiSecurity.js.map +1 -1
- package/dist/cjs/OpenApi.js +55 -105
- package/dist/cjs/OpenApi.js.map +1 -1
- package/dist/cjs/OpenApiJsonSchema.js +7 -4
- package/dist/cjs/OpenApiJsonSchema.js.map +1 -1
- package/dist/dts/HttpApi.d.ts +3 -0
- package/dist/dts/HttpApi.d.ts.map +1 -1
- package/dist/dts/HttpApiSchema.d.ts.map +1 -1
- package/dist/dts/HttpApiSecurity.d.ts +2 -0
- package/dist/dts/HttpApiSecurity.d.ts.map +1 -1
- package/dist/dts/OpenApi.d.ts +2 -1
- package/dist/dts/OpenApi.d.ts.map +1 -1
- package/dist/dts/OpenApiJsonSchema.d.ts.map +1 -1
- package/dist/esm/HttpApi.js +22 -18
- package/dist/esm/HttpApi.js.map +1 -1
- package/dist/esm/HttpApiSchema.js +30 -3
- package/dist/esm/HttpApiSchema.js.map +1 -1
- package/dist/esm/HttpApiSecurity.js +2 -0
- package/dist/esm/HttpApiSecurity.js.map +1 -1
- package/dist/esm/OpenApi.js +55 -105
- package/dist/esm/OpenApi.js.map +1 -1
- package/dist/esm/OpenApiJsonSchema.js +4 -2
- package/dist/esm/OpenApiJsonSchema.js.map +1 -1
- package/package.json +2 -2
- package/src/HttpApi.ts +23 -23
- package/src/HttpApiSchema.ts +33 -8
- package/src/HttpApiSecurity.ts +2 -0
- package/src/OpenApi.ts +80 -101
- package/src/OpenApiJsonSchema.ts +9 -1
package/README.md
CHANGED
|
@@ -9,19 +9,48 @@ Welcome to the documentation for `@effect/platform`, a library designed for crea
|
|
|
9
9
|
|
|
10
10
|
## Overview
|
|
11
11
|
|
|
12
|
-
The `HttpApi
|
|
12
|
+
The `HttpApi*` modules offer a flexible and declarative way to define HTTP APIs.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
- Provide a Swagger documentation page
|
|
16
|
-
- Derive a fully-typed client
|
|
14
|
+
To define an API, create a set of `HttpEndpoint`s. Each endpoint is described by a path, a method, and schemas for the request and response.
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
Collections of endpoints are grouped in an `HttpApiGroup`, and multiple groups can be merged into a complete `HttpApi`.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
HttpApi
|
|
20
|
+
├── HttpGroup
|
|
21
|
+
│ ├── HttpEndpoint
|
|
22
|
+
│ └── HttpEndpoint
|
|
23
|
+
└── HttpGroup
|
|
24
|
+
├── HttpEndpoint
|
|
25
|
+
├── HttpEndpoint
|
|
26
|
+
└── HttpEndpoint
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Once your API is defined, the same definition can be reused for multiple purposes:
|
|
30
|
+
|
|
31
|
+
- **Starting a Server**: Use the API definition to implement and serve endpoints.
|
|
32
|
+
- **Generating Documentation**: Create a Swagger page to document the API.
|
|
33
|
+
- **Deriving a Client**: Generate a fully-typed client for your API.
|
|
34
|
+
|
|
35
|
+
Benefits of a Single API Definition:
|
|
36
|
+
|
|
37
|
+
- **Consistency**: A single definition ensures the server, documentation, and client remain aligned.
|
|
38
|
+
- **Reduced Maintenance**: Changes to the API are reflected across all related components.
|
|
39
|
+
- **Simplified Workflow**: Avoids duplication by consolidating API details in one place.
|
|
19
40
|
|
|
20
41
|
## Hello World
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
### Defining and Implementing an API
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
This example demonstrates how to define and implement a simple API with a single endpoint that returns a string response. The structure of the API is as follows:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
HttpApi ("MyApi)
|
|
49
|
+
└── HttpGroup ("Greetings")
|
|
50
|
+
└── HttpEndpoint ("hello-world")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Example** (Hello World Definition)
|
|
25
54
|
|
|
26
55
|
```ts
|
|
27
56
|
import {
|
|
@@ -55,15 +84,21 @@ const ServerLive = HttpApiBuilder.serve().pipe(
|
|
|
55
84
|
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
56
85
|
)
|
|
57
86
|
|
|
58
|
-
//
|
|
87
|
+
// Launch the server
|
|
59
88
|
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
60
89
|
```
|
|
61
90
|
|
|
62
|
-
|
|
91
|
+
After running the code, open a browser and navigate to http://localhost:3000. The server will respond with:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Hello, World!
|
|
95
|
+
```
|
|
63
96
|
|
|
64
97
|
### Serving The Auto Generated Swagger Documentation
|
|
65
98
|
|
|
66
|
-
You can
|
|
99
|
+
You can enhance your API by adding auto-generated Swagger documentation using the `HttpApiSwagger` module. This makes it easier for developers to explore and interact with your API.
|
|
100
|
+
|
|
101
|
+
To include Swagger in your server setup, provide the `HttpApiSwagger.layer` when configuring the server.
|
|
67
102
|
|
|
68
103
|
**Example** (Serving Swagger Documentation)
|
|
69
104
|
|
|
@@ -101,15 +136,17 @@ const ServerLive = HttpApiBuilder.serve().pipe(
|
|
|
101
136
|
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
102
137
|
```
|
|
103
138
|
|
|
104
|
-
|
|
139
|
+
After running the server, open your browser and navigate to http://localhost:3000/docs.
|
|
140
|
+
|
|
141
|
+
This URL will display the Swagger documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively.
|
|
105
142
|
|
|
106
143
|

|
|
107
144
|
|
|
108
145
|
### Deriving a Client
|
|
109
146
|
|
|
110
|
-
|
|
147
|
+
Once you have defined your API, you can generate a client to interact with it using the `HttpApiClient` module. This allows you to call your API endpoints without manually handling HTTP requests.
|
|
111
148
|
|
|
112
|
-
**Example** (Deriving a Client)
|
|
149
|
+
**Example** (Deriving and Using a Client)
|
|
113
150
|
|
|
114
151
|
```ts
|
|
115
152
|
import {
|
|
@@ -161,96 +198,450 @@ Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
|
|
|
161
198
|
// Output: Hello, World!
|
|
162
199
|
```
|
|
163
200
|
|
|
164
|
-
##
|
|
201
|
+
## Defining a HttpApiEndpoint
|
|
202
|
+
|
|
203
|
+
An `HttpApiEndpoint` represents a single endpoint in your API. Each endpoint is defined with a name, path, HTTP method, and optional schemas for requests and responses. This allows you to describe the structure and behavior of your API.
|
|
204
|
+
|
|
205
|
+
Below is an example of a simple CRUD API for managing users, which includes the following endpoints:
|
|
206
|
+
|
|
207
|
+
- `GET /users` - Retrieve all users.
|
|
208
|
+
- `GET /users/:userId` - Retrieve a specific user by ID.
|
|
209
|
+
- `POST /users` - Create a new user.
|
|
210
|
+
- `DELETE /users/:userId` - Delete a user by ID.
|
|
211
|
+
- `PATCH /users/:userId` - Update a user by ID.
|
|
212
|
+
|
|
213
|
+
### GET
|
|
214
|
+
|
|
215
|
+
The `HttpApiEndpoint.get` method allows you to define a GET endpoint by specifying its name, path, and optionally, a schema for the response.
|
|
216
|
+
|
|
217
|
+
To define the structure of successful responses, use the `.addSuccess` method. If no schema is provided, the default response status is `204 No Content`.
|
|
165
218
|
|
|
166
|
-
|
|
219
|
+
**Example** (Defining a GET Endpoint to Retrieve All Users)
|
|
167
220
|
|
|
168
|
-
|
|
221
|
+
```ts
|
|
222
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
223
|
+
import { Schema } from "effect"
|
|
224
|
+
|
|
225
|
+
// Define a schema representing a User entity
|
|
226
|
+
const User = Schema.Struct({
|
|
227
|
+
id: Schema.Number,
|
|
228
|
+
name: Schema.String,
|
|
229
|
+
createdAt: Schema.DateTimeUtc
|
|
230
|
+
})
|
|
169
231
|
|
|
232
|
+
// Define the "getUsers" endpoint, returning a list of users
|
|
233
|
+
const getUsers = HttpApiEndpoint
|
|
234
|
+
// ┌─── Endpoint name
|
|
235
|
+
// │ ┌─── Endpoint path
|
|
236
|
+
// ▼ ▼
|
|
237
|
+
.get("getUsers", "/users")
|
|
238
|
+
// Define the success schema for the response (optional).
|
|
239
|
+
// If no response schema is specified, the default response is `204 No Content`.
|
|
240
|
+
.addSuccess(Schema.Array(User))
|
|
170
241
|
```
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
242
|
+
|
|
243
|
+
### Path Parameters
|
|
244
|
+
|
|
245
|
+
Path parameters allow you to include dynamic segments in your endpoint's path. There are two ways to define path parameters in your API.
|
|
246
|
+
|
|
247
|
+
#### Using setPath
|
|
248
|
+
|
|
249
|
+
The `setPath` method allows you to explicitly define path parameters by associating them with a schema.
|
|
250
|
+
|
|
251
|
+
**Example** (Defining Parameters with setPath)
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
255
|
+
import { Schema } from "effect"
|
|
256
|
+
|
|
257
|
+
const User = Schema.Struct({
|
|
258
|
+
id: Schema.Number,
|
|
259
|
+
name: Schema.String,
|
|
260
|
+
createdAt: Schema.DateTimeUtc
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Define a GET endpoint with a path parameter ":id"
|
|
264
|
+
const getUser = HttpApiEndpoint.get("getUser", "/user/:id")
|
|
265
|
+
.setPath(
|
|
266
|
+
Schema.Struct({
|
|
267
|
+
// Define a schema for the "id" path parameter
|
|
268
|
+
id: Schema.NumberFromString
|
|
269
|
+
})
|
|
270
|
+
)
|
|
271
|
+
.addSuccess(User)
|
|
179
272
|
```
|
|
180
273
|
|
|
181
|
-
|
|
274
|
+
#### Using Template Strings
|
|
275
|
+
|
|
276
|
+
You can also define path parameters by embedding them in a template string with the help of `HttpApiSchema.param`.
|
|
277
|
+
|
|
278
|
+
**Example** (Defining Parameters using a Template String)
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
282
|
+
import { Schema } from "effect"
|
|
283
|
+
|
|
284
|
+
const User = Schema.Struct({
|
|
285
|
+
id: Schema.Number,
|
|
286
|
+
name: Schema.String,
|
|
287
|
+
createdAt: Schema.DateTimeUtc
|
|
288
|
+
})
|
|
182
289
|
|
|
183
|
-
|
|
290
|
+
// Create a path parameter using HttpApiSchema.param
|
|
291
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
292
|
+
|
|
293
|
+
// Define the GET endpoint using a template string
|
|
294
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
295
|
+
User
|
|
296
|
+
)
|
|
297
|
+
```
|
|
184
298
|
|
|
185
|
-
|
|
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
|
|
299
|
+
### POST
|
|
189
300
|
|
|
190
|
-
|
|
301
|
+
The `HttpApiEndpoint.post` method is used to define an endpoint for creating resources. You can specify a schema for the request body (payload) and a schema for the successful response.
|
|
302
|
+
|
|
303
|
+
**Example** (Defining a POST Endpoint with Payload and Success Schemas)
|
|
191
304
|
|
|
192
305
|
```ts
|
|
193
|
-
import { HttpApiEndpoint
|
|
306
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
194
307
|
import { Schema } from "effect"
|
|
195
308
|
|
|
196
|
-
//
|
|
197
|
-
|
|
309
|
+
// Define a schema for the user object
|
|
310
|
+
const User = Schema.Struct({
|
|
198
311
|
id: Schema.Number,
|
|
199
312
|
name: Schema.String,
|
|
200
313
|
createdAt: Schema.DateTimeUtc
|
|
201
|
-
})
|
|
314
|
+
})
|
|
202
315
|
|
|
203
|
-
//
|
|
204
|
-
const
|
|
316
|
+
// Define a POST endpoint for creating a new user
|
|
317
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
318
|
+
// Define the request body schema (payload)
|
|
319
|
+
.setPayload(
|
|
320
|
+
Schema.Struct({
|
|
321
|
+
name: Schema.String
|
|
322
|
+
})
|
|
323
|
+
)
|
|
324
|
+
// Define the schema for a successful response
|
|
325
|
+
.addSuccess(User)
|
|
326
|
+
```
|
|
205
327
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
328
|
+
### DELETE
|
|
329
|
+
|
|
330
|
+
The `HttpApiEndpoint.del` method is used to define an endpoint for deleting a resource.
|
|
331
|
+
|
|
332
|
+
**Example** (Defining a DELETE Endpoint with Path Parameters)
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
336
|
+
import { Schema } from "effect"
|
|
337
|
+
|
|
338
|
+
// Define a path parameter for the user ID
|
|
339
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
340
|
+
|
|
341
|
+
// Define a DELETE endpoint to delete a user by ID
|
|
342
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### PATCH
|
|
346
|
+
|
|
347
|
+
The `HttpApiEndpoint.patch` method is used to define an endpoint for partially updating a resource. This method allows you to specify a schema for the request payload and a schema for the successful response.
|
|
348
|
+
|
|
349
|
+
**Example** (Defining a PATCH Endpoint for Updating a User)
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
353
|
+
import { Schema } from "effect"
|
|
354
|
+
|
|
355
|
+
// Define a schema for the user object
|
|
356
|
+
const User = Schema.Struct({
|
|
357
|
+
id: Schema.Number,
|
|
358
|
+
name: Schema.String,
|
|
359
|
+
createdAt: Schema.DateTimeUtc
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// Define a path parameter for the user ID
|
|
363
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
364
|
+
|
|
365
|
+
// Define a PATCH endpoint to update a user's name by ID
|
|
366
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
367
|
+
// Specify the schema for the request payload
|
|
368
|
+
.setPayload(
|
|
369
|
+
Schema.Struct({
|
|
370
|
+
name: Schema.String // Only the name can be updated
|
|
371
|
+
})
|
|
213
372
|
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
373
|
+
// Specify the schema for a successful response
|
|
374
|
+
.addSuccess(User)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Catch-All Endpoints
|
|
378
|
+
|
|
379
|
+
The path can also be `"*"` to match any incoming path. This is useful for defining a catch-all endpoint to handle unmatched routes or provide a fallback response.
|
|
380
|
+
|
|
381
|
+
**Example** (Defining a Catch-All Endpoint)
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
385
|
+
|
|
386
|
+
const catchAll = HttpApiEndpoint.get("catchAll", "*")
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Setting URL Parameters
|
|
390
|
+
|
|
391
|
+
The `setUrlParams` method allows you to define the structure of URL parameters for an endpoint. You can specify the schema for each parameter and include metadata such as descriptions to provide additional context.
|
|
392
|
+
|
|
393
|
+
**Example** (Defining URL Parameters with Metadata)
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
397
|
+
import { Schema } from "effect"
|
|
398
|
+
|
|
399
|
+
const User = Schema.Struct({
|
|
400
|
+
id: Schema.Number,
|
|
401
|
+
name: Schema.String,
|
|
402
|
+
createdAt: Schema.DateTimeUtc
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
406
|
+
// Specify the URL parameters schema
|
|
407
|
+
.setUrlParams(
|
|
408
|
+
Schema.Struct({
|
|
409
|
+
// Parameter "page" for pagination
|
|
410
|
+
page: Schema.NumberFromString,
|
|
411
|
+
// Parameter "sort" for sorting options with an added description
|
|
412
|
+
sort: Schema.String.annotations({
|
|
413
|
+
description: "Sorting criteria (e.g., 'name', 'date')"
|
|
414
|
+
})
|
|
415
|
+
})
|
|
226
416
|
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
417
|
+
.addSuccess(Schema.Array(User))
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### Defining an Array of Values for a URL Parameter
|
|
421
|
+
|
|
422
|
+
When defining a URL parameter that accepts multiple values, you can use the `Schema.Array` combinator. This allows the parameter to handle an array of items, with each item adhering to a specified schema.
|
|
423
|
+
|
|
424
|
+
**Example** (Defining an Array of String Values for a URL Parameter)
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
|
|
428
|
+
import { Schema } from "effect"
|
|
429
|
+
|
|
430
|
+
const api = HttpApi.make("myApi").add(
|
|
431
|
+
HttpApiGroup.make("group").add(
|
|
432
|
+
HttpApiEndpoint.get("get", "/")
|
|
433
|
+
.setUrlParams(
|
|
233
434
|
Schema.Struct({
|
|
234
|
-
|
|
435
|
+
// Define "a" as an array of strings
|
|
436
|
+
a: Schema.Array(Schema.String)
|
|
235
437
|
})
|
|
236
438
|
)
|
|
439
|
+
.addSuccess(Schema.String)
|
|
237
440
|
)
|
|
441
|
+
)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
You can test this endpoint by passing an array of values in the query string. For example:
|
|
445
|
+
|
|
446
|
+
```sh
|
|
447
|
+
curl "http://localhost:3000/?a=1&a=2"
|
|
238
448
|
```
|
|
239
449
|
|
|
240
|
-
|
|
450
|
+
The query string sends two values (`1` and `2`) for the `a` parameter. The server will process and validate these values according to the schema.
|
|
451
|
+
|
|
452
|
+
### Status Codes
|
|
453
|
+
|
|
454
|
+
By default, the success status code is `200 OK`. You can change it by annotating the schema with a custom status.
|
|
241
455
|
|
|
242
|
-
**Example** (Defining a
|
|
456
|
+
**Example** (Defining a GET Endpoint with a custom status code)
|
|
243
457
|
|
|
244
458
|
```ts
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
459
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
460
|
+
import { Schema } from "effect"
|
|
461
|
+
|
|
462
|
+
const User = Schema.Struct({
|
|
463
|
+
id: Schema.Number,
|
|
464
|
+
name: Schema.String,
|
|
465
|
+
createdAt: Schema.DateTimeUtc
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
469
|
+
// Override the default success status
|
|
470
|
+
.addSuccess(Schema.Array(User), { status: 206 })
|
|
249
471
|
```
|
|
250
472
|
|
|
251
|
-
###
|
|
473
|
+
### Handling Multipart Requests
|
|
252
474
|
|
|
253
|
-
|
|
475
|
+
To support file uploads, you can use the `HttpApiSchema.Multipart` API. This allows you to define an endpoint's payload schema as a multipart request, specifying the structure of the data, including file uploads, with the `Multipart` module.
|
|
476
|
+
|
|
477
|
+
**Example** (Defining an Endpoint for File Uploads)
|
|
478
|
+
|
|
479
|
+
In this example, the `HttpApiSchema.Multipart` function marks the payload as a multipart request. The `files` field uses `Multipart.FilesSchema` to handle uploaded file data automatically.
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
import { HttpApiEndpoint, HttpApiSchema, Multipart } from "@effect/platform"
|
|
483
|
+
import { Schema } from "effect"
|
|
484
|
+
|
|
485
|
+
const upload = HttpApiEndpoint.post("upload", "/users/upload").setPayload(
|
|
486
|
+
// Specify that the payload is a multipart request
|
|
487
|
+
HttpApiSchema.Multipart(
|
|
488
|
+
Schema.Struct({
|
|
489
|
+
// Define a "files" field to handle file uploads
|
|
490
|
+
files: Multipart.FilesSchema
|
|
491
|
+
})
|
|
492
|
+
).addSuccess(Schema.String)
|
|
493
|
+
)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
You can test this endpoint by sending a multipart request with a file upload. For example:
|
|
497
|
+
|
|
498
|
+
```sh
|
|
499
|
+
echo "Sample file content" | curl -X POST -F "files=@-" http://localhost:3000/users/upload
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Changing the Request Encoding
|
|
503
|
+
|
|
504
|
+
By default, API requests are encoded as JSON. If your application requires a different format, you can customize the request encoding using the `HttpApiSchema.withEncoding` method. This allows you to define the encoding type and content type of the request.
|
|
505
|
+
|
|
506
|
+
**Example** (Customizing Request Encoding)
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
510
|
+
import { Schema } from "effect"
|
|
511
|
+
|
|
512
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
513
|
+
// Set the request payload as a string encoded with URL parameters
|
|
514
|
+
.setPayload(
|
|
515
|
+
Schema.Struct({
|
|
516
|
+
a: Schema.String // Parameter "a" must be a string
|
|
517
|
+
})
|
|
518
|
+
// Specify the encoding as URL parameters
|
|
519
|
+
.pipe(HttpApiSchema.withEncoding({ kind: "UrlParams" }))
|
|
520
|
+
)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Changing the Response Encoding
|
|
524
|
+
|
|
525
|
+
By default, API responses are encoded as JSON. If your application requires a different format, you can customize the encoding using the `HttpApiSchema.withEncoding` API. This method lets you define the type and content type of the response.
|
|
526
|
+
|
|
527
|
+
**Example** (Returning Data as `text/csv`)
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
531
|
+
import { Schema } from "effect"
|
|
532
|
+
|
|
533
|
+
const csv = HttpApiEndpoint.get("csv")`/users/csv`
|
|
534
|
+
// Set the success response as a string with CSV encoding
|
|
535
|
+
.addSuccess(
|
|
536
|
+
Schema.String.pipe(
|
|
537
|
+
HttpApiSchema.withEncoding({
|
|
538
|
+
// Specify the type of the response
|
|
539
|
+
kind: "Text",
|
|
540
|
+
// Define the content type as text/csv
|
|
541
|
+
contentType: "text/csv"
|
|
542
|
+
})
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Setting Request Headers
|
|
548
|
+
|
|
549
|
+
The `HttpApiEndpoint.setHeaders` method allows you to define the expected structure of request headers. You can specify the schema for each header and include additional metadata, such as descriptions.
|
|
550
|
+
|
|
551
|
+
**Example** (Defining Request Headers with Metadata)
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
555
|
+
import { Schema } from "effect"
|
|
556
|
+
|
|
557
|
+
const User = Schema.Struct({
|
|
558
|
+
id: Schema.Number,
|
|
559
|
+
name: Schema.String,
|
|
560
|
+
createdAt: Schema.DateTimeUtc
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
564
|
+
// Specify the headers schema
|
|
565
|
+
.setHeaders(
|
|
566
|
+
Schema.Struct({
|
|
567
|
+
// Header must be a string
|
|
568
|
+
"X-API-Key": Schema.String,
|
|
569
|
+
// Header must be a string with an added description
|
|
570
|
+
"X-Request-ID": Schema.String.annotations({
|
|
571
|
+
description: "Unique identifier for the request"
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
)
|
|
575
|
+
.addSuccess(Schema.Array(User))
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Defining a HttpApiGroup
|
|
579
|
+
|
|
580
|
+
You can group related endpoints under a single entity by using `HttpApiGroup.make`. This can help organize your code and provide a clearer structure for your API.
|
|
581
|
+
|
|
582
|
+
**Example** (Creating a Group for User-Related Endpoints)
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
|
|
586
|
+
import { Schema } from "effect"
|
|
587
|
+
|
|
588
|
+
const User = Schema.Struct({
|
|
589
|
+
id: Schema.Number,
|
|
590
|
+
name: Schema.String,
|
|
591
|
+
createdAt: Schema.DateTimeUtc
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
595
|
+
|
|
596
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
597
|
+
Schema.Array(User)
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
601
|
+
User
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
605
|
+
.setPayload(
|
|
606
|
+
Schema.Struct({
|
|
607
|
+
name: Schema.String
|
|
608
|
+
})
|
|
609
|
+
)
|
|
610
|
+
.addSuccess(User)
|
|
611
|
+
|
|
612
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
613
|
+
|
|
614
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
615
|
+
.setPayload(
|
|
616
|
+
Schema.Struct({
|
|
617
|
+
name: Schema.String
|
|
618
|
+
})
|
|
619
|
+
)
|
|
620
|
+
.addSuccess(User)
|
|
621
|
+
|
|
622
|
+
// Group all user-related endpoints
|
|
623
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
624
|
+
.add(getUsers)
|
|
625
|
+
.add(getUser)
|
|
626
|
+
.add(createUser)
|
|
627
|
+
.add(deleteUser)
|
|
628
|
+
.add(updateUser)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
If you would like to create a more opaque type for the group, you can extend `HttpApiGroup` with a class.
|
|
632
|
+
|
|
633
|
+
**Example** (Creating a Group with an Opaque Type)
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
// Create an opaque class extending HttpApiGroup
|
|
637
|
+
class UsersGroup extends HttpApiGroup.make("users").add(getUsers).add(getUser) {
|
|
638
|
+
// Additional endpoints or methods can be added here
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Creating the Top-Level HttpApi
|
|
643
|
+
|
|
644
|
+
After defining your groups, you can combine them into one `HttpApi` representing your entire set of endpoints.
|
|
254
645
|
|
|
255
646
|
**Example** (Combining Groups into a Top-Level API)
|
|
256
647
|
|
|
@@ -263,56 +654,78 @@ import {
|
|
|
263
654
|
} from "@effect/platform"
|
|
264
655
|
import { Schema } from "effect"
|
|
265
656
|
|
|
266
|
-
|
|
657
|
+
const User = Schema.Struct({
|
|
267
658
|
id: Schema.Number,
|
|
268
659
|
name: Schema.String,
|
|
269
660
|
createdAt: Schema.DateTimeUtc
|
|
270
|
-
})
|
|
661
|
+
})
|
|
271
662
|
|
|
272
|
-
const
|
|
663
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
273
664
|
|
|
274
|
-
|
|
275
|
-
.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
665
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
666
|
+
Schema.Array(User)
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
670
|
+
User
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
674
|
+
.setPayload(
|
|
675
|
+
Schema.Struct({
|
|
676
|
+
name: Schema.String
|
|
677
|
+
})
|
|
284
678
|
)
|
|
285
|
-
.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)
|
|
679
|
+
.addSuccess(User)
|
|
680
|
+
|
|
681
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
682
|
+
|
|
683
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
684
|
+
.setPayload(
|
|
685
|
+
Schema.Struct({
|
|
686
|
+
name: Schema.String
|
|
687
|
+
})
|
|
688
|
+
)
|
|
689
|
+
.addSuccess(User)
|
|
690
|
+
|
|
691
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
692
|
+
.add(getUsers)
|
|
693
|
+
.add(getUser)
|
|
694
|
+
.add(createUser)
|
|
695
|
+
.add(deleteUser)
|
|
696
|
+
.add(updateUser)
|
|
295
697
|
|
|
296
|
-
// Combine the groups into
|
|
297
|
-
|
|
698
|
+
// Combine the groups into one API
|
|
699
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
298
700
|
|
|
299
|
-
// Alternatively,
|
|
300
|
-
|
|
701
|
+
// Alternatively, create an opaque class for your API
|
|
702
|
+
class MyApi extends HttpApi.make("myApi").add(usersGroup) {}
|
|
301
703
|
```
|
|
302
704
|
|
|
303
|
-
|
|
705
|
+
## Adding errors
|
|
304
706
|
|
|
305
|
-
Error responses
|
|
707
|
+
Error responses allow your API to handle different failure scenarios. These responses can be defined at various levels:
|
|
306
708
|
|
|
307
|
-
- Use `HttpApiEndpoint.addError` to add
|
|
308
|
-
- Use `HttpApiGroup.addError` to add
|
|
309
|
-
- Use `HttpApi.addError` to
|
|
709
|
+
- **Endpoint-level errors**: Use `HttpApiEndpoint.addError` to add errors specific to an endpoint.
|
|
710
|
+
- **Group-level errors**: Use `HttpApiGroup.addError` to add errors applicable to all endpoints in a group.
|
|
711
|
+
- **API-level errors**: Use `HttpApi.addError` to define errors that apply to every endpoint in the API.
|
|
310
712
|
|
|
311
|
-
Group-level and API-level errors are
|
|
713
|
+
Group-level and API-level errors are useful for handling shared issues like authentication failures, especially when managed through middleware.
|
|
312
714
|
|
|
313
|
-
**Example** (
|
|
715
|
+
**Example** (Defining Error Responses for Endpoints and Groups)
|
|
314
716
|
|
|
315
717
|
```ts
|
|
718
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
|
|
719
|
+
import { Schema } from "effect"
|
|
720
|
+
|
|
721
|
+
const User = Schema.Struct({
|
|
722
|
+
id: Schema.Number,
|
|
723
|
+
name: Schema.String,
|
|
724
|
+
createdAt: Schema.DateTimeUtc
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
728
|
+
|
|
316
729
|
// Define error schemas
|
|
317
730
|
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
318
731
|
"UserNotFound",
|
|
@@ -324,85 +737,108 @@ class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
|
324
737
|
{}
|
|
325
738
|
) {}
|
|
326
739
|
|
|
327
|
-
|
|
328
|
-
.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
740
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
741
|
+
Schema.Array(User)
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
745
|
+
.addSuccess(User)
|
|
746
|
+
// Add a 404 error response for this endpoint
|
|
747
|
+
.addError(UserNotFound, { status: 404 })
|
|
748
|
+
|
|
749
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
750
|
+
.add(getUsers)
|
|
751
|
+
.add(getUser)
|
|
752
|
+
// ...etc...
|
|
753
|
+
// Add a 401 error response for the entire group
|
|
754
|
+
.addError(Unauthorized, { status: 401 })
|
|
338
755
|
```
|
|
339
756
|
|
|
340
|
-
You can
|
|
757
|
+
You can assign multiple error responses to a single endpoint by calling `HttpApiEndpoint.addError` multiple times. This is useful when different types of errors might occur for a single operation.
|
|
758
|
+
|
|
759
|
+
**Example** (Adding Multiple Errors to an Endpoint)
|
|
760
|
+
|
|
761
|
+
```ts
|
|
762
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
763
|
+
// Add a 404 error response for when the user is not found
|
|
764
|
+
.addError(UserNotFound, { status: 404 })
|
|
765
|
+
// Add a 401 error response for unauthorized access
|
|
766
|
+
.addError(Unauthorized, { status: 401 })
|
|
767
|
+
```
|
|
341
768
|
|
|
342
|
-
###
|
|
769
|
+
### Predefined Empty Error Types
|
|
343
770
|
|
|
344
|
-
|
|
771
|
+
The `HttpApiError` module provides a set of predefined empty error types that you can use in your endpoints. These error types help standardize common HTTP error responses, such as `404 Not Found` or `401 Unauthorized`. Using these predefined types simplifies error handling and ensures consistency across your API.
|
|
345
772
|
|
|
346
|
-
**Example** (
|
|
773
|
+
**Example** (Adding a Predefined Error to an Endpoint)
|
|
347
774
|
|
|
348
775
|
```ts
|
|
349
|
-
import {
|
|
350
|
-
HttpApiEndpoint,
|
|
351
|
-
HttpApiGroup,
|
|
352
|
-
HttpApiSchema,
|
|
353
|
-
Multipart
|
|
354
|
-
} from "@effect/platform"
|
|
776
|
+
import { HttpApiEndpoint, HttpApiError, HttpApiSchema } from "@effect/platform"
|
|
355
777
|
import { Schema } from "effect"
|
|
356
778
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
)
|
|
367
|
-
)
|
|
779
|
+
const User = Schema.Struct({
|
|
780
|
+
id: Schema.Number,
|
|
781
|
+
name: Schema.String,
|
|
782
|
+
createdAt: Schema.DateTimeUtc
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
786
|
+
|
|
787
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
788
|
+
.addSuccess(User)
|
|
789
|
+
.addError(HttpApiError.NotFound)
|
|
368
790
|
```
|
|
369
791
|
|
|
370
|
-
|
|
792
|
+
| Name | Status | Description |
|
|
793
|
+
| --------------------- | ------ | -------------------------------------------------------------------------------------------------- |
|
|
794
|
+
| `HttpApiDecodeError` | 400 | Represents an error where the request did not match the expected schema. Includes detailed issues. |
|
|
795
|
+
| `BadRequest` | 400 | Indicates that the request was malformed or invalid. |
|
|
796
|
+
| `Unauthorized` | 401 | Indicates that authentication is required but missing or invalid. |
|
|
797
|
+
| `Forbidden` | 403 | Indicates that the client does not have permission to access the requested resource. |
|
|
798
|
+
| `NotFound` | 404 | Indicates that the requested resource could not be found. |
|
|
799
|
+
| `MethodNotAllowed` | 405 | Indicates that the HTTP method used is not allowed for the requested resource. |
|
|
800
|
+
| `NotAcceptable` | 406 | Indicates that the requested resource cannot be delivered in a format acceptable to the client. |
|
|
801
|
+
| `RequestTimeout` | 408 | Indicates that the server timed out waiting for the client request. |
|
|
802
|
+
| `Conflict` | 409 | Indicates a conflict in the request, such as conflicting data. |
|
|
803
|
+
| `Gone` | 410 | Indicates that the requested resource is no longer available and will not return. |
|
|
804
|
+
| `InternalServerError` | 500 | Indicates an unexpected server error occurred. |
|
|
805
|
+
| `NotImplemented` | 501 | Indicates that the requested functionality is not implemented on the server. |
|
|
806
|
+
| `ServiceUnavailable` | 503 | Indicates that the server is temporarily unavailable, often due to maintenance or overload. |
|
|
371
807
|
|
|
372
|
-
|
|
808
|
+
## Prefixing
|
|
373
809
|
|
|
374
|
-
|
|
810
|
+
Prefixes can be added to endpoints, groups, or an entire API to simplify the management of common paths. This is especially useful when defining multiple related endpoints that share a common base URL.
|
|
375
811
|
|
|
376
|
-
**Example** (
|
|
812
|
+
**Example** (Using Prefixes for Common Path Management)
|
|
377
813
|
|
|
378
814
|
```ts
|
|
379
|
-
import { HttpApiEndpoint, HttpApiGroup
|
|
815
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
|
|
380
816
|
import { Schema } from "effect"
|
|
381
817
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
contentType: "text/csv"
|
|
391
|
-
})
|
|
818
|
+
const api = HttpApi.make("api")
|
|
819
|
+
.add(
|
|
820
|
+
HttpApiGroup.make("group")
|
|
821
|
+
.add(
|
|
822
|
+
HttpApiEndpoint.get("getRoot", "/")
|
|
823
|
+
.addSuccess(Schema.String)
|
|
824
|
+
// Prefix for this endpoint
|
|
825
|
+
.prefix("/endpointPrefix")
|
|
392
826
|
)
|
|
393
|
-
|
|
394
|
-
|
|
827
|
+
.add(HttpApiEndpoint.get("getA", "/a").addSuccess(Schema.String))
|
|
828
|
+
// Prefix for all endpoints in the group
|
|
829
|
+
.prefix("/groupPrefix")
|
|
830
|
+
)
|
|
831
|
+
// Prefix for the entire API
|
|
832
|
+
.prefix("/apiPrefix")
|
|
395
833
|
```
|
|
396
834
|
|
|
397
835
|
## Implementing a Server
|
|
398
836
|
|
|
399
|
-
|
|
400
|
-
endpoints.
|
|
837
|
+
After defining your API, you can implement a server to handle its endpoints. The `HttpApiBuilder` module provides tools to help you connect your API's structure to the logic that serves requests.
|
|
401
838
|
|
|
402
|
-
|
|
403
|
-
server.
|
|
839
|
+
Here, we will create a simple example with a `getUser` endpoint organized within a `users` group.
|
|
404
840
|
|
|
405
|
-
|
|
841
|
+
**Example** (Defining the `users` Group and API)
|
|
406
842
|
|
|
407
843
|
```ts
|
|
408
844
|
import {
|
|
@@ -413,19 +849,19 @@ import {
|
|
|
413
849
|
} from "@effect/platform"
|
|
414
850
|
import { Schema } from "effect"
|
|
415
851
|
|
|
416
|
-
|
|
852
|
+
const User = Schema.Struct({
|
|
417
853
|
id: Schema.Number,
|
|
418
854
|
name: Schema.String,
|
|
419
855
|
createdAt: Schema.DateTimeUtc
|
|
420
|
-
})
|
|
856
|
+
})
|
|
421
857
|
|
|
422
|
-
const
|
|
858
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
423
859
|
|
|
424
|
-
|
|
425
|
-
HttpApiEndpoint.get("
|
|
426
|
-
)
|
|
860
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
861
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
862
|
+
)
|
|
427
863
|
|
|
428
|
-
|
|
864
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
429
865
|
```
|
|
430
866
|
|
|
431
867
|
### Implementing a HttpApiGroup
|
|
@@ -442,7 +878,7 @@ Each endpoint in the group is connected to its logic using the `HttpApiBuilder.h
|
|
|
442
878
|
|
|
443
879
|
The `HttpApiBuilder.group` API produces a `Layer` that can later be provided to the server implementation.
|
|
444
880
|
|
|
445
|
-
**Example** (Implementing
|
|
881
|
+
**Example** (Implementing a Group with Endpoint Logic)
|
|
446
882
|
|
|
447
883
|
```ts
|
|
448
884
|
import {
|
|
@@ -454,19 +890,19 @@ import {
|
|
|
454
890
|
} from "@effect/platform"
|
|
455
891
|
import { DateTime, Effect, Schema } from "effect"
|
|
456
892
|
|
|
457
|
-
|
|
893
|
+
const User = Schema.Struct({
|
|
458
894
|
id: Schema.Number,
|
|
459
895
|
name: Schema.String,
|
|
460
896
|
createdAt: Schema.DateTimeUtc
|
|
461
|
-
})
|
|
897
|
+
})
|
|
462
898
|
|
|
463
|
-
const
|
|
899
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
464
900
|
|
|
465
|
-
|
|
466
|
-
HttpApiEndpoint.get("
|
|
467
|
-
)
|
|
901
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
902
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
903
|
+
)
|
|
468
904
|
|
|
469
|
-
|
|
905
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
470
906
|
|
|
471
907
|
// --------------------------------------------
|
|
472
908
|
// Implementation
|
|
@@ -474,25 +910,25 @@ class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
|
474
910
|
|
|
475
911
|
// ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">>
|
|
476
912
|
// ▼
|
|
477
|
-
const
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
HttpApiBuilder.group(
|
|
913
|
+
const usersGroupLive =
|
|
914
|
+
// ┌─── The Whole API
|
|
915
|
+
// │ ┌─── The Group you are implementing
|
|
916
|
+
// ▼ ▼
|
|
917
|
+
HttpApiBuilder.group(api, "users", (handlers) =>
|
|
482
918
|
handlers.handle(
|
|
483
919
|
// ┌─── The Endpoint you are implementing
|
|
484
920
|
// ▼
|
|
485
|
-
"
|
|
921
|
+
"getUser",
|
|
486
922
|
// Provide the handler logic for the endpoint.
|
|
487
923
|
// The parameters & payload are passed to the handler function.
|
|
488
|
-
({ path: {
|
|
924
|
+
({ path: { id } }) =>
|
|
489
925
|
Effect.succeed(
|
|
490
926
|
// Return a mock user object with the provided ID
|
|
491
|
-
|
|
492
|
-
id
|
|
927
|
+
{
|
|
928
|
+
id,
|
|
493
929
|
name: "John Doe",
|
|
494
930
|
createdAt: DateTime.unsafeNow()
|
|
495
|
-
}
|
|
931
|
+
}
|
|
496
932
|
)
|
|
497
933
|
)
|
|
498
934
|
)
|
|
@@ -516,19 +952,25 @@ import {
|
|
|
516
952
|
} from "@effect/platform"
|
|
517
953
|
import { Context, Effect, Schema } from "effect"
|
|
518
954
|
|
|
519
|
-
|
|
955
|
+
const User = Schema.Struct({
|
|
520
956
|
id: Schema.Number,
|
|
521
957
|
name: Schema.String,
|
|
522
958
|
createdAt: Schema.DateTimeUtc
|
|
523
|
-
})
|
|
959
|
+
})
|
|
524
960
|
|
|
525
|
-
const
|
|
961
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
526
962
|
|
|
527
|
-
|
|
528
|
-
HttpApiEndpoint.get("
|
|
529
|
-
)
|
|
963
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
964
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
965
|
+
)
|
|
530
966
|
|
|
531
|
-
|
|
967
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
968
|
+
|
|
969
|
+
// --------------------------------------------
|
|
970
|
+
// Implementation
|
|
971
|
+
// --------------------------------------------
|
|
972
|
+
|
|
973
|
+
type User = typeof User.Type
|
|
532
974
|
|
|
533
975
|
// Define the UsersRepository service
|
|
534
976
|
class UsersRepository extends Context.Tag("UsersRepository")<
|
|
@@ -538,14 +980,16 @@ class UsersRepository extends Context.Tag("UsersRepository")<
|
|
|
538
980
|
}
|
|
539
981
|
>() {}
|
|
540
982
|
|
|
983
|
+
// Implement the `users` group with access to the UsersRepository service
|
|
984
|
+
//
|
|
541
985
|
// ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">, never, UsersRepository>
|
|
542
986
|
// ▼
|
|
543
|
-
const
|
|
987
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
544
988
|
Effect.gen(function* () {
|
|
545
989
|
// Access the UsersRepository service
|
|
546
990
|
const repository = yield* UsersRepository
|
|
547
|
-
return handlers.handle("
|
|
548
|
-
repository.findById(
|
|
991
|
+
return handlers.handle("getUser", ({ path: { id } }) =>
|
|
992
|
+
repository.findById(id)
|
|
549
993
|
)
|
|
550
994
|
})
|
|
551
995
|
)
|
|
@@ -567,34 +1011,27 @@ import {
|
|
|
567
1011
|
} from "@effect/platform"
|
|
568
1012
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
569
1013
|
|
|
570
|
-
|
|
1014
|
+
const User = Schema.Struct({
|
|
571
1015
|
id: Schema.Number,
|
|
572
1016
|
name: Schema.String,
|
|
573
1017
|
createdAt: Schema.DateTimeUtc
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
1018
|
+
})
|
|
577
1019
|
|
|
578
|
-
|
|
579
|
-
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
580
|
-
) {}
|
|
1020
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
581
1021
|
|
|
582
|
-
|
|
1022
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1023
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1024
|
+
)
|
|
583
1025
|
|
|
584
|
-
|
|
585
|
-
// Implementation
|
|
586
|
-
// --------------------------------------------
|
|
1026
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
587
1027
|
|
|
588
|
-
const
|
|
589
|
-
handlers.handle("
|
|
590
|
-
Effect.succeed(
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
createdAt: DateTime.unsafeNow()
|
|
596
|
-
})
|
|
597
|
-
)
|
|
1028
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1029
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1030
|
+
Effect.succeed({
|
|
1031
|
+
id,
|
|
1032
|
+
name: "John Doe",
|
|
1033
|
+
createdAt: DateTime.unsafeNow()
|
|
1034
|
+
})
|
|
598
1035
|
)
|
|
599
1036
|
)
|
|
600
1037
|
|
|
@@ -602,16 +1039,14 @@ const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
|
602
1039
|
//
|
|
603
1040
|
// ┌─── Layer<HttpApi.Api, never, never>
|
|
604
1041
|
// ▼
|
|
605
|
-
const MyApiLive = HttpApiBuilder.api(
|
|
1042
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
606
1043
|
```
|
|
607
1044
|
|
|
608
1045
|
### Serving the API
|
|
609
1046
|
|
|
610
|
-
You can serve your API using the `HttpApiBuilder.serve`
|
|
1047
|
+
You can serve your API using the `HttpApiBuilder.serve` function. This utility builds an `HttpApp` from an `HttpApi` instance and uses an `HttpServer` to handle requests. Middleware can be added to customize or enhance the server's behavior.
|
|
611
1048
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
**Example** (Serving an API with Middleware)
|
|
1049
|
+
**Example** (Setting Up and Serving an API with Middleware)
|
|
615
1050
|
|
|
616
1051
|
```ts
|
|
617
1052
|
import {
|
|
@@ -627,50 +1062,228 @@ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
|
627
1062
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
628
1063
|
import { createServer } from "node:http"
|
|
629
1064
|
|
|
630
|
-
|
|
1065
|
+
const User = Schema.Struct({
|
|
631
1066
|
id: Schema.Number,
|
|
632
1067
|
name: Schema.String,
|
|
633
1068
|
createdAt: Schema.DateTimeUtc
|
|
634
|
-
})
|
|
1069
|
+
})
|
|
635
1070
|
|
|
636
|
-
const
|
|
1071
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
637
1072
|
|
|
638
|
-
|
|
639
|
-
HttpApiEndpoint.get("
|
|
640
|
-
)
|
|
1073
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1074
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1075
|
+
)
|
|
641
1076
|
|
|
642
|
-
|
|
1077
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
643
1078
|
|
|
644
|
-
const
|
|
645
|
-
handlers.handle("
|
|
646
|
-
Effect.succeed(
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
})
|
|
652
|
-
)
|
|
1079
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1080
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1081
|
+
Effect.succeed({
|
|
1082
|
+
id,
|
|
1083
|
+
name: "John Doe",
|
|
1084
|
+
createdAt: DateTime.unsafeNow()
|
|
1085
|
+
})
|
|
653
1086
|
)
|
|
654
1087
|
)
|
|
655
1088
|
|
|
656
|
-
const MyApiLive = HttpApiBuilder.api(
|
|
1089
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
657
1090
|
|
|
658
|
-
//
|
|
1091
|
+
// Configure and serve the API
|
|
659
1092
|
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
660
|
-
// Add middleware
|
|
1093
|
+
// Add CORS middleware to handle cross-origin requests
|
|
661
1094
|
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
662
1095
|
// Provide the API implementation
|
|
663
1096
|
Layer.provide(MyApiLive),
|
|
664
1097
|
// Log the server's listening address
|
|
665
1098
|
HttpServer.withLogAddress,
|
|
666
|
-
//
|
|
1099
|
+
// Set up the Node.js HTTP server
|
|
1100
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
// Launch the server
|
|
1104
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
### Accessing the HttpServerRequest
|
|
1108
|
+
|
|
1109
|
+
In some cases, you may need to access details about the incoming `HttpServerRequest` within an endpoint handler. The HttpServerRequest module provides access to the request object, allowing you to inspect properties such as the HTTP method or headers.
|
|
1110
|
+
|
|
1111
|
+
**Example** (Accessing the Request Object in a GET Endpoint)
|
|
1112
|
+
|
|
1113
|
+
```ts
|
|
1114
|
+
import {
|
|
1115
|
+
HttpApi,
|
|
1116
|
+
HttpApiBuilder,
|
|
1117
|
+
HttpApiEndpoint,
|
|
1118
|
+
HttpApiGroup,
|
|
1119
|
+
HttpMiddleware,
|
|
1120
|
+
HttpServer,
|
|
1121
|
+
HttpServerRequest
|
|
1122
|
+
} from "@effect/platform"
|
|
1123
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1124
|
+
import { Effect, Layer, Schema } from "effect"
|
|
1125
|
+
import { createServer } from "node:http"
|
|
1126
|
+
|
|
1127
|
+
const api = HttpApi.make("myApi").add(
|
|
1128
|
+
HttpApiGroup.make("group").add(
|
|
1129
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
1130
|
+
)
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1134
|
+
handlers.handle("get", () =>
|
|
1135
|
+
Effect.gen(function* () {
|
|
1136
|
+
// Access the incoming request
|
|
1137
|
+
const req = yield* HttpServerRequest.HttpServerRequest
|
|
1138
|
+
|
|
1139
|
+
// Log the HTTP method for demonstration purposes
|
|
1140
|
+
console.log(req.method)
|
|
1141
|
+
|
|
1142
|
+
// Return a response
|
|
1143
|
+
return "Hello, World!"
|
|
1144
|
+
})
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1149
|
+
|
|
1150
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1151
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1152
|
+
Layer.provide(MyApiLive),
|
|
1153
|
+
HttpServer.withLogAddress,
|
|
667
1154
|
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
668
1155
|
)
|
|
669
1156
|
|
|
670
|
-
// run the server
|
|
671
1157
|
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
672
1158
|
```
|
|
673
1159
|
|
|
1160
|
+
### Streaming Requests
|
|
1161
|
+
|
|
1162
|
+
Streaming requests allow you to send large or continuous data streams to the server. In this example, we define an API that accepts a stream of binary data and decodes it into a string.
|
|
1163
|
+
|
|
1164
|
+
**Example** (Handling Streaming Requests)
|
|
1165
|
+
|
|
1166
|
+
```ts
|
|
1167
|
+
import {
|
|
1168
|
+
HttpApi,
|
|
1169
|
+
HttpApiBuilder,
|
|
1170
|
+
HttpApiEndpoint,
|
|
1171
|
+
HttpApiGroup,
|
|
1172
|
+
HttpApiSchema,
|
|
1173
|
+
HttpMiddleware,
|
|
1174
|
+
HttpServer
|
|
1175
|
+
} from "@effect/platform"
|
|
1176
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1177
|
+
import { Effect, Layer, Schema } from "effect"
|
|
1178
|
+
import { createServer } from "node:http"
|
|
1179
|
+
|
|
1180
|
+
const api = HttpApi.make("myApi").add(
|
|
1181
|
+
HttpApiGroup.make("group").add(
|
|
1182
|
+
HttpApiEndpoint.post("acceptStream", "/stream")
|
|
1183
|
+
// Define the payload as a Uint8Array with a specific encoding
|
|
1184
|
+
.setPayload(
|
|
1185
|
+
Schema.Uint8ArrayFromSelf.pipe(
|
|
1186
|
+
HttpApiSchema.withEncoding({
|
|
1187
|
+
kind: "Uint8Array",
|
|
1188
|
+
contentType: "application/octet-stream"
|
|
1189
|
+
})
|
|
1190
|
+
)
|
|
1191
|
+
)
|
|
1192
|
+
.addSuccess(Schema.String)
|
|
1193
|
+
)
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1197
|
+
handlers.handle("acceptStream", (req) =>
|
|
1198
|
+
// Decode the incoming binary data into a string
|
|
1199
|
+
Effect.succeed(new TextDecoder().decode(req.payload))
|
|
1200
|
+
)
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1204
|
+
|
|
1205
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1206
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1207
|
+
Layer.provide(MyApiLive),
|
|
1208
|
+
HttpServer.withLogAddress,
|
|
1209
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
You can test the streaming request using `curl` or any tool that supports sending binary data. For example:
|
|
1216
|
+
|
|
1217
|
+
```sh
|
|
1218
|
+
echo "abc" | curl -X POST 'http://localhost:3000/stream' --data-binary @- -H "Content-Type: application/octet-stream"
|
|
1219
|
+
# Output: abc
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
### Streaming Responses
|
|
1223
|
+
|
|
1224
|
+
To handle streaming responses in your API, you can use `handleRaw`. The `HttpServerResponse.stream` function is designed to return a continuous stream of data as the response.
|
|
1225
|
+
|
|
1226
|
+
**Example** (Implementing a Streaming Endpoint)
|
|
1227
|
+
|
|
1228
|
+
```ts
|
|
1229
|
+
import {
|
|
1230
|
+
HttpApi,
|
|
1231
|
+
HttpApiBuilder,
|
|
1232
|
+
HttpApiEndpoint,
|
|
1233
|
+
HttpApiGroup,
|
|
1234
|
+
HttpApiSchema,
|
|
1235
|
+
HttpMiddleware,
|
|
1236
|
+
HttpServer,
|
|
1237
|
+
HttpServerResponse
|
|
1238
|
+
} from "@effect/platform"
|
|
1239
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1240
|
+
import { Layer, Schedule, Schema, Stream } from "effect"
|
|
1241
|
+
import { createServer } from "node:http"
|
|
1242
|
+
|
|
1243
|
+
// Define the API with a single streaming endpoint
|
|
1244
|
+
const api = HttpApi.make("myApi").add(
|
|
1245
|
+
HttpApiGroup.make("group").add(
|
|
1246
|
+
HttpApiEndpoint.get("getStream", "/stream").addSuccess(
|
|
1247
|
+
Schema.String.pipe(
|
|
1248
|
+
HttpApiSchema.withEncoding({
|
|
1249
|
+
kind: "Text",
|
|
1250
|
+
contentType: "application/octet-stream"
|
|
1251
|
+
})
|
|
1252
|
+
)
|
|
1253
|
+
)
|
|
1254
|
+
)
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
// Simulate a stream of data
|
|
1258
|
+
const stream = Stream.make("a", "b", "c").pipe(
|
|
1259
|
+
Stream.schedule(Schedule.spaced("500 millis")),
|
|
1260
|
+
Stream.map((s) => new TextEncoder().encode(s))
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1264
|
+
handlers.handleRaw("getStream", () => HttpServerResponse.stream(stream))
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1268
|
+
|
|
1269
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1270
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1271
|
+
Layer.provide(MyApiLive),
|
|
1272
|
+
HttpServer.withLogAddress,
|
|
1273
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
You can test the streaming response using `curl` or any similar HTTP client that supports streaming:
|
|
1280
|
+
|
|
1281
|
+
```sh
|
|
1282
|
+
curl 'http://localhost:3000/stream' --no-buffer
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
The response will stream data (`a`, `b`, `c`) with a 500ms interval between each item.
|
|
1286
|
+
|
|
674
1287
|
## Middlewares
|
|
675
1288
|
|
|
676
1289
|
### Defining Middleware
|
|
@@ -692,7 +1305,8 @@ You can define middleware using the `HttpApiMiddleware.Tag` class, which lets yo
|
|
|
692
1305
|
import {
|
|
693
1306
|
HttpApiEndpoint,
|
|
694
1307
|
HttpApiGroup,
|
|
695
|
-
HttpApiMiddleware
|
|
1308
|
+
HttpApiMiddleware,
|
|
1309
|
+
HttpApiSchema
|
|
696
1310
|
} from "@effect/platform"
|
|
697
1311
|
import { Schema } from "effect"
|
|
698
1312
|
|
|
@@ -708,14 +1322,23 @@ class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
|
|
|
708
1322
|
failure: LoggerError
|
|
709
1323
|
}) {}
|
|
710
1324
|
|
|
711
|
-
|
|
1325
|
+
const User = Schema.Struct({
|
|
1326
|
+
id: Schema.Number,
|
|
1327
|
+
name: Schema.String,
|
|
1328
|
+
createdAt: Schema.DateTimeUtc
|
|
1329
|
+
})
|
|
1330
|
+
|
|
1331
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1332
|
+
|
|
1333
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
712
1334
|
.add(
|
|
713
|
-
HttpApiEndpoint.get("
|
|
1335
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
1336
|
+
.addSuccess(User)
|
|
714
1337
|
// Apply the middleware to a single endpoint
|
|
715
1338
|
.middleware(Logger)
|
|
716
1339
|
)
|
|
717
1340
|
// Or apply the middleware to the entire group
|
|
718
|
-
.middleware(Logger)
|
|
1341
|
+
.middleware(Logger)
|
|
719
1342
|
```
|
|
720
1343
|
|
|
721
1344
|
### Implementing HttpApiMiddleware
|
|
@@ -754,16 +1377,30 @@ import {
|
|
|
754
1377
|
HttpApiEndpoint,
|
|
755
1378
|
HttpApiGroup,
|
|
756
1379
|
HttpApiMiddleware,
|
|
1380
|
+
HttpApiSchema,
|
|
757
1381
|
HttpServerRequest
|
|
758
1382
|
} from "@effect/platform"
|
|
759
1383
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
760
1384
|
|
|
761
|
-
|
|
1385
|
+
// Define a schema for errors returned by the logger middleware
|
|
1386
|
+
class LoggerError extends Schema.TaggedError<LoggerError>()(
|
|
1387
|
+
"LoggerError",
|
|
1388
|
+
{}
|
|
1389
|
+
) {}
|
|
1390
|
+
|
|
1391
|
+
// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag
|
|
1392
|
+
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
|
|
1393
|
+
// Optionally define the error schema for the middleware
|
|
1394
|
+
failure: LoggerError
|
|
1395
|
+
}) {}
|
|
762
1396
|
|
|
763
1397
|
const LoggerLive = Layer.effect(
|
|
764
1398
|
Logger,
|
|
765
1399
|
Effect.gen(function* () {
|
|
766
1400
|
yield* Effect.log("creating Logger middleware")
|
|
1401
|
+
|
|
1402
|
+
// Middleware implementation as an Effect
|
|
1403
|
+
// that can access the `HttpServerRequest` context.
|
|
767
1404
|
return Effect.gen(function* () {
|
|
768
1405
|
const request = yield* HttpServerRequest.HttpServerRequest
|
|
769
1406
|
yield* Effect.log(`Request: ${request.method} ${request.url}`)
|
|
@@ -771,29 +1408,33 @@ const LoggerLive = Layer.effect(
|
|
|
771
1408
|
})
|
|
772
1409
|
)
|
|
773
1410
|
|
|
774
|
-
|
|
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")({
|
|
1411
|
+
const User = Schema.Struct({
|
|
783
1412
|
id: Schema.Number,
|
|
784
1413
|
name: Schema.String,
|
|
785
1414
|
createdAt: Schema.DateTimeUtc
|
|
786
|
-
})
|
|
1415
|
+
})
|
|
787
1416
|
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1417
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1418
|
+
|
|
1419
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
1420
|
+
.add(
|
|
1421
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
1422
|
+
.addSuccess(User)
|
|
1423
|
+
// Apply the middleware to a single endpoint
|
|
1424
|
+
.middleware(Logger)
|
|
1425
|
+
)
|
|
1426
|
+
// Or apply the middleware to the entire group
|
|
1427
|
+
.middleware(Logger)
|
|
1428
|
+
|
|
1429
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
1430
|
+
|
|
1431
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1432
|
+
handlers.handle("getUser", (req) =>
|
|
1433
|
+
Effect.succeed({
|
|
1434
|
+
id: req.path.id,
|
|
1435
|
+
name: "John Doe",
|
|
1436
|
+
createdAt: DateTime.unsafeNow()
|
|
1437
|
+
})
|
|
797
1438
|
)
|
|
798
1439
|
).pipe(
|
|
799
1440
|
// Provide the Logger middleware to the group
|
|
@@ -819,8 +1460,9 @@ These security annotations can be used alongside `HttpApiMiddleware` to create m
|
|
|
819
1460
|
|
|
820
1461
|
```ts
|
|
821
1462
|
import {
|
|
822
|
-
|
|
1463
|
+
HttpApi,
|
|
823
1464
|
HttpApiEndpoint,
|
|
1465
|
+
HttpApiGroup,
|
|
824
1466
|
HttpApiMiddleware,
|
|
825
1467
|
HttpApiSchema,
|
|
826
1468
|
HttpApiSecurity
|
|
@@ -860,14 +1502,20 @@ class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
|
860
1502
|
}
|
|
861
1503
|
) {}
|
|
862
1504
|
|
|
863
|
-
|
|
1505
|
+
const api = HttpApi.make("api")
|
|
864
1506
|
.add(
|
|
865
|
-
|
|
866
|
-
|
|
1507
|
+
HttpApiGroup.make("group")
|
|
1508
|
+
.add(
|
|
1509
|
+
HttpApiEndpoint.get("get", "/")
|
|
1510
|
+
.addSuccess(Schema.String)
|
|
1511
|
+
// Apply the middleware to a single endpoint
|
|
1512
|
+
.middleware(Authorization)
|
|
1513
|
+
)
|
|
1514
|
+
// Or apply the middleware to the entire group
|
|
867
1515
|
.middleware(Authorization)
|
|
868
1516
|
)
|
|
869
|
-
// Or apply the middleware to the entire
|
|
870
|
-
.middleware(Authorization)
|
|
1517
|
+
// Or apply the middleware to the entire API
|
|
1518
|
+
.middleware(Authorization)
|
|
871
1519
|
```
|
|
872
1520
|
|
|
873
1521
|
### Implementing HttpApiSecurity middleware
|
|
@@ -899,7 +1547,9 @@ class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
|
899
1547
|
{
|
|
900
1548
|
failure: Unauthorized,
|
|
901
1549
|
provides: CurrentUser,
|
|
902
|
-
security: {
|
|
1550
|
+
security: {
|
|
1551
|
+
myBearer: HttpApiSecurity.bearer
|
|
1552
|
+
}
|
|
903
1553
|
}
|
|
904
1554
|
) {}
|
|
905
1555
|
|
|
@@ -926,6 +1576,46 @@ const AuthorizationLive = Layer.effect(
|
|
|
926
1576
|
)
|
|
927
1577
|
```
|
|
928
1578
|
|
|
1579
|
+
### Adding Descriptions to Security Definitions
|
|
1580
|
+
|
|
1581
|
+
The `HttpApiSecurity.annotate` function allows you to add metadata, such as a description, to your security definitions. This metadata is displayed in the Swagger documentation, making it easier for developers to understand your API's security requirements.
|
|
1582
|
+
|
|
1583
|
+
**Example** (Adding a Description to a Bearer Token Security Definition)
|
|
1584
|
+
|
|
1585
|
+
```ts
|
|
1586
|
+
import {
|
|
1587
|
+
HttpApiMiddleware,
|
|
1588
|
+
HttpApiSchema,
|
|
1589
|
+
HttpApiSecurity,
|
|
1590
|
+
OpenApi
|
|
1591
|
+
} from "@effect/platform"
|
|
1592
|
+
import { Context, Schema } from "effect"
|
|
1593
|
+
|
|
1594
|
+
class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}
|
|
1595
|
+
|
|
1596
|
+
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
1597
|
+
"Unauthorized",
|
|
1598
|
+
{},
|
|
1599
|
+
HttpApiSchema.annotations({ status: 401 })
|
|
1600
|
+
) {}
|
|
1601
|
+
|
|
1602
|
+
class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
|
|
1603
|
+
|
|
1604
|
+
class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
1605
|
+
"Authorization",
|
|
1606
|
+
{
|
|
1607
|
+
failure: Unauthorized,
|
|
1608
|
+
provides: CurrentUser,
|
|
1609
|
+
security: {
|
|
1610
|
+
myBearer: HttpApiSecurity.bearer.pipe(
|
|
1611
|
+
// Add a description to the security definition
|
|
1612
|
+
HttpApiSecurity.annotate(OpenApi.Description, "my description")
|
|
1613
|
+
)
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
) {}
|
|
1617
|
+
```
|
|
1618
|
+
|
|
929
1619
|
### Setting HttpApiSecurity cookies
|
|
930
1620
|
|
|
931
1621
|
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.
|
|
@@ -970,33 +1660,31 @@ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
|
970
1660
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
971
1661
|
import { createServer } from "node:http"
|
|
972
1662
|
|
|
973
|
-
|
|
1663
|
+
const User = Schema.Struct({
|
|
974
1664
|
id: Schema.Number,
|
|
975
1665
|
name: Schema.String,
|
|
976
1666
|
createdAt: Schema.DateTimeUtc
|
|
977
|
-
})
|
|
1667
|
+
})
|
|
978
1668
|
|
|
979
|
-
const
|
|
1669
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
980
1670
|
|
|
981
|
-
|
|
982
|
-
HttpApiEndpoint.get("
|
|
983
|
-
)
|
|
1671
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1672
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1673
|
+
)
|
|
984
1674
|
|
|
985
|
-
|
|
1675
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
986
1676
|
|
|
987
|
-
const
|
|
988
|
-
handlers.handle("
|
|
989
|
-
Effect.succeed(
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
})
|
|
995
|
-
)
|
|
1677
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1678
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1679
|
+
Effect.succeed({
|
|
1680
|
+
id,
|
|
1681
|
+
name: "John Doe",
|
|
1682
|
+
createdAt: DateTime.unsafeNow()
|
|
1683
|
+
})
|
|
996
1684
|
)
|
|
997
1685
|
)
|
|
998
1686
|
|
|
999
|
-
const MyApiLive = HttpApiBuilder.api(
|
|
1687
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
1000
1688
|
|
|
1001
1689
|
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1002
1690
|
// Add the Swagger documentation layer
|
|
@@ -1020,42 +1708,427 @@ Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
|
1020
1708
|
|
|
1021
1709
|
### Adding OpenAPI Annotations
|
|
1022
1710
|
|
|
1023
|
-
You can
|
|
1711
|
+
You can add OpenAPI annotations to your API to include metadata such as titles, descriptions, and more. These annotations help generate richer API documentation.
|
|
1712
|
+
|
|
1713
|
+
#### HttpApi
|
|
1714
|
+
|
|
1715
|
+
Below is a list of available annotations for a top-level `HttpApi`. They can be added using the `.annotate` method:
|
|
1716
|
+
|
|
1717
|
+
| Annotation | Description |
|
|
1718
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
1719
|
+
| `HttpApi.AdditionalSchemas` | Adds custom schemas to the final OpenAPI specification. Only schemas with an `identifier` annotation are included. |
|
|
1720
|
+
| `OpenApi.Description` | Sets a general description for the API. |
|
|
1721
|
+
| `OpenApi.License` | Defines the license used by the API. |
|
|
1722
|
+
| `OpenApi.Summary` | Provides a brief summary of the API. |
|
|
1723
|
+
| `OpenApi.Servers` | Lists server URLs and optional metadata such as variables. |
|
|
1724
|
+
| `OpenApi.Override` | Merges the supplied fields into the resulting specification. |
|
|
1725
|
+
| `OpenApi.Transform` | Allows you to modify the final specification with a custom function. |
|
|
1726
|
+
|
|
1727
|
+
**Example** (Annotating the Top-Level API)
|
|
1728
|
+
|
|
1729
|
+
```ts
|
|
1730
|
+
import { HttpApi, OpenApi } from "@effect/platform"
|
|
1731
|
+
import { Schema } from "effect"
|
|
1732
|
+
|
|
1733
|
+
const api = HttpApi.make("api")
|
|
1734
|
+
// Provide additional schemas
|
|
1735
|
+
.annotate(HttpApi.AdditionalSchemas, [
|
|
1736
|
+
Schema.String.annotations({ identifier: "MyString" })
|
|
1737
|
+
])
|
|
1738
|
+
// Add a description
|
|
1739
|
+
.annotate(OpenApi.Description, "my description")
|
|
1740
|
+
// Set license information
|
|
1741
|
+
.annotate(OpenApi.License, { name: "MIT", url: "http://example.com" })
|
|
1742
|
+
// Provide a summary
|
|
1743
|
+
.annotate(OpenApi.Summary, "my summary")
|
|
1744
|
+
// Define servers
|
|
1745
|
+
.annotate(OpenApi.Servers, [
|
|
1746
|
+
{
|
|
1747
|
+
url: "http://example.com",
|
|
1748
|
+
description: "example",
|
|
1749
|
+
variables: { a: { default: "b", enum: ["c"], description: "d" } }
|
|
1750
|
+
}
|
|
1751
|
+
])
|
|
1752
|
+
// Override parts of the generated specification
|
|
1753
|
+
.annotate(OpenApi.Override, {
|
|
1754
|
+
tags: [{ name: "a", description: "a-description" }]
|
|
1755
|
+
})
|
|
1756
|
+
// Apply a transform function to the final specification
|
|
1757
|
+
.annotate(OpenApi.Transform, (spec) => ({
|
|
1758
|
+
...spec,
|
|
1759
|
+
tags: [...spec.tags, { name: "b", description: "b-description" }]
|
|
1760
|
+
}))
|
|
1761
|
+
|
|
1762
|
+
// Generate the OpenAPI specification from the annotated API
|
|
1763
|
+
const spec = OpenApi.fromApi(api)
|
|
1764
|
+
|
|
1765
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
1766
|
+
/*
|
|
1767
|
+
Output:
|
|
1768
|
+
{
|
|
1769
|
+
"openapi": "3.1.0",
|
|
1770
|
+
"info": {
|
|
1771
|
+
"title": "Api",
|
|
1772
|
+
"version": "0.0.1",
|
|
1773
|
+
"description": "my description",
|
|
1774
|
+
"license": {
|
|
1775
|
+
"name": "MIT",
|
|
1776
|
+
"url": "http://example.com"
|
|
1777
|
+
},
|
|
1778
|
+
"summary": "my summary"
|
|
1779
|
+
},
|
|
1780
|
+
"paths": {},
|
|
1781
|
+
"tags": [
|
|
1782
|
+
{ "name": "a", "description": "a-description" },
|
|
1783
|
+
{ "name": "b", "description": "b-description" }
|
|
1784
|
+
],
|
|
1785
|
+
"components": {
|
|
1786
|
+
"schemas": {
|
|
1787
|
+
"MyString": {
|
|
1788
|
+
"type": "string"
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
"securitySchemes": {}
|
|
1792
|
+
},
|
|
1793
|
+
"security": [],
|
|
1794
|
+
"servers": [
|
|
1795
|
+
{
|
|
1796
|
+
"url": "http://example.com",
|
|
1797
|
+
"description": "example",
|
|
1798
|
+
"variables": {
|
|
1799
|
+
"a": {
|
|
1800
|
+
"default": "b",
|
|
1801
|
+
"enum": [
|
|
1802
|
+
"c"
|
|
1803
|
+
],
|
|
1804
|
+
"description": "d"
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
]
|
|
1809
|
+
}
|
|
1810
|
+
*/
|
|
1811
|
+
```
|
|
1812
|
+
|
|
1813
|
+
#### HttpApiGroup
|
|
1814
|
+
|
|
1815
|
+
The following annotations can be added to an `HttpApiGroup`:
|
|
1816
|
+
|
|
1817
|
+
| Annotation | Description |
|
|
1818
|
+
| ---------------------- | --------------------------------------------------------------------- |
|
|
1819
|
+
| `OpenApi.Description` | Sets a description for this group. |
|
|
1820
|
+
| `OpenApi.ExternalDocs` | Provides external documentation links for the group. |
|
|
1821
|
+
| `OpenApi.Override` | Merges specified fields into the resulting specification. |
|
|
1822
|
+
| `OpenApi.Transform` | Lets you modify the final group specification with a custom function. |
|
|
1823
|
+
| `OpenApi.Exclude` | Excludes the group from the final OpenAPI specification. |
|
|
1824
|
+
|
|
1825
|
+
**Example** (Annotating a Group)
|
|
1826
|
+
|
|
1827
|
+
```ts
|
|
1828
|
+
import { HttpApi, HttpApiGroup, OpenApi } from "@effect/platform"
|
|
1829
|
+
|
|
1830
|
+
const api = HttpApi.make("api")
|
|
1831
|
+
.add(
|
|
1832
|
+
HttpApiGroup.make("group")
|
|
1833
|
+
// Add a description for the group
|
|
1834
|
+
.annotate(OpenApi.Description, "my description")
|
|
1835
|
+
// Provide external documentation links
|
|
1836
|
+
.annotate(OpenApi.ExternalDocs, {
|
|
1837
|
+
url: "http://example.com",
|
|
1838
|
+
description: "example"
|
|
1839
|
+
})
|
|
1840
|
+
// Override parts of the final output
|
|
1841
|
+
.annotate(OpenApi.Override, { name: "my name" })
|
|
1842
|
+
// Transform the final specification for this group
|
|
1843
|
+
.annotate(OpenApi.Transform, (spec) => ({
|
|
1844
|
+
...spec,
|
|
1845
|
+
name: spec.name + "-transformed"
|
|
1846
|
+
}))
|
|
1847
|
+
)
|
|
1848
|
+
.add(
|
|
1849
|
+
HttpApiGroup.make("excluded")
|
|
1850
|
+
// Exclude the group from the final specification
|
|
1851
|
+
.annotate(OpenApi.Exclude, true)
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
// Generate the OpenAPI spec
|
|
1855
|
+
const spec = OpenApi.fromApi(api)
|
|
1856
|
+
|
|
1857
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
1858
|
+
/*
|
|
1859
|
+
Output:
|
|
1860
|
+
{
|
|
1861
|
+
"openapi": "3.1.0",
|
|
1862
|
+
"info": {
|
|
1863
|
+
"title": "Api",
|
|
1864
|
+
"version": "0.0.1"
|
|
1865
|
+
},
|
|
1866
|
+
"paths": {},
|
|
1867
|
+
"tags": [
|
|
1868
|
+
{
|
|
1869
|
+
"name": "my name-transformed",
|
|
1870
|
+
"description": "my description",
|
|
1871
|
+
"externalDocs": {
|
|
1872
|
+
"url": "http://example.com",
|
|
1873
|
+
"description": "example"
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
],
|
|
1877
|
+
"components": {
|
|
1878
|
+
"schemas": {},
|
|
1879
|
+
"securitySchemes": {}
|
|
1880
|
+
},
|
|
1881
|
+
"security": []
|
|
1882
|
+
}
|
|
1883
|
+
*/
|
|
1884
|
+
```
|
|
1885
|
+
|
|
1886
|
+
#### HttpApiEndpoint
|
|
1024
1887
|
|
|
1025
|
-
|
|
1888
|
+
For an `HttpApiEndpoint`, you can use the following annotations:
|
|
1026
1889
|
|
|
1027
|
-
|
|
1890
|
+
| Annotation | Description |
|
|
1891
|
+
| ---------------------- | --------------------------------------------------------------------------- |
|
|
1892
|
+
| `OpenApi.Description` | Adds a description for this endpoint. |
|
|
1893
|
+
| `OpenApi.Summary` | Provides a short summary of the endpoint's purpose. |
|
|
1894
|
+
| `OpenApi.Deprecated` | Marks the endpoint as deprecated. |
|
|
1895
|
+
| `OpenApi.ExternalDocs` | Supplies external documentation links for the endpoint. |
|
|
1896
|
+
| `OpenApi.Override` | Merges specified fields into the resulting specification for this endpoint. |
|
|
1897
|
+
| `OpenApi.Transform` | Lets you modify the final endpoint specification with a custom function. |
|
|
1898
|
+
| `OpenApi.Exclude` | Excludes the endpoint from the final OpenAPI specification. |
|
|
1028
1899
|
|
|
1029
|
-
|
|
1030
|
-
- These annotations will appear in the generated OpenAPI documentation.
|
|
1900
|
+
**Example** (Annotating an Endpoint)
|
|
1031
1901
|
|
|
1032
1902
|
```ts
|
|
1033
|
-
import {
|
|
1903
|
+
import {
|
|
1904
|
+
HttpApi,
|
|
1905
|
+
HttpApiEndpoint,
|
|
1906
|
+
HttpApiGroup,
|
|
1907
|
+
OpenApi
|
|
1908
|
+
} from "@effect/platform"
|
|
1909
|
+
import { Schema } from "effect"
|
|
1910
|
+
|
|
1911
|
+
const api = HttpApi.make("api").add(
|
|
1912
|
+
HttpApiGroup.make("group")
|
|
1913
|
+
.add(
|
|
1914
|
+
HttpApiEndpoint.get("get", "/")
|
|
1915
|
+
.addSuccess(Schema.String)
|
|
1916
|
+
// Add a description
|
|
1917
|
+
.annotate(OpenApi.Description, "my description")
|
|
1918
|
+
// Provide a summary
|
|
1919
|
+
.annotate(OpenApi.Summary, "my summary")
|
|
1920
|
+
// Mark the endpoint as deprecated
|
|
1921
|
+
.annotate(OpenApi.Deprecated, true)
|
|
1922
|
+
// Provide external documentation
|
|
1923
|
+
.annotate(OpenApi.ExternalDocs, {
|
|
1924
|
+
url: "http://example.com",
|
|
1925
|
+
description: "example"
|
|
1926
|
+
})
|
|
1927
|
+
)
|
|
1928
|
+
.add(
|
|
1929
|
+
HttpApiEndpoint.get("excluded", "/excluded")
|
|
1930
|
+
.addSuccess(Schema.String)
|
|
1931
|
+
// Exclude this endpoint from the final specification
|
|
1932
|
+
.annotate(OpenApi.Exclude, true)
|
|
1933
|
+
)
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1936
|
+
// Generate the OpenAPI spec
|
|
1937
|
+
const spec = OpenApi.fromApi(api)
|
|
1938
|
+
|
|
1939
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
1940
|
+
/*
|
|
1941
|
+
Output:
|
|
1942
|
+
{
|
|
1943
|
+
"openapi": "3.1.0",
|
|
1944
|
+
"info": {
|
|
1945
|
+
"title": "Api",
|
|
1946
|
+
"version": "0.0.1"
|
|
1947
|
+
},
|
|
1948
|
+
"paths": {
|
|
1949
|
+
"/": {
|
|
1950
|
+
"get": {
|
|
1951
|
+
"tags": [
|
|
1952
|
+
"group"
|
|
1953
|
+
],
|
|
1954
|
+
"operationId": "my operationId-transformed",
|
|
1955
|
+
"parameters": [],
|
|
1956
|
+
"security": [],
|
|
1957
|
+
"responses": {
|
|
1958
|
+
"200": {
|
|
1959
|
+
"description": "a string",
|
|
1960
|
+
"content": {
|
|
1961
|
+
"application/json": {
|
|
1962
|
+
"schema": {
|
|
1963
|
+
"type": "string"
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
},
|
|
1968
|
+
"400": {
|
|
1969
|
+
"description": "The request did not match the expected schema",
|
|
1970
|
+
"content": {
|
|
1971
|
+
"application/json": {
|
|
1972
|
+
"schema": {
|
|
1973
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
"description": "my description",
|
|
1980
|
+
"summary": "my summary",
|
|
1981
|
+
"deprecated": true,
|
|
1982
|
+
"externalDocs": {
|
|
1983
|
+
"url": "http://example.com",
|
|
1984
|
+
"description": "example"
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
},
|
|
1989
|
+
...
|
|
1990
|
+
}
|
|
1991
|
+
*/
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1994
|
+
The default response description is "Success". You can override this by annotating the schema.
|
|
1995
|
+
|
|
1996
|
+
**Example** (Defining a custom response description)
|
|
1997
|
+
|
|
1998
|
+
```ts
|
|
1999
|
+
import {
|
|
2000
|
+
HttpApi,
|
|
2001
|
+
HttpApiEndpoint,
|
|
2002
|
+
HttpApiGroup,
|
|
2003
|
+
OpenApi
|
|
2004
|
+
} from "@effect/platform"
|
|
2005
|
+
import { Schema } from "effect"
|
|
2006
|
+
|
|
2007
|
+
const User = Schema.Struct({
|
|
2008
|
+
id: Schema.Number,
|
|
2009
|
+
name: Schema.String,
|
|
2010
|
+
createdAt: Schema.DateTimeUtc
|
|
2011
|
+
}).annotations({ identifier: "User" })
|
|
1034
2012
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
.addSuccess(
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
// or multiple at once
|
|
1041
|
-
.annotateContext(
|
|
1042
|
-
OpenApi.annotations({
|
|
1043
|
-
title: "Users API",
|
|
1044
|
-
description: "API for managing users"
|
|
2013
|
+
const api = HttpApi.make("api").add(
|
|
2014
|
+
HttpApiGroup.make("group").add(
|
|
2015
|
+
HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
2016
|
+
Schema.Array(User).annotations({
|
|
2017
|
+
description: "Returns an array of users"
|
|
1045
2018
|
})
|
|
1046
2019
|
)
|
|
1047
|
-
)
|
|
2020
|
+
)
|
|
2021
|
+
)
|
|
2022
|
+
|
|
2023
|
+
const spec = OpenApi.fromApi(api)
|
|
2024
|
+
|
|
2025
|
+
console.log(JSON.stringify(spec.paths, null, 2))
|
|
2026
|
+
/*
|
|
2027
|
+
Output:
|
|
2028
|
+
{
|
|
2029
|
+
"/users": {
|
|
2030
|
+
"get": {
|
|
2031
|
+
"tags": [
|
|
2032
|
+
"group"
|
|
2033
|
+
],
|
|
2034
|
+
"operationId": "group.getUsers",
|
|
2035
|
+
"parameters": [],
|
|
2036
|
+
"security": [],
|
|
2037
|
+
"responses": {
|
|
2038
|
+
"200": {
|
|
2039
|
+
"description": "Returns an array of users",
|
|
2040
|
+
"content": {
|
|
2041
|
+
"application/json": {
|
|
2042
|
+
"schema": {
|
|
2043
|
+
"type": "array",
|
|
2044
|
+
"items": {
|
|
2045
|
+
"$ref": "#/components/schemas/User"
|
|
2046
|
+
},
|
|
2047
|
+
"description": "Returns an array of users"
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
},
|
|
2052
|
+
"400": {
|
|
2053
|
+
"description": "The request did not match the expected schema",
|
|
2054
|
+
"content": {
|
|
2055
|
+
"application/json": {
|
|
2056
|
+
"schema": {
|
|
2057
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
*/
|
|
1048
2067
|
```
|
|
1049
2068
|
|
|
1050
|
-
|
|
2069
|
+
### Top Level Groups
|
|
1051
2070
|
|
|
1052
|
-
|
|
2071
|
+
When a group is marked as `topLevel`, the operation IDs of its endpoints do not include the group name as a prefix. This is helpful when you want to group endpoints under a shared tag without adding a redundant prefix to their operation IDs.
|
|
2072
|
+
|
|
2073
|
+
**Example** (Using a Top-Level Group)
|
|
1053
2074
|
|
|
1054
2075
|
```ts
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
2076
|
+
import {
|
|
2077
|
+
HttpApi,
|
|
2078
|
+
HttpApiEndpoint,
|
|
2079
|
+
HttpApiGroup,
|
|
2080
|
+
OpenApi
|
|
2081
|
+
} from "@effect/platform"
|
|
2082
|
+
import { Schema } from "effect"
|
|
2083
|
+
|
|
2084
|
+
const api = HttpApi.make("api").add(
|
|
2085
|
+
// Mark the group as top-level
|
|
2086
|
+
HttpApiGroup.make("group", { topLevel: true }).add(
|
|
2087
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
2088
|
+
)
|
|
2089
|
+
)
|
|
2090
|
+
|
|
2091
|
+
// Generate the OpenAPI spec
|
|
2092
|
+
const spec = OpenApi.fromApi(api)
|
|
2093
|
+
|
|
2094
|
+
console.log(JSON.stringify(spec.paths, null, 2))
|
|
2095
|
+
/*
|
|
2096
|
+
Output:
|
|
2097
|
+
{
|
|
2098
|
+
"/": {
|
|
2099
|
+
"get": {
|
|
2100
|
+
"tags": [
|
|
2101
|
+
"group"
|
|
2102
|
+
],
|
|
2103
|
+
"operationId": "get", // The operation ID is not prefixed with "group"
|
|
2104
|
+
"parameters": [],
|
|
2105
|
+
"security": [],
|
|
2106
|
+
"responses": {
|
|
2107
|
+
"200": {
|
|
2108
|
+
"description": "a string",
|
|
2109
|
+
"content": {
|
|
2110
|
+
"application/json": {
|
|
2111
|
+
"schema": {
|
|
2112
|
+
"type": "string"
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
"400": {
|
|
2118
|
+
"description": "The request did not match the expected schema",
|
|
2119
|
+
"content": {
|
|
2120
|
+
"application/json": {
|
|
2121
|
+
"schema": {
|
|
2122
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
*/
|
|
1059
2132
|
```
|
|
1060
2133
|
|
|
1061
2134
|
## Deriving a Client
|
|
@@ -1083,33 +2156,31 @@ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
|
1083
2156
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
1084
2157
|
import { createServer } from "node:http"
|
|
1085
2158
|
|
|
1086
|
-
|
|
2159
|
+
const User = Schema.Struct({
|
|
1087
2160
|
id: Schema.Number,
|
|
1088
2161
|
name: Schema.String,
|
|
1089
2162
|
createdAt: Schema.DateTimeUtc
|
|
1090
|
-
})
|
|
2163
|
+
})
|
|
1091
2164
|
|
|
1092
|
-
const
|
|
2165
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1093
2166
|
|
|
1094
|
-
|
|
1095
|
-
HttpApiEndpoint.get("
|
|
1096
|
-
)
|
|
2167
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
2168
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
2169
|
+
)
|
|
1097
2170
|
|
|
1098
|
-
|
|
2171
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
1099
2172
|
|
|
1100
|
-
const
|
|
1101
|
-
handlers.handle("
|
|
1102
|
-
Effect.succeed(
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
})
|
|
1108
|
-
)
|
|
2173
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
2174
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
2175
|
+
Effect.succeed({
|
|
2176
|
+
id,
|
|
2177
|
+
name: "John Doe",
|
|
2178
|
+
createdAt: DateTime.unsafeNow()
|
|
2179
|
+
})
|
|
1109
2180
|
)
|
|
1110
2181
|
)
|
|
1111
2182
|
|
|
1112
|
-
const MyApiLive = HttpApiBuilder.api(
|
|
2183
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
1113
2184
|
|
|
1114
2185
|
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1115
2186
|
Layer.provide(HttpApiSwagger.layer()),
|
|
@@ -1124,11 +2195,11 @@ Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
|
1124
2195
|
// Create a program that derives and uses the client
|
|
1125
2196
|
const program = Effect.gen(function* () {
|
|
1126
2197
|
// Derive the client
|
|
1127
|
-
const client = yield* HttpApiClient.make(
|
|
2198
|
+
const client = yield* HttpApiClient.make(api, {
|
|
1128
2199
|
baseUrl: "http://localhost:3000"
|
|
1129
2200
|
})
|
|
1130
|
-
// Call the `
|
|
1131
|
-
const user = yield* client.users.
|
|
2201
|
+
// Call the `getUser` endpoint
|
|
2202
|
+
const user = yield* client.users.getUser({ path: { id: 1 } })
|
|
1132
2203
|
console.log(user)
|
|
1133
2204
|
})
|
|
1134
2205
|
|
|
@@ -1144,6 +2215,101 @@ User {
|
|
|
1144
2215
|
*/
|
|
1145
2216
|
```
|
|
1146
2217
|
|
|
2218
|
+
### Top Level Groups
|
|
2219
|
+
|
|
2220
|
+
When a group is marked as `topLevel`, the methods on the client are not nested under the group name. This can simplify client usage by providing direct access to the endpoint methods.
|
|
2221
|
+
|
|
2222
|
+
**Example** (Using a Top-Level Group in the Client)
|
|
2223
|
+
|
|
2224
|
+
```ts
|
|
2225
|
+
import {
|
|
2226
|
+
HttpApi,
|
|
2227
|
+
HttpApiClient,
|
|
2228
|
+
HttpApiEndpoint,
|
|
2229
|
+
HttpApiGroup
|
|
2230
|
+
} from "@effect/platform"
|
|
2231
|
+
import { Effect, Schema } from "effect"
|
|
2232
|
+
|
|
2233
|
+
const api = HttpApi.make("api").add(
|
|
2234
|
+
// Mark the group as top-level
|
|
2235
|
+
HttpApiGroup.make("group", { topLevel: true }).add(
|
|
2236
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
2237
|
+
)
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
const program = Effect.gen(function* () {
|
|
2241
|
+
const client = yield* HttpApiClient.make(api, {
|
|
2242
|
+
baseUrl: "http://localhost:3000"
|
|
2243
|
+
})
|
|
2244
|
+
// The `get` method is not nested under the "group" name
|
|
2245
|
+
const user = yield* client.get()
|
|
2246
|
+
console.log(user)
|
|
2247
|
+
})
|
|
2248
|
+
```
|
|
2249
|
+
|
|
2250
|
+
## Converting to a Web Handler
|
|
2251
|
+
|
|
2252
|
+
You can convert your `HttpApi` implementation into a web handler using the `HttpApiBuilder.toWebHandler` API. This approach enables you to serve your API through a custom server setup.
|
|
2253
|
+
|
|
2254
|
+
**Example** (Creating and Serving a Web Handler)
|
|
2255
|
+
|
|
2256
|
+
```ts
|
|
2257
|
+
import {
|
|
2258
|
+
HttpApi,
|
|
2259
|
+
HttpApiBuilder,
|
|
2260
|
+
HttpApiEndpoint,
|
|
2261
|
+
HttpApiGroup,
|
|
2262
|
+
HttpApiSwagger,
|
|
2263
|
+
HttpServer
|
|
2264
|
+
} from "@effect/platform"
|
|
2265
|
+
import { Effect, Layer, Schema } from "effect"
|
|
2266
|
+
import * as http from "node:http"
|
|
2267
|
+
|
|
2268
|
+
const api = HttpApi.make("myApi").add(
|
|
2269
|
+
HttpApiGroup.make("group").add(
|
|
2270
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
2271
|
+
)
|
|
2272
|
+
)
|
|
2273
|
+
|
|
2274
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
2275
|
+
handlers.handle("get", () => Effect.succeed("Hello, world!"))
|
|
2276
|
+
)
|
|
2277
|
+
|
|
2278
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
2279
|
+
|
|
2280
|
+
const SwaggerLayer = HttpApiSwagger.layer().pipe(Layer.provide(MyApiLive))
|
|
2281
|
+
|
|
2282
|
+
// Convert the API to a web handler
|
|
2283
|
+
const { dispose, handler } = HttpApiBuilder.toWebHandler(
|
|
2284
|
+
Layer.mergeAll(MyApiLive, SwaggerLayer, HttpServer.layerContext)
|
|
2285
|
+
)
|
|
2286
|
+
|
|
2287
|
+
// Serving the handler using a custom HTTP server
|
|
2288
|
+
http
|
|
2289
|
+
.createServer(async (req, res) => {
|
|
2290
|
+
const url = `http://${req.headers.host}${req.url}`
|
|
2291
|
+
const init: RequestInit = {
|
|
2292
|
+
method: req.method!
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const response = await handler(new Request(url, init))
|
|
2296
|
+
|
|
2297
|
+
res.writeHead(
|
|
2298
|
+
response.status,
|
|
2299
|
+
response.statusText,
|
|
2300
|
+
Object.fromEntries(response.headers.entries())
|
|
2301
|
+
)
|
|
2302
|
+
const responseBody = await response.arrayBuffer()
|
|
2303
|
+
res.end(Buffer.from(responseBody))
|
|
2304
|
+
})
|
|
2305
|
+
.listen(3000, () => {
|
|
2306
|
+
console.log("Server running at http://localhost:3000/")
|
|
2307
|
+
})
|
|
2308
|
+
.on("close", () => {
|
|
2309
|
+
dispose()
|
|
2310
|
+
})
|
|
2311
|
+
```
|
|
2312
|
+
|
|
1147
2313
|
# HTTP Client
|
|
1148
2314
|
|
|
1149
2315
|
## Overview
|