@effect/platform 0.72.1 → 0.73.0
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 +2159 -356
- package/Url/package.json +6 -0
- package/dist/cjs/HttpApi.js +22 -18
- package/dist/cjs/HttpApi.js.map +1 -1
- package/dist/cjs/HttpApiEndpoint.js.map +1 -1
- package/dist/cjs/HttpApiGroup.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 +132 -142
- 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/cjs/Runtime.js.map +1 -1
- package/dist/cjs/Url.js +259 -0
- package/dist/cjs/Url.js.map +1 -0
- package/dist/cjs/index.js +3 -1
- package/dist/dts/HttpApi.d.ts +4 -2
- package/dist/dts/HttpApi.d.ts.map +1 -1
- package/dist/dts/HttpApiBuilder.d.ts +1 -1
- package/dist/dts/HttpApiBuilder.d.ts.map +1 -1
- package/dist/dts/HttpApiEndpoint.d.ts +16 -8
- package/dist/dts/HttpApiEndpoint.d.ts.map +1 -1
- package/dist/dts/HttpApiGroup.d.ts +1 -2
- package/dist/dts/HttpApiGroup.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 +102 -111
- package/dist/dts/OpenApi.d.ts.map +1 -1
- package/dist/dts/OpenApiJsonSchema.d.ts.map +1 -1
- package/dist/dts/Runtime.d.ts +48 -0
- package/dist/dts/Runtime.d.ts.map +1 -1
- package/dist/dts/Url.d.ts +591 -0
- package/dist/dts/Url.d.ts.map +1 -0
- package/dist/dts/index.d.ts +4 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/HttpApi.js +22 -18
- package/dist/esm/HttpApi.js.map +1 -1
- package/dist/esm/HttpApiEndpoint.js.map +1 -1
- package/dist/esm/HttpApiGroup.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 +132 -141
- package/dist/esm/OpenApi.js.map +1 -1
- package/dist/esm/OpenApiJsonSchema.js +4 -2
- package/dist/esm/OpenApiJsonSchema.js.map +1 -1
- package/dist/esm/Runtime.js.map +1 -1
- package/dist/esm/Url.js +248 -0
- package/dist/esm/Url.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +10 -2
- package/src/HttpApi.ts +25 -26
- package/src/HttpApiBuilder.ts +1 -1
- package/src/HttpApiEndpoint.ts +22 -13
- package/src/HttpApiGroup.ts +2 -3
- package/src/HttpApiSchema.ts +33 -8
- package/src/HttpApiSecurity.ts +2 -0
- package/src/OpenApi.ts +244 -272
- package/src/OpenApiJsonSchema.ts +9 -1
- package/src/Runtime.ts +48 -0
- package/src/Url.ts +632 -0
- package/src/index.ts +5 -0
package/README.md
CHANGED
|
@@ -5,23 +5,157 @@ Welcome to the documentation for `@effect/platform`, a library designed for crea
|
|
|
5
5
|
> [!WARNING]
|
|
6
6
|
> This documentation focuses on **unstable modules**. For stable modules, refer to the [official website documentation](https://effect.website/docs/guides/platform/introduction).
|
|
7
7
|
|
|
8
|
+
# Running Your Main Program with runMain
|
|
9
|
+
|
|
10
|
+
`runMain` helps you execute a main effect with built-in error handling, logging, and signal management. You can concentrate on your effect while `runMain` looks after finalizing resources, logging errors, and setting exit codes.
|
|
11
|
+
|
|
12
|
+
- **Exit Codes**
|
|
13
|
+
If your effect fails or is interrupted, `runMain` assigns a suitable exit code (for example, `1` for errors and `0` for success).
|
|
14
|
+
- **Logs**
|
|
15
|
+
By default, it records errors. This can be turned off if needed.
|
|
16
|
+
- **Pretty Logging**
|
|
17
|
+
By default, error messages are recorded using a "pretty" format. You can switch this off when required.
|
|
18
|
+
- **Interrupt Handling**
|
|
19
|
+
If the application receives `SIGINT` (Ctrl+C) or a similar signal, `runMain` will interrupt the effect and still run any necessary teardown steps.
|
|
20
|
+
- **Teardown Logic**
|
|
21
|
+
You can rely on the default teardown or define your own. The default sets an exit code of `1` for a non-interrupted failure.
|
|
22
|
+
|
|
23
|
+
## Usage Options
|
|
24
|
+
|
|
25
|
+
When calling `runMain`, pass in a configuration object with these fields (all optional):
|
|
26
|
+
|
|
27
|
+
- `disableErrorReporting`: If `true`, errors are not automatically logged.
|
|
28
|
+
- `disablePrettyLogger`: If `true`, it avoids adding the "pretty" logger.
|
|
29
|
+
- `teardown`: Provide a custom function for finalizing the program. If missing, the default sets exit code `1` for a non-interrupted failure.
|
|
30
|
+
|
|
31
|
+
**Example** (Running a Successful Program)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
35
|
+
import { Effect } from "effect"
|
|
36
|
+
|
|
37
|
+
const success = Effect.succeed("Hello, World!")
|
|
38
|
+
|
|
39
|
+
NodeRuntime.runMain(success)
|
|
40
|
+
// No Output
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Example** (Running a Failing Program)
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
47
|
+
import { Effect } from "effect"
|
|
48
|
+
|
|
49
|
+
const failure = Effect.fail("Uh oh!")
|
|
50
|
+
|
|
51
|
+
NodeRuntime.runMain(failure)
|
|
52
|
+
/*
|
|
53
|
+
Output:
|
|
54
|
+
[12:43:07.186] ERROR (#0):
|
|
55
|
+
Error: Uh oh!
|
|
56
|
+
*/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Example** (Running a Failing Program Without Pretty Logger)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
63
|
+
import { Effect } from "effect"
|
|
64
|
+
|
|
65
|
+
const failure = Effect.fail("Uh oh!")
|
|
66
|
+
|
|
67
|
+
NodeRuntime.runMain(failure, { disablePrettyLogger: true })
|
|
68
|
+
/*
|
|
69
|
+
Output:
|
|
70
|
+
timestamp=2025-01-14T11:43:46.276Z level=ERROR fiber=#0 cause="Error: Uh oh!"
|
|
71
|
+
*/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Example** (Running a Failing Program Without Error Reporting)
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
78
|
+
import { Effect } from "effect"
|
|
79
|
+
|
|
80
|
+
const failure = Effect.fail("Uh oh!")
|
|
81
|
+
|
|
82
|
+
NodeRuntime.runMain(failure, { disableErrorReporting: true })
|
|
83
|
+
// No Output
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Example** (Running a Failing Program With Custom Teardown)
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
90
|
+
import { Effect } from "effect"
|
|
91
|
+
|
|
92
|
+
const failure = Effect.fail("Uh oh!")
|
|
93
|
+
|
|
94
|
+
NodeRuntime.runMain(failure, {
|
|
95
|
+
teardown: function customTeardown(exit, onExit) {
|
|
96
|
+
if (exit._tag === "Failure") {
|
|
97
|
+
console.error("Program ended with an error.")
|
|
98
|
+
onExit(1)
|
|
99
|
+
} else {
|
|
100
|
+
console.log("Program finished successfully.")
|
|
101
|
+
onExit(0)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
/*
|
|
106
|
+
Output:
|
|
107
|
+
[12:46:39.871] ERROR (#0):
|
|
108
|
+
Error: Uh oh!
|
|
109
|
+
Program ended with an error.
|
|
110
|
+
*/
|
|
111
|
+
```
|
|
112
|
+
|
|
8
113
|
# HTTP API
|
|
9
114
|
|
|
10
115
|
## Overview
|
|
11
116
|
|
|
12
|
-
The `HttpApi
|
|
117
|
+
The `HttpApi*` modules offer a flexible and declarative way to define HTTP APIs.
|
|
13
118
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
Collections of endpoints are grouped in an `HttpApiGroup`, and multiple groups can be merged into a complete `HttpApi`.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
HttpApi
|
|
125
|
+
├── HttpGroup
|
|
126
|
+
│ ├── HttpEndpoint
|
|
127
|
+
│ └── HttpEndpoint
|
|
128
|
+
└── HttpGroup
|
|
129
|
+
├── HttpEndpoint
|
|
130
|
+
├── HttpEndpoint
|
|
131
|
+
└── HttpEndpoint
|
|
132
|
+
```
|
|
17
133
|
|
|
18
|
-
|
|
134
|
+
Once your API is defined, the same definition can be reused for multiple purposes:
|
|
135
|
+
|
|
136
|
+
- **Starting a Server**: Use the API definition to implement and serve endpoints.
|
|
137
|
+
- **Generating Documentation**: Create a Swagger page to document the API.
|
|
138
|
+
- **Deriving a Client**: Generate a fully-typed client for your API.
|
|
139
|
+
|
|
140
|
+
Benefits of a Single API Definition:
|
|
141
|
+
|
|
142
|
+
- **Consistency**: A single definition ensures the server, documentation, and client remain aligned.
|
|
143
|
+
- **Reduced Maintenance**: Changes to the API are reflected across all related components.
|
|
144
|
+
- **Simplified Workflow**: Avoids duplication by consolidating API details in one place.
|
|
19
145
|
|
|
20
146
|
## Hello World
|
|
21
147
|
|
|
22
|
-
|
|
148
|
+
### Defining and Implementing an API
|
|
149
|
+
|
|
150
|
+
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:
|
|
23
151
|
|
|
24
|
-
|
|
152
|
+
```
|
|
153
|
+
HttpApi ("MyApi)
|
|
154
|
+
└── HttpGroup ("Greetings")
|
|
155
|
+
└── HttpEndpoint ("hello-world")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Example** (Hello World Definition)
|
|
25
159
|
|
|
26
160
|
```ts
|
|
27
161
|
import {
|
|
@@ -55,15 +189,21 @@ const ServerLive = HttpApiBuilder.serve().pipe(
|
|
|
55
189
|
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
56
190
|
)
|
|
57
191
|
|
|
58
|
-
//
|
|
192
|
+
// Launch the server
|
|
59
193
|
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
60
194
|
```
|
|
61
195
|
|
|
62
|
-
|
|
196
|
+
After running the code, open a browser and navigate to http://localhost:3000. The server will respond with:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
Hello, World!
|
|
200
|
+
```
|
|
63
201
|
|
|
64
202
|
### Serving The Auto Generated Swagger Documentation
|
|
65
203
|
|
|
66
|
-
You can
|
|
204
|
+
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.
|
|
205
|
+
|
|
206
|
+
To include Swagger in your server setup, provide the `HttpApiSwagger.layer` when configuring the server.
|
|
67
207
|
|
|
68
208
|
**Example** (Serving Swagger Documentation)
|
|
69
209
|
|
|
@@ -101,15 +241,17 @@ const ServerLive = HttpApiBuilder.serve().pipe(
|
|
|
101
241
|
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
102
242
|
```
|
|
103
243
|
|
|
104
|
-
|
|
244
|
+
After running the server, open your browser and navigate to http://localhost:3000/docs.
|
|
245
|
+
|
|
246
|
+
This URL will display the Swagger documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively.
|
|
105
247
|
|
|
106
248
|

|
|
107
249
|
|
|
108
250
|
### Deriving a Client
|
|
109
251
|
|
|
110
|
-
|
|
252
|
+
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
253
|
|
|
112
|
-
**Example** (Deriving a Client)
|
|
254
|
+
**Example** (Deriving and Using a Client)
|
|
113
255
|
|
|
114
256
|
```ts
|
|
115
257
|
import {
|
|
@@ -161,96 +303,450 @@ Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
|
|
|
161
303
|
// Output: Hello, World!
|
|
162
304
|
```
|
|
163
305
|
|
|
164
|
-
##
|
|
306
|
+
## Defining a HttpApiEndpoint
|
|
307
|
+
|
|
308
|
+
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.
|
|
309
|
+
|
|
310
|
+
Below is an example of a simple CRUD API for managing users, which includes the following endpoints:
|
|
311
|
+
|
|
312
|
+
- `GET /users` - Retrieve all users.
|
|
313
|
+
- `GET /users/:userId` - Retrieve a specific user by ID.
|
|
314
|
+
- `POST /users` - Create a new user.
|
|
315
|
+
- `DELETE /users/:userId` - Delete a user by ID.
|
|
316
|
+
- `PATCH /users/:userId` - Update a user by ID.
|
|
317
|
+
|
|
318
|
+
### GET
|
|
165
319
|
|
|
166
|
-
|
|
320
|
+
The `HttpApiEndpoint.get` method allows you to define a GET endpoint by specifying its name, path, and optionally, a schema for the response.
|
|
167
321
|
|
|
168
|
-
|
|
322
|
+
To define the structure of successful responses, use the `.addSuccess` method. If no schema is provided, the default response status is `204 No Content`.
|
|
323
|
+
|
|
324
|
+
**Example** (Defining a GET Endpoint to Retrieve All Users)
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
328
|
+
import { Schema } from "effect"
|
|
329
|
+
|
|
330
|
+
// Define a schema representing a User entity
|
|
331
|
+
const User = Schema.Struct({
|
|
332
|
+
id: Schema.Number,
|
|
333
|
+
name: Schema.String,
|
|
334
|
+
createdAt: Schema.DateTimeUtc
|
|
335
|
+
})
|
|
169
336
|
|
|
337
|
+
// Define the "getUsers" endpoint, returning a list of users
|
|
338
|
+
const getUsers = HttpApiEndpoint
|
|
339
|
+
// ┌─── Endpoint name
|
|
340
|
+
// │ ┌─── Endpoint path
|
|
341
|
+
// ▼ ▼
|
|
342
|
+
.get("getUsers", "/users")
|
|
343
|
+
// Define the success schema for the response (optional).
|
|
344
|
+
// If no response schema is specified, the default response is `204 No Content`.
|
|
345
|
+
.addSuccess(Schema.Array(User))
|
|
170
346
|
```
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
347
|
+
|
|
348
|
+
### Path Parameters
|
|
349
|
+
|
|
350
|
+
Path parameters allow you to include dynamic segments in your endpoint's path. There are two ways to define path parameters in your API.
|
|
351
|
+
|
|
352
|
+
#### Using setPath
|
|
353
|
+
|
|
354
|
+
The `setPath` method allows you to explicitly define path parameters by associating them with a schema.
|
|
355
|
+
|
|
356
|
+
**Example** (Defining Parameters with setPath)
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
360
|
+
import { Schema } from "effect"
|
|
361
|
+
|
|
362
|
+
const User = Schema.Struct({
|
|
363
|
+
id: Schema.Number,
|
|
364
|
+
name: Schema.String,
|
|
365
|
+
createdAt: Schema.DateTimeUtc
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// Define a GET endpoint with a path parameter ":id"
|
|
369
|
+
const getUser = HttpApiEndpoint.get("getUser", "/user/:id")
|
|
370
|
+
.setPath(
|
|
371
|
+
Schema.Struct({
|
|
372
|
+
// Define a schema for the "id" path parameter
|
|
373
|
+
id: Schema.NumberFromString
|
|
374
|
+
})
|
|
375
|
+
)
|
|
376
|
+
.addSuccess(User)
|
|
179
377
|
```
|
|
180
378
|
|
|
181
|
-
|
|
379
|
+
#### Using Template Strings
|
|
182
380
|
|
|
183
|
-
|
|
381
|
+
You can also define path parameters by embedding them in a template string with the help of `HttpApiSchema.param`.
|
|
184
382
|
|
|
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
|
|
383
|
+
**Example** (Defining Parameters using a Template String)
|
|
189
384
|
|
|
190
|
-
|
|
385
|
+
```ts
|
|
386
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
387
|
+
import { Schema } from "effect"
|
|
388
|
+
|
|
389
|
+
const User = Schema.Struct({
|
|
390
|
+
id: Schema.Number,
|
|
391
|
+
name: Schema.String,
|
|
392
|
+
createdAt: Schema.DateTimeUtc
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Create a path parameter using HttpApiSchema.param
|
|
396
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
397
|
+
|
|
398
|
+
// Define the GET endpoint using a template string
|
|
399
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
400
|
+
User
|
|
401
|
+
)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### POST
|
|
405
|
+
|
|
406
|
+
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.
|
|
407
|
+
|
|
408
|
+
**Example** (Defining a POST Endpoint with Payload and Success Schemas)
|
|
191
409
|
|
|
192
410
|
```ts
|
|
193
|
-
import { HttpApiEndpoint
|
|
411
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
194
412
|
import { Schema } from "effect"
|
|
195
413
|
|
|
196
|
-
//
|
|
197
|
-
|
|
414
|
+
// Define a schema for the user object
|
|
415
|
+
const User = Schema.Struct({
|
|
198
416
|
id: Schema.Number,
|
|
199
417
|
name: Schema.String,
|
|
200
418
|
createdAt: Schema.DateTimeUtc
|
|
201
|
-
})
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// Define a POST endpoint for creating a new user
|
|
422
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
423
|
+
// Define the request body schema (payload)
|
|
424
|
+
.setPayload(
|
|
425
|
+
Schema.Struct({
|
|
426
|
+
name: Schema.String
|
|
427
|
+
})
|
|
428
|
+
)
|
|
429
|
+
// Define the schema for a successful response
|
|
430
|
+
.addSuccess(User)
|
|
431
|
+
```
|
|
202
432
|
|
|
203
|
-
|
|
204
|
-
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
433
|
+
### DELETE
|
|
205
434
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
435
|
+
The `HttpApiEndpoint.del` method is used to define an endpoint for deleting a resource.
|
|
436
|
+
|
|
437
|
+
**Example** (Defining a DELETE Endpoint with Path Parameters)
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
441
|
+
import { Schema } from "effect"
|
|
442
|
+
|
|
443
|
+
// Define a path parameter for the user ID
|
|
444
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
445
|
+
|
|
446
|
+
// Define a DELETE endpoint to delete a user by ID
|
|
447
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### PATCH
|
|
451
|
+
|
|
452
|
+
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.
|
|
453
|
+
|
|
454
|
+
**Example** (Defining a PATCH Endpoint for Updating a User)
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
458
|
+
import { Schema } from "effect"
|
|
459
|
+
|
|
460
|
+
// Define a schema for the user object
|
|
461
|
+
const User = Schema.Struct({
|
|
462
|
+
id: Schema.Number,
|
|
463
|
+
name: Schema.String,
|
|
464
|
+
createdAt: Schema.DateTimeUtc
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
// Define a path parameter for the user ID
|
|
468
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
469
|
+
|
|
470
|
+
// Define a PATCH endpoint to update a user's name by ID
|
|
471
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
472
|
+
// Specify the schema for the request payload
|
|
473
|
+
.setPayload(
|
|
474
|
+
Schema.Struct({
|
|
475
|
+
name: Schema.String // Only the name can be updated
|
|
476
|
+
})
|
|
213
477
|
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
478
|
+
// Specify the schema for a successful response
|
|
479
|
+
.addSuccess(User)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Catch-All Endpoints
|
|
483
|
+
|
|
484
|
+
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.
|
|
485
|
+
|
|
486
|
+
**Example** (Defining a Catch-All Endpoint)
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
490
|
+
|
|
491
|
+
const catchAll = HttpApiEndpoint.get("catchAll", "*")
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Setting URL Parameters
|
|
495
|
+
|
|
496
|
+
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.
|
|
497
|
+
|
|
498
|
+
**Example** (Defining URL Parameters with Metadata)
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
502
|
+
import { Schema } from "effect"
|
|
503
|
+
|
|
504
|
+
const User = Schema.Struct({
|
|
505
|
+
id: Schema.Number,
|
|
506
|
+
name: Schema.String,
|
|
507
|
+
createdAt: Schema.DateTimeUtc
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
511
|
+
// Specify the URL parameters schema
|
|
512
|
+
.setUrlParams(
|
|
513
|
+
Schema.Struct({
|
|
514
|
+
// Parameter "page" for pagination
|
|
515
|
+
page: Schema.NumberFromString,
|
|
516
|
+
// Parameter "sort" for sorting options with an added description
|
|
517
|
+
sort: Schema.String.annotations({
|
|
518
|
+
description: "Sorting criteria (e.g., 'name', 'date')"
|
|
519
|
+
})
|
|
520
|
+
})
|
|
226
521
|
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
522
|
+
.addSuccess(Schema.Array(User))
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
#### Defining an Array of Values for a URL Parameter
|
|
526
|
+
|
|
527
|
+
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.
|
|
528
|
+
|
|
529
|
+
**Example** (Defining an Array of String Values for a URL Parameter)
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
|
|
533
|
+
import { Schema } from "effect"
|
|
534
|
+
|
|
535
|
+
const api = HttpApi.make("myApi").add(
|
|
536
|
+
HttpApiGroup.make("group").add(
|
|
537
|
+
HttpApiEndpoint.get("get", "/")
|
|
538
|
+
.setUrlParams(
|
|
233
539
|
Schema.Struct({
|
|
234
|
-
|
|
540
|
+
// Define "a" as an array of strings
|
|
541
|
+
a: Schema.Array(Schema.String)
|
|
235
542
|
})
|
|
236
543
|
)
|
|
544
|
+
.addSuccess(Schema.String)
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
You can test this endpoint by passing an array of values in the query string. For example:
|
|
550
|
+
|
|
551
|
+
```sh
|
|
552
|
+
curl "http://localhost:3000/?a=1&a=2"
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
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.
|
|
556
|
+
|
|
557
|
+
### Status Codes
|
|
558
|
+
|
|
559
|
+
By default, the success status code is `200 OK`. You can change it by annotating the schema with a custom status.
|
|
560
|
+
|
|
561
|
+
**Example** (Defining a GET Endpoint with a custom status code)
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
565
|
+
import { Schema } from "effect"
|
|
566
|
+
|
|
567
|
+
const User = Schema.Struct({
|
|
568
|
+
id: Schema.Number,
|
|
569
|
+
name: Schema.String,
|
|
570
|
+
createdAt: Schema.DateTimeUtc
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
574
|
+
// Override the default success status
|
|
575
|
+
.addSuccess(Schema.Array(User), { status: 206 })
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Handling Multipart Requests
|
|
579
|
+
|
|
580
|
+
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.
|
|
581
|
+
|
|
582
|
+
**Example** (Defining an Endpoint for File Uploads)
|
|
583
|
+
|
|
584
|
+
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.
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
import { HttpApiEndpoint, HttpApiSchema, Multipart } from "@effect/platform"
|
|
588
|
+
import { Schema } from "effect"
|
|
589
|
+
|
|
590
|
+
const upload = HttpApiEndpoint.post("upload", "/users/upload").setPayload(
|
|
591
|
+
// Specify that the payload is a multipart request
|
|
592
|
+
HttpApiSchema.Multipart(
|
|
593
|
+
Schema.Struct({
|
|
594
|
+
// Define a "files" field to handle file uploads
|
|
595
|
+
files: Multipart.FilesSchema
|
|
596
|
+
})
|
|
597
|
+
).addSuccess(Schema.String)
|
|
598
|
+
)
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
You can test this endpoint by sending a multipart request with a file upload. For example:
|
|
602
|
+
|
|
603
|
+
```sh
|
|
604
|
+
echo "Sample file content" | curl -X POST -F "files=@-" http://localhost:3000/users/upload
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Changing the Request Encoding
|
|
608
|
+
|
|
609
|
+
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.
|
|
610
|
+
|
|
611
|
+
**Example** (Customizing Request Encoding)
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
615
|
+
import { Schema } from "effect"
|
|
616
|
+
|
|
617
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
618
|
+
// Set the request payload as a string encoded with URL parameters
|
|
619
|
+
.setPayload(
|
|
620
|
+
Schema.Struct({
|
|
621
|
+
a: Schema.String // Parameter "a" must be a string
|
|
622
|
+
})
|
|
623
|
+
// Specify the encoding as URL parameters
|
|
624
|
+
.pipe(HttpApiSchema.withEncoding({ kind: "UrlParams" }))
|
|
625
|
+
)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Changing the Response Encoding
|
|
629
|
+
|
|
630
|
+
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.
|
|
631
|
+
|
|
632
|
+
**Example** (Returning Data as `text/csv`)
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
636
|
+
import { Schema } from "effect"
|
|
637
|
+
|
|
638
|
+
const csv = HttpApiEndpoint.get("csv")`/users/csv`
|
|
639
|
+
// Set the success response as a string with CSV encoding
|
|
640
|
+
.addSuccess(
|
|
641
|
+
Schema.String.pipe(
|
|
642
|
+
HttpApiSchema.withEncoding({
|
|
643
|
+
// Specify the type of the response
|
|
644
|
+
kind: "Text",
|
|
645
|
+
// Define the content type as text/csv
|
|
646
|
+
contentType: "text/csv"
|
|
647
|
+
})
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Setting Request Headers
|
|
653
|
+
|
|
654
|
+
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.
|
|
655
|
+
|
|
656
|
+
**Example** (Defining Request Headers with Metadata)
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
660
|
+
import { Schema } from "effect"
|
|
661
|
+
|
|
662
|
+
const User = Schema.Struct({
|
|
663
|
+
id: Schema.Number,
|
|
664
|
+
name: Schema.String,
|
|
665
|
+
createdAt: Schema.DateTimeUtc
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
669
|
+
// Specify the headers schema
|
|
670
|
+
.setHeaders(
|
|
671
|
+
Schema.Struct({
|
|
672
|
+
// Header must be a string
|
|
673
|
+
"X-API-Key": Schema.String,
|
|
674
|
+
// Header must be a string with an added description
|
|
675
|
+
"X-Request-ID": Schema.String.annotations({
|
|
676
|
+
description: "Unique identifier for the request"
|
|
677
|
+
})
|
|
678
|
+
})
|
|
679
|
+
)
|
|
680
|
+
.addSuccess(Schema.Array(User))
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
## Defining a HttpApiGroup
|
|
684
|
+
|
|
685
|
+
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.
|
|
686
|
+
|
|
687
|
+
**Example** (Creating a Group for User-Related Endpoints)
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
|
|
691
|
+
import { Schema } from "effect"
|
|
692
|
+
|
|
693
|
+
const User = Schema.Struct({
|
|
694
|
+
id: Schema.Number,
|
|
695
|
+
name: Schema.String,
|
|
696
|
+
createdAt: Schema.DateTimeUtc
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
700
|
+
|
|
701
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
702
|
+
Schema.Array(User)
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
706
|
+
User
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
710
|
+
.setPayload(
|
|
711
|
+
Schema.Struct({
|
|
712
|
+
name: Schema.String
|
|
713
|
+
})
|
|
714
|
+
)
|
|
715
|
+
.addSuccess(User)
|
|
716
|
+
|
|
717
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
718
|
+
|
|
719
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
720
|
+
.setPayload(
|
|
721
|
+
Schema.Struct({
|
|
722
|
+
name: Schema.String
|
|
723
|
+
})
|
|
237
724
|
)
|
|
725
|
+
.addSuccess(User)
|
|
726
|
+
|
|
727
|
+
// Group all user-related endpoints
|
|
728
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
729
|
+
.add(getUsers)
|
|
730
|
+
.add(getUser)
|
|
731
|
+
.add(createUser)
|
|
732
|
+
.add(deleteUser)
|
|
733
|
+
.add(updateUser)
|
|
238
734
|
```
|
|
239
735
|
|
|
240
|
-
|
|
736
|
+
If you would like to create a more opaque type for the group, you can extend `HttpApiGroup` with a class.
|
|
241
737
|
|
|
242
|
-
**Example** (
|
|
738
|
+
**Example** (Creating a Group with an Opaque Type)
|
|
243
739
|
|
|
244
740
|
```ts
|
|
245
|
-
class
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
741
|
+
// Create an opaque class extending HttpApiGroup
|
|
742
|
+
class UsersGroup extends HttpApiGroup.make("users").add(getUsers).add(getUser) {
|
|
743
|
+
// Additional endpoints or methods can be added here
|
|
744
|
+
}
|
|
249
745
|
```
|
|
250
746
|
|
|
251
|
-
|
|
747
|
+
## Creating the Top-Level HttpApi
|
|
252
748
|
|
|
253
|
-
After defining your groups, you can combine them into
|
|
749
|
+
After defining your groups, you can combine them into one `HttpApi` representing your entire set of endpoints.
|
|
254
750
|
|
|
255
751
|
**Example** (Combining Groups into a Top-Level API)
|
|
256
752
|
|
|
@@ -263,56 +759,78 @@ import {
|
|
|
263
759
|
} from "@effect/platform"
|
|
264
760
|
import { Schema } from "effect"
|
|
265
761
|
|
|
266
|
-
|
|
762
|
+
const User = Schema.Struct({
|
|
267
763
|
id: Schema.Number,
|
|
268
764
|
name: Schema.String,
|
|
269
765
|
createdAt: Schema.DateTimeUtc
|
|
270
|
-
})
|
|
766
|
+
})
|
|
271
767
|
|
|
272
|
-
const
|
|
768
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
273
769
|
|
|
274
|
-
|
|
275
|
-
.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
770
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
771
|
+
Schema.Array(User)
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
775
|
+
User
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
779
|
+
.setPayload(
|
|
780
|
+
Schema.Struct({
|
|
781
|
+
name: Schema.String
|
|
782
|
+
})
|
|
284
783
|
)
|
|
285
|
-
.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)
|
|
784
|
+
.addSuccess(User)
|
|
785
|
+
|
|
786
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
787
|
+
|
|
788
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
789
|
+
.setPayload(
|
|
790
|
+
Schema.Struct({
|
|
791
|
+
name: Schema.String
|
|
792
|
+
})
|
|
793
|
+
)
|
|
794
|
+
.addSuccess(User)
|
|
795
|
+
|
|
796
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
797
|
+
.add(getUsers)
|
|
798
|
+
.add(getUser)
|
|
799
|
+
.add(createUser)
|
|
800
|
+
.add(deleteUser)
|
|
801
|
+
.add(updateUser)
|
|
295
802
|
|
|
296
|
-
// Combine the groups into
|
|
297
|
-
|
|
803
|
+
// Combine the groups into one API
|
|
804
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
298
805
|
|
|
299
|
-
// Alternatively,
|
|
300
|
-
|
|
806
|
+
// Alternatively, create an opaque class for your API
|
|
807
|
+
class MyApi extends HttpApi.make("myApi").add(usersGroup) {}
|
|
301
808
|
```
|
|
302
809
|
|
|
303
|
-
|
|
810
|
+
## Adding errors
|
|
304
811
|
|
|
305
|
-
Error responses
|
|
812
|
+
Error responses allow your API to handle different failure scenarios. These responses can be defined at various levels:
|
|
306
813
|
|
|
307
|
-
- Use `HttpApiEndpoint.addError` to add
|
|
308
|
-
- Use `HttpApiGroup.addError` to add
|
|
309
|
-
- Use `HttpApi.addError` to
|
|
814
|
+
- **Endpoint-level errors**: Use `HttpApiEndpoint.addError` to add errors specific to an endpoint.
|
|
815
|
+
- **Group-level errors**: Use `HttpApiGroup.addError` to add errors applicable to all endpoints in a group.
|
|
816
|
+
- **API-level errors**: Use `HttpApi.addError` to define errors that apply to every endpoint in the API.
|
|
310
817
|
|
|
311
|
-
Group-level and API-level errors are
|
|
818
|
+
Group-level and API-level errors are useful for handling shared issues like authentication failures, especially when managed through middleware.
|
|
312
819
|
|
|
313
|
-
**Example** (
|
|
820
|
+
**Example** (Defining Error Responses for Endpoints and Groups)
|
|
314
821
|
|
|
315
822
|
```ts
|
|
823
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
|
|
824
|
+
import { Schema } from "effect"
|
|
825
|
+
|
|
826
|
+
const User = Schema.Struct({
|
|
827
|
+
id: Schema.Number,
|
|
828
|
+
name: Schema.String,
|
|
829
|
+
createdAt: Schema.DateTimeUtc
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
833
|
+
|
|
316
834
|
// Define error schemas
|
|
317
835
|
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
318
836
|
"UserNotFound",
|
|
@@ -324,85 +842,108 @@ class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
|
324
842
|
{}
|
|
325
843
|
) {}
|
|
326
844
|
|
|
327
|
-
|
|
328
|
-
.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
845
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
846
|
+
Schema.Array(User)
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
850
|
+
.addSuccess(User)
|
|
851
|
+
// Add a 404 error response for this endpoint
|
|
852
|
+
.addError(UserNotFound, { status: 404 })
|
|
853
|
+
|
|
854
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
855
|
+
.add(getUsers)
|
|
856
|
+
.add(getUser)
|
|
857
|
+
// ...etc...
|
|
858
|
+
// Add a 401 error response for the entire group
|
|
859
|
+
.addError(Unauthorized, { status: 401 })
|
|
338
860
|
```
|
|
339
861
|
|
|
340
|
-
You can
|
|
862
|
+
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.
|
|
863
|
+
|
|
864
|
+
**Example** (Adding Multiple Errors to an Endpoint)
|
|
341
865
|
|
|
342
|
-
|
|
866
|
+
```ts
|
|
867
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
868
|
+
// Add a 404 error response for when the user is not found
|
|
869
|
+
.addError(UserNotFound, { status: 404 })
|
|
870
|
+
// Add a 401 error response for unauthorized access
|
|
871
|
+
.addError(Unauthorized, { status: 401 })
|
|
872
|
+
```
|
|
343
873
|
|
|
344
|
-
|
|
874
|
+
### Predefined Empty Error Types
|
|
345
875
|
|
|
346
|
-
|
|
876
|
+
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.
|
|
877
|
+
|
|
878
|
+
**Example** (Adding a Predefined Error to an Endpoint)
|
|
347
879
|
|
|
348
880
|
```ts
|
|
349
|
-
import {
|
|
350
|
-
HttpApiEndpoint,
|
|
351
|
-
HttpApiGroup,
|
|
352
|
-
HttpApiSchema,
|
|
353
|
-
Multipart
|
|
354
|
-
} from "@effect/platform"
|
|
881
|
+
import { HttpApiEndpoint, HttpApiError, HttpApiSchema } from "@effect/platform"
|
|
355
882
|
import { Schema } from "effect"
|
|
356
883
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
)
|
|
367
|
-
)
|
|
884
|
+
const User = Schema.Struct({
|
|
885
|
+
id: Schema.Number,
|
|
886
|
+
name: Schema.String,
|
|
887
|
+
createdAt: Schema.DateTimeUtc
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
891
|
+
|
|
892
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
893
|
+
.addSuccess(User)
|
|
894
|
+
.addError(HttpApiError.NotFound)
|
|
368
895
|
```
|
|
369
896
|
|
|
370
|
-
|
|
897
|
+
| Name | Status | Description |
|
|
898
|
+
| --------------------- | ------ | -------------------------------------------------------------------------------------------------- |
|
|
899
|
+
| `HttpApiDecodeError` | 400 | Represents an error where the request did not match the expected schema. Includes detailed issues. |
|
|
900
|
+
| `BadRequest` | 400 | Indicates that the request was malformed or invalid. |
|
|
901
|
+
| `Unauthorized` | 401 | Indicates that authentication is required but missing or invalid. |
|
|
902
|
+
| `Forbidden` | 403 | Indicates that the client does not have permission to access the requested resource. |
|
|
903
|
+
| `NotFound` | 404 | Indicates that the requested resource could not be found. |
|
|
904
|
+
| `MethodNotAllowed` | 405 | Indicates that the HTTP method used is not allowed for the requested resource. |
|
|
905
|
+
| `NotAcceptable` | 406 | Indicates that the requested resource cannot be delivered in a format acceptable to the client. |
|
|
906
|
+
| `RequestTimeout` | 408 | Indicates that the server timed out waiting for the client request. |
|
|
907
|
+
| `Conflict` | 409 | Indicates a conflict in the request, such as conflicting data. |
|
|
908
|
+
| `Gone` | 410 | Indicates that the requested resource is no longer available and will not return. |
|
|
909
|
+
| `InternalServerError` | 500 | Indicates an unexpected server error occurred. |
|
|
910
|
+
| `NotImplemented` | 501 | Indicates that the requested functionality is not implemented on the server. |
|
|
911
|
+
| `ServiceUnavailable` | 503 | Indicates that the server is temporarily unavailable, often due to maintenance or overload. |
|
|
371
912
|
|
|
372
|
-
|
|
913
|
+
## Prefixing
|
|
373
914
|
|
|
374
|
-
|
|
915
|
+
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
916
|
|
|
376
|
-
**Example** (
|
|
917
|
+
**Example** (Using Prefixes for Common Path Management)
|
|
377
918
|
|
|
378
919
|
```ts
|
|
379
|
-
import { HttpApiEndpoint, HttpApiGroup
|
|
920
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
|
|
380
921
|
import { Schema } from "effect"
|
|
381
922
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
contentType: "text/csv"
|
|
391
|
-
})
|
|
923
|
+
const api = HttpApi.make("api")
|
|
924
|
+
.add(
|
|
925
|
+
HttpApiGroup.make("group")
|
|
926
|
+
.add(
|
|
927
|
+
HttpApiEndpoint.get("getRoot", "/")
|
|
928
|
+
.addSuccess(Schema.String)
|
|
929
|
+
// Prefix for this endpoint
|
|
930
|
+
.prefix("/endpointPrefix")
|
|
392
931
|
)
|
|
393
|
-
|
|
394
|
-
|
|
932
|
+
.add(HttpApiEndpoint.get("getA", "/a").addSuccess(Schema.String))
|
|
933
|
+
// Prefix for all endpoints in the group
|
|
934
|
+
.prefix("/groupPrefix")
|
|
935
|
+
)
|
|
936
|
+
// Prefix for the entire API
|
|
937
|
+
.prefix("/apiPrefix")
|
|
395
938
|
```
|
|
396
939
|
|
|
397
940
|
## Implementing a Server
|
|
398
941
|
|
|
399
|
-
|
|
400
|
-
endpoints.
|
|
942
|
+
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
943
|
|
|
402
|
-
|
|
403
|
-
server.
|
|
944
|
+
Here, we will create a simple example with a `getUser` endpoint organized within a `users` group.
|
|
404
945
|
|
|
405
|
-
|
|
946
|
+
**Example** (Defining the `users` Group and API)
|
|
406
947
|
|
|
407
948
|
```ts
|
|
408
949
|
import {
|
|
@@ -413,19 +954,19 @@ import {
|
|
|
413
954
|
} from "@effect/platform"
|
|
414
955
|
import { Schema } from "effect"
|
|
415
956
|
|
|
416
|
-
|
|
957
|
+
const User = Schema.Struct({
|
|
417
958
|
id: Schema.Number,
|
|
418
959
|
name: Schema.String,
|
|
419
960
|
createdAt: Schema.DateTimeUtc
|
|
420
|
-
})
|
|
961
|
+
})
|
|
421
962
|
|
|
422
|
-
const
|
|
963
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
423
964
|
|
|
424
|
-
|
|
425
|
-
HttpApiEndpoint.get("
|
|
426
|
-
)
|
|
965
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
966
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
967
|
+
)
|
|
427
968
|
|
|
428
|
-
|
|
969
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
429
970
|
```
|
|
430
971
|
|
|
431
972
|
### Implementing a HttpApiGroup
|
|
@@ -442,7 +983,7 @@ Each endpoint in the group is connected to its logic using the `HttpApiBuilder.h
|
|
|
442
983
|
|
|
443
984
|
The `HttpApiBuilder.group` API produces a `Layer` that can later be provided to the server implementation.
|
|
444
985
|
|
|
445
|
-
**Example** (Implementing
|
|
986
|
+
**Example** (Implementing a Group with Endpoint Logic)
|
|
446
987
|
|
|
447
988
|
```ts
|
|
448
989
|
import {
|
|
@@ -454,19 +995,19 @@ import {
|
|
|
454
995
|
} from "@effect/platform"
|
|
455
996
|
import { DateTime, Effect, Schema } from "effect"
|
|
456
997
|
|
|
457
|
-
|
|
998
|
+
const User = Schema.Struct({
|
|
458
999
|
id: Schema.Number,
|
|
459
1000
|
name: Schema.String,
|
|
460
1001
|
createdAt: Schema.DateTimeUtc
|
|
461
|
-
})
|
|
1002
|
+
})
|
|
462
1003
|
|
|
463
|
-
const
|
|
1004
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
464
1005
|
|
|
465
|
-
|
|
466
|
-
HttpApiEndpoint.get("
|
|
467
|
-
)
|
|
1006
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1007
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1008
|
+
)
|
|
468
1009
|
|
|
469
|
-
|
|
1010
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
470
1011
|
|
|
471
1012
|
// --------------------------------------------
|
|
472
1013
|
// Implementation
|
|
@@ -474,25 +1015,25 @@ class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
|
|
|
474
1015
|
|
|
475
1016
|
// ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">>
|
|
476
1017
|
// ▼
|
|
477
|
-
const
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
HttpApiBuilder.group(
|
|
1018
|
+
const usersGroupLive =
|
|
1019
|
+
// ┌─── The Whole API
|
|
1020
|
+
// │ ┌─── The Group you are implementing
|
|
1021
|
+
// ▼ ▼
|
|
1022
|
+
HttpApiBuilder.group(api, "users", (handlers) =>
|
|
482
1023
|
handlers.handle(
|
|
483
1024
|
// ┌─── The Endpoint you are implementing
|
|
484
1025
|
// ▼
|
|
485
|
-
"
|
|
1026
|
+
"getUser",
|
|
486
1027
|
// Provide the handler logic for the endpoint.
|
|
487
1028
|
// The parameters & payload are passed to the handler function.
|
|
488
|
-
({ path: {
|
|
1029
|
+
({ path: { id } }) =>
|
|
489
1030
|
Effect.succeed(
|
|
490
1031
|
// Return a mock user object with the provided ID
|
|
491
|
-
|
|
492
|
-
id
|
|
1032
|
+
{
|
|
1033
|
+
id,
|
|
493
1034
|
name: "John Doe",
|
|
494
1035
|
createdAt: DateTime.unsafeNow()
|
|
495
|
-
}
|
|
1036
|
+
}
|
|
496
1037
|
)
|
|
497
1038
|
)
|
|
498
1039
|
)
|
|
@@ -516,19 +1057,25 @@ import {
|
|
|
516
1057
|
} from "@effect/platform"
|
|
517
1058
|
import { Context, Effect, Schema } from "effect"
|
|
518
1059
|
|
|
519
|
-
|
|
1060
|
+
const User = Schema.Struct({
|
|
520
1061
|
id: Schema.Number,
|
|
521
1062
|
name: Schema.String,
|
|
522
1063
|
createdAt: Schema.DateTimeUtc
|
|
523
|
-
})
|
|
1064
|
+
})
|
|
524
1065
|
|
|
525
|
-
const
|
|
1066
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
526
1067
|
|
|
527
|
-
|
|
528
|
-
HttpApiEndpoint.get("
|
|
529
|
-
)
|
|
1068
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1069
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1070
|
+
)
|
|
530
1071
|
|
|
531
|
-
|
|
1072
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
1073
|
+
|
|
1074
|
+
// --------------------------------------------
|
|
1075
|
+
// Implementation
|
|
1076
|
+
// --------------------------------------------
|
|
1077
|
+
|
|
1078
|
+
type User = typeof User.Type
|
|
532
1079
|
|
|
533
1080
|
// Define the UsersRepository service
|
|
534
1081
|
class UsersRepository extends Context.Tag("UsersRepository")<
|
|
@@ -538,14 +1085,16 @@ class UsersRepository extends Context.Tag("UsersRepository")<
|
|
|
538
1085
|
}
|
|
539
1086
|
>() {}
|
|
540
1087
|
|
|
1088
|
+
// Implement the `users` group with access to the UsersRepository service
|
|
1089
|
+
//
|
|
541
1090
|
// ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">, never, UsersRepository>
|
|
542
1091
|
// ▼
|
|
543
|
-
const
|
|
1092
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
544
1093
|
Effect.gen(function* () {
|
|
545
1094
|
// Access the UsersRepository service
|
|
546
1095
|
const repository = yield* UsersRepository
|
|
547
|
-
return handlers.handle("
|
|
548
|
-
repository.findById(
|
|
1096
|
+
return handlers.handle("getUser", ({ path: { id } }) =>
|
|
1097
|
+
repository.findById(id)
|
|
549
1098
|
)
|
|
550
1099
|
})
|
|
551
1100
|
)
|
|
@@ -567,34 +1116,27 @@ import {
|
|
|
567
1116
|
} from "@effect/platform"
|
|
568
1117
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
569
1118
|
|
|
570
|
-
|
|
1119
|
+
const User = Schema.Struct({
|
|
571
1120
|
id: Schema.Number,
|
|
572
1121
|
name: Schema.String,
|
|
573
1122
|
createdAt: Schema.DateTimeUtc
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
1123
|
+
})
|
|
577
1124
|
|
|
578
|
-
|
|
579
|
-
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
580
|
-
) {}
|
|
1125
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
581
1126
|
|
|
582
|
-
|
|
1127
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1128
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1129
|
+
)
|
|
583
1130
|
|
|
584
|
-
|
|
585
|
-
// Implementation
|
|
586
|
-
// --------------------------------------------
|
|
1131
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
587
1132
|
|
|
588
|
-
const
|
|
589
|
-
handlers.handle("
|
|
590
|
-
Effect.succeed(
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
createdAt: DateTime.unsafeNow()
|
|
596
|
-
})
|
|
597
|
-
)
|
|
1133
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1134
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1135
|
+
Effect.succeed({
|
|
1136
|
+
id,
|
|
1137
|
+
name: "John Doe",
|
|
1138
|
+
createdAt: DateTime.unsafeNow()
|
|
1139
|
+
})
|
|
598
1140
|
)
|
|
599
1141
|
)
|
|
600
1142
|
|
|
@@ -602,16 +1144,14 @@ const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
|
602
1144
|
//
|
|
603
1145
|
// ┌─── Layer<HttpApi.Api, never, never>
|
|
604
1146
|
// ▼
|
|
605
|
-
const MyApiLive = HttpApiBuilder.api(
|
|
1147
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
606
1148
|
```
|
|
607
1149
|
|
|
608
1150
|
### Serving the API
|
|
609
1151
|
|
|
610
|
-
You can serve your API using the `HttpApiBuilder.serve`
|
|
611
|
-
|
|
612
|
-
Optionally, you can provide middleware to enhance the `HttpApp` before serving it.
|
|
1152
|
+
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.
|
|
613
1153
|
|
|
614
|
-
**Example** (Serving an API with Middleware)
|
|
1154
|
+
**Example** (Setting Up and Serving an API with Middleware)
|
|
615
1155
|
|
|
616
1156
|
```ts
|
|
617
1157
|
import {
|
|
@@ -627,50 +1167,228 @@ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
|
627
1167
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
628
1168
|
import { createServer } from "node:http"
|
|
629
1169
|
|
|
630
|
-
|
|
1170
|
+
const User = Schema.Struct({
|
|
631
1171
|
id: Schema.Number,
|
|
632
1172
|
name: Schema.String,
|
|
633
1173
|
createdAt: Schema.DateTimeUtc
|
|
634
|
-
})
|
|
1174
|
+
})
|
|
635
1175
|
|
|
636
|
-
const
|
|
1176
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
637
1177
|
|
|
638
|
-
|
|
639
|
-
HttpApiEndpoint.get("
|
|
640
|
-
)
|
|
1178
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1179
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1180
|
+
)
|
|
641
1181
|
|
|
642
|
-
|
|
1182
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
643
1183
|
|
|
644
|
-
const
|
|
645
|
-
handlers.handle("
|
|
646
|
-
Effect.succeed(
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
})
|
|
652
|
-
)
|
|
1184
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1185
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1186
|
+
Effect.succeed({
|
|
1187
|
+
id,
|
|
1188
|
+
name: "John Doe",
|
|
1189
|
+
createdAt: DateTime.unsafeNow()
|
|
1190
|
+
})
|
|
653
1191
|
)
|
|
654
1192
|
)
|
|
655
1193
|
|
|
656
|
-
const MyApiLive = HttpApiBuilder.api(
|
|
1194
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
657
1195
|
|
|
658
|
-
//
|
|
1196
|
+
// Configure and serve the API
|
|
659
1197
|
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
660
|
-
// Add middleware
|
|
1198
|
+
// Add CORS middleware to handle cross-origin requests
|
|
661
1199
|
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
662
1200
|
// Provide the API implementation
|
|
663
1201
|
Layer.provide(MyApiLive),
|
|
664
1202
|
// Log the server's listening address
|
|
665
1203
|
HttpServer.withLogAddress,
|
|
666
|
-
//
|
|
1204
|
+
// Set up the Node.js HTTP server
|
|
1205
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
// Launch the server
|
|
1209
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
### Accessing the HttpServerRequest
|
|
1213
|
+
|
|
1214
|
+
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.
|
|
1215
|
+
|
|
1216
|
+
**Example** (Accessing the Request Object in a GET Endpoint)
|
|
1217
|
+
|
|
1218
|
+
```ts
|
|
1219
|
+
import {
|
|
1220
|
+
HttpApi,
|
|
1221
|
+
HttpApiBuilder,
|
|
1222
|
+
HttpApiEndpoint,
|
|
1223
|
+
HttpApiGroup,
|
|
1224
|
+
HttpMiddleware,
|
|
1225
|
+
HttpServer,
|
|
1226
|
+
HttpServerRequest
|
|
1227
|
+
} from "@effect/platform"
|
|
1228
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1229
|
+
import { Effect, Layer, Schema } from "effect"
|
|
1230
|
+
import { createServer } from "node:http"
|
|
1231
|
+
|
|
1232
|
+
const api = HttpApi.make("myApi").add(
|
|
1233
|
+
HttpApiGroup.make("group").add(
|
|
1234
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
1235
|
+
)
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1239
|
+
handlers.handle("get", () =>
|
|
1240
|
+
Effect.gen(function* () {
|
|
1241
|
+
// Access the incoming request
|
|
1242
|
+
const req = yield* HttpServerRequest.HttpServerRequest
|
|
1243
|
+
|
|
1244
|
+
// Log the HTTP method for demonstration purposes
|
|
1245
|
+
console.log(req.method)
|
|
1246
|
+
|
|
1247
|
+
// Return a response
|
|
1248
|
+
return "Hello, World!"
|
|
1249
|
+
})
|
|
1250
|
+
)
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1254
|
+
|
|
1255
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1256
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1257
|
+
Layer.provide(MyApiLive),
|
|
1258
|
+
HttpServer.withLogAddress,
|
|
1259
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
### Streaming Requests
|
|
1266
|
+
|
|
1267
|
+
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.
|
|
1268
|
+
|
|
1269
|
+
**Example** (Handling Streaming Requests)
|
|
1270
|
+
|
|
1271
|
+
```ts
|
|
1272
|
+
import {
|
|
1273
|
+
HttpApi,
|
|
1274
|
+
HttpApiBuilder,
|
|
1275
|
+
HttpApiEndpoint,
|
|
1276
|
+
HttpApiGroup,
|
|
1277
|
+
HttpApiSchema,
|
|
1278
|
+
HttpMiddleware,
|
|
1279
|
+
HttpServer
|
|
1280
|
+
} from "@effect/platform"
|
|
1281
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1282
|
+
import { Effect, Layer, Schema } from "effect"
|
|
1283
|
+
import { createServer } from "node:http"
|
|
1284
|
+
|
|
1285
|
+
const api = HttpApi.make("myApi").add(
|
|
1286
|
+
HttpApiGroup.make("group").add(
|
|
1287
|
+
HttpApiEndpoint.post("acceptStream", "/stream")
|
|
1288
|
+
// Define the payload as a Uint8Array with a specific encoding
|
|
1289
|
+
.setPayload(
|
|
1290
|
+
Schema.Uint8ArrayFromSelf.pipe(
|
|
1291
|
+
HttpApiSchema.withEncoding({
|
|
1292
|
+
kind: "Uint8Array",
|
|
1293
|
+
contentType: "application/octet-stream"
|
|
1294
|
+
})
|
|
1295
|
+
)
|
|
1296
|
+
)
|
|
1297
|
+
.addSuccess(Schema.String)
|
|
1298
|
+
)
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1302
|
+
handlers.handle("acceptStream", (req) =>
|
|
1303
|
+
// Decode the incoming binary data into a string
|
|
1304
|
+
Effect.succeed(new TextDecoder().decode(req.payload))
|
|
1305
|
+
)
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1309
|
+
|
|
1310
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1311
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1312
|
+
Layer.provide(MyApiLive),
|
|
1313
|
+
HttpServer.withLogAddress,
|
|
1314
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
You can test the streaming request using `curl` or any tool that supports sending binary data. For example:
|
|
1321
|
+
|
|
1322
|
+
```sh
|
|
1323
|
+
echo "abc" | curl -X POST 'http://localhost:3000/stream' --data-binary @- -H "Content-Type: application/octet-stream"
|
|
1324
|
+
# Output: abc
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
### Streaming Responses
|
|
1328
|
+
|
|
1329
|
+
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.
|
|
1330
|
+
|
|
1331
|
+
**Example** (Implementing a Streaming Endpoint)
|
|
1332
|
+
|
|
1333
|
+
```ts
|
|
1334
|
+
import {
|
|
1335
|
+
HttpApi,
|
|
1336
|
+
HttpApiBuilder,
|
|
1337
|
+
HttpApiEndpoint,
|
|
1338
|
+
HttpApiGroup,
|
|
1339
|
+
HttpApiSchema,
|
|
1340
|
+
HttpMiddleware,
|
|
1341
|
+
HttpServer,
|
|
1342
|
+
HttpServerResponse
|
|
1343
|
+
} from "@effect/platform"
|
|
1344
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1345
|
+
import { Layer, Schedule, Schema, Stream } from "effect"
|
|
1346
|
+
import { createServer } from "node:http"
|
|
1347
|
+
|
|
1348
|
+
// Define the API with a single streaming endpoint
|
|
1349
|
+
const api = HttpApi.make("myApi").add(
|
|
1350
|
+
HttpApiGroup.make("group").add(
|
|
1351
|
+
HttpApiEndpoint.get("getStream", "/stream").addSuccess(
|
|
1352
|
+
Schema.String.pipe(
|
|
1353
|
+
HttpApiSchema.withEncoding({
|
|
1354
|
+
kind: "Text",
|
|
1355
|
+
contentType: "application/octet-stream"
|
|
1356
|
+
})
|
|
1357
|
+
)
|
|
1358
|
+
)
|
|
1359
|
+
)
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
// Simulate a stream of data
|
|
1363
|
+
const stream = Stream.make("a", "b", "c").pipe(
|
|
1364
|
+
Stream.schedule(Schedule.spaced("500 millis")),
|
|
1365
|
+
Stream.map((s) => new TextEncoder().encode(s))
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1369
|
+
handlers.handleRaw("getStream", () => HttpServerResponse.stream(stream))
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1373
|
+
|
|
1374
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1375
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1376
|
+
Layer.provide(MyApiLive),
|
|
1377
|
+
HttpServer.withLogAddress,
|
|
667
1378
|
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
668
1379
|
)
|
|
669
1380
|
|
|
670
|
-
// run the server
|
|
671
1381
|
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
672
1382
|
```
|
|
673
1383
|
|
|
1384
|
+
You can test the streaming response using `curl` or any similar HTTP client that supports streaming:
|
|
1385
|
+
|
|
1386
|
+
```sh
|
|
1387
|
+
curl 'http://localhost:3000/stream' --no-buffer
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
The response will stream data (`a`, `b`, `c`) with a 500ms interval between each item.
|
|
1391
|
+
|
|
674
1392
|
## Middlewares
|
|
675
1393
|
|
|
676
1394
|
### Defining Middleware
|
|
@@ -692,7 +1410,8 @@ You can define middleware using the `HttpApiMiddleware.Tag` class, which lets yo
|
|
|
692
1410
|
import {
|
|
693
1411
|
HttpApiEndpoint,
|
|
694
1412
|
HttpApiGroup,
|
|
695
|
-
HttpApiMiddleware
|
|
1413
|
+
HttpApiMiddleware,
|
|
1414
|
+
HttpApiSchema
|
|
696
1415
|
} from "@effect/platform"
|
|
697
1416
|
import { Schema } from "effect"
|
|
698
1417
|
|
|
@@ -708,14 +1427,23 @@ class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
|
|
|
708
1427
|
failure: LoggerError
|
|
709
1428
|
}) {}
|
|
710
1429
|
|
|
711
|
-
|
|
1430
|
+
const User = Schema.Struct({
|
|
1431
|
+
id: Schema.Number,
|
|
1432
|
+
name: Schema.String,
|
|
1433
|
+
createdAt: Schema.DateTimeUtc
|
|
1434
|
+
})
|
|
1435
|
+
|
|
1436
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1437
|
+
|
|
1438
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
712
1439
|
.add(
|
|
713
|
-
HttpApiEndpoint.get("
|
|
1440
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
1441
|
+
.addSuccess(User)
|
|
714
1442
|
// Apply the middleware to a single endpoint
|
|
715
1443
|
.middleware(Logger)
|
|
716
1444
|
)
|
|
717
1445
|
// Or apply the middleware to the entire group
|
|
718
|
-
.middleware(Logger)
|
|
1446
|
+
.middleware(Logger)
|
|
719
1447
|
```
|
|
720
1448
|
|
|
721
1449
|
### Implementing HttpApiMiddleware
|
|
@@ -754,16 +1482,30 @@ import {
|
|
|
754
1482
|
HttpApiEndpoint,
|
|
755
1483
|
HttpApiGroup,
|
|
756
1484
|
HttpApiMiddleware,
|
|
1485
|
+
HttpApiSchema,
|
|
757
1486
|
HttpServerRequest
|
|
758
1487
|
} from "@effect/platform"
|
|
759
1488
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
760
1489
|
|
|
761
|
-
|
|
1490
|
+
// Define a schema for errors returned by the logger middleware
|
|
1491
|
+
class LoggerError extends Schema.TaggedError<LoggerError>()(
|
|
1492
|
+
"LoggerError",
|
|
1493
|
+
{}
|
|
1494
|
+
) {}
|
|
1495
|
+
|
|
1496
|
+
// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag
|
|
1497
|
+
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
|
|
1498
|
+
// Optionally define the error schema for the middleware
|
|
1499
|
+
failure: LoggerError
|
|
1500
|
+
}) {}
|
|
762
1501
|
|
|
763
1502
|
const LoggerLive = Layer.effect(
|
|
764
1503
|
Logger,
|
|
765
1504
|
Effect.gen(function* () {
|
|
766
1505
|
yield* Effect.log("creating Logger middleware")
|
|
1506
|
+
|
|
1507
|
+
// Middleware implementation as an Effect
|
|
1508
|
+
// that can access the `HttpServerRequest` context.
|
|
767
1509
|
return Effect.gen(function* () {
|
|
768
1510
|
const request = yield* HttpServerRequest.HttpServerRequest
|
|
769
1511
|
yield* Effect.log(`Request: ${request.method} ${request.url}`)
|
|
@@ -771,29 +1513,33 @@ const LoggerLive = Layer.effect(
|
|
|
771
1513
|
})
|
|
772
1514
|
)
|
|
773
1515
|
|
|
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")({
|
|
1516
|
+
const User = Schema.Struct({
|
|
783
1517
|
id: Schema.Number,
|
|
784
1518
|
name: Schema.String,
|
|
785
1519
|
createdAt: Schema.DateTimeUtc
|
|
786
|
-
})
|
|
1520
|
+
})
|
|
787
1521
|
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1522
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1523
|
+
|
|
1524
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
1525
|
+
.add(
|
|
1526
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
1527
|
+
.addSuccess(User)
|
|
1528
|
+
// Apply the middleware to a single endpoint
|
|
1529
|
+
.middleware(Logger)
|
|
1530
|
+
)
|
|
1531
|
+
// Or apply the middleware to the entire group
|
|
1532
|
+
.middleware(Logger)
|
|
1533
|
+
|
|
1534
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
1535
|
+
|
|
1536
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1537
|
+
handlers.handle("getUser", (req) =>
|
|
1538
|
+
Effect.succeed({
|
|
1539
|
+
id: req.path.id,
|
|
1540
|
+
name: "John Doe",
|
|
1541
|
+
createdAt: DateTime.unsafeNow()
|
|
1542
|
+
})
|
|
797
1543
|
)
|
|
798
1544
|
).pipe(
|
|
799
1545
|
// Provide the Logger middleware to the group
|
|
@@ -819,8 +1565,9 @@ These security annotations can be used alongside `HttpApiMiddleware` to create m
|
|
|
819
1565
|
|
|
820
1566
|
```ts
|
|
821
1567
|
import {
|
|
822
|
-
|
|
1568
|
+
HttpApi,
|
|
823
1569
|
HttpApiEndpoint,
|
|
1570
|
+
HttpApiGroup,
|
|
824
1571
|
HttpApiMiddleware,
|
|
825
1572
|
HttpApiSchema,
|
|
826
1573
|
HttpApiSecurity
|
|
@@ -860,14 +1607,20 @@ class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
|
860
1607
|
}
|
|
861
1608
|
) {}
|
|
862
1609
|
|
|
863
|
-
|
|
1610
|
+
const api = HttpApi.make("api")
|
|
864
1611
|
.add(
|
|
865
|
-
|
|
866
|
-
|
|
1612
|
+
HttpApiGroup.make("group")
|
|
1613
|
+
.add(
|
|
1614
|
+
HttpApiEndpoint.get("get", "/")
|
|
1615
|
+
.addSuccess(Schema.String)
|
|
1616
|
+
// Apply the middleware to a single endpoint
|
|
1617
|
+
.middleware(Authorization)
|
|
1618
|
+
)
|
|
1619
|
+
// Or apply the middleware to the entire group
|
|
867
1620
|
.middleware(Authorization)
|
|
868
1621
|
)
|
|
869
|
-
// Or apply the middleware to the entire
|
|
870
|
-
.middleware(Authorization)
|
|
1622
|
+
// Or apply the middleware to the entire API
|
|
1623
|
+
.middleware(Authorization)
|
|
871
1624
|
```
|
|
872
1625
|
|
|
873
1626
|
### Implementing HttpApiSecurity middleware
|
|
@@ -899,7 +1652,9 @@ class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
|
899
1652
|
{
|
|
900
1653
|
failure: Unauthorized,
|
|
901
1654
|
provides: CurrentUser,
|
|
902
|
-
security: {
|
|
1655
|
+
security: {
|
|
1656
|
+
myBearer: HttpApiSecurity.bearer
|
|
1657
|
+
}
|
|
903
1658
|
}
|
|
904
1659
|
) {}
|
|
905
1660
|
|
|
@@ -926,6 +1681,46 @@ const AuthorizationLive = Layer.effect(
|
|
|
926
1681
|
)
|
|
927
1682
|
```
|
|
928
1683
|
|
|
1684
|
+
### Adding Descriptions to Security Definitions
|
|
1685
|
+
|
|
1686
|
+
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.
|
|
1687
|
+
|
|
1688
|
+
**Example** (Adding a Description to a Bearer Token Security Definition)
|
|
1689
|
+
|
|
1690
|
+
```ts
|
|
1691
|
+
import {
|
|
1692
|
+
HttpApiMiddleware,
|
|
1693
|
+
HttpApiSchema,
|
|
1694
|
+
HttpApiSecurity,
|
|
1695
|
+
OpenApi
|
|
1696
|
+
} from "@effect/platform"
|
|
1697
|
+
import { Context, Schema } from "effect"
|
|
1698
|
+
|
|
1699
|
+
class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}
|
|
1700
|
+
|
|
1701
|
+
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
1702
|
+
"Unauthorized",
|
|
1703
|
+
{},
|
|
1704
|
+
HttpApiSchema.annotations({ status: 401 })
|
|
1705
|
+
) {}
|
|
1706
|
+
|
|
1707
|
+
class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
|
|
1708
|
+
|
|
1709
|
+
class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
1710
|
+
"Authorization",
|
|
1711
|
+
{
|
|
1712
|
+
failure: Unauthorized,
|
|
1713
|
+
provides: CurrentUser,
|
|
1714
|
+
security: {
|
|
1715
|
+
myBearer: HttpApiSecurity.bearer.pipe(
|
|
1716
|
+
// Add a description to the security definition
|
|
1717
|
+
HttpApiSecurity.annotate(OpenApi.Description, "my description")
|
|
1718
|
+
)
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
) {}
|
|
1722
|
+
```
|
|
1723
|
+
|
|
929
1724
|
### Setting HttpApiSecurity cookies
|
|
930
1725
|
|
|
931
1726
|
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 +1765,31 @@ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
|
970
1765
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
971
1766
|
import { createServer } from "node:http"
|
|
972
1767
|
|
|
973
|
-
|
|
1768
|
+
const User = Schema.Struct({
|
|
974
1769
|
id: Schema.Number,
|
|
975
1770
|
name: Schema.String,
|
|
976
1771
|
createdAt: Schema.DateTimeUtc
|
|
977
|
-
})
|
|
1772
|
+
})
|
|
978
1773
|
|
|
979
|
-
const
|
|
1774
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
980
1775
|
|
|
981
|
-
|
|
982
|
-
HttpApiEndpoint.get("
|
|
983
|
-
)
|
|
1776
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1777
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1778
|
+
)
|
|
984
1779
|
|
|
985
|
-
|
|
1780
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
986
1781
|
|
|
987
|
-
const
|
|
988
|
-
handlers.handle("
|
|
989
|
-
Effect.succeed(
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
})
|
|
995
|
-
)
|
|
1782
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1783
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1784
|
+
Effect.succeed({
|
|
1785
|
+
id,
|
|
1786
|
+
name: "John Doe",
|
|
1787
|
+
createdAt: DateTime.unsafeNow()
|
|
1788
|
+
})
|
|
996
1789
|
)
|
|
997
1790
|
)
|
|
998
1791
|
|
|
999
|
-
const MyApiLive = HttpApiBuilder.api(
|
|
1792
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
1000
1793
|
|
|
1001
1794
|
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1002
1795
|
// Add the Swagger documentation layer
|
|
@@ -1020,42 +1813,427 @@ Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
|
1020
1813
|
|
|
1021
1814
|
### Adding OpenAPI Annotations
|
|
1022
1815
|
|
|
1023
|
-
You can
|
|
1816
|
+
You can add OpenAPI annotations to your API to include metadata such as titles, descriptions, and more. These annotations help generate richer API documentation.
|
|
1817
|
+
|
|
1818
|
+
#### HttpApi
|
|
1819
|
+
|
|
1820
|
+
Below is a list of available annotations for a top-level `HttpApi`. They can be added using the `.annotate` method:
|
|
1821
|
+
|
|
1822
|
+
| Annotation | Description |
|
|
1823
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
1824
|
+
| `HttpApi.AdditionalSchemas` | Adds custom schemas to the final OpenAPI specification. Only schemas with an `identifier` annotation are included. |
|
|
1825
|
+
| `OpenApi.Description` | Sets a general description for the API. |
|
|
1826
|
+
| `OpenApi.License` | Defines the license used by the API. |
|
|
1827
|
+
| `OpenApi.Summary` | Provides a brief summary of the API. |
|
|
1828
|
+
| `OpenApi.Servers` | Lists server URLs and optional metadata such as variables. |
|
|
1829
|
+
| `OpenApi.Override` | Merges the supplied fields into the resulting specification. |
|
|
1830
|
+
| `OpenApi.Transform` | Allows you to modify the final specification with a custom function. |
|
|
1831
|
+
|
|
1832
|
+
**Example** (Annotating the Top-Level API)
|
|
1833
|
+
|
|
1834
|
+
```ts
|
|
1835
|
+
import { HttpApi, OpenApi } from "@effect/platform"
|
|
1836
|
+
import { Schema } from "effect"
|
|
1837
|
+
|
|
1838
|
+
const api = HttpApi.make("api")
|
|
1839
|
+
// Provide additional schemas
|
|
1840
|
+
.annotate(HttpApi.AdditionalSchemas, [
|
|
1841
|
+
Schema.String.annotations({ identifier: "MyString" })
|
|
1842
|
+
])
|
|
1843
|
+
// Add a description
|
|
1844
|
+
.annotate(OpenApi.Description, "my description")
|
|
1845
|
+
// Set license information
|
|
1846
|
+
.annotate(OpenApi.License, { name: "MIT", url: "http://example.com" })
|
|
1847
|
+
// Provide a summary
|
|
1848
|
+
.annotate(OpenApi.Summary, "my summary")
|
|
1849
|
+
// Define servers
|
|
1850
|
+
.annotate(OpenApi.Servers, [
|
|
1851
|
+
{
|
|
1852
|
+
url: "http://example.com",
|
|
1853
|
+
description: "example",
|
|
1854
|
+
variables: { a: { default: "b", enum: ["c"], description: "d" } }
|
|
1855
|
+
}
|
|
1856
|
+
])
|
|
1857
|
+
// Override parts of the generated specification
|
|
1858
|
+
.annotate(OpenApi.Override, {
|
|
1859
|
+
tags: [{ name: "a", description: "a-description" }]
|
|
1860
|
+
})
|
|
1861
|
+
// Apply a transform function to the final specification
|
|
1862
|
+
.annotate(OpenApi.Transform, (spec) => ({
|
|
1863
|
+
...spec,
|
|
1864
|
+
tags: [...spec.tags, { name: "b", description: "b-description" }]
|
|
1865
|
+
}))
|
|
1866
|
+
|
|
1867
|
+
// Generate the OpenAPI specification from the annotated API
|
|
1868
|
+
const spec = OpenApi.fromApi(api)
|
|
1869
|
+
|
|
1870
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
1871
|
+
/*
|
|
1872
|
+
Output:
|
|
1873
|
+
{
|
|
1874
|
+
"openapi": "3.1.0",
|
|
1875
|
+
"info": {
|
|
1876
|
+
"title": "Api",
|
|
1877
|
+
"version": "0.0.1",
|
|
1878
|
+
"description": "my description",
|
|
1879
|
+
"license": {
|
|
1880
|
+
"name": "MIT",
|
|
1881
|
+
"url": "http://example.com"
|
|
1882
|
+
},
|
|
1883
|
+
"summary": "my summary"
|
|
1884
|
+
},
|
|
1885
|
+
"paths": {},
|
|
1886
|
+
"tags": [
|
|
1887
|
+
{ "name": "a", "description": "a-description" },
|
|
1888
|
+
{ "name": "b", "description": "b-description" }
|
|
1889
|
+
],
|
|
1890
|
+
"components": {
|
|
1891
|
+
"schemas": {
|
|
1892
|
+
"MyString": {
|
|
1893
|
+
"type": "string"
|
|
1894
|
+
}
|
|
1895
|
+
},
|
|
1896
|
+
"securitySchemes": {}
|
|
1897
|
+
},
|
|
1898
|
+
"security": [],
|
|
1899
|
+
"servers": [
|
|
1900
|
+
{
|
|
1901
|
+
"url": "http://example.com",
|
|
1902
|
+
"description": "example",
|
|
1903
|
+
"variables": {
|
|
1904
|
+
"a": {
|
|
1905
|
+
"default": "b",
|
|
1906
|
+
"enum": [
|
|
1907
|
+
"c"
|
|
1908
|
+
],
|
|
1909
|
+
"description": "d"
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
]
|
|
1914
|
+
}
|
|
1915
|
+
*/
|
|
1916
|
+
```
|
|
1917
|
+
|
|
1918
|
+
#### HttpApiGroup
|
|
1024
1919
|
|
|
1025
|
-
|
|
1920
|
+
The following annotations can be added to an `HttpApiGroup`:
|
|
1026
1921
|
|
|
1027
|
-
|
|
1922
|
+
| Annotation | Description |
|
|
1923
|
+
| ---------------------- | --------------------------------------------------------------------- |
|
|
1924
|
+
| `OpenApi.Description` | Sets a description for this group. |
|
|
1925
|
+
| `OpenApi.ExternalDocs` | Provides external documentation links for the group. |
|
|
1926
|
+
| `OpenApi.Override` | Merges specified fields into the resulting specification. |
|
|
1927
|
+
| `OpenApi.Transform` | Lets you modify the final group specification with a custom function. |
|
|
1928
|
+
| `OpenApi.Exclude` | Excludes the group from the final OpenAPI specification. |
|
|
1028
1929
|
|
|
1029
|
-
|
|
1030
|
-
- These annotations will appear in the generated OpenAPI documentation.
|
|
1930
|
+
**Example** (Annotating a Group)
|
|
1031
1931
|
|
|
1032
1932
|
```ts
|
|
1033
|
-
import { OpenApi } from "@effect/platform"
|
|
1933
|
+
import { HttpApi, HttpApiGroup, OpenApi } from "@effect/platform"
|
|
1034
1934
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
.
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
description: "API for managing users"
|
|
1935
|
+
const api = HttpApi.make("api")
|
|
1936
|
+
.add(
|
|
1937
|
+
HttpApiGroup.make("group")
|
|
1938
|
+
// Add a description for the group
|
|
1939
|
+
.annotate(OpenApi.Description, "my description")
|
|
1940
|
+
// Provide external documentation links
|
|
1941
|
+
.annotate(OpenApi.ExternalDocs, {
|
|
1942
|
+
url: "http://example.com",
|
|
1943
|
+
description: "example"
|
|
1045
1944
|
})
|
|
1945
|
+
// Override parts of the final output
|
|
1946
|
+
.annotate(OpenApi.Override, { name: "my name" })
|
|
1947
|
+
// Transform the final specification for this group
|
|
1948
|
+
.annotate(OpenApi.Transform, (spec) => ({
|
|
1949
|
+
...spec,
|
|
1950
|
+
name: spec.name + "-transformed"
|
|
1951
|
+
}))
|
|
1952
|
+
)
|
|
1953
|
+
.add(
|
|
1954
|
+
HttpApiGroup.make("excluded")
|
|
1955
|
+
// Exclude the group from the final specification
|
|
1956
|
+
.annotate(OpenApi.Exclude, true)
|
|
1957
|
+
)
|
|
1958
|
+
|
|
1959
|
+
// Generate the OpenAPI spec
|
|
1960
|
+
const spec = OpenApi.fromApi(api)
|
|
1961
|
+
|
|
1962
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
1963
|
+
/*
|
|
1964
|
+
Output:
|
|
1965
|
+
{
|
|
1966
|
+
"openapi": "3.1.0",
|
|
1967
|
+
"info": {
|
|
1968
|
+
"title": "Api",
|
|
1969
|
+
"version": "0.0.1"
|
|
1970
|
+
},
|
|
1971
|
+
"paths": {},
|
|
1972
|
+
"tags": [
|
|
1973
|
+
{
|
|
1974
|
+
"name": "my name-transformed",
|
|
1975
|
+
"description": "my description",
|
|
1976
|
+
"externalDocs": {
|
|
1977
|
+
"url": "http://example.com",
|
|
1978
|
+
"description": "example"
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
],
|
|
1982
|
+
"components": {
|
|
1983
|
+
"schemas": {},
|
|
1984
|
+
"securitySchemes": {}
|
|
1985
|
+
},
|
|
1986
|
+
"security": []
|
|
1987
|
+
}
|
|
1988
|
+
*/
|
|
1989
|
+
```
|
|
1990
|
+
|
|
1991
|
+
#### HttpApiEndpoint
|
|
1992
|
+
|
|
1993
|
+
For an `HttpApiEndpoint`, you can use the following annotations:
|
|
1994
|
+
|
|
1995
|
+
| Annotation | Description |
|
|
1996
|
+
| ---------------------- | --------------------------------------------------------------------------- |
|
|
1997
|
+
| `OpenApi.Description` | Adds a description for this endpoint. |
|
|
1998
|
+
| `OpenApi.Summary` | Provides a short summary of the endpoint's purpose. |
|
|
1999
|
+
| `OpenApi.Deprecated` | Marks the endpoint as deprecated. |
|
|
2000
|
+
| `OpenApi.ExternalDocs` | Supplies external documentation links for the endpoint. |
|
|
2001
|
+
| `OpenApi.Override` | Merges specified fields into the resulting specification for this endpoint. |
|
|
2002
|
+
| `OpenApi.Transform` | Lets you modify the final endpoint specification with a custom function. |
|
|
2003
|
+
| `OpenApi.Exclude` | Excludes the endpoint from the final OpenAPI specification. |
|
|
2004
|
+
|
|
2005
|
+
**Example** (Annotating an Endpoint)
|
|
2006
|
+
|
|
2007
|
+
```ts
|
|
2008
|
+
import {
|
|
2009
|
+
HttpApi,
|
|
2010
|
+
HttpApiEndpoint,
|
|
2011
|
+
HttpApiGroup,
|
|
2012
|
+
OpenApi
|
|
2013
|
+
} from "@effect/platform"
|
|
2014
|
+
import { Schema } from "effect"
|
|
2015
|
+
|
|
2016
|
+
const api = HttpApi.make("api").add(
|
|
2017
|
+
HttpApiGroup.make("group")
|
|
2018
|
+
.add(
|
|
2019
|
+
HttpApiEndpoint.get("get", "/")
|
|
2020
|
+
.addSuccess(Schema.String)
|
|
2021
|
+
// Add a description
|
|
2022
|
+
.annotate(OpenApi.Description, "my description")
|
|
2023
|
+
// Provide a summary
|
|
2024
|
+
.annotate(OpenApi.Summary, "my summary")
|
|
2025
|
+
// Mark the endpoint as deprecated
|
|
2026
|
+
.annotate(OpenApi.Deprecated, true)
|
|
2027
|
+
// Provide external documentation
|
|
2028
|
+
.annotate(OpenApi.ExternalDocs, {
|
|
2029
|
+
url: "http://example.com",
|
|
2030
|
+
description: "example"
|
|
2031
|
+
})
|
|
1046
2032
|
)
|
|
1047
|
-
|
|
2033
|
+
.add(
|
|
2034
|
+
HttpApiEndpoint.get("excluded", "/excluded")
|
|
2035
|
+
.addSuccess(Schema.String)
|
|
2036
|
+
// Exclude this endpoint from the final specification
|
|
2037
|
+
.annotate(OpenApi.Exclude, true)
|
|
2038
|
+
)
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
// Generate the OpenAPI spec
|
|
2042
|
+
const spec = OpenApi.fromApi(api)
|
|
2043
|
+
|
|
2044
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
2045
|
+
/*
|
|
2046
|
+
Output:
|
|
2047
|
+
{
|
|
2048
|
+
"openapi": "3.1.0",
|
|
2049
|
+
"info": {
|
|
2050
|
+
"title": "Api",
|
|
2051
|
+
"version": "0.0.1"
|
|
2052
|
+
},
|
|
2053
|
+
"paths": {
|
|
2054
|
+
"/": {
|
|
2055
|
+
"get": {
|
|
2056
|
+
"tags": [
|
|
2057
|
+
"group"
|
|
2058
|
+
],
|
|
2059
|
+
"operationId": "my operationId-transformed",
|
|
2060
|
+
"parameters": [],
|
|
2061
|
+
"security": [],
|
|
2062
|
+
"responses": {
|
|
2063
|
+
"200": {
|
|
2064
|
+
"description": "a string",
|
|
2065
|
+
"content": {
|
|
2066
|
+
"application/json": {
|
|
2067
|
+
"schema": {
|
|
2068
|
+
"type": "string"
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
},
|
|
2073
|
+
"400": {
|
|
2074
|
+
"description": "The request did not match the expected schema",
|
|
2075
|
+
"content": {
|
|
2076
|
+
"application/json": {
|
|
2077
|
+
"schema": {
|
|
2078
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
"description": "my description",
|
|
2085
|
+
"summary": "my summary",
|
|
2086
|
+
"deprecated": true,
|
|
2087
|
+
"externalDocs": {
|
|
2088
|
+
"url": "http://example.com",
|
|
2089
|
+
"description": "example"
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
},
|
|
2094
|
+
...
|
|
2095
|
+
}
|
|
2096
|
+
*/
|
|
2097
|
+
```
|
|
2098
|
+
|
|
2099
|
+
The default response description is "Success". You can override this by annotating the schema.
|
|
2100
|
+
|
|
2101
|
+
**Example** (Defining a custom response description)
|
|
2102
|
+
|
|
2103
|
+
```ts
|
|
2104
|
+
import {
|
|
2105
|
+
HttpApi,
|
|
2106
|
+
HttpApiEndpoint,
|
|
2107
|
+
HttpApiGroup,
|
|
2108
|
+
OpenApi
|
|
2109
|
+
} from "@effect/platform"
|
|
2110
|
+
import { Schema } from "effect"
|
|
2111
|
+
|
|
2112
|
+
const User = Schema.Struct({
|
|
2113
|
+
id: Schema.Number,
|
|
2114
|
+
name: Schema.String,
|
|
2115
|
+
createdAt: Schema.DateTimeUtc
|
|
2116
|
+
}).annotations({ identifier: "User" })
|
|
2117
|
+
|
|
2118
|
+
const api = HttpApi.make("api").add(
|
|
2119
|
+
HttpApiGroup.make("group").add(
|
|
2120
|
+
HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
2121
|
+
Schema.Array(User).annotations({
|
|
2122
|
+
description: "Returns an array of users"
|
|
2123
|
+
})
|
|
2124
|
+
)
|
|
2125
|
+
)
|
|
2126
|
+
)
|
|
2127
|
+
|
|
2128
|
+
const spec = OpenApi.fromApi(api)
|
|
2129
|
+
|
|
2130
|
+
console.log(JSON.stringify(spec.paths, null, 2))
|
|
2131
|
+
/*
|
|
2132
|
+
Output:
|
|
2133
|
+
{
|
|
2134
|
+
"/users": {
|
|
2135
|
+
"get": {
|
|
2136
|
+
"tags": [
|
|
2137
|
+
"group"
|
|
2138
|
+
],
|
|
2139
|
+
"operationId": "group.getUsers",
|
|
2140
|
+
"parameters": [],
|
|
2141
|
+
"security": [],
|
|
2142
|
+
"responses": {
|
|
2143
|
+
"200": {
|
|
2144
|
+
"description": "Returns an array of users",
|
|
2145
|
+
"content": {
|
|
2146
|
+
"application/json": {
|
|
2147
|
+
"schema": {
|
|
2148
|
+
"type": "array",
|
|
2149
|
+
"items": {
|
|
2150
|
+
"$ref": "#/components/schemas/User"
|
|
2151
|
+
},
|
|
2152
|
+
"description": "Returns an array of users"
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
},
|
|
2157
|
+
"400": {
|
|
2158
|
+
"description": "The request did not match the expected schema",
|
|
2159
|
+
"content": {
|
|
2160
|
+
"application/json": {
|
|
2161
|
+
"schema": {
|
|
2162
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
*/
|
|
1048
2172
|
```
|
|
1049
2173
|
|
|
1050
|
-
|
|
2174
|
+
### Top Level Groups
|
|
2175
|
+
|
|
2176
|
+
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.
|
|
1051
2177
|
|
|
1052
|
-
**Example** (
|
|
2178
|
+
**Example** (Using a Top-Level Group)
|
|
1053
2179
|
|
|
1054
2180
|
```ts
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
2181
|
+
import {
|
|
2182
|
+
HttpApi,
|
|
2183
|
+
HttpApiEndpoint,
|
|
2184
|
+
HttpApiGroup,
|
|
2185
|
+
OpenApi
|
|
2186
|
+
} from "@effect/platform"
|
|
2187
|
+
import { Schema } from "effect"
|
|
2188
|
+
|
|
2189
|
+
const api = HttpApi.make("api").add(
|
|
2190
|
+
// Mark the group as top-level
|
|
2191
|
+
HttpApiGroup.make("group", { topLevel: true }).add(
|
|
2192
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
2193
|
+
)
|
|
2194
|
+
)
|
|
2195
|
+
|
|
2196
|
+
// Generate the OpenAPI spec
|
|
2197
|
+
const spec = OpenApi.fromApi(api)
|
|
2198
|
+
|
|
2199
|
+
console.log(JSON.stringify(spec.paths, null, 2))
|
|
2200
|
+
/*
|
|
2201
|
+
Output:
|
|
2202
|
+
{
|
|
2203
|
+
"/": {
|
|
2204
|
+
"get": {
|
|
2205
|
+
"tags": [
|
|
2206
|
+
"group"
|
|
2207
|
+
],
|
|
2208
|
+
"operationId": "get", // The operation ID is not prefixed with "group"
|
|
2209
|
+
"parameters": [],
|
|
2210
|
+
"security": [],
|
|
2211
|
+
"responses": {
|
|
2212
|
+
"200": {
|
|
2213
|
+
"description": "a string",
|
|
2214
|
+
"content": {
|
|
2215
|
+
"application/json": {
|
|
2216
|
+
"schema": {
|
|
2217
|
+
"type": "string"
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
},
|
|
2222
|
+
"400": {
|
|
2223
|
+
"description": "The request did not match the expected schema",
|
|
2224
|
+
"content": {
|
|
2225
|
+
"application/json": {
|
|
2226
|
+
"schema": {
|
|
2227
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
*/
|
|
1059
2237
|
```
|
|
1060
2238
|
|
|
1061
2239
|
## Deriving a Client
|
|
@@ -1083,65 +2261,158 @@ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
|
1083
2261
|
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
1084
2262
|
import { createServer } from "node:http"
|
|
1085
2263
|
|
|
1086
|
-
|
|
2264
|
+
const User = Schema.Struct({
|
|
1087
2265
|
id: Schema.Number,
|
|
1088
2266
|
name: Schema.String,
|
|
1089
2267
|
createdAt: Schema.DateTimeUtc
|
|
1090
|
-
})
|
|
2268
|
+
})
|
|
1091
2269
|
|
|
1092
|
-
const
|
|
2270
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1093
2271
|
|
|
1094
|
-
|
|
1095
|
-
HttpApiEndpoint.get("
|
|
1096
|
-
)
|
|
2272
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
2273
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
2274
|
+
)
|
|
2275
|
+
|
|
2276
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
2277
|
+
|
|
2278
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
2279
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
2280
|
+
Effect.succeed({
|
|
2281
|
+
id,
|
|
2282
|
+
name: "John Doe",
|
|
2283
|
+
createdAt: DateTime.unsafeNow()
|
|
2284
|
+
})
|
|
2285
|
+
)
|
|
2286
|
+
)
|
|
2287
|
+
|
|
2288
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
2289
|
+
|
|
2290
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
2291
|
+
Layer.provide(HttpApiSwagger.layer()),
|
|
2292
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
2293
|
+
Layer.provide(MyApiLive),
|
|
2294
|
+
HttpServer.withLogAddress,
|
|
2295
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
2296
|
+
)
|
|
2297
|
+
|
|
2298
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
2299
|
+
|
|
2300
|
+
// Create a program that derives and uses the client
|
|
2301
|
+
const program = Effect.gen(function* () {
|
|
2302
|
+
// Derive the client
|
|
2303
|
+
const client = yield* HttpApiClient.make(api, {
|
|
2304
|
+
baseUrl: "http://localhost:3000"
|
|
2305
|
+
})
|
|
2306
|
+
// Call the `getUser` endpoint
|
|
2307
|
+
const user = yield* client.users.getUser({ path: { id: 1 } })
|
|
2308
|
+
console.log(user)
|
|
2309
|
+
})
|
|
2310
|
+
|
|
2311
|
+
// Provide a Fetch-based HTTP client and run the program
|
|
2312
|
+
Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
|
|
2313
|
+
/*
|
|
2314
|
+
Example Output:
|
|
2315
|
+
User {
|
|
2316
|
+
id: 1,
|
|
2317
|
+
name: 'John Doe',
|
|
2318
|
+
createdAt: DateTime.Utc(2025-01-04T15:14:49.562Z)
|
|
2319
|
+
}
|
|
2320
|
+
*/
|
|
2321
|
+
```
|
|
2322
|
+
|
|
2323
|
+
### Top Level Groups
|
|
2324
|
+
|
|
2325
|
+
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.
|
|
2326
|
+
|
|
2327
|
+
**Example** (Using a Top-Level Group in the Client)
|
|
2328
|
+
|
|
2329
|
+
```ts
|
|
2330
|
+
import {
|
|
2331
|
+
HttpApi,
|
|
2332
|
+
HttpApiClient,
|
|
2333
|
+
HttpApiEndpoint,
|
|
2334
|
+
HttpApiGroup
|
|
2335
|
+
} from "@effect/platform"
|
|
2336
|
+
import { Effect, Schema } from "effect"
|
|
2337
|
+
|
|
2338
|
+
const api = HttpApi.make("api").add(
|
|
2339
|
+
// Mark the group as top-level
|
|
2340
|
+
HttpApiGroup.make("group", { topLevel: true }).add(
|
|
2341
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
2342
|
+
)
|
|
2343
|
+
)
|
|
2344
|
+
|
|
2345
|
+
const program = Effect.gen(function* () {
|
|
2346
|
+
const client = yield* HttpApiClient.make(api, {
|
|
2347
|
+
baseUrl: "http://localhost:3000"
|
|
2348
|
+
})
|
|
2349
|
+
// The `get` method is not nested under the "group" name
|
|
2350
|
+
const user = yield* client.get()
|
|
2351
|
+
console.log(user)
|
|
2352
|
+
})
|
|
2353
|
+
```
|
|
1097
2354
|
|
|
1098
|
-
|
|
2355
|
+
## Converting to a Web Handler
|
|
2356
|
+
|
|
2357
|
+
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.
|
|
2358
|
+
|
|
2359
|
+
**Example** (Creating and Serving a Web Handler)
|
|
2360
|
+
|
|
2361
|
+
```ts
|
|
2362
|
+
import {
|
|
2363
|
+
HttpApi,
|
|
2364
|
+
HttpApiBuilder,
|
|
2365
|
+
HttpApiEndpoint,
|
|
2366
|
+
HttpApiGroup,
|
|
2367
|
+
HttpApiSwagger,
|
|
2368
|
+
HttpServer
|
|
2369
|
+
} from "@effect/platform"
|
|
2370
|
+
import { Effect, Layer, Schema } from "effect"
|
|
2371
|
+
import * as http from "node:http"
|
|
1099
2372
|
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
new User({
|
|
1104
|
-
id: userId,
|
|
1105
|
-
name: "John Doe",
|
|
1106
|
-
createdAt: DateTime.unsafeNow()
|
|
1107
|
-
})
|
|
1108
|
-
)
|
|
2373
|
+
const api = HttpApi.make("myApi").add(
|
|
2374
|
+
HttpApiGroup.make("group").add(
|
|
2375
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
1109
2376
|
)
|
|
1110
2377
|
)
|
|
1111
2378
|
|
|
1112
|
-
const
|
|
2379
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
2380
|
+
handlers.handle("get", () => Effect.succeed("Hello, world!"))
|
|
2381
|
+
)
|
|
1113
2382
|
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
2383
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
2384
|
+
|
|
2385
|
+
const SwaggerLayer = HttpApiSwagger.layer().pipe(Layer.provide(MyApiLive))
|
|
2386
|
+
|
|
2387
|
+
// Convert the API to a web handler
|
|
2388
|
+
const { dispose, handler } = HttpApiBuilder.toWebHandler(
|
|
2389
|
+
Layer.mergeAll(MyApiLive, SwaggerLayer, HttpServer.layerContext)
|
|
1120
2390
|
)
|
|
1121
2391
|
|
|
1122
|
-
|
|
2392
|
+
// Serving the handler using a custom HTTP server
|
|
2393
|
+
http
|
|
2394
|
+
.createServer(async (req, res) => {
|
|
2395
|
+
const url = `http://${req.headers.host}${req.url}`
|
|
2396
|
+
const init: RequestInit = {
|
|
2397
|
+
method: req.method!
|
|
2398
|
+
}
|
|
1123
2399
|
|
|
1124
|
-
|
|
1125
|
-
const program = Effect.gen(function* () {
|
|
1126
|
-
// Derive the client
|
|
1127
|
-
const client = yield* HttpApiClient.make(MyApi, {
|
|
1128
|
-
baseUrl: "http://localhost:3000"
|
|
1129
|
-
})
|
|
1130
|
-
// Call the `findById` endpoint
|
|
1131
|
-
const user = yield* client.users.findById({ path: { userId: 1 } })
|
|
1132
|
-
console.log(user)
|
|
1133
|
-
})
|
|
2400
|
+
const response = await handler(new Request(url, init))
|
|
1134
2401
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
2402
|
+
res.writeHead(
|
|
2403
|
+
response.status,
|
|
2404
|
+
response.statusText,
|
|
2405
|
+
Object.fromEntries(response.headers.entries())
|
|
2406
|
+
)
|
|
2407
|
+
const responseBody = await response.arrayBuffer()
|
|
2408
|
+
res.end(Buffer.from(responseBody))
|
|
2409
|
+
})
|
|
2410
|
+
.listen(3000, () => {
|
|
2411
|
+
console.log("Server running at http://localhost:3000/")
|
|
2412
|
+
})
|
|
2413
|
+
.on("close", () => {
|
|
2414
|
+
dispose()
|
|
2415
|
+
})
|
|
1145
2416
|
```
|
|
1146
2417
|
|
|
1147
2418
|
# HTTP Client
|
|
@@ -3144,3 +4415,535 @@ const handler = HttpApp.toWebHandler(router)
|
|
|
3144
4415
|
const response = await handler(new Request("http://localhost:3000/foo"))
|
|
3145
4416
|
console.log(await response.text()) // Output: content 2
|
|
3146
4417
|
```
|
|
4418
|
+
|
|
4419
|
+
# Url
|
|
4420
|
+
|
|
4421
|
+
The `Url` module provides utilities for constructing and working with `URL` objects in a functional style. It includes:
|
|
4422
|
+
|
|
4423
|
+
- A safe constructor for parsing URLs from strings.
|
|
4424
|
+
- Functions for immutably updating `URL` properties like `host`, `href`, and `search`.
|
|
4425
|
+
- Tools for reading and modifying URL parameters using the `UrlParams` module.
|
|
4426
|
+
- A focus on immutability, creating new `URL` instances for every change.
|
|
4427
|
+
|
|
4428
|
+
## Creating a URL
|
|
4429
|
+
|
|
4430
|
+
### fromString
|
|
4431
|
+
|
|
4432
|
+
This function takes a string and attempts to parse it into a `URL` object. If the string is invalid, it returns an `Either.Left` containing an `IllegalArgumentException` with the error details. Otherwise, it returns an `Either.Right` containing the parsed `URL`.
|
|
4433
|
+
|
|
4434
|
+
You can optionally provide a `base` parameter to resolve relative URLs. When supplied, the function treats the input `url` as relative to the `base`.
|
|
4435
|
+
|
|
4436
|
+
**Example** (Parsing a URL with Optional Base)
|
|
4437
|
+
|
|
4438
|
+
```ts
|
|
4439
|
+
import { Url } from "@effect/platform"
|
|
4440
|
+
import { Either } from "effect"
|
|
4441
|
+
|
|
4442
|
+
// Parse an absolute URL
|
|
4443
|
+
//
|
|
4444
|
+
// ┌─── Either<URL, IllegalArgumentException>
|
|
4445
|
+
// ▼
|
|
4446
|
+
const parsed = Url.fromString("https://example.com/path")
|
|
4447
|
+
|
|
4448
|
+
if (Either.isRight(parsed)) {
|
|
4449
|
+
console.log("Parsed URL:", parsed.right.toString())
|
|
4450
|
+
} else {
|
|
4451
|
+
console.log("Error:", parsed.left.message)
|
|
4452
|
+
}
|
|
4453
|
+
// Output: Parsed URL: https://example.com/path
|
|
4454
|
+
|
|
4455
|
+
// Parse a relative URL with a base
|
|
4456
|
+
const relativeParsed = Url.fromString("/relative-path", "https://example.com")
|
|
4457
|
+
|
|
4458
|
+
if (Either.isRight(relativeParsed)) {
|
|
4459
|
+
console.log("Parsed relative URL:", relativeParsed.right.toString())
|
|
4460
|
+
} else {
|
|
4461
|
+
console.log("Error:", relativeParsed.left.message)
|
|
4462
|
+
}
|
|
4463
|
+
// Output: Parsed relative URL: https://example.com/relative-path
|
|
4464
|
+
```
|
|
4465
|
+
|
|
4466
|
+
## Immutably Changing URL Properties
|
|
4467
|
+
|
|
4468
|
+
The `Url` module offers a set of functions for updating properties of a `URL` object without modifying the original instance. These functions create and return a new `URL` with the specified updates, preserving the immutability of the original.
|
|
4469
|
+
|
|
4470
|
+
### Available Setters
|
|
4471
|
+
|
|
4472
|
+
| Setter | Description |
|
|
4473
|
+
| ------------- | --------------------------------------------------------- |
|
|
4474
|
+
| `setHash` | Updates the hash fragment of the URL. |
|
|
4475
|
+
| `setHost` | Updates the host (domain and port) of the URL. |
|
|
4476
|
+
| `setHostname` | Updates the domain of the URL without modifying the port. |
|
|
4477
|
+
| `setHref` | Replaces the entire URL string. |
|
|
4478
|
+
| `setPassword` | Updates the password used for authentication. |
|
|
4479
|
+
| `setPathname` | Updates the path of the URL. |
|
|
4480
|
+
| `setPort` | Updates the port of the URL. |
|
|
4481
|
+
| `setProtocol` | Updates the protocol (e.g., `http`, `https`). |
|
|
4482
|
+
| `setSearch` | Updates the query string of the URL. |
|
|
4483
|
+
| `setUsername` | Updates the username used for authentication. |
|
|
4484
|
+
|
|
4485
|
+
**Example** (Using Setters to Modify URL Properties)
|
|
4486
|
+
|
|
4487
|
+
```ts
|
|
4488
|
+
import { Url } from "@effect/platform"
|
|
4489
|
+
import { pipe } from "effect"
|
|
4490
|
+
|
|
4491
|
+
const myUrl = new URL("https://example.com")
|
|
4492
|
+
|
|
4493
|
+
// Changing protocol, host, and port
|
|
4494
|
+
const newUrl = pipe(
|
|
4495
|
+
myUrl,
|
|
4496
|
+
Url.setProtocol("http:"),
|
|
4497
|
+
Url.setHost("google.com"),
|
|
4498
|
+
Url.setPort("8080")
|
|
4499
|
+
)
|
|
4500
|
+
|
|
4501
|
+
console.log("Original:", myUrl.toString())
|
|
4502
|
+
// Output: Original: https://example.com/
|
|
4503
|
+
|
|
4504
|
+
console.log("New:", newUrl.toString())
|
|
4505
|
+
// Output: New: http://google.com:8080/
|
|
4506
|
+
```
|
|
4507
|
+
|
|
4508
|
+
### mutate
|
|
4509
|
+
|
|
4510
|
+
For more advanced modifications, use the `mutate` function. It clones the original `URL` object and applies a callback to the clone, allowing multiple updates at once.
|
|
4511
|
+
|
|
4512
|
+
**Example** (Applying Multiple Changes with `mutate`)
|
|
4513
|
+
|
|
4514
|
+
```ts
|
|
4515
|
+
import { Url } from "@effect/platform"
|
|
4516
|
+
|
|
4517
|
+
const myUrl = new URL("https://example.com")
|
|
4518
|
+
|
|
4519
|
+
const mutatedUrl = Url.mutate(myUrl, (url) => {
|
|
4520
|
+
url.username = "user"
|
|
4521
|
+
url.password = "pass"
|
|
4522
|
+
})
|
|
4523
|
+
|
|
4524
|
+
console.log("Mutated:", mutatedUrl.toString())
|
|
4525
|
+
// Output: Mutated: https://user:pass@example.com/
|
|
4526
|
+
```
|
|
4527
|
+
|
|
4528
|
+
## Reading and Writing URL Parameters
|
|
4529
|
+
|
|
4530
|
+
The `Url` module provides utilities for working with URL query parameters. These utilities allow you to read existing parameters and write new ones, all while maintaining immutability. This functionality is supported by the `UrlParams` module.
|
|
4531
|
+
|
|
4532
|
+
You can extract the query parameters from a `URL` object using the `urlParams` function.
|
|
4533
|
+
|
|
4534
|
+
To modify or add query parameters, use the `setUrlParams` function. This function creates a new `URL` with the updated query string.
|
|
4535
|
+
|
|
4536
|
+
**Example** (Reading and Writing Parameters)
|
|
4537
|
+
|
|
4538
|
+
```ts
|
|
4539
|
+
import { Url, UrlParams } from "@effect/platform"
|
|
4540
|
+
|
|
4541
|
+
const myUrl = new URL("https://example.com?foo=bar")
|
|
4542
|
+
|
|
4543
|
+
// Read parameters
|
|
4544
|
+
const params = Url.urlParams(myUrl)
|
|
4545
|
+
|
|
4546
|
+
console.log(params)
|
|
4547
|
+
// Output: [ [ 'foo', 'bar' ] ]
|
|
4548
|
+
|
|
4549
|
+
// Write parameters
|
|
4550
|
+
const updatedUrl = Url.setUrlParams(
|
|
4551
|
+
myUrl,
|
|
4552
|
+
UrlParams.fromInput([["key", "value"]])
|
|
4553
|
+
)
|
|
4554
|
+
|
|
4555
|
+
console.log(updatedUrl.toString())
|
|
4556
|
+
// Output: https://example.com/?key=value
|
|
4557
|
+
```
|
|
4558
|
+
|
|
4559
|
+
### Modifying URL Parameters
|
|
4560
|
+
|
|
4561
|
+
The `modifyUrlParams` function allows you to read, modify, and overwrite URL parameters in a single operation.
|
|
4562
|
+
|
|
4563
|
+
**Example** (Appending a Parameter to a URL)
|
|
4564
|
+
|
|
4565
|
+
```ts
|
|
4566
|
+
import { Url, UrlParams } from "@effect/platform"
|
|
4567
|
+
|
|
4568
|
+
const myUrl = new URL("https://example.com?foo=bar")
|
|
4569
|
+
|
|
4570
|
+
const changedUrl = Url.modifyUrlParams(myUrl, UrlParams.append("key", "value"))
|
|
4571
|
+
|
|
4572
|
+
console.log(changedUrl.toString())
|
|
4573
|
+
// Output: https://example.com/?foo=bar&key=value
|
|
4574
|
+
```
|
|
4575
|
+
|
|
4576
|
+
# OpenApiJsonSchema
|
|
4577
|
+
|
|
4578
|
+
The `OpenApiJsonSchema` module provides utilities to transform `Schema` objects into JSON schemas that comply with the OpenAPI Specification. These utilities are especially helpful for generating OpenAPI documentation or working with tools that require OpenAPI-compliant schemas.
|
|
4579
|
+
|
|
4580
|
+
## Creating a JSON Schema from a Schema
|
|
4581
|
+
|
|
4582
|
+
This module enables you to convert `Schema` objects into OpenAPI-compatible JSON schemas, making it easy to integrate with tools like Swagger or other OpenAPI-based frameworks.
|
|
4583
|
+
|
|
4584
|
+
**Example** (Generating a JSON Schema from a String Schema)
|
|
4585
|
+
|
|
4586
|
+
```ts
|
|
4587
|
+
import { OpenApiJsonSchema } from "@effect/platform"
|
|
4588
|
+
import { Schema } from "effect"
|
|
4589
|
+
|
|
4590
|
+
const schema = Schema.String
|
|
4591
|
+
|
|
4592
|
+
// Convert the schema to OpenAPI JSON Schema
|
|
4593
|
+
const openApiSchema = OpenApiJsonSchema.make(schema)
|
|
4594
|
+
|
|
4595
|
+
console.log(JSON.stringify(openApiSchema, null, 2))
|
|
4596
|
+
/*
|
|
4597
|
+
Output:
|
|
4598
|
+
{
|
|
4599
|
+
"type": "string"
|
|
4600
|
+
}
|
|
4601
|
+
*/
|
|
4602
|
+
```
|
|
4603
|
+
|
|
4604
|
+
## Differences from JSONSchema
|
|
4605
|
+
|
|
4606
|
+
The `OpenApiJsonSchema` module differs from the `JSONSchema` module in several ways. These differences are tailored to align with the OpenAPI Specification.
|
|
4607
|
+
|
|
4608
|
+
### `$schema` Property Omission
|
|
4609
|
+
|
|
4610
|
+
OpenAPI schemas do not include the `$schema` property, while JSON schemas do.
|
|
4611
|
+
|
|
4612
|
+
**Example** (Comparison of `$schema` Property)
|
|
4613
|
+
|
|
4614
|
+
```ts
|
|
4615
|
+
import { OpenApiJsonSchema } from "@effect/platform"
|
|
4616
|
+
import { JSONSchema, Schema } from "effect"
|
|
4617
|
+
|
|
4618
|
+
const schema = Schema.String
|
|
4619
|
+
|
|
4620
|
+
const openApiSchema = OpenApiJsonSchema.make(schema)
|
|
4621
|
+
const jsonSchema = JSONSchema.make(schema)
|
|
4622
|
+
|
|
4623
|
+
console.log(JSON.stringify(openApiSchema, null, 2))
|
|
4624
|
+
/*
|
|
4625
|
+
Output:
|
|
4626
|
+
{
|
|
4627
|
+
"type": "string"
|
|
4628
|
+
}
|
|
4629
|
+
*/
|
|
4630
|
+
|
|
4631
|
+
console.log(JSON.stringify(jsonSchema, null, 2))
|
|
4632
|
+
/*
|
|
4633
|
+
Output:
|
|
4634
|
+
{
|
|
4635
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4636
|
+
"type": "string"
|
|
4637
|
+
}
|
|
4638
|
+
*/
|
|
4639
|
+
```
|
|
4640
|
+
|
|
4641
|
+
### Handling of `null` Values
|
|
4642
|
+
|
|
4643
|
+
OpenAPI does not support `{ "type": "null" }`. Instead, it uses an `enum` containing `null` to represent nullable values.
|
|
4644
|
+
|
|
4645
|
+
**Example** (Representation of `null` Values)
|
|
4646
|
+
|
|
4647
|
+
```ts
|
|
4648
|
+
import { OpenApiJsonSchema } from "@effect/platform"
|
|
4649
|
+
import { JSONSchema, Schema } from "effect"
|
|
4650
|
+
|
|
4651
|
+
const schema = Schema.Null
|
|
4652
|
+
|
|
4653
|
+
const openApiSchema = OpenApiJsonSchema.make(schema)
|
|
4654
|
+
const jsonSchema = JSONSchema.make(schema)
|
|
4655
|
+
|
|
4656
|
+
console.log(JSON.stringify(openApiSchema, null, 2))
|
|
4657
|
+
/*
|
|
4658
|
+
Output:
|
|
4659
|
+
{
|
|
4660
|
+
"enum": [
|
|
4661
|
+
null
|
|
4662
|
+
]
|
|
4663
|
+
}
|
|
4664
|
+
*/
|
|
4665
|
+
|
|
4666
|
+
console.log(JSON.stringify(jsonSchema, null, 2))
|
|
4667
|
+
/*
|
|
4668
|
+
Output:
|
|
4669
|
+
{
|
|
4670
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4671
|
+
"type": "null"
|
|
4672
|
+
}
|
|
4673
|
+
*/
|
|
4674
|
+
```
|
|
4675
|
+
|
|
4676
|
+
### Nullable Values
|
|
4677
|
+
|
|
4678
|
+
OpenAPI uses the `nullable` property to indicate that a value can be `null`, whereas JSON schemas use an `anyOf` structure.
|
|
4679
|
+
|
|
4680
|
+
**Example** (Nullable Property Representation)
|
|
4681
|
+
|
|
4682
|
+
```ts
|
|
4683
|
+
import { OpenApiJsonSchema } from "@effect/platform"
|
|
4684
|
+
import { JSONSchema, Schema } from "effect"
|
|
4685
|
+
|
|
4686
|
+
const schema = Schema.NullOr(Schema.String)
|
|
4687
|
+
|
|
4688
|
+
const openApiSchema = OpenApiJsonSchema.make(schema)
|
|
4689
|
+
const jsonSchema = JSONSchema.make(schema)
|
|
4690
|
+
|
|
4691
|
+
console.log(JSON.stringify(openApiSchema, null, 2))
|
|
4692
|
+
/*
|
|
4693
|
+
Output:
|
|
4694
|
+
{
|
|
4695
|
+
"type": "string",
|
|
4696
|
+
"nullable": true
|
|
4697
|
+
}
|
|
4698
|
+
*/
|
|
4699
|
+
|
|
4700
|
+
console.log(JSON.stringify(jsonSchema, null, 2))
|
|
4701
|
+
/*
|
|
4702
|
+
Output:
|
|
4703
|
+
{
|
|
4704
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4705
|
+
"anyOf": [
|
|
4706
|
+
{
|
|
4707
|
+
"type": "string"
|
|
4708
|
+
},
|
|
4709
|
+
{
|
|
4710
|
+
"type": "null"
|
|
4711
|
+
}
|
|
4712
|
+
]
|
|
4713
|
+
}
|
|
4714
|
+
*/
|
|
4715
|
+
```
|
|
4716
|
+
|
|
4717
|
+
### `contentSchema` Support
|
|
4718
|
+
|
|
4719
|
+
OpenAPI schemas include a `contentSchema` property, which allows you to describe the structure of the content for a media type (e.g., `application/json`). This feature is not available in JSON schemas (Draft 7), making `contentSchema` particularly useful for defining structured payloads in OpenAPI documentation.
|
|
4720
|
+
|
|
4721
|
+
**Note**: Use `contentSchema` to define the internal structure of media types like `application/json` in OpenAPI specifications. This property provides clarity and detail for tools and users interacting with the API, especially when handling structured payloads.
|
|
4722
|
+
|
|
4723
|
+
**Example** (Defining a Schema with `contentSchema` for JSON Content)
|
|
4724
|
+
|
|
4725
|
+
```ts
|
|
4726
|
+
import { OpenApiJsonSchema } from "@effect/platform"
|
|
4727
|
+
import { JSONSchema, Schema } from "effect"
|
|
4728
|
+
|
|
4729
|
+
// Define a schema for parsing JSON content
|
|
4730
|
+
const schema = Schema.parseJson(Schema.Struct({ a: Schema.String }))
|
|
4731
|
+
|
|
4732
|
+
const openApiSchema = OpenApiJsonSchema.make(schema)
|
|
4733
|
+
const jsonSchema = JSONSchema.make(schema)
|
|
4734
|
+
|
|
4735
|
+
console.log(JSON.stringify(openApiSchema, null, 2))
|
|
4736
|
+
/*
|
|
4737
|
+
Output:
|
|
4738
|
+
{
|
|
4739
|
+
"type": "string",
|
|
4740
|
+
"contentMediaType": "application/json",
|
|
4741
|
+
"contentSchema": {
|
|
4742
|
+
"type": "object",
|
|
4743
|
+
"required": [
|
|
4744
|
+
"a"
|
|
4745
|
+
],
|
|
4746
|
+
"properties": {
|
|
4747
|
+
"a": {
|
|
4748
|
+
"type": "string"
|
|
4749
|
+
}
|
|
4750
|
+
},
|
|
4751
|
+
"additionalProperties": false
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
*/
|
|
4755
|
+
|
|
4756
|
+
console.log(JSON.stringify(jsonSchema, null, 2))
|
|
4757
|
+
/*
|
|
4758
|
+
Output:
|
|
4759
|
+
{
|
|
4760
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4761
|
+
"type": "object",
|
|
4762
|
+
"required": [
|
|
4763
|
+
"a"
|
|
4764
|
+
],
|
|
4765
|
+
"properties": {
|
|
4766
|
+
"a": {
|
|
4767
|
+
"type": "string"
|
|
4768
|
+
}
|
|
4769
|
+
},
|
|
4770
|
+
"additionalProperties": false
|
|
4771
|
+
}
|
|
4772
|
+
*/
|
|
4773
|
+
```
|
|
4774
|
+
|
|
4775
|
+
### makeWithDefs
|
|
4776
|
+
|
|
4777
|
+
The `makeWithDefs` function generates OpenAPI-compatible JSON schemas and collects schema definitions in a shared object. This is especially useful for consolidating multiple schemas into a single OpenAPI specification, enabling schema reuse across your API.
|
|
4778
|
+
|
|
4779
|
+
**Example** (Generating OpenAPI Schema with Definitions)
|
|
4780
|
+
|
|
4781
|
+
```ts
|
|
4782
|
+
import { OpenApiJsonSchema } from "@effect/platform"
|
|
4783
|
+
import { Schema } from "effect"
|
|
4784
|
+
|
|
4785
|
+
// Define a schema with an identifier annotation
|
|
4786
|
+
const schema = Schema.Struct({ a: Schema.String }).annotations({
|
|
4787
|
+
identifier: "MyStruct"
|
|
4788
|
+
})
|
|
4789
|
+
|
|
4790
|
+
// Create a definitions object
|
|
4791
|
+
const defs = {}
|
|
4792
|
+
|
|
4793
|
+
// Generate the OpenAPI schema while collecting definitions
|
|
4794
|
+
const openApiSchema = OpenApiJsonSchema.makeWithDefs(schema, { defs })
|
|
4795
|
+
|
|
4796
|
+
console.log(JSON.stringify(openApiSchema, null, 2))
|
|
4797
|
+
/*
|
|
4798
|
+
Output:
|
|
4799
|
+
{
|
|
4800
|
+
"$ref": "#/components/schemas/MyStruct"
|
|
4801
|
+
}
|
|
4802
|
+
*/
|
|
4803
|
+
|
|
4804
|
+
console.log(JSON.stringify(defs, null, 2))
|
|
4805
|
+
/*
|
|
4806
|
+
Output:
|
|
4807
|
+
{
|
|
4808
|
+
"MyStruct": {
|
|
4809
|
+
"type": "object",
|
|
4810
|
+
"required": [
|
|
4811
|
+
"a"
|
|
4812
|
+
],
|
|
4813
|
+
"properties": {
|
|
4814
|
+
"a": {
|
|
4815
|
+
"type": "string"
|
|
4816
|
+
}
|
|
4817
|
+
},
|
|
4818
|
+
"additionalProperties": false
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
*/
|
|
4822
|
+
```
|
|
4823
|
+
|
|
4824
|
+
**Example** (Combining Multiple Schemas into One OpenAPI Specification)
|
|
4825
|
+
|
|
4826
|
+
```ts
|
|
4827
|
+
import { OpenApiJsonSchema } from "@effect/platform"
|
|
4828
|
+
import { Schema } from "effect"
|
|
4829
|
+
|
|
4830
|
+
// Define multiple schemas with unique identifiers
|
|
4831
|
+
const schema1 = Schema.Struct({ a: Schema.String }).annotations({
|
|
4832
|
+
identifier: "MyStruct1"
|
|
4833
|
+
})
|
|
4834
|
+
const schema2 = Schema.Struct({ b: Schema.Number }).annotations({
|
|
4835
|
+
identifier: "MyStruct2"
|
|
4836
|
+
})
|
|
4837
|
+
|
|
4838
|
+
// Create a shared definitions object
|
|
4839
|
+
const defs = {}
|
|
4840
|
+
|
|
4841
|
+
// Use `makeWithDefs` to generate schemas for API paths
|
|
4842
|
+
const paths = {
|
|
4843
|
+
paths: {
|
|
4844
|
+
"/path1": {
|
|
4845
|
+
get: {
|
|
4846
|
+
responses: {
|
|
4847
|
+
"200": {
|
|
4848
|
+
content: {
|
|
4849
|
+
"application/json": {
|
|
4850
|
+
schema: OpenApiJsonSchema.makeWithDefs(schema1, { defs })
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
},
|
|
4857
|
+
"/path2": {
|
|
4858
|
+
get: {
|
|
4859
|
+
responses: {
|
|
4860
|
+
"200": {
|
|
4861
|
+
content: {
|
|
4862
|
+
"application/json": {
|
|
4863
|
+
schema: OpenApiJsonSchema.makeWithDefs(schema2, { defs })
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4873
|
+
// Combine paths and definitions into a single OpenAPI schema
|
|
4874
|
+
const openApiSchema = {
|
|
4875
|
+
components: {
|
|
4876
|
+
schemas: defs
|
|
4877
|
+
},
|
|
4878
|
+
paths
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
console.log(JSON.stringify(openApiSchema, null, 2))
|
|
4882
|
+
/*
|
|
4883
|
+
Output:
|
|
4884
|
+
{
|
|
4885
|
+
"components": {
|
|
4886
|
+
"schemas": {
|
|
4887
|
+
"MyStruct1": {
|
|
4888
|
+
"type": "object",
|
|
4889
|
+
"required": [
|
|
4890
|
+
"a"
|
|
4891
|
+
],
|
|
4892
|
+
"properties": {
|
|
4893
|
+
"a": {
|
|
4894
|
+
"type": "string"
|
|
4895
|
+
}
|
|
4896
|
+
},
|
|
4897
|
+
"additionalProperties": false
|
|
4898
|
+
},
|
|
4899
|
+
"MyStruct2": {
|
|
4900
|
+
"type": "object",
|
|
4901
|
+
"required": [
|
|
4902
|
+
"b"
|
|
4903
|
+
],
|
|
4904
|
+
"properties": {
|
|
4905
|
+
"b": {
|
|
4906
|
+
"type": "number"
|
|
4907
|
+
}
|
|
4908
|
+
},
|
|
4909
|
+
"additionalProperties": false
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
},
|
|
4913
|
+
"paths": {
|
|
4914
|
+
"paths": {
|
|
4915
|
+
"/path1": {
|
|
4916
|
+
"get": {
|
|
4917
|
+
"responses": {
|
|
4918
|
+
"200": {
|
|
4919
|
+
"content": {
|
|
4920
|
+
"application/json": {
|
|
4921
|
+
"schema": {
|
|
4922
|
+
"$ref": "#/components/schemas/MyStruct1"
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
},
|
|
4930
|
+
"/path2": {
|
|
4931
|
+
"get": {
|
|
4932
|
+
"responses": {
|
|
4933
|
+
"200": {
|
|
4934
|
+
"content": {
|
|
4935
|
+
"application/json": {
|
|
4936
|
+
"schema": {
|
|
4937
|
+
"$ref": "#/components/schemas/MyStruct2"
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
*/
|
|
4949
|
+
```
|