@effect/platform 0.72.0 → 0.72.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2112 -437
- package/dist/cjs/HttpApi.js +22 -18
- package/dist/cjs/HttpApi.js.map +1 -1
- package/dist/cjs/HttpApiSchema.js +33 -4
- package/dist/cjs/HttpApiSchema.js.map +1 -1
- package/dist/cjs/HttpApiSecurity.js +2 -0
- package/dist/cjs/HttpApiSecurity.js.map +1 -1
- package/dist/cjs/OpenApi.js +55 -105
- package/dist/cjs/OpenApi.js.map +1 -1
- package/dist/cjs/OpenApiJsonSchema.js +7 -4
- package/dist/cjs/OpenApiJsonSchema.js.map +1 -1
- package/dist/dts/HttpApi.d.ts +3 -0
- package/dist/dts/HttpApi.d.ts.map +1 -1
- package/dist/dts/HttpApiSchema.d.ts.map +1 -1
- package/dist/dts/HttpApiSecurity.d.ts +2 -0
- package/dist/dts/HttpApiSecurity.d.ts.map +1 -1
- package/dist/dts/OpenApi.d.ts +2 -1
- package/dist/dts/OpenApi.d.ts.map +1 -1
- package/dist/dts/OpenApiJsonSchema.d.ts.map +1 -1
- package/dist/esm/HttpApi.js +22 -18
- package/dist/esm/HttpApi.js.map +1 -1
- package/dist/esm/HttpApiSchema.js +30 -3
- package/dist/esm/HttpApiSchema.js.map +1 -1
- package/dist/esm/HttpApiSecurity.js +2 -0
- package/dist/esm/HttpApiSecurity.js.map +1 -1
- package/dist/esm/OpenApi.js +55 -105
- package/dist/esm/OpenApi.js.map +1 -1
- package/dist/esm/OpenApiJsonSchema.js +4 -2
- package/dist/esm/OpenApiJsonSchema.js.map +1 -1
- package/package.json +2 -2
- package/src/HttpApi.ts +23 -23
- package/src/HttpApiSchema.ts +33 -8
- package/src/HttpApiSecurity.ts +2 -0
- package/src/OpenApi.ts +80 -101
- package/src/OpenApiJsonSchema.ts +9 -1
package/README.md
CHANGED
|
@@ -9,630 +9,2305 @@ Welcome to the documentation for `@effect/platform`, a library designed for crea
|
|
|
9
9
|
|
|
10
10
|
## Overview
|
|
11
11
|
|
|
12
|
-
The `HttpApi
|
|
13
|
-
You can create an API by combining multiple endpoints, each with its own set of
|
|
14
|
-
schemas that define the request and response types.
|
|
12
|
+
The `HttpApi*` modules offer a flexible and declarative way to define HTTP APIs.
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
a client that can interact with the server.
|
|
14
|
+
To define an API, create a set of `HttpEndpoint`s. Each endpoint is described by a path, a method, and schemas for the request and response.
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
Collections of endpoints are grouped in an `HttpApiGroup`, and multiple groups can be merged into a complete `HttpApi`.
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
```
|
|
19
|
+
HttpApi
|
|
20
|
+
├── HttpGroup
|
|
21
|
+
│ ├── HttpEndpoint
|
|
22
|
+
│ └── HttpEndpoint
|
|
23
|
+
└── HttpGroup
|
|
24
|
+
├── HttpEndpoint
|
|
25
|
+
├── HttpEndpoint
|
|
26
|
+
└── HttpEndpoint
|
|
27
|
+
```
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
other groups to create a complete API.
|
|
29
|
+
Once your API is defined, the same definition can be reused for multiple purposes:
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
- **Starting a Server**: Use the API definition to implement and serve endpoints.
|
|
32
|
+
- **Generating Documentation**: Create a Swagger page to document the API.
|
|
33
|
+
- **Deriving a Client**: Generate a fully-typed client for your API.
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
`HttpApiGroup` that contains our endpoints.
|
|
35
|
+
Benefits of a Single API Definition:
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
- **Consistency**: A single definition ensures the server, documentation, and client remain aligned.
|
|
38
|
+
- **Reduced Maintenance**: Changes to the API are reflected across all related components.
|
|
39
|
+
- **Simplified Workflow**: Avoids duplication by consolidating API details in one place.
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
class User extends Schema.Class<User>("User")({
|
|
39
|
-
id: Schema.Number,
|
|
40
|
-
name: Schema.String,
|
|
41
|
-
createdAt: Schema.DateTimeUtc
|
|
42
|
-
}) {}
|
|
41
|
+
## Hello World
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
43
|
+
### Defining and Implementing an API
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.addSuccess(User)
|
|
54
|
-
)
|
|
55
|
-
.add(
|
|
56
|
-
// you can also pass the path as a string and use `.setPath` to define the
|
|
57
|
-
// path parameter schema
|
|
58
|
-
HttpApiEndpoint.post("create", "/users")
|
|
59
|
-
.addSuccess(User)
|
|
60
|
-
// and here is a Schema for the request payload / body
|
|
61
|
-
//
|
|
62
|
-
// this is a POST request, so the payload is in the body
|
|
63
|
-
// but for a GET request, the payload would be in the URL search params
|
|
64
|
-
.setPayload(
|
|
65
|
-
Schema.Struct({
|
|
66
|
-
name: Schema.String
|
|
67
|
-
})
|
|
68
|
-
)
|
|
69
|
-
)
|
|
70
|
-
// by default, the endpoint will respond with a 204 No Content
|
|
71
|
-
.add(HttpApiEndpoint.del("delete")`/users/${UserIdParam}`)
|
|
72
|
-
.add(
|
|
73
|
-
HttpApiEndpoint.patch("update")`/users/${UserIdParam}`
|
|
74
|
-
.addSuccess(User)
|
|
75
|
-
.setPayload(
|
|
76
|
-
Schema.Struct({
|
|
77
|
-
name: Schema.String
|
|
78
|
-
})
|
|
79
|
-
)
|
|
80
|
-
)
|
|
45
|
+
This example demonstrates how to define and implement a simple API with a single endpoint that returns a string response. The structure of the API is as follows:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
HttpApi ("MyApi)
|
|
49
|
+
└── HttpGroup ("Greetings")
|
|
50
|
+
└── HttpEndpoint ("hello-world")
|
|
81
51
|
```
|
|
82
52
|
|
|
83
|
-
|
|
84
|
-
We will use this API style in the following examples:
|
|
53
|
+
**Example** (Hello World Definition)
|
|
85
54
|
|
|
86
55
|
```ts
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
56
|
+
import {
|
|
57
|
+
HttpApi,
|
|
58
|
+
HttpApiBuilder,
|
|
59
|
+
HttpApiEndpoint,
|
|
60
|
+
HttpApiGroup
|
|
61
|
+
} from "@effect/platform"
|
|
62
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
63
|
+
import { Effect, Layer, Schema } from "effect"
|
|
64
|
+
import { createServer } from "node:http"
|
|
65
|
+
|
|
66
|
+
// Define our API with one group named "Greetings" and one endpoint called "hello-world"
|
|
67
|
+
const MyApi = HttpApi.make("MyApi").add(
|
|
68
|
+
HttpApiGroup.make("Greetings").add(
|
|
69
|
+
HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
|
|
70
|
+
)
|
|
71
|
+
)
|
|
92
72
|
|
|
93
|
-
|
|
73
|
+
// Implement the "Greetings" group
|
|
74
|
+
const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
|
|
75
|
+
handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
|
|
76
|
+
)
|
|
94
77
|
|
|
95
|
-
|
|
78
|
+
// Provide the implementation for the API
|
|
79
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))
|
|
96
80
|
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
// Set up the server using NodeHttpServer on port 3000
|
|
82
|
+
const ServerLive = HttpApiBuilder.serve().pipe(
|
|
83
|
+
Layer.provide(MyApiLive),
|
|
84
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
85
|
+
)
|
|
99
86
|
|
|
100
|
-
|
|
87
|
+
// Launch the server
|
|
88
|
+
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
101
89
|
```
|
|
102
90
|
|
|
103
|
-
|
|
91
|
+
After running the code, open a browser and navigate to http://localhost:3000. The server will respond with:
|
|
104
92
|
|
|
105
|
-
```ts
|
|
106
|
-
const api = HttpApi.make("myApi").add(usersApi)
|
|
107
93
|
```
|
|
94
|
+
Hello, World!
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Serving The Auto Generated Swagger Documentation
|
|
108
98
|
|
|
109
|
-
|
|
99
|
+
You can enhance your API by adding auto-generated Swagger documentation using the `HttpApiSwagger` module. This makes it easier for developers to explore and interact with your API.
|
|
110
100
|
|
|
111
|
-
|
|
101
|
+
To include Swagger in your server setup, provide the `HttpApiSwagger.layer` when configuring the server.
|
|
112
102
|
|
|
113
|
-
|
|
103
|
+
**Example** (Serving Swagger Documentation)
|
|
114
104
|
|
|
115
105
|
```ts
|
|
116
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
HttpApi,
|
|
108
|
+
HttpApiBuilder,
|
|
109
|
+
HttpApiEndpoint,
|
|
110
|
+
HttpApiGroup,
|
|
111
|
+
HttpApiSwagger
|
|
112
|
+
} from "@effect/platform"
|
|
113
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
114
|
+
import { Effect, Layer, Schema } from "effect"
|
|
115
|
+
import { createServer } from "node:http"
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
.add(
|
|
120
|
-
HttpApiEndpoint.get("
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// add an OpenApi title & description
|
|
124
|
-
// You can set one attribute at a time
|
|
125
|
-
.annotate(OpenApi.Title, "Users API")
|
|
126
|
-
// or multiple at once
|
|
127
|
-
.annotateContext(
|
|
128
|
-
OpenApi.annotations({
|
|
129
|
-
title: "Users API",
|
|
130
|
-
description: "API for managing users"
|
|
131
|
-
})
|
|
132
|
-
) {}
|
|
133
|
-
```
|
|
117
|
+
const MyApi = HttpApi.make("MyApi").add(
|
|
118
|
+
HttpApiGroup.make("Greetings").add(
|
|
119
|
+
HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
|
|
120
|
+
)
|
|
121
|
+
)
|
|
134
122
|
|
|
135
|
-
|
|
136
|
-
|
|
123
|
+
const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
|
|
124
|
+
handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
|
|
125
|
+
)
|
|
137
126
|
|
|
138
|
-
|
|
127
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))
|
|
139
128
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.
|
|
143
|
-
.
|
|
129
|
+
const ServerLive = HttpApiBuilder.serve().pipe(
|
|
130
|
+
// Provide the Swagger layer so clients can access auto-generated docs
|
|
131
|
+
Layer.provide(HttpApiSwagger.layer()),
|
|
132
|
+
Layer.provide(MyApiLive),
|
|
133
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
144
137
|
```
|
|
145
138
|
|
|
146
|
-
|
|
139
|
+
After running the server, open your browser and navigate to http://localhost:3000/docs.
|
|
147
140
|
|
|
148
|
-
|
|
141
|
+
This URL will display the Swagger documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively.
|
|
149
142
|
|
|
150
|
-
-
|
|
151
|
-
- `HttpApiGroup.addError` - add an error response for all endpoints in a group
|
|
152
|
-
- `HttpApi.addError` - add an error response for all endpoints in the api
|
|
143
|
+

|
|
153
144
|
|
|
154
|
-
|
|
155
|
-
can be used in middleware.
|
|
145
|
+
### Deriving a Client
|
|
156
146
|
|
|
157
|
-
|
|
147
|
+
Once you have defined your API, you can generate a client to interact with it using the `HttpApiClient` module. This allows you to call your API endpoints without manually handling HTTP requests.
|
|
158
148
|
|
|
159
|
-
|
|
160
|
-
// define the error schemas
|
|
161
|
-
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
162
|
-
"UserNotFound",
|
|
163
|
-
{}
|
|
164
|
-
) {}
|
|
149
|
+
**Example** (Deriving and Using a Client)
|
|
165
150
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
151
|
+
```ts
|
|
152
|
+
import {
|
|
153
|
+
FetchHttpClient,
|
|
154
|
+
HttpApi,
|
|
155
|
+
HttpApiBuilder,
|
|
156
|
+
HttpApiClient,
|
|
157
|
+
HttpApiEndpoint,
|
|
158
|
+
HttpApiGroup,
|
|
159
|
+
HttpApiSwagger
|
|
160
|
+
} from "@effect/platform"
|
|
161
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
162
|
+
import { Effect, Layer, Schema } from "effect"
|
|
163
|
+
import { createServer } from "node:http"
|
|
170
164
|
|
|
171
|
-
|
|
172
|
-
.add(
|
|
173
|
-
HttpApiEndpoint.get("
|
|
174
|
-
// here we are adding our error response
|
|
175
|
-
.addError(UserNotFound, { status: 404 })
|
|
176
|
-
.addSuccess(User)
|
|
165
|
+
const MyApi = HttpApi.make("MyApi").add(
|
|
166
|
+
HttpApiGroup.make("Greetings").add(
|
|
167
|
+
HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
|
|
177
168
|
)
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
|
|
172
|
+
handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))
|
|
176
|
+
|
|
177
|
+
const ServerLive = HttpApiBuilder.serve().pipe(
|
|
178
|
+
Layer.provide(HttpApiSwagger.layer()),
|
|
179
|
+
Layer.provide(MyApiLive),
|
|
180
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
|
|
184
|
+
|
|
185
|
+
// Create a program that derives and uses the client
|
|
186
|
+
const program = Effect.gen(function* () {
|
|
187
|
+
// Derive the client
|
|
188
|
+
const client = yield* HttpApiClient.make(MyApi, {
|
|
189
|
+
baseUrl: "http://localhost:3000"
|
|
190
|
+
})
|
|
191
|
+
// Call the "hello-world" endpoint
|
|
192
|
+
const hello = yield* client.Greetings["hello-world"]()
|
|
193
|
+
console.log(hello)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Provide a Fetch-based HTTP client and run the program
|
|
197
|
+
Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
|
|
198
|
+
// Output: Hello, World!
|
|
180
199
|
```
|
|
181
200
|
|
|
182
|
-
|
|
183
|
-
just by calling `HttpApiEndpoint.addError` multiple times.
|
|
201
|
+
## Defining a HttpApiEndpoint
|
|
184
202
|
|
|
185
|
-
|
|
203
|
+
An `HttpApiEndpoint` represents a single endpoint in your API. Each endpoint is defined with a name, path, HTTP method, and optional schemas for requests and responses. This allows you to describe the structure and behavior of your API.
|
|
186
204
|
|
|
187
|
-
|
|
188
|
-
api to flag a `HttpApiEndpoint` payload schema as a multipart request.
|
|
205
|
+
Below is an example of a simple CRUD API for managing users, which includes the following endpoints:
|
|
189
206
|
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
- `GET /users` - Retrieve all users.
|
|
208
|
+
- `GET /users/:userId` - Retrieve a specific user by ID.
|
|
209
|
+
- `POST /users` - Create a new user.
|
|
210
|
+
- `DELETE /users/:userId` - Delete a user by ID.
|
|
211
|
+
- `PATCH /users/:userId` - Update a user by ID.
|
|
212
|
+
|
|
213
|
+
### GET
|
|
214
|
+
|
|
215
|
+
The `HttpApiEndpoint.get` method allows you to define a GET endpoint by specifying its name, path, and optionally, a schema for the response.
|
|
216
|
+
|
|
217
|
+
To define the structure of successful responses, use the `.addSuccess` method. If no schema is provided, the default response status is `204 No Content`.
|
|
218
|
+
|
|
219
|
+
**Example** (Defining a GET Endpoint to Retrieve All Users)
|
|
192
220
|
|
|
193
221
|
```ts
|
|
194
|
-
import {
|
|
222
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
223
|
+
import { Schema } from "effect"
|
|
195
224
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
// Define a schema representing a User entity
|
|
226
|
+
const User = Schema.Struct({
|
|
227
|
+
id: Schema.Number,
|
|
228
|
+
name: Schema.String,
|
|
229
|
+
createdAt: Schema.DateTimeUtc
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Define the "getUsers" endpoint, returning a list of users
|
|
233
|
+
const getUsers = HttpApiEndpoint
|
|
234
|
+
// ┌─── Endpoint name
|
|
235
|
+
// │ ┌─── Endpoint path
|
|
236
|
+
// ▼ ▼
|
|
237
|
+
.get("getUsers", "/users")
|
|
238
|
+
// Define the success schema for the response (optional).
|
|
239
|
+
// If no response schema is specified, the default response is `204 No Content`.
|
|
240
|
+
.addSuccess(Schema.Array(User))
|
|
206
241
|
```
|
|
207
242
|
|
|
208
|
-
###
|
|
243
|
+
### Path Parameters
|
|
244
|
+
|
|
245
|
+
Path parameters allow you to include dynamic segments in your endpoint's path. There are two ways to define path parameters in your API.
|
|
246
|
+
|
|
247
|
+
#### Using setPath
|
|
209
248
|
|
|
210
|
-
|
|
211
|
-
the `HttpApiSchema.withEncoding` api.
|
|
249
|
+
The `setPath` method allows you to explicitly define path parameters by associating them with a schema.
|
|
212
250
|
|
|
213
|
-
|
|
251
|
+
**Example** (Defining Parameters with setPath)
|
|
214
252
|
|
|
215
253
|
```ts
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
254
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
255
|
+
import { Schema } from "effect"
|
|
256
|
+
|
|
257
|
+
const User = Schema.Struct({
|
|
258
|
+
id: Schema.Number,
|
|
259
|
+
name: Schema.String,
|
|
260
|
+
createdAt: Schema.DateTimeUtc
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Define a GET endpoint with a path parameter ":id"
|
|
264
|
+
const getUser = HttpApiEndpoint.get("getUser", "/user/:id")
|
|
265
|
+
.setPath(
|
|
266
|
+
Schema.Struct({
|
|
267
|
+
// Define a schema for the "id" path parameter
|
|
268
|
+
id: Schema.NumberFromString
|
|
269
|
+
})
|
|
224
270
|
)
|
|
225
|
-
)
|
|
271
|
+
.addSuccess(User)
|
|
226
272
|
```
|
|
227
273
|
|
|
228
|
-
|
|
274
|
+
#### Using Template Strings
|
|
275
|
+
|
|
276
|
+
You can also define path parameters by embedding them in a template string with the help of `HttpApiSchema.param`.
|
|
229
277
|
|
|
230
|
-
|
|
231
|
-
|
|
278
|
+
**Example** (Defining Parameters using a Template String)
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
282
|
+
import { Schema } from "effect"
|
|
283
|
+
|
|
284
|
+
const User = Schema.Struct({
|
|
285
|
+
id: Schema.Number,
|
|
286
|
+
name: Schema.String,
|
|
287
|
+
createdAt: Schema.DateTimeUtc
|
|
288
|
+
})
|
|
232
289
|
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
// Create a path parameter using HttpApiSchema.param
|
|
291
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
235
292
|
|
|
236
|
-
|
|
293
|
+
// Define the GET endpoint using a template string
|
|
294
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
295
|
+
User
|
|
296
|
+
)
|
|
297
|
+
```
|
|
237
298
|
|
|
238
|
-
|
|
299
|
+
### POST
|
|
239
300
|
|
|
240
|
-
The `
|
|
241
|
-
and a function that adds the handlers required for the group.
|
|
301
|
+
The `HttpApiEndpoint.post` method is used to define an endpoint for creating resources. You can specify a schema for the request body (payload) and a schema for the successful response.
|
|
242
302
|
|
|
243
|
-
|
|
303
|
+
**Example** (Defining a POST Endpoint with Payload and Success Schemas)
|
|
244
304
|
|
|
245
305
|
```ts
|
|
246
|
-
import {
|
|
247
|
-
|
|
248
|
-
HttpApiBuilder,
|
|
249
|
-
HttpApiEndpoint,
|
|
250
|
-
HttpApiGroup,
|
|
251
|
-
HttpApiSchema
|
|
252
|
-
} from "@effect/platform"
|
|
253
|
-
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
306
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
307
|
+
import { Schema } from "effect"
|
|
254
308
|
|
|
255
|
-
//
|
|
256
|
-
|
|
309
|
+
// Define a schema for the user object
|
|
310
|
+
const User = Schema.Struct({
|
|
257
311
|
id: Schema.Number,
|
|
258
312
|
name: Schema.String,
|
|
259
313
|
createdAt: Schema.DateTimeUtc
|
|
260
|
-
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// Define a POST endpoint for creating a new user
|
|
317
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
318
|
+
// Define the request body schema (payload)
|
|
319
|
+
.setPayload(
|
|
320
|
+
Schema.Struct({
|
|
321
|
+
name: Schema.String
|
|
322
|
+
})
|
|
323
|
+
)
|
|
324
|
+
// Define the schema for a successful response
|
|
325
|
+
.addSuccess(User)
|
|
326
|
+
```
|
|
261
327
|
|
|
262
|
-
|
|
263
|
-
const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
|
|
328
|
+
### DELETE
|
|
264
329
|
|
|
265
|
-
|
|
266
|
-
HttpApiEndpoint.get("findById")`/users/${UserIdParam}`.addSuccess(User)
|
|
267
|
-
) {}
|
|
330
|
+
The `HttpApiEndpoint.del` method is used to define an endpoint for deleting a resource.
|
|
268
331
|
|
|
269
|
-
|
|
332
|
+
**Example** (Defining a DELETE Endpoint with Path Parameters)
|
|
270
333
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
334
|
+
```ts
|
|
335
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
336
|
+
import { Schema } from "effect"
|
|
274
337
|
|
|
275
|
-
//
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
.handle("findById", ({ path: { userId } }) =>
|
|
281
|
-
Effect.succeed(
|
|
282
|
-
new User({
|
|
283
|
-
id: userId,
|
|
284
|
-
name: "John Doe",
|
|
285
|
-
createdAt: DateTime.unsafeNow()
|
|
286
|
-
})
|
|
287
|
-
)
|
|
288
|
-
)
|
|
289
|
-
)
|
|
338
|
+
// Define a path parameter for the user ID
|
|
339
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
340
|
+
|
|
341
|
+
// Define a DELETE endpoint to delete a user by ID
|
|
342
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
290
343
|
```
|
|
291
344
|
|
|
292
|
-
###
|
|
345
|
+
### PATCH
|
|
346
|
+
|
|
347
|
+
The `HttpApiEndpoint.patch` method is used to define an endpoint for partially updating a resource. This method allows you to specify a schema for the request payload and a schema for the successful response.
|
|
293
348
|
|
|
294
|
-
|
|
295
|
-
`Effect` from the `HttpApiBuilder.group` api.
|
|
349
|
+
**Example** (Defining a PATCH Endpoint for Updating a User)
|
|
296
350
|
|
|
297
351
|
```ts
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
{
|
|
301
|
-
readonly findById: (id: number) => Effect.Effect<User>
|
|
302
|
-
}
|
|
303
|
-
>() {}
|
|
352
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
353
|
+
import { Schema } from "effect"
|
|
304
354
|
|
|
305
|
-
//
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
355
|
+
// Define a schema for the user object
|
|
356
|
+
const User = Schema.Struct({
|
|
357
|
+
id: Schema.Number,
|
|
358
|
+
name: Schema.String,
|
|
359
|
+
createdAt: Schema.DateTimeUtc
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// Define a path parameter for the user ID
|
|
363
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
364
|
+
|
|
365
|
+
// Define a PATCH endpoint to update a user's name by ID
|
|
366
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
367
|
+
// Specify the schema for the request payload
|
|
368
|
+
.setPayload(
|
|
369
|
+
Schema.Struct({
|
|
370
|
+
name: Schema.String // Only the name can be updated
|
|
371
|
+
})
|
|
372
|
+
)
|
|
373
|
+
// Specify the schema for a successful response
|
|
374
|
+
.addSuccess(User)
|
|
319
375
|
```
|
|
320
376
|
|
|
321
|
-
###
|
|
377
|
+
### Catch-All Endpoints
|
|
322
378
|
|
|
323
|
-
|
|
379
|
+
The path can also be `"*"` to match any incoming path. This is useful for defining a catch-all endpoint to handle unmatched routes or provide a fallback response.
|
|
324
380
|
|
|
325
|
-
|
|
326
|
-
to add all the group implementations.
|
|
381
|
+
**Example** (Defining a Catch-All Endpoint)
|
|
327
382
|
|
|
328
383
|
```ts
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
)
|
|
384
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
385
|
+
|
|
386
|
+
const catchAll = HttpApiEndpoint.get("catchAll", "*")
|
|
332
387
|
```
|
|
333
388
|
|
|
334
|
-
###
|
|
389
|
+
### Setting URL Parameters
|
|
335
390
|
|
|
336
|
-
|
|
391
|
+
The `setUrlParams` method allows you to define the structure of URL parameters for an endpoint. You can specify the schema for each parameter and include metadata such as descriptions to provide additional context.
|
|
337
392
|
|
|
338
|
-
|
|
339
|
-
use some of the middleware Layer's from the `HttpApiBuilder` module.
|
|
393
|
+
**Example** (Defining URL Parameters with Metadata)
|
|
340
394
|
|
|
341
395
|
```ts
|
|
342
|
-
import {
|
|
343
|
-
import {
|
|
344
|
-
import { createServer } from "node:http"
|
|
396
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
397
|
+
import { Schema } from "effect"
|
|
345
398
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// Provide the API implementation
|
|
352
|
-
Layer.provide(MyApiLive),
|
|
353
|
-
// Log the address the server is listening on
|
|
354
|
-
HttpServer.withLogAddress,
|
|
355
|
-
// Provide the HTTP server implementation
|
|
356
|
-
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
357
|
-
)
|
|
399
|
+
const User = Schema.Struct({
|
|
400
|
+
id: Schema.Number,
|
|
401
|
+
name: Schema.String,
|
|
402
|
+
createdAt: Schema.DateTimeUtc
|
|
403
|
+
})
|
|
358
404
|
|
|
359
|
-
|
|
360
|
-
|
|
405
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
406
|
+
// Specify the URL parameters schema
|
|
407
|
+
.setUrlParams(
|
|
408
|
+
Schema.Struct({
|
|
409
|
+
// Parameter "page" for pagination
|
|
410
|
+
page: Schema.NumberFromString,
|
|
411
|
+
// Parameter "sort" for sorting options with an added description
|
|
412
|
+
sort: Schema.String.annotations({
|
|
413
|
+
description: "Sorting criteria (e.g., 'name', 'date')"
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
)
|
|
417
|
+
.addSuccess(Schema.Array(User))
|
|
361
418
|
```
|
|
362
419
|
|
|
363
|
-
|
|
420
|
+
#### Defining an Array of Values for a URL Parameter
|
|
364
421
|
|
|
365
|
-
|
|
422
|
+
When defining a URL parameter that accepts multiple values, you can use the `Schema.Array` combinator. This allows the parameter to handle an array of items, with each item adhering to a specified schema.
|
|
366
423
|
|
|
367
|
-
|
|
368
|
-
implementation:
|
|
424
|
+
**Example** (Defining an Array of String Values for a URL Parameter)
|
|
369
425
|
|
|
370
426
|
```ts
|
|
371
|
-
import {
|
|
427
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
|
|
428
|
+
import { Schema } from "effect"
|
|
372
429
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
430
|
+
const api = HttpApi.make("myApi").add(
|
|
431
|
+
HttpApiGroup.make("group").add(
|
|
432
|
+
HttpApiEndpoint.get("get", "/")
|
|
433
|
+
.setUrlParams(
|
|
434
|
+
Schema.Struct({
|
|
435
|
+
// Define "a" as an array of strings
|
|
436
|
+
a: Schema.Array(Schema.String)
|
|
437
|
+
})
|
|
438
|
+
)
|
|
439
|
+
.addSuccess(Schema.String)
|
|
440
|
+
)
|
|
384
441
|
)
|
|
385
442
|
```
|
|
386
443
|
|
|
387
|
-
|
|
444
|
+
You can test this endpoint by passing an array of values in the query string. For example:
|
|
388
445
|
|
|
389
|
-
|
|
446
|
+
```sh
|
|
447
|
+
curl "http://localhost:3000/?a=1&a=2"
|
|
448
|
+
```
|
|
390
449
|
|
|
391
|
-
The `
|
|
450
|
+
The query string sends two values (`1` and `2`) for the `a` parameter. The server will process and validate these values according to the schema.
|
|
392
451
|
|
|
393
|
-
|
|
394
|
-
allows you to set:
|
|
452
|
+
### Status Codes
|
|
395
453
|
|
|
396
|
-
|
|
397
|
-
- `provides` - a `Context.Tag` that the middleware will provide
|
|
398
|
-
- `security` - `HttpApiSecurity` definitions that the middleware will
|
|
399
|
-
implement
|
|
400
|
-
- `optional` - a boolean that indicates that if the middleware fails with an
|
|
401
|
-
expected error, the request should continue. When using optional middleware,
|
|
402
|
-
`provides` & `failure` options will not affect the handlers or final error type.
|
|
454
|
+
By default, the success status code is `200 OK`. You can change it by annotating the schema with a custom status.
|
|
403
455
|
|
|
404
|
-
|
|
456
|
+
**Example** (Defining a GET Endpoint with a custom status code)
|
|
405
457
|
|
|
406
458
|
```ts
|
|
407
|
-
import {
|
|
408
|
-
HttpApiEndpoint,
|
|
409
|
-
HttpApiGroup,
|
|
410
|
-
HttpApiMiddleware
|
|
411
|
-
} from "@effect/platform"
|
|
459
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
412
460
|
import { Schema } from "effect"
|
|
413
461
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
// first extend the HttpApiMiddleware.Tag class
|
|
420
|
-
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
|
|
421
|
-
// optionally define any errors that the middleware can return
|
|
422
|
-
failure: LoggerError
|
|
423
|
-
}) {}
|
|
462
|
+
const User = Schema.Struct({
|
|
463
|
+
id: Schema.Number,
|
|
464
|
+
name: Schema.String,
|
|
465
|
+
createdAt: Schema.DateTimeUtc
|
|
466
|
+
})
|
|
424
467
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.
|
|
428
|
-
HttpApiEndpoint.get("findById")`/${Schema.NumberFromString}`
|
|
429
|
-
// apply the middleware to a single endpoint
|
|
430
|
-
.middleware(Logger)
|
|
431
|
-
)
|
|
432
|
-
// or apply the middleware to the group
|
|
433
|
-
.middleware(Logger) {}
|
|
468
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
469
|
+
// Override the default success status
|
|
470
|
+
.addSuccess(Schema.Array(User), { status: 206 })
|
|
434
471
|
```
|
|
435
472
|
|
|
436
|
-
###
|
|
437
|
-
|
|
438
|
-
The `HttpApiSecurity` module provides a way to add security annotations to your
|
|
439
|
-
API.
|
|
473
|
+
### Handling Multipart Requests
|
|
440
474
|
|
|
441
|
-
|
|
475
|
+
To support file uploads, you can use the `HttpApiSchema.Multipart` API. This allows you to define an endpoint's payload schema as a multipart request, specifying the structure of the data, including file uploads, with the `Multipart` module.
|
|
442
476
|
|
|
443
|
-
|
|
444
|
-
parameters, or cookies.
|
|
445
|
-
- `HttpApiSecurity.basicAuth` - HTTP Basic authentication.
|
|
446
|
-
- `HttpApiSecurity.bearerAuth` - Bearer token authentication.
|
|
477
|
+
**Example** (Defining an Endpoint for File Uploads)
|
|
447
478
|
|
|
448
|
-
|
|
449
|
-
to define middleware that will protect your endpoints.
|
|
479
|
+
In this example, the `HttpApiSchema.Multipart` function marks the payload as a multipart request. The `files` field uses `Multipart.FilesSchema` to handle uploaded file data automatically.
|
|
450
480
|
|
|
451
481
|
```ts
|
|
452
|
-
import {
|
|
453
|
-
|
|
482
|
+
import { HttpApiEndpoint, HttpApiSchema, Multipart } from "@effect/platform"
|
|
483
|
+
import { Schema } from "effect"
|
|
484
|
+
|
|
485
|
+
const upload = HttpApiEndpoint.post("upload", "/users/upload").setPayload(
|
|
486
|
+
// Specify that the payload is a multipart request
|
|
487
|
+
HttpApiSchema.Multipart(
|
|
488
|
+
Schema.Struct({
|
|
489
|
+
// Define a "files" field to handle file uploads
|
|
490
|
+
files: Multipart.FilesSchema
|
|
491
|
+
})
|
|
492
|
+
).addSuccess(Schema.String)
|
|
493
|
+
)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
You can test this endpoint by sending a multipart request with a file upload. For example:
|
|
497
|
+
|
|
498
|
+
```sh
|
|
499
|
+
echo "Sample file content" | curl -X POST -F "files=@-" http://localhost:3000/users/upload
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Changing the Request Encoding
|
|
503
|
+
|
|
504
|
+
By default, API requests are encoded as JSON. If your application requires a different format, you can customize the request encoding using the `HttpApiSchema.withEncoding` method. This allows you to define the encoding type and content type of the request.
|
|
505
|
+
|
|
506
|
+
**Example** (Customizing Request Encoding)
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
510
|
+
import { Schema } from "effect"
|
|
511
|
+
|
|
512
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
513
|
+
// Set the request payload as a string encoded with URL parameters
|
|
514
|
+
.setPayload(
|
|
515
|
+
Schema.Struct({
|
|
516
|
+
a: Schema.String // Parameter "a" must be a string
|
|
517
|
+
})
|
|
518
|
+
// Specify the encoding as URL parameters
|
|
519
|
+
.pipe(HttpApiSchema.withEncoding({ kind: "UrlParams" }))
|
|
520
|
+
)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Changing the Response Encoding
|
|
524
|
+
|
|
525
|
+
By default, API responses are encoded as JSON. If your application requires a different format, you can customize the encoding using the `HttpApiSchema.withEncoding` API. This method lets you define the type and content type of the response.
|
|
526
|
+
|
|
527
|
+
**Example** (Returning Data as `text/csv`)
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
|
|
531
|
+
import { Schema } from "effect"
|
|
532
|
+
|
|
533
|
+
const csv = HttpApiEndpoint.get("csv")`/users/csv`
|
|
534
|
+
// Set the success response as a string with CSV encoding
|
|
535
|
+
.addSuccess(
|
|
536
|
+
Schema.String.pipe(
|
|
537
|
+
HttpApiSchema.withEncoding({
|
|
538
|
+
// Specify the type of the response
|
|
539
|
+
kind: "Text",
|
|
540
|
+
// Define the content type as text/csv
|
|
541
|
+
contentType: "text/csv"
|
|
542
|
+
})
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Setting Request Headers
|
|
548
|
+
|
|
549
|
+
The `HttpApiEndpoint.setHeaders` method allows you to define the expected structure of request headers. You can specify the schema for each header and include additional metadata, such as descriptions.
|
|
550
|
+
|
|
551
|
+
**Example** (Defining Request Headers with Metadata)
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
import { HttpApiEndpoint } from "@effect/platform"
|
|
555
|
+
import { Schema } from "effect"
|
|
556
|
+
|
|
557
|
+
const User = Schema.Struct({
|
|
558
|
+
id: Schema.Number,
|
|
559
|
+
name: Schema.String,
|
|
560
|
+
createdAt: Schema.DateTimeUtc
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users")
|
|
564
|
+
// Specify the headers schema
|
|
565
|
+
.setHeaders(
|
|
566
|
+
Schema.Struct({
|
|
567
|
+
// Header must be a string
|
|
568
|
+
"X-API-Key": Schema.String,
|
|
569
|
+
// Header must be a string with an added description
|
|
570
|
+
"X-Request-ID": Schema.String.annotations({
|
|
571
|
+
description: "Unique identifier for the request"
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
)
|
|
575
|
+
.addSuccess(Schema.Array(User))
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Defining a HttpApiGroup
|
|
579
|
+
|
|
580
|
+
You can group related endpoints under a single entity by using `HttpApiGroup.make`. This can help organize your code and provide a clearer structure for your API.
|
|
581
|
+
|
|
582
|
+
**Example** (Creating a Group for User-Related Endpoints)
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
|
|
586
|
+
import { Schema } from "effect"
|
|
587
|
+
|
|
588
|
+
const User = Schema.Struct({
|
|
589
|
+
id: Schema.Number,
|
|
590
|
+
name: Schema.String,
|
|
591
|
+
createdAt: Schema.DateTimeUtc
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
595
|
+
|
|
596
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
597
|
+
Schema.Array(User)
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
601
|
+
User
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
605
|
+
.setPayload(
|
|
606
|
+
Schema.Struct({
|
|
607
|
+
name: Schema.String
|
|
608
|
+
})
|
|
609
|
+
)
|
|
610
|
+
.addSuccess(User)
|
|
611
|
+
|
|
612
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
613
|
+
|
|
614
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
615
|
+
.setPayload(
|
|
616
|
+
Schema.Struct({
|
|
617
|
+
name: Schema.String
|
|
618
|
+
})
|
|
619
|
+
)
|
|
620
|
+
.addSuccess(User)
|
|
621
|
+
|
|
622
|
+
// Group all user-related endpoints
|
|
623
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
624
|
+
.add(getUsers)
|
|
625
|
+
.add(getUser)
|
|
626
|
+
.add(createUser)
|
|
627
|
+
.add(deleteUser)
|
|
628
|
+
.add(updateUser)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
If you would like to create a more opaque type for the group, you can extend `HttpApiGroup` with a class.
|
|
632
|
+
|
|
633
|
+
**Example** (Creating a Group with an Opaque Type)
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
// Create an opaque class extending HttpApiGroup
|
|
637
|
+
class UsersGroup extends HttpApiGroup.make("users").add(getUsers).add(getUser) {
|
|
638
|
+
// Additional endpoints or methods can be added here
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Creating the Top-Level HttpApi
|
|
643
|
+
|
|
644
|
+
After defining your groups, you can combine them into one `HttpApi` representing your entire set of endpoints.
|
|
645
|
+
|
|
646
|
+
**Example** (Combining Groups into a Top-Level API)
|
|
647
|
+
|
|
648
|
+
```ts
|
|
649
|
+
import {
|
|
650
|
+
HttpApi,
|
|
651
|
+
HttpApiEndpoint,
|
|
652
|
+
HttpApiGroup,
|
|
653
|
+
HttpApiSchema
|
|
654
|
+
} from "@effect/platform"
|
|
655
|
+
import { Schema } from "effect"
|
|
656
|
+
|
|
657
|
+
const User = Schema.Struct({
|
|
658
|
+
id: Schema.Number,
|
|
659
|
+
name: Schema.String,
|
|
660
|
+
createdAt: Schema.DateTimeUtc
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
664
|
+
|
|
665
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
666
|
+
Schema.Array(User)
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
|
|
670
|
+
User
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
const createUser = HttpApiEndpoint.post("createUser", "/users")
|
|
674
|
+
.setPayload(
|
|
675
|
+
Schema.Struct({
|
|
676
|
+
name: Schema.String
|
|
677
|
+
})
|
|
678
|
+
)
|
|
679
|
+
.addSuccess(User)
|
|
680
|
+
|
|
681
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
682
|
+
|
|
683
|
+
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
|
|
684
|
+
.setPayload(
|
|
685
|
+
Schema.Struct({
|
|
686
|
+
name: Schema.String
|
|
687
|
+
})
|
|
688
|
+
)
|
|
689
|
+
.addSuccess(User)
|
|
690
|
+
|
|
691
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
692
|
+
.add(getUsers)
|
|
693
|
+
.add(getUser)
|
|
694
|
+
.add(createUser)
|
|
695
|
+
.add(deleteUser)
|
|
696
|
+
.add(updateUser)
|
|
697
|
+
|
|
698
|
+
// Combine the groups into one API
|
|
699
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
700
|
+
|
|
701
|
+
// Alternatively, create an opaque class for your API
|
|
702
|
+
class MyApi extends HttpApi.make("myApi").add(usersGroup) {}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
## Adding errors
|
|
706
|
+
|
|
707
|
+
Error responses allow your API to handle different failure scenarios. These responses can be defined at various levels:
|
|
708
|
+
|
|
709
|
+
- **Endpoint-level errors**: Use `HttpApiEndpoint.addError` to add errors specific to an endpoint.
|
|
710
|
+
- **Group-level errors**: Use `HttpApiGroup.addError` to add errors applicable to all endpoints in a group.
|
|
711
|
+
- **API-level errors**: Use `HttpApi.addError` to define errors that apply to every endpoint in the API.
|
|
712
|
+
|
|
713
|
+
Group-level and API-level errors are useful for handling shared issues like authentication failures, especially when managed through middleware.
|
|
714
|
+
|
|
715
|
+
**Example** (Defining Error Responses for Endpoints and Groups)
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
|
|
719
|
+
import { Schema } from "effect"
|
|
720
|
+
|
|
721
|
+
const User = Schema.Struct({
|
|
722
|
+
id: Schema.Number,
|
|
723
|
+
name: Schema.String,
|
|
724
|
+
createdAt: Schema.DateTimeUtc
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
728
|
+
|
|
729
|
+
// Define error schemas
|
|
730
|
+
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
731
|
+
"UserNotFound",
|
|
732
|
+
{}
|
|
733
|
+
) {}
|
|
734
|
+
|
|
735
|
+
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
736
|
+
"Unauthorized",
|
|
737
|
+
{}
|
|
738
|
+
) {}
|
|
739
|
+
|
|
740
|
+
const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
741
|
+
Schema.Array(User)
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
745
|
+
.addSuccess(User)
|
|
746
|
+
// Add a 404 error response for this endpoint
|
|
747
|
+
.addError(UserNotFound, { status: 404 })
|
|
748
|
+
|
|
749
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
750
|
+
.add(getUsers)
|
|
751
|
+
.add(getUser)
|
|
752
|
+
// ...etc...
|
|
753
|
+
// Add a 401 error response for the entire group
|
|
754
|
+
.addError(Unauthorized, { status: 401 })
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
You can assign multiple error responses to a single endpoint by calling `HttpApiEndpoint.addError` multiple times. This is useful when different types of errors might occur for a single operation.
|
|
758
|
+
|
|
759
|
+
**Example** (Adding Multiple Errors to an Endpoint)
|
|
760
|
+
|
|
761
|
+
```ts
|
|
762
|
+
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
|
|
763
|
+
// Add a 404 error response for when the user is not found
|
|
764
|
+
.addError(UserNotFound, { status: 404 })
|
|
765
|
+
// Add a 401 error response for unauthorized access
|
|
766
|
+
.addError(Unauthorized, { status: 401 })
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### Predefined Empty Error Types
|
|
770
|
+
|
|
771
|
+
The `HttpApiError` module provides a set of predefined empty error types that you can use in your endpoints. These error types help standardize common HTTP error responses, such as `404 Not Found` or `401 Unauthorized`. Using these predefined types simplifies error handling and ensures consistency across your API.
|
|
772
|
+
|
|
773
|
+
**Example** (Adding a Predefined Error to an Endpoint)
|
|
774
|
+
|
|
775
|
+
```ts
|
|
776
|
+
import { HttpApiEndpoint, HttpApiError, HttpApiSchema } from "@effect/platform"
|
|
777
|
+
import { Schema } from "effect"
|
|
778
|
+
|
|
779
|
+
const User = Schema.Struct({
|
|
780
|
+
id: Schema.Number,
|
|
781
|
+
name: Schema.String,
|
|
782
|
+
createdAt: Schema.DateTimeUtc
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
786
|
+
|
|
787
|
+
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
788
|
+
.addSuccess(User)
|
|
789
|
+
.addError(HttpApiError.NotFound)
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
| Name | Status | Description |
|
|
793
|
+
| --------------------- | ------ | -------------------------------------------------------------------------------------------------- |
|
|
794
|
+
| `HttpApiDecodeError` | 400 | Represents an error where the request did not match the expected schema. Includes detailed issues. |
|
|
795
|
+
| `BadRequest` | 400 | Indicates that the request was malformed or invalid. |
|
|
796
|
+
| `Unauthorized` | 401 | Indicates that authentication is required but missing or invalid. |
|
|
797
|
+
| `Forbidden` | 403 | Indicates that the client does not have permission to access the requested resource. |
|
|
798
|
+
| `NotFound` | 404 | Indicates that the requested resource could not be found. |
|
|
799
|
+
| `MethodNotAllowed` | 405 | Indicates that the HTTP method used is not allowed for the requested resource. |
|
|
800
|
+
| `NotAcceptable` | 406 | Indicates that the requested resource cannot be delivered in a format acceptable to the client. |
|
|
801
|
+
| `RequestTimeout` | 408 | Indicates that the server timed out waiting for the client request. |
|
|
802
|
+
| `Conflict` | 409 | Indicates a conflict in the request, such as conflicting data. |
|
|
803
|
+
| `Gone` | 410 | Indicates that the requested resource is no longer available and will not return. |
|
|
804
|
+
| `InternalServerError` | 500 | Indicates an unexpected server error occurred. |
|
|
805
|
+
| `NotImplemented` | 501 | Indicates that the requested functionality is not implemented on the server. |
|
|
806
|
+
| `ServiceUnavailable` | 503 | Indicates that the server is temporarily unavailable, often due to maintenance or overload. |
|
|
807
|
+
|
|
808
|
+
## Prefixing
|
|
809
|
+
|
|
810
|
+
Prefixes can be added to endpoints, groups, or an entire API to simplify the management of common paths. This is especially useful when defining multiple related endpoints that share a common base URL.
|
|
811
|
+
|
|
812
|
+
**Example** (Using Prefixes for Common Path Management)
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
|
|
816
|
+
import { Schema } from "effect"
|
|
817
|
+
|
|
818
|
+
const api = HttpApi.make("api")
|
|
819
|
+
.add(
|
|
820
|
+
HttpApiGroup.make("group")
|
|
821
|
+
.add(
|
|
822
|
+
HttpApiEndpoint.get("getRoot", "/")
|
|
823
|
+
.addSuccess(Schema.String)
|
|
824
|
+
// Prefix for this endpoint
|
|
825
|
+
.prefix("/endpointPrefix")
|
|
826
|
+
)
|
|
827
|
+
.add(HttpApiEndpoint.get("getA", "/a").addSuccess(Schema.String))
|
|
828
|
+
// Prefix for all endpoints in the group
|
|
829
|
+
.prefix("/groupPrefix")
|
|
830
|
+
)
|
|
831
|
+
// Prefix for the entire API
|
|
832
|
+
.prefix("/apiPrefix")
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
## Implementing a Server
|
|
836
|
+
|
|
837
|
+
After defining your API, you can implement a server to handle its endpoints. The `HttpApiBuilder` module provides tools to help you connect your API's structure to the logic that serves requests.
|
|
838
|
+
|
|
839
|
+
Here, we will create a simple example with a `getUser` endpoint organized within a `users` group.
|
|
840
|
+
|
|
841
|
+
**Example** (Defining the `users` Group and API)
|
|
842
|
+
|
|
843
|
+
```ts
|
|
844
|
+
import {
|
|
845
|
+
HttpApi,
|
|
846
|
+
HttpApiEndpoint,
|
|
847
|
+
HttpApiGroup,
|
|
848
|
+
HttpApiSchema
|
|
849
|
+
} from "@effect/platform"
|
|
850
|
+
import { Schema } from "effect"
|
|
851
|
+
|
|
852
|
+
const User = Schema.Struct({
|
|
853
|
+
id: Schema.Number,
|
|
854
|
+
name: Schema.String,
|
|
855
|
+
createdAt: Schema.DateTimeUtc
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
859
|
+
|
|
860
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
861
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Implementing a HttpApiGroup
|
|
868
|
+
|
|
869
|
+
The `HttpApiBuilder.group` API is used to implement a specific group of endpoints within an `HttpApi` definition. It requires the following inputs:
|
|
870
|
+
|
|
871
|
+
| Input | Description |
|
|
872
|
+
| --------------------------------- | ----------------------------------------------------------------------- |
|
|
873
|
+
| The complete `HttpApi` definition | The overall API structure that includes the group you are implementing. |
|
|
874
|
+
| The name of the group | The specific group you are focusing on within the API. |
|
|
875
|
+
| A function to add handlers | A function that defines how each endpoint in the group is handled. |
|
|
876
|
+
|
|
877
|
+
Each endpoint in the group is connected to its logic using the `HttpApiBuilder.handle` method, which maps the endpoint's definition to its corresponding implementation.
|
|
878
|
+
|
|
879
|
+
The `HttpApiBuilder.group` API produces a `Layer` that can later be provided to the server implementation.
|
|
880
|
+
|
|
881
|
+
**Example** (Implementing a Group with Endpoint Logic)
|
|
882
|
+
|
|
883
|
+
```ts
|
|
884
|
+
import {
|
|
885
|
+
HttpApi,
|
|
886
|
+
HttpApiBuilder,
|
|
887
|
+
HttpApiEndpoint,
|
|
888
|
+
HttpApiGroup,
|
|
889
|
+
HttpApiSchema
|
|
890
|
+
} from "@effect/platform"
|
|
891
|
+
import { DateTime, Effect, Schema } from "effect"
|
|
892
|
+
|
|
893
|
+
const User = Schema.Struct({
|
|
894
|
+
id: Schema.Number,
|
|
895
|
+
name: Schema.String,
|
|
896
|
+
createdAt: Schema.DateTimeUtc
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
900
|
+
|
|
901
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
902
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
906
|
+
|
|
907
|
+
// --------------------------------------------
|
|
908
|
+
// Implementation
|
|
909
|
+
// --------------------------------------------
|
|
910
|
+
|
|
911
|
+
// ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">>
|
|
912
|
+
// ▼
|
|
913
|
+
const usersGroupLive =
|
|
914
|
+
// ┌─── The Whole API
|
|
915
|
+
// │ ┌─── The Group you are implementing
|
|
916
|
+
// ▼ ▼
|
|
917
|
+
HttpApiBuilder.group(api, "users", (handlers) =>
|
|
918
|
+
handlers.handle(
|
|
919
|
+
// ┌─── The Endpoint you are implementing
|
|
920
|
+
// ▼
|
|
921
|
+
"getUser",
|
|
922
|
+
// Provide the handler logic for the endpoint.
|
|
923
|
+
// The parameters & payload are passed to the handler function.
|
|
924
|
+
({ path: { id } }) =>
|
|
925
|
+
Effect.succeed(
|
|
926
|
+
// Return a mock user object with the provided ID
|
|
927
|
+
{
|
|
928
|
+
id,
|
|
929
|
+
name: "John Doe",
|
|
930
|
+
createdAt: DateTime.unsafeNow()
|
|
931
|
+
}
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
)
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
Using `HttpApiBuilder.group`, you connect the structure of your API to its logic, enabling you to focus on each endpoint's functionality in isolation. Each handler receives the parameters and payload for the request, making it easy to process input and generate a response.
|
|
938
|
+
|
|
939
|
+
### Using Services Inside a HttpApiGroup
|
|
940
|
+
|
|
941
|
+
If your handlers need to use services, you can easily integrate them because the `HttpApiBuilder.group` API allows you to return an `Effect`. This ensures that external services can be accessed and utilized directly within your handlers.
|
|
942
|
+
|
|
943
|
+
**Example** (Using Services in a Group Implementation)
|
|
944
|
+
|
|
945
|
+
```ts
|
|
946
|
+
import {
|
|
947
|
+
HttpApi,
|
|
948
|
+
HttpApiBuilder,
|
|
949
|
+
HttpApiEndpoint,
|
|
950
|
+
HttpApiGroup,
|
|
951
|
+
HttpApiSchema
|
|
952
|
+
} from "@effect/platform"
|
|
953
|
+
import { Context, Effect, Schema } from "effect"
|
|
954
|
+
|
|
955
|
+
const User = Schema.Struct({
|
|
956
|
+
id: Schema.Number,
|
|
957
|
+
name: Schema.String,
|
|
958
|
+
createdAt: Schema.DateTimeUtc
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
962
|
+
|
|
963
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
964
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
968
|
+
|
|
969
|
+
// --------------------------------------------
|
|
970
|
+
// Implementation
|
|
971
|
+
// --------------------------------------------
|
|
972
|
+
|
|
973
|
+
type User = typeof User.Type
|
|
974
|
+
|
|
975
|
+
// Define the UsersRepository service
|
|
976
|
+
class UsersRepository extends Context.Tag("UsersRepository")<
|
|
977
|
+
UsersRepository,
|
|
978
|
+
{
|
|
979
|
+
readonly findById: (id: number) => Effect.Effect<User>
|
|
980
|
+
}
|
|
981
|
+
>() {}
|
|
982
|
+
|
|
983
|
+
// Implement the `users` group with access to the UsersRepository service
|
|
984
|
+
//
|
|
985
|
+
// ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">, never, UsersRepository>
|
|
986
|
+
// ▼
|
|
987
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
988
|
+
Effect.gen(function* () {
|
|
989
|
+
// Access the UsersRepository service
|
|
990
|
+
const repository = yield* UsersRepository
|
|
991
|
+
return handlers.handle("getUser", ({ path: { id } }) =>
|
|
992
|
+
repository.findById(id)
|
|
993
|
+
)
|
|
994
|
+
})
|
|
995
|
+
)
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### Implementing a HttpApi
|
|
999
|
+
|
|
1000
|
+
Once all your groups are implemented, you can create a top-level implementation to combine them into a unified API. This is done using the `HttpApiBuilder.api` API, which generates a `Layer`. You then use `Layer.provide` to include the implementations of all the groups into the top-level `HttpApi`.
|
|
1001
|
+
|
|
1002
|
+
**Example** (Combining Group Implementations into a Top-Level API)
|
|
1003
|
+
|
|
1004
|
+
```ts
|
|
1005
|
+
import {
|
|
1006
|
+
HttpApi,
|
|
1007
|
+
HttpApiBuilder,
|
|
1008
|
+
HttpApiEndpoint,
|
|
1009
|
+
HttpApiGroup,
|
|
1010
|
+
HttpApiSchema
|
|
1011
|
+
} from "@effect/platform"
|
|
1012
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
1013
|
+
|
|
1014
|
+
const User = Schema.Struct({
|
|
1015
|
+
id: Schema.Number,
|
|
1016
|
+
name: Schema.String,
|
|
1017
|
+
createdAt: Schema.DateTimeUtc
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1021
|
+
|
|
1022
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1023
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
1027
|
+
|
|
1028
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1029
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1030
|
+
Effect.succeed({
|
|
1031
|
+
id,
|
|
1032
|
+
name: "John Doe",
|
|
1033
|
+
createdAt: DateTime.unsafeNow()
|
|
1034
|
+
})
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
// Combine all group implementations into the top-level API
|
|
1039
|
+
//
|
|
1040
|
+
// ┌─── Layer<HttpApi.Api, never, never>
|
|
1041
|
+
// ▼
|
|
1042
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
### Serving the API
|
|
1046
|
+
|
|
1047
|
+
You can serve your API using the `HttpApiBuilder.serve` function. This utility builds an `HttpApp` from an `HttpApi` instance and uses an `HttpServer` to handle requests. Middleware can be added to customize or enhance the server's behavior.
|
|
1048
|
+
|
|
1049
|
+
**Example** (Setting Up and Serving an API with Middleware)
|
|
1050
|
+
|
|
1051
|
+
```ts
|
|
1052
|
+
import {
|
|
1053
|
+
HttpApi,
|
|
1054
|
+
HttpApiBuilder,
|
|
1055
|
+
HttpApiEndpoint,
|
|
1056
|
+
HttpApiGroup,
|
|
1057
|
+
HttpApiSchema,
|
|
1058
|
+
HttpMiddleware,
|
|
1059
|
+
HttpServer
|
|
1060
|
+
} from "@effect/platform"
|
|
1061
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1062
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
1063
|
+
import { createServer } from "node:http"
|
|
1064
|
+
|
|
1065
|
+
const User = Schema.Struct({
|
|
1066
|
+
id: Schema.Number,
|
|
1067
|
+
name: Schema.String,
|
|
1068
|
+
createdAt: Schema.DateTimeUtc
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1072
|
+
|
|
1073
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1074
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
1078
|
+
|
|
1079
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1080
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1081
|
+
Effect.succeed({
|
|
1082
|
+
id,
|
|
1083
|
+
name: "John Doe",
|
|
1084
|
+
createdAt: DateTime.unsafeNow()
|
|
1085
|
+
})
|
|
1086
|
+
)
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
1090
|
+
|
|
1091
|
+
// Configure and serve the API
|
|
1092
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1093
|
+
// Add CORS middleware to handle cross-origin requests
|
|
1094
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1095
|
+
// Provide the API implementation
|
|
1096
|
+
Layer.provide(MyApiLive),
|
|
1097
|
+
// Log the server's listening address
|
|
1098
|
+
HttpServer.withLogAddress,
|
|
1099
|
+
// Set up the Node.js HTTP server
|
|
1100
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
// Launch the server
|
|
1104
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
### Accessing the HttpServerRequest
|
|
1108
|
+
|
|
1109
|
+
In some cases, you may need to access details about the incoming `HttpServerRequest` within an endpoint handler. The HttpServerRequest module provides access to the request object, allowing you to inspect properties such as the HTTP method or headers.
|
|
1110
|
+
|
|
1111
|
+
**Example** (Accessing the Request Object in a GET Endpoint)
|
|
1112
|
+
|
|
1113
|
+
```ts
|
|
1114
|
+
import {
|
|
1115
|
+
HttpApi,
|
|
1116
|
+
HttpApiBuilder,
|
|
1117
|
+
HttpApiEndpoint,
|
|
1118
|
+
HttpApiGroup,
|
|
1119
|
+
HttpMiddleware,
|
|
1120
|
+
HttpServer,
|
|
1121
|
+
HttpServerRequest
|
|
1122
|
+
} from "@effect/platform"
|
|
1123
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1124
|
+
import { Effect, Layer, Schema } from "effect"
|
|
1125
|
+
import { createServer } from "node:http"
|
|
1126
|
+
|
|
1127
|
+
const api = HttpApi.make("myApi").add(
|
|
1128
|
+
HttpApiGroup.make("group").add(
|
|
1129
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
1130
|
+
)
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1134
|
+
handlers.handle("get", () =>
|
|
1135
|
+
Effect.gen(function* () {
|
|
1136
|
+
// Access the incoming request
|
|
1137
|
+
const req = yield* HttpServerRequest.HttpServerRequest
|
|
1138
|
+
|
|
1139
|
+
// Log the HTTP method for demonstration purposes
|
|
1140
|
+
console.log(req.method)
|
|
1141
|
+
|
|
1142
|
+
// Return a response
|
|
1143
|
+
return "Hello, World!"
|
|
1144
|
+
})
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1149
|
+
|
|
1150
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1151
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1152
|
+
Layer.provide(MyApiLive),
|
|
1153
|
+
HttpServer.withLogAddress,
|
|
1154
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
### Streaming Requests
|
|
1161
|
+
|
|
1162
|
+
Streaming requests allow you to send large or continuous data streams to the server. In this example, we define an API that accepts a stream of binary data and decodes it into a string.
|
|
1163
|
+
|
|
1164
|
+
**Example** (Handling Streaming Requests)
|
|
1165
|
+
|
|
1166
|
+
```ts
|
|
1167
|
+
import {
|
|
1168
|
+
HttpApi,
|
|
1169
|
+
HttpApiBuilder,
|
|
1170
|
+
HttpApiEndpoint,
|
|
1171
|
+
HttpApiGroup,
|
|
1172
|
+
HttpApiSchema,
|
|
1173
|
+
HttpMiddleware,
|
|
1174
|
+
HttpServer
|
|
1175
|
+
} from "@effect/platform"
|
|
1176
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1177
|
+
import { Effect, Layer, Schema } from "effect"
|
|
1178
|
+
import { createServer } from "node:http"
|
|
1179
|
+
|
|
1180
|
+
const api = HttpApi.make("myApi").add(
|
|
1181
|
+
HttpApiGroup.make("group").add(
|
|
1182
|
+
HttpApiEndpoint.post("acceptStream", "/stream")
|
|
1183
|
+
// Define the payload as a Uint8Array with a specific encoding
|
|
1184
|
+
.setPayload(
|
|
1185
|
+
Schema.Uint8ArrayFromSelf.pipe(
|
|
1186
|
+
HttpApiSchema.withEncoding({
|
|
1187
|
+
kind: "Uint8Array",
|
|
1188
|
+
contentType: "application/octet-stream"
|
|
1189
|
+
})
|
|
1190
|
+
)
|
|
1191
|
+
)
|
|
1192
|
+
.addSuccess(Schema.String)
|
|
1193
|
+
)
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1197
|
+
handlers.handle("acceptStream", (req) =>
|
|
1198
|
+
// Decode the incoming binary data into a string
|
|
1199
|
+
Effect.succeed(new TextDecoder().decode(req.payload))
|
|
1200
|
+
)
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1204
|
+
|
|
1205
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1206
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1207
|
+
Layer.provide(MyApiLive),
|
|
1208
|
+
HttpServer.withLogAddress,
|
|
1209
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
You can test the streaming request using `curl` or any tool that supports sending binary data. For example:
|
|
1216
|
+
|
|
1217
|
+
```sh
|
|
1218
|
+
echo "abc" | curl -X POST 'http://localhost:3000/stream' --data-binary @- -H "Content-Type: application/octet-stream"
|
|
1219
|
+
# Output: abc
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
### Streaming Responses
|
|
1223
|
+
|
|
1224
|
+
To handle streaming responses in your API, you can use `handleRaw`. The `HttpServerResponse.stream` function is designed to return a continuous stream of data as the response.
|
|
1225
|
+
|
|
1226
|
+
**Example** (Implementing a Streaming Endpoint)
|
|
1227
|
+
|
|
1228
|
+
```ts
|
|
1229
|
+
import {
|
|
1230
|
+
HttpApi,
|
|
1231
|
+
HttpApiBuilder,
|
|
1232
|
+
HttpApiEndpoint,
|
|
1233
|
+
HttpApiGroup,
|
|
1234
|
+
HttpApiSchema,
|
|
1235
|
+
HttpMiddleware,
|
|
1236
|
+
HttpServer,
|
|
1237
|
+
HttpServerResponse
|
|
1238
|
+
} from "@effect/platform"
|
|
1239
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1240
|
+
import { Layer, Schedule, Schema, Stream } from "effect"
|
|
1241
|
+
import { createServer } from "node:http"
|
|
1242
|
+
|
|
1243
|
+
// Define the API with a single streaming endpoint
|
|
1244
|
+
const api = HttpApi.make("myApi").add(
|
|
1245
|
+
HttpApiGroup.make("group").add(
|
|
1246
|
+
HttpApiEndpoint.get("getStream", "/stream").addSuccess(
|
|
1247
|
+
Schema.String.pipe(
|
|
1248
|
+
HttpApiSchema.withEncoding({
|
|
1249
|
+
kind: "Text",
|
|
1250
|
+
contentType: "application/octet-stream"
|
|
1251
|
+
})
|
|
1252
|
+
)
|
|
1253
|
+
)
|
|
1254
|
+
)
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
// Simulate a stream of data
|
|
1258
|
+
const stream = Stream.make("a", "b", "c").pipe(
|
|
1259
|
+
Stream.schedule(Schedule.spaced("500 millis")),
|
|
1260
|
+
Stream.map((s) => new TextEncoder().encode(s))
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
1264
|
+
handlers.handleRaw("getStream", () => HttpServerResponse.stream(stream))
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
1268
|
+
|
|
1269
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1270
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1271
|
+
Layer.provide(MyApiLive),
|
|
1272
|
+
HttpServer.withLogAddress,
|
|
1273
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
You can test the streaming response using `curl` or any similar HTTP client that supports streaming:
|
|
1280
|
+
|
|
1281
|
+
```sh
|
|
1282
|
+
curl 'http://localhost:3000/stream' --no-buffer
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
The response will stream data (`a`, `b`, `c`) with a 500ms interval between each item.
|
|
1286
|
+
|
|
1287
|
+
## Middlewares
|
|
1288
|
+
|
|
1289
|
+
### Defining Middleware
|
|
1290
|
+
|
|
1291
|
+
The `HttpApiMiddleware` module allows you to add middleware to your API. Middleware can enhance your API by introducing features like logging, authentication, or additional error handling.
|
|
1292
|
+
|
|
1293
|
+
You can define middleware using the `HttpApiMiddleware.Tag` class, which lets you specify:
|
|
1294
|
+
|
|
1295
|
+
| Option | Description |
|
|
1296
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1297
|
+
| `failure` | A schema that describes any errors the middleware might return. |
|
|
1298
|
+
| `provides` | A `Context.Tag` representing the resource or data the middleware will provide to subsequent handlers. |
|
|
1299
|
+
| `security` | Definitions from `HttpApiSecurity` that the middleware will implement, such as authentication mechanisms. |
|
|
1300
|
+
| `optional` | A boolean indicating whether the request should continue if the middleware fails with an expected error. When `optional` is set to `true`, the `provides` and `failure` options do not affect the final error type or handlers. |
|
|
1301
|
+
|
|
1302
|
+
**Example** (Defining a Logger Middleware)
|
|
1303
|
+
|
|
1304
|
+
```ts
|
|
1305
|
+
import {
|
|
1306
|
+
HttpApiEndpoint,
|
|
1307
|
+
HttpApiGroup,
|
|
1308
|
+
HttpApiMiddleware,
|
|
1309
|
+
HttpApiSchema
|
|
1310
|
+
} from "@effect/platform"
|
|
1311
|
+
import { Schema } from "effect"
|
|
1312
|
+
|
|
1313
|
+
// Define a schema for errors returned by the logger middleware
|
|
1314
|
+
class LoggerError extends Schema.TaggedError<LoggerError>()(
|
|
1315
|
+
"LoggerError",
|
|
1316
|
+
{}
|
|
1317
|
+
) {}
|
|
1318
|
+
|
|
1319
|
+
// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag
|
|
1320
|
+
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
|
|
1321
|
+
// Optionally define the error schema for the middleware
|
|
1322
|
+
failure: LoggerError
|
|
1323
|
+
}) {}
|
|
1324
|
+
|
|
1325
|
+
const User = Schema.Struct({
|
|
1326
|
+
id: Schema.Number,
|
|
1327
|
+
name: Schema.String,
|
|
1328
|
+
createdAt: Schema.DateTimeUtc
|
|
1329
|
+
})
|
|
1330
|
+
|
|
1331
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1332
|
+
|
|
1333
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
1334
|
+
.add(
|
|
1335
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
1336
|
+
.addSuccess(User)
|
|
1337
|
+
// Apply the middleware to a single endpoint
|
|
1338
|
+
.middleware(Logger)
|
|
1339
|
+
)
|
|
1340
|
+
// Or apply the middleware to the entire group
|
|
1341
|
+
.middleware(Logger)
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
### Implementing HttpApiMiddleware
|
|
1345
|
+
|
|
1346
|
+
Once you have defined your `HttpApiMiddleware`, you can implement it as a `Layer`. This allows the middleware to be applied to specific API groups or endpoints, enabling modular and reusable behavior.
|
|
1347
|
+
|
|
1348
|
+
**Example** (Implementing and Using Logger Middleware)
|
|
1349
|
+
|
|
1350
|
+
```ts
|
|
1351
|
+
import { HttpApiMiddleware, HttpServerRequest } from "@effect/platform"
|
|
1352
|
+
import { Effect, Layer } from "effect"
|
|
1353
|
+
|
|
1354
|
+
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger") {}
|
|
1355
|
+
|
|
1356
|
+
const LoggerLive = Layer.effect(
|
|
1357
|
+
Logger,
|
|
1358
|
+
Effect.gen(function* () {
|
|
1359
|
+
yield* Effect.log("creating Logger middleware")
|
|
1360
|
+
|
|
1361
|
+
// Middleware implementation as an Effect
|
|
1362
|
+
// that can access the `HttpServerRequest` context.
|
|
1363
|
+
return Effect.gen(function* () {
|
|
1364
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
1365
|
+
yield* Effect.log(`Request: ${request.method} ${request.url}`)
|
|
1366
|
+
})
|
|
1367
|
+
})
|
|
1368
|
+
)
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
After implementing the middleware, you can attach it to your API groups or specific endpoints using the `Layer` APIs.
|
|
1372
|
+
|
|
1373
|
+
```ts
|
|
1374
|
+
import {
|
|
1375
|
+
HttpApi,
|
|
1376
|
+
HttpApiBuilder,
|
|
1377
|
+
HttpApiEndpoint,
|
|
1378
|
+
HttpApiGroup,
|
|
1379
|
+
HttpApiMiddleware,
|
|
1380
|
+
HttpApiSchema,
|
|
1381
|
+
HttpServerRequest
|
|
1382
|
+
} from "@effect/platform"
|
|
1383
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
1384
|
+
|
|
1385
|
+
// Define a schema for errors returned by the logger middleware
|
|
1386
|
+
class LoggerError extends Schema.TaggedError<LoggerError>()(
|
|
1387
|
+
"LoggerError",
|
|
1388
|
+
{}
|
|
1389
|
+
) {}
|
|
1390
|
+
|
|
1391
|
+
// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag
|
|
1392
|
+
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
|
|
1393
|
+
// Optionally define the error schema for the middleware
|
|
1394
|
+
failure: LoggerError
|
|
1395
|
+
}) {}
|
|
1396
|
+
|
|
1397
|
+
const LoggerLive = Layer.effect(
|
|
1398
|
+
Logger,
|
|
1399
|
+
Effect.gen(function* () {
|
|
1400
|
+
yield* Effect.log("creating Logger middleware")
|
|
1401
|
+
|
|
1402
|
+
// Middleware implementation as an Effect
|
|
1403
|
+
// that can access the `HttpServerRequest` context.
|
|
1404
|
+
return Effect.gen(function* () {
|
|
1405
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
1406
|
+
yield* Effect.log(`Request: ${request.method} ${request.url}`)
|
|
1407
|
+
})
|
|
1408
|
+
})
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
const User = Schema.Struct({
|
|
1412
|
+
id: Schema.Number,
|
|
1413
|
+
name: Schema.String,
|
|
1414
|
+
createdAt: Schema.DateTimeUtc
|
|
1415
|
+
})
|
|
1416
|
+
|
|
1417
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
1418
|
+
|
|
1419
|
+
const usersGroup = HttpApiGroup.make("users")
|
|
1420
|
+
.add(
|
|
1421
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`
|
|
1422
|
+
.addSuccess(User)
|
|
1423
|
+
// Apply the middleware to a single endpoint
|
|
1424
|
+
.middleware(Logger)
|
|
1425
|
+
)
|
|
1426
|
+
// Or apply the middleware to the entire group
|
|
1427
|
+
.middleware(Logger)
|
|
1428
|
+
|
|
1429
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
1430
|
+
|
|
1431
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1432
|
+
handlers.handle("getUser", (req) =>
|
|
1433
|
+
Effect.succeed({
|
|
1434
|
+
id: req.path.id,
|
|
1435
|
+
name: "John Doe",
|
|
1436
|
+
createdAt: DateTime.unsafeNow()
|
|
1437
|
+
})
|
|
1438
|
+
)
|
|
1439
|
+
).pipe(
|
|
1440
|
+
// Provide the Logger middleware to the group
|
|
1441
|
+
Layer.provide(LoggerLive)
|
|
1442
|
+
)
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
### Defining security middleware
|
|
1446
|
+
|
|
1447
|
+
The `HttpApiSecurity` module enables you to add security annotations to your API. These annotations specify the type of authorization required to access specific endpoints.
|
|
1448
|
+
|
|
1449
|
+
Supported authorization types include:
|
|
1450
|
+
|
|
1451
|
+
| Authorization Type | Description |
|
|
1452
|
+
| ------------------------ | ---------------------------------------------------------------- |
|
|
1453
|
+
| `HttpApiSecurity.apiKey` | API key authorization via headers, query parameters, or cookies. |
|
|
1454
|
+
| `HttpApiSecurity.basic` | HTTP Basic authentication. |
|
|
1455
|
+
| `HttpApiSecurity.bearer` | Bearer token authentication. |
|
|
1456
|
+
|
|
1457
|
+
These security annotations can be used alongside `HttpApiMiddleware` to create middleware that protects your API endpoints.
|
|
1458
|
+
|
|
1459
|
+
**Example** (Defining Security Middleware)
|
|
1460
|
+
|
|
1461
|
+
```ts
|
|
1462
|
+
import {
|
|
1463
|
+
HttpApi,
|
|
454
1464
|
HttpApiEndpoint,
|
|
1465
|
+
HttpApiGroup,
|
|
1466
|
+
HttpApiMiddleware,
|
|
1467
|
+
HttpApiSchema,
|
|
1468
|
+
HttpApiSecurity
|
|
1469
|
+
} from "@effect/platform"
|
|
1470
|
+
import { Context, Schema } from "effect"
|
|
1471
|
+
|
|
1472
|
+
// Define a schema for the "User"
|
|
1473
|
+
class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}
|
|
1474
|
+
|
|
1475
|
+
// Define a schema for the "Unauthorized" error
|
|
1476
|
+
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
1477
|
+
"Unauthorized",
|
|
1478
|
+
{},
|
|
1479
|
+
// Specify the HTTP status code for unauthorized errors
|
|
1480
|
+
HttpApiSchema.annotations({ status: 401 })
|
|
1481
|
+
) {}
|
|
1482
|
+
|
|
1483
|
+
// Define a Context.Tag for the authenticated user
|
|
1484
|
+
class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
|
|
1485
|
+
|
|
1486
|
+
// Create the Authorization middleware
|
|
1487
|
+
class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
1488
|
+
"Authorization",
|
|
1489
|
+
{
|
|
1490
|
+
// Define the error schema for unauthorized access
|
|
1491
|
+
failure: Unauthorized,
|
|
1492
|
+
// Specify the resource this middleware will provide
|
|
1493
|
+
provides: CurrentUser,
|
|
1494
|
+
// Add security definitions
|
|
1495
|
+
security: {
|
|
1496
|
+
// ┌─── Custom name for the security definition
|
|
1497
|
+
// ▼
|
|
1498
|
+
myBearer: HttpApiSecurity.bearer
|
|
1499
|
+
// Additional security definitions can be added here.
|
|
1500
|
+
// They will attempt to be resolved in the order they are defined.
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
) {}
|
|
1504
|
+
|
|
1505
|
+
const api = HttpApi.make("api")
|
|
1506
|
+
.add(
|
|
1507
|
+
HttpApiGroup.make("group")
|
|
1508
|
+
.add(
|
|
1509
|
+
HttpApiEndpoint.get("get", "/")
|
|
1510
|
+
.addSuccess(Schema.String)
|
|
1511
|
+
// Apply the middleware to a single endpoint
|
|
1512
|
+
.middleware(Authorization)
|
|
1513
|
+
)
|
|
1514
|
+
// Or apply the middleware to the entire group
|
|
1515
|
+
.middleware(Authorization)
|
|
1516
|
+
)
|
|
1517
|
+
// Or apply the middleware to the entire API
|
|
1518
|
+
.middleware(Authorization)
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
### Implementing HttpApiSecurity middleware
|
|
1522
|
+
|
|
1523
|
+
When using `HttpApiSecurity` in your middleware, the implementation involves creating a `Layer` with security handlers tailored to your requirements. Below is an example demonstrating how to implement middleware for `HttpApiSecurity.bearer` authentication.
|
|
1524
|
+
|
|
1525
|
+
**Example** (Implementing Bearer Token Authentication Middleware)
|
|
1526
|
+
|
|
1527
|
+
```ts
|
|
1528
|
+
import {
|
|
455
1529
|
HttpApiMiddleware,
|
|
456
1530
|
HttpApiSchema,
|
|
457
1531
|
HttpApiSecurity
|
|
458
1532
|
} from "@effect/platform"
|
|
1533
|
+
import { Context, Effect, Layer, Redacted, Schema } from "effect"
|
|
1534
|
+
|
|
1535
|
+
class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}
|
|
1536
|
+
|
|
1537
|
+
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
1538
|
+
"Unauthorized",
|
|
1539
|
+
{},
|
|
1540
|
+
HttpApiSchema.annotations({ status: 401 })
|
|
1541
|
+
) {}
|
|
1542
|
+
|
|
1543
|
+
class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
|
|
1544
|
+
|
|
1545
|
+
class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
1546
|
+
"Authorization",
|
|
1547
|
+
{
|
|
1548
|
+
failure: Unauthorized,
|
|
1549
|
+
provides: CurrentUser,
|
|
1550
|
+
security: {
|
|
1551
|
+
myBearer: HttpApiSecurity.bearer
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
) {}
|
|
1555
|
+
|
|
1556
|
+
const AuthorizationLive = Layer.effect(
|
|
1557
|
+
Authorization,
|
|
1558
|
+
Effect.gen(function* () {
|
|
1559
|
+
yield* Effect.log("creating Authorization middleware")
|
|
1560
|
+
|
|
1561
|
+
// Return the security handlers for the middleware
|
|
1562
|
+
return {
|
|
1563
|
+
// Define the handler for the Bearer token
|
|
1564
|
+
// The Bearer token is redacted for security
|
|
1565
|
+
myBearer: (bearerToken) =>
|
|
1566
|
+
Effect.gen(function* () {
|
|
1567
|
+
yield* Effect.log(
|
|
1568
|
+
"checking bearer token",
|
|
1569
|
+
Redacted.value(bearerToken)
|
|
1570
|
+
)
|
|
1571
|
+
// Return a mock User object as the CurrentUser
|
|
1572
|
+
return new User({ id: 1 })
|
|
1573
|
+
})
|
|
1574
|
+
}
|
|
1575
|
+
})
|
|
1576
|
+
)
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
### Adding Descriptions to Security Definitions
|
|
1580
|
+
|
|
1581
|
+
The `HttpApiSecurity.annotate` function allows you to add metadata, such as a description, to your security definitions. This metadata is displayed in the Swagger documentation, making it easier for developers to understand your API's security requirements.
|
|
1582
|
+
|
|
1583
|
+
**Example** (Adding a Description to a Bearer Token Security Definition)
|
|
1584
|
+
|
|
1585
|
+
```ts
|
|
1586
|
+
import {
|
|
1587
|
+
HttpApiMiddleware,
|
|
1588
|
+
HttpApiSchema,
|
|
1589
|
+
HttpApiSecurity,
|
|
1590
|
+
OpenApi
|
|
1591
|
+
} from "@effect/platform"
|
|
459
1592
|
import { Context, Schema } from "effect"
|
|
460
1593
|
|
|
461
|
-
class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}
|
|
1594
|
+
class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}
|
|
1595
|
+
|
|
1596
|
+
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
1597
|
+
"Unauthorized",
|
|
1598
|
+
{},
|
|
1599
|
+
HttpApiSchema.annotations({ status: 401 })
|
|
1600
|
+
) {}
|
|
1601
|
+
|
|
1602
|
+
class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
|
|
1603
|
+
|
|
1604
|
+
class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
|
|
1605
|
+
"Authorization",
|
|
1606
|
+
{
|
|
1607
|
+
failure: Unauthorized,
|
|
1608
|
+
provides: CurrentUser,
|
|
1609
|
+
security: {
|
|
1610
|
+
myBearer: HttpApiSecurity.bearer.pipe(
|
|
1611
|
+
// Add a description to the security definition
|
|
1612
|
+
HttpApiSecurity.annotate(OpenApi.Description, "my description")
|
|
1613
|
+
)
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
) {}
|
|
1617
|
+
```
|
|
1618
|
+
|
|
1619
|
+
### Setting HttpApiSecurity cookies
|
|
1620
|
+
|
|
1621
|
+
To set a security cookie from within a handler, you can use the `HttpApiBuilder.securitySetCookie` API. This method sets a cookie with default properties, including the `HttpOnly` and `Secure` flags, ensuring the cookie is not accessible via JavaScript and is transmitted over secure connections.
|
|
1622
|
+
|
|
1623
|
+
**Example** (Setting a Security Cookie in a Login Handler)
|
|
1624
|
+
|
|
1625
|
+
```ts
|
|
1626
|
+
// Define the security configuration for an API key stored in a cookie
|
|
1627
|
+
const security = HttpApiSecurity.apiKey({
|
|
1628
|
+
// Specify that the API key is stored in a cookie
|
|
1629
|
+
in: "cookie"
|
|
1630
|
+
// Define the cookie name,
|
|
1631
|
+
key: "token"
|
|
1632
|
+
})
|
|
1633
|
+
|
|
1634
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
1635
|
+
handlers.handle("login", () =>
|
|
1636
|
+
// Set the security cookie with a redacted value
|
|
1637
|
+
HttpApiBuilder.securitySetCookie(security, Redacted.make("keep me secret"))
|
|
1638
|
+
)
|
|
1639
|
+
)
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
## Serving Swagger documentation
|
|
1643
|
+
|
|
1644
|
+
You can add Swagger documentation to your API using the `HttpApiSwagger` module. This integration provides an interactive interface for developers to explore and test your API. To enable Swagger, you simply provide the `HttpApiSwagger.layer` to your server implementation.
|
|
1645
|
+
|
|
1646
|
+
**Example** (Adding Swagger Documentation to an API)
|
|
1647
|
+
|
|
1648
|
+
```ts
|
|
1649
|
+
import {
|
|
1650
|
+
HttpApi,
|
|
1651
|
+
HttpApiBuilder,
|
|
1652
|
+
HttpApiEndpoint,
|
|
1653
|
+
HttpApiGroup,
|
|
1654
|
+
HttpApiSchema,
|
|
1655
|
+
HttpApiSwagger,
|
|
1656
|
+
HttpMiddleware,
|
|
1657
|
+
HttpServer
|
|
1658
|
+
} from "@effect/platform"
|
|
1659
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
1660
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
1661
|
+
import { createServer } from "node:http"
|
|
1662
|
+
|
|
1663
|
+
const User = Schema.Struct({
|
|
1664
|
+
id: Schema.Number,
|
|
1665
|
+
name: Schema.String,
|
|
1666
|
+
createdAt: Schema.DateTimeUtc
|
|
1667
|
+
})
|
|
1668
|
+
|
|
1669
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
462
1670
|
|
|
463
|
-
|
|
464
|
-
"
|
|
465
|
-
|
|
466
|
-
HttpApiSchema.annotations({ status: 401 })
|
|
467
|
-
) {}
|
|
1671
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
1672
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
1673
|
+
)
|
|
468
1674
|
|
|
469
|
-
|
|
1675
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
470
1676
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1677
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
1678
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
1679
|
+
Effect.succeed({
|
|
1680
|
+
id,
|
|
1681
|
+
name: "John Doe",
|
|
1682
|
+
createdAt: DateTime.unsafeNow()
|
|
1683
|
+
})
|
|
1684
|
+
)
|
|
1685
|
+
)
|
|
1686
|
+
|
|
1687
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
1688
|
+
|
|
1689
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
1690
|
+
// Add the Swagger documentation layer
|
|
1691
|
+
Layer.provide(
|
|
1692
|
+
HttpApiSwagger.layer({
|
|
1693
|
+
// Specify the Swagger documentation path.
|
|
1694
|
+
// "/docs" is the default path.
|
|
1695
|
+
path: "/docs"
|
|
1696
|
+
})
|
|
1697
|
+
),
|
|
1698
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
1699
|
+
Layer.provide(MyApiLive),
|
|
1700
|
+
HttpServer.withLogAddress,
|
|
1701
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+

|
|
1708
|
+
|
|
1709
|
+
### Adding OpenAPI Annotations
|
|
1710
|
+
|
|
1711
|
+
You can add OpenAPI annotations to your API to include metadata such as titles, descriptions, and more. These annotations help generate richer API documentation.
|
|
1712
|
+
|
|
1713
|
+
#### HttpApi
|
|
1714
|
+
|
|
1715
|
+
Below is a list of available annotations for a top-level `HttpApi`. They can be added using the `.annotate` method:
|
|
1716
|
+
|
|
1717
|
+
| Annotation | Description |
|
|
1718
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
1719
|
+
| `HttpApi.AdditionalSchemas` | Adds custom schemas to the final OpenAPI specification. Only schemas with an `identifier` annotation are included. |
|
|
1720
|
+
| `OpenApi.Description` | Sets a general description for the API. |
|
|
1721
|
+
| `OpenApi.License` | Defines the license used by the API. |
|
|
1722
|
+
| `OpenApi.Summary` | Provides a brief summary of the API. |
|
|
1723
|
+
| `OpenApi.Servers` | Lists server URLs and optional metadata such as variables. |
|
|
1724
|
+
| `OpenApi.Override` | Merges the supplied fields into the resulting specification. |
|
|
1725
|
+
| `OpenApi.Transform` | Allows you to modify the final specification with a custom function. |
|
|
1726
|
+
|
|
1727
|
+
**Example** (Annotating the Top-Level API)
|
|
1728
|
+
|
|
1729
|
+
```ts
|
|
1730
|
+
import { HttpApi, OpenApi } from "@effect/platform"
|
|
1731
|
+
import { Schema } from "effect"
|
|
1732
|
+
|
|
1733
|
+
const api = HttpApi.make("api")
|
|
1734
|
+
// Provide additional schemas
|
|
1735
|
+
.annotate(HttpApi.AdditionalSchemas, [
|
|
1736
|
+
Schema.String.annotations({ identifier: "MyString" })
|
|
1737
|
+
])
|
|
1738
|
+
// Add a description
|
|
1739
|
+
.annotate(OpenApi.Description, "my description")
|
|
1740
|
+
// Set license information
|
|
1741
|
+
.annotate(OpenApi.License, { name: "MIT", url: "http://example.com" })
|
|
1742
|
+
// Provide a summary
|
|
1743
|
+
.annotate(OpenApi.Summary, "my summary")
|
|
1744
|
+
// Define servers
|
|
1745
|
+
.annotate(OpenApi.Servers, [
|
|
1746
|
+
{
|
|
1747
|
+
url: "http://example.com",
|
|
1748
|
+
description: "example",
|
|
1749
|
+
variables: { a: { default: "b", enum: ["c"], description: "d" } }
|
|
485
1750
|
}
|
|
486
|
-
|
|
487
|
-
|
|
1751
|
+
])
|
|
1752
|
+
// Override parts of the generated specification
|
|
1753
|
+
.annotate(OpenApi.Override, {
|
|
1754
|
+
tags: [{ name: "a", description: "a-description" }]
|
|
1755
|
+
})
|
|
1756
|
+
// Apply a transform function to the final specification
|
|
1757
|
+
.annotate(OpenApi.Transform, (spec) => ({
|
|
1758
|
+
...spec,
|
|
1759
|
+
tags: [...spec.tags, { name: "b", description: "b-description" }]
|
|
1760
|
+
}))
|
|
1761
|
+
|
|
1762
|
+
// Generate the OpenAPI specification from the annotated API
|
|
1763
|
+
const spec = OpenApi.fromApi(api)
|
|
1764
|
+
|
|
1765
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
1766
|
+
/*
|
|
1767
|
+
Output:
|
|
1768
|
+
{
|
|
1769
|
+
"openapi": "3.1.0",
|
|
1770
|
+
"info": {
|
|
1771
|
+
"title": "Api",
|
|
1772
|
+
"version": "0.0.1",
|
|
1773
|
+
"description": "my description",
|
|
1774
|
+
"license": {
|
|
1775
|
+
"name": "MIT",
|
|
1776
|
+
"url": "http://example.com"
|
|
1777
|
+
},
|
|
1778
|
+
"summary": "my summary"
|
|
1779
|
+
},
|
|
1780
|
+
"paths": {},
|
|
1781
|
+
"tags": [
|
|
1782
|
+
{ "name": "a", "description": "a-description" },
|
|
1783
|
+
{ "name": "b", "description": "b-description" }
|
|
1784
|
+
],
|
|
1785
|
+
"components": {
|
|
1786
|
+
"schemas": {
|
|
1787
|
+
"MyString": {
|
|
1788
|
+
"type": "string"
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
"securitySchemes": {}
|
|
1792
|
+
},
|
|
1793
|
+
"security": [],
|
|
1794
|
+
"servers": [
|
|
1795
|
+
{
|
|
1796
|
+
"url": "http://example.com",
|
|
1797
|
+
"description": "example",
|
|
1798
|
+
"variables": {
|
|
1799
|
+
"a": {
|
|
1800
|
+
"default": "b",
|
|
1801
|
+
"enum": [
|
|
1802
|
+
"c"
|
|
1803
|
+
],
|
|
1804
|
+
"description": "d"
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
]
|
|
1809
|
+
}
|
|
1810
|
+
*/
|
|
1811
|
+
```
|
|
1812
|
+
|
|
1813
|
+
#### HttpApiGroup
|
|
1814
|
+
|
|
1815
|
+
The following annotations can be added to an `HttpApiGroup`:
|
|
1816
|
+
|
|
1817
|
+
| Annotation | Description |
|
|
1818
|
+
| ---------------------- | --------------------------------------------------------------------- |
|
|
1819
|
+
| `OpenApi.Description` | Sets a description for this group. |
|
|
1820
|
+
| `OpenApi.ExternalDocs` | Provides external documentation links for the group. |
|
|
1821
|
+
| `OpenApi.Override` | Merges specified fields into the resulting specification. |
|
|
1822
|
+
| `OpenApi.Transform` | Lets you modify the final group specification with a custom function. |
|
|
1823
|
+
| `OpenApi.Exclude` | Excludes the group from the final OpenAPI specification. |
|
|
1824
|
+
|
|
1825
|
+
**Example** (Annotating a Group)
|
|
488
1826
|
|
|
489
|
-
|
|
490
|
-
|
|
1827
|
+
```ts
|
|
1828
|
+
import { HttpApi, HttpApiGroup, OpenApi } from "@effect/platform"
|
|
1829
|
+
|
|
1830
|
+
const api = HttpApi.make("api")
|
|
491
1831
|
.add(
|
|
492
|
-
|
|
493
|
-
//
|
|
494
|
-
.
|
|
1832
|
+
HttpApiGroup.make("group")
|
|
1833
|
+
// Add a description for the group
|
|
1834
|
+
.annotate(OpenApi.Description, "my description")
|
|
1835
|
+
// Provide external documentation links
|
|
1836
|
+
.annotate(OpenApi.ExternalDocs, {
|
|
1837
|
+
url: "http://example.com",
|
|
1838
|
+
description: "example"
|
|
1839
|
+
})
|
|
1840
|
+
// Override parts of the final output
|
|
1841
|
+
.annotate(OpenApi.Override, { name: "my name" })
|
|
1842
|
+
// Transform the final specification for this group
|
|
1843
|
+
.annotate(OpenApi.Transform, (spec) => ({
|
|
1844
|
+
...spec,
|
|
1845
|
+
name: spec.name + "-transformed"
|
|
1846
|
+
}))
|
|
495
1847
|
)
|
|
496
|
-
|
|
497
|
-
|
|
1848
|
+
.add(
|
|
1849
|
+
HttpApiGroup.make("excluded")
|
|
1850
|
+
// Exclude the group from the final specification
|
|
1851
|
+
.annotate(OpenApi.Exclude, true)
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
// Generate the OpenAPI spec
|
|
1855
|
+
const spec = OpenApi.fromApi(api)
|
|
1856
|
+
|
|
1857
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
1858
|
+
/*
|
|
1859
|
+
Output:
|
|
1860
|
+
{
|
|
1861
|
+
"openapi": "3.1.0",
|
|
1862
|
+
"info": {
|
|
1863
|
+
"title": "Api",
|
|
1864
|
+
"version": "0.0.1"
|
|
1865
|
+
},
|
|
1866
|
+
"paths": {},
|
|
1867
|
+
"tags": [
|
|
1868
|
+
{
|
|
1869
|
+
"name": "my name-transformed",
|
|
1870
|
+
"description": "my description",
|
|
1871
|
+
"externalDocs": {
|
|
1872
|
+
"url": "http://example.com",
|
|
1873
|
+
"description": "example"
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
],
|
|
1877
|
+
"components": {
|
|
1878
|
+
"schemas": {},
|
|
1879
|
+
"securitySchemes": {}
|
|
1880
|
+
},
|
|
1881
|
+
"security": []
|
|
1882
|
+
}
|
|
1883
|
+
*/
|
|
498
1884
|
```
|
|
499
1885
|
|
|
500
|
-
|
|
1886
|
+
#### HttpApiEndpoint
|
|
501
1887
|
|
|
502
|
-
|
|
503
|
-
`HttpApiMiddleware.Tag` definition to implement your middleware.
|
|
1888
|
+
For an `HttpApiEndpoint`, you can use the following annotations:
|
|
504
1889
|
|
|
505
|
-
|
|
506
|
-
|
|
1890
|
+
| Annotation | Description |
|
|
1891
|
+
| ---------------------- | --------------------------------------------------------------------------- |
|
|
1892
|
+
| `OpenApi.Description` | Adds a description for this endpoint. |
|
|
1893
|
+
| `OpenApi.Summary` | Provides a short summary of the endpoint's purpose. |
|
|
1894
|
+
| `OpenApi.Deprecated` | Marks the endpoint as deprecated. |
|
|
1895
|
+
| `OpenApi.ExternalDocs` | Supplies external documentation links for the endpoint. |
|
|
1896
|
+
| `OpenApi.Override` | Merges specified fields into the resulting specification for this endpoint. |
|
|
1897
|
+
| `OpenApi.Transform` | Lets you modify the final endpoint specification with a custom function. |
|
|
1898
|
+
| `OpenApi.Exclude` | Excludes the endpoint from the final OpenAPI specification. |
|
|
507
1899
|
|
|
508
|
-
|
|
1900
|
+
**Example** (Annotating an Endpoint)
|
|
509
1901
|
|
|
510
1902
|
```ts
|
|
511
|
-
import {
|
|
512
|
-
|
|
1903
|
+
import {
|
|
1904
|
+
HttpApi,
|
|
1905
|
+
HttpApiEndpoint,
|
|
1906
|
+
HttpApiGroup,
|
|
1907
|
+
OpenApi
|
|
1908
|
+
} from "@effect/platform"
|
|
1909
|
+
import { Schema } from "effect"
|
|
513
1910
|
|
|
514
|
-
|
|
1911
|
+
const api = HttpApi.make("api").add(
|
|
1912
|
+
HttpApiGroup.make("group")
|
|
1913
|
+
.add(
|
|
1914
|
+
HttpApiEndpoint.get("get", "/")
|
|
1915
|
+
.addSuccess(Schema.String)
|
|
1916
|
+
// Add a description
|
|
1917
|
+
.annotate(OpenApi.Description, "my description")
|
|
1918
|
+
// Provide a summary
|
|
1919
|
+
.annotate(OpenApi.Summary, "my summary")
|
|
1920
|
+
// Mark the endpoint as deprecated
|
|
1921
|
+
.annotate(OpenApi.Deprecated, true)
|
|
1922
|
+
// Provide external documentation
|
|
1923
|
+
.annotate(OpenApi.ExternalDocs, {
|
|
1924
|
+
url: "http://example.com",
|
|
1925
|
+
description: "example"
|
|
1926
|
+
})
|
|
1927
|
+
)
|
|
1928
|
+
.add(
|
|
1929
|
+
HttpApiEndpoint.get("excluded", "/excluded")
|
|
1930
|
+
.addSuccess(Schema.String)
|
|
1931
|
+
// Exclude this endpoint from the final specification
|
|
1932
|
+
.annotate(OpenApi.Exclude, true)
|
|
1933
|
+
)
|
|
1934
|
+
)
|
|
515
1935
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1936
|
+
// Generate the OpenAPI spec
|
|
1937
|
+
const spec = OpenApi.fromApi(api)
|
|
1938
|
+
|
|
1939
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
1940
|
+
/*
|
|
1941
|
+
Output:
|
|
1942
|
+
{
|
|
1943
|
+
"openapi": "3.1.0",
|
|
1944
|
+
"info": {
|
|
1945
|
+
"title": "Api",
|
|
1946
|
+
"version": "0.0.1"
|
|
1947
|
+
},
|
|
1948
|
+
"paths": {
|
|
1949
|
+
"/": {
|
|
1950
|
+
"get": {
|
|
1951
|
+
"tags": [
|
|
1952
|
+
"group"
|
|
1953
|
+
],
|
|
1954
|
+
"operationId": "my operationId-transformed",
|
|
1955
|
+
"parameters": [],
|
|
1956
|
+
"security": [],
|
|
1957
|
+
"responses": {
|
|
1958
|
+
"200": {
|
|
1959
|
+
"description": "a string",
|
|
1960
|
+
"content": {
|
|
1961
|
+
"application/json": {
|
|
1962
|
+
"schema": {
|
|
1963
|
+
"type": "string"
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
},
|
|
1968
|
+
"400": {
|
|
1969
|
+
"description": "The request did not match the expected schema",
|
|
1970
|
+
"content": {
|
|
1971
|
+
"application/json": {
|
|
1972
|
+
"schema": {
|
|
1973
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
"description": "my description",
|
|
1980
|
+
"summary": "my summary",
|
|
1981
|
+
"deprecated": true,
|
|
1982
|
+
"externalDocs": {
|
|
1983
|
+
"url": "http://example.com",
|
|
1984
|
+
"description": "example"
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
},
|
|
1989
|
+
...
|
|
1990
|
+
}
|
|
1991
|
+
*/
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1994
|
+
The default response description is "Success". You can override this by annotating the schema.
|
|
1995
|
+
|
|
1996
|
+
**Example** (Defining a custom response description)
|
|
1997
|
+
|
|
1998
|
+
```ts
|
|
1999
|
+
import {
|
|
2000
|
+
HttpApi,
|
|
2001
|
+
HttpApiEndpoint,
|
|
2002
|
+
HttpApiGroup,
|
|
2003
|
+
OpenApi
|
|
2004
|
+
} from "@effect/platform"
|
|
2005
|
+
import { Schema } from "effect"
|
|
2006
|
+
|
|
2007
|
+
const User = Schema.Struct({
|
|
2008
|
+
id: Schema.Number,
|
|
2009
|
+
name: Schema.String,
|
|
2010
|
+
createdAt: Schema.DateTimeUtc
|
|
2011
|
+
}).annotations({ identifier: "User" })
|
|
520
2012
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
yield* Effect.log(`Request: ${request.method} ${request.url}`)
|
|
2013
|
+
const api = HttpApi.make("api").add(
|
|
2014
|
+
HttpApiGroup.make("group").add(
|
|
2015
|
+
HttpApiEndpoint.get("getUsers", "/users").addSuccess(
|
|
2016
|
+
Schema.Array(User).annotations({
|
|
2017
|
+
description: "Returns an array of users"
|
|
527
2018
|
})
|
|
528
2019
|
)
|
|
529
|
-
|
|
2020
|
+
)
|
|
530
2021
|
)
|
|
2022
|
+
|
|
2023
|
+
const spec = OpenApi.fromApi(api)
|
|
2024
|
+
|
|
2025
|
+
console.log(JSON.stringify(spec.paths, null, 2))
|
|
2026
|
+
/*
|
|
2027
|
+
Output:
|
|
2028
|
+
{
|
|
2029
|
+
"/users": {
|
|
2030
|
+
"get": {
|
|
2031
|
+
"tags": [
|
|
2032
|
+
"group"
|
|
2033
|
+
],
|
|
2034
|
+
"operationId": "group.getUsers",
|
|
2035
|
+
"parameters": [],
|
|
2036
|
+
"security": [],
|
|
2037
|
+
"responses": {
|
|
2038
|
+
"200": {
|
|
2039
|
+
"description": "Returns an array of users",
|
|
2040
|
+
"content": {
|
|
2041
|
+
"application/json": {
|
|
2042
|
+
"schema": {
|
|
2043
|
+
"type": "array",
|
|
2044
|
+
"items": {
|
|
2045
|
+
"$ref": "#/components/schemas/User"
|
|
2046
|
+
},
|
|
2047
|
+
"description": "Returns an array of users"
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
},
|
|
2052
|
+
"400": {
|
|
2053
|
+
"description": "The request did not match the expected schema",
|
|
2054
|
+
"content": {
|
|
2055
|
+
"application/json": {
|
|
2056
|
+
"schema": {
|
|
2057
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
*/
|
|
531
2067
|
```
|
|
532
2068
|
|
|
533
|
-
|
|
2069
|
+
### Top Level Groups
|
|
2070
|
+
|
|
2071
|
+
When a group is marked as `topLevel`, the operation IDs of its endpoints do not include the group name as a prefix. This is helpful when you want to group endpoints under a shared tag without adding a redundant prefix to their operation IDs.
|
|
2072
|
+
|
|
2073
|
+
**Example** (Using a Top-Level Group)
|
|
534
2074
|
|
|
535
2075
|
```ts
|
|
536
|
-
|
|
537
|
-
|
|
2076
|
+
import {
|
|
2077
|
+
HttpApi,
|
|
2078
|
+
HttpApiEndpoint,
|
|
2079
|
+
HttpApiGroup,
|
|
2080
|
+
OpenApi
|
|
2081
|
+
} from "@effect/platform"
|
|
2082
|
+
import { Schema } from "effect"
|
|
2083
|
+
|
|
2084
|
+
const api = HttpApi.make("api").add(
|
|
2085
|
+
// Mark the group as top-level
|
|
2086
|
+
HttpApiGroup.make("group", { topLevel: true }).add(
|
|
2087
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
2088
|
+
)
|
|
538
2089
|
)
|
|
2090
|
+
|
|
2091
|
+
// Generate the OpenAPI spec
|
|
2092
|
+
const spec = OpenApi.fromApi(api)
|
|
2093
|
+
|
|
2094
|
+
console.log(JSON.stringify(spec.paths, null, 2))
|
|
2095
|
+
/*
|
|
2096
|
+
Output:
|
|
2097
|
+
{
|
|
2098
|
+
"/": {
|
|
2099
|
+
"get": {
|
|
2100
|
+
"tags": [
|
|
2101
|
+
"group"
|
|
2102
|
+
],
|
|
2103
|
+
"operationId": "get", // The operation ID is not prefixed with "group"
|
|
2104
|
+
"parameters": [],
|
|
2105
|
+
"security": [],
|
|
2106
|
+
"responses": {
|
|
2107
|
+
"200": {
|
|
2108
|
+
"description": "a string",
|
|
2109
|
+
"content": {
|
|
2110
|
+
"application/json": {
|
|
2111
|
+
"schema": {
|
|
2112
|
+
"type": "string"
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
"400": {
|
|
2118
|
+
"description": "The request did not match the expected schema",
|
|
2119
|
+
"content": {
|
|
2120
|
+
"application/json": {
|
|
2121
|
+
"schema": {
|
|
2122
|
+
"$ref": "#/components/schemas/HttpApiDecodeError"
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
*/
|
|
539
2132
|
```
|
|
540
2133
|
|
|
541
|
-
|
|
2134
|
+
## Deriving a Client
|
|
542
2135
|
|
|
543
|
-
|
|
544
|
-
looks a bit different.
|
|
2136
|
+
After defining your API, you can derive a client that interacts with the server. The `HttpApiClient` module simplifies the process by providing tools to generate a client based on your API definition.
|
|
545
2137
|
|
|
546
|
-
|
|
2138
|
+
**Example** (Deriving and Using a Client)
|
|
2139
|
+
|
|
2140
|
+
This example demonstrates how to create a client for an API and use it to call an endpoint.
|
|
547
2141
|
|
|
548
2142
|
```ts
|
|
549
2143
|
import {
|
|
550
|
-
|
|
2144
|
+
FetchHttpClient,
|
|
2145
|
+
HttpApi,
|
|
2146
|
+
HttpApiBuilder,
|
|
2147
|
+
HttpApiClient,
|
|
2148
|
+
HttpApiEndpoint,
|
|
2149
|
+
HttpApiGroup,
|
|
551
2150
|
HttpApiSchema,
|
|
552
|
-
|
|
2151
|
+
HttpApiSwagger,
|
|
2152
|
+
HttpMiddleware,
|
|
2153
|
+
HttpServer
|
|
553
2154
|
} from "@effect/platform"
|
|
554
|
-
import {
|
|
555
|
-
|
|
556
|
-
|
|
2155
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
2156
|
+
import { DateTime, Effect, Layer, Schema } from "effect"
|
|
2157
|
+
import { createServer } from "node:http"
|
|
557
2158
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
)
|
|
2159
|
+
const User = Schema.Struct({
|
|
2160
|
+
id: Schema.Number,
|
|
2161
|
+
name: Schema.String,
|
|
2162
|
+
createdAt: Schema.DateTimeUtc
|
|
2163
|
+
})
|
|
563
2164
|
|
|
564
|
-
|
|
2165
|
+
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
|
|
565
2166
|
|
|
566
|
-
|
|
567
|
-
"
|
|
568
|
-
|
|
569
|
-
failure: Unauthorized,
|
|
570
|
-
provides: CurrentUser,
|
|
571
|
-
security: { myBearer: HttpApiSecurity.bearer }
|
|
572
|
-
}
|
|
573
|
-
) {}
|
|
2167
|
+
const usersGroup = HttpApiGroup.make("users").add(
|
|
2168
|
+
HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
|
|
2169
|
+
)
|
|
574
2170
|
|
|
575
|
-
const
|
|
576
|
-
Authorization,
|
|
577
|
-
Effect.gen(function* () {
|
|
578
|
-
yield* Effect.log("creating Authorization middleware")
|
|
2171
|
+
const api = HttpApi.make("myApi").add(usersGroup)
|
|
579
2172
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
Redacted.value(bearerToken)
|
|
587
|
-
)
|
|
588
|
-
// return the `User` that will be provided as the `CurrentUser`
|
|
589
|
-
return new User({ id: 1 })
|
|
590
|
-
})
|
|
2173
|
+
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
2174
|
+
handlers.handle("getUser", ({ path: { id } }) =>
|
|
2175
|
+
Effect.succeed({
|
|
2176
|
+
id,
|
|
2177
|
+
name: "John Doe",
|
|
2178
|
+
createdAt: DateTime.unsafeNow()
|
|
591
2179
|
})
|
|
592
|
-
|
|
2180
|
+
)
|
|
2181
|
+
)
|
|
2182
|
+
|
|
2183
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
|
|
2184
|
+
|
|
2185
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
2186
|
+
Layer.provide(HttpApiSwagger.layer()),
|
|
2187
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
2188
|
+
Layer.provide(MyApiLive),
|
|
2189
|
+
HttpServer.withLogAddress,
|
|
2190
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
593
2191
|
)
|
|
2192
|
+
|
|
2193
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
2194
|
+
|
|
2195
|
+
// Create a program that derives and uses the client
|
|
2196
|
+
const program = Effect.gen(function* () {
|
|
2197
|
+
// Derive the client
|
|
2198
|
+
const client = yield* HttpApiClient.make(api, {
|
|
2199
|
+
baseUrl: "http://localhost:3000"
|
|
2200
|
+
})
|
|
2201
|
+
// Call the `getUser` endpoint
|
|
2202
|
+
const user = yield* client.users.getUser({ path: { id: 1 } })
|
|
2203
|
+
console.log(user)
|
|
2204
|
+
})
|
|
2205
|
+
|
|
2206
|
+
// Provide a Fetch-based HTTP client and run the program
|
|
2207
|
+
Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
|
|
2208
|
+
/*
|
|
2209
|
+
Example Output:
|
|
2210
|
+
User {
|
|
2211
|
+
id: 1,
|
|
2212
|
+
name: 'John Doe',
|
|
2213
|
+
createdAt: DateTime.Utc(2025-01-04T15:14:49.562Z)
|
|
2214
|
+
}
|
|
2215
|
+
*/
|
|
594
2216
|
```
|
|
595
2217
|
|
|
596
|
-
###
|
|
2218
|
+
### Top Level Groups
|
|
597
2219
|
|
|
598
|
-
|
|
599
|
-
`HttpApiBuilder.securitySetCookie` api.
|
|
2220
|
+
When a group is marked as `topLevel`, the methods on the client are not nested under the group name. This can simplify client usage by providing direct access to the endpoint methods.
|
|
600
2221
|
|
|
601
|
-
|
|
2222
|
+
**Example** (Using a Top-Level Group in the Client)
|
|
602
2223
|
|
|
603
2224
|
```ts
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
2225
|
+
import {
|
|
2226
|
+
HttpApi,
|
|
2227
|
+
HttpApiClient,
|
|
2228
|
+
HttpApiEndpoint,
|
|
2229
|
+
HttpApiGroup
|
|
2230
|
+
} from "@effect/platform"
|
|
2231
|
+
import { Effect, Schema } from "effect"
|
|
608
2232
|
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
2233
|
+
const api = HttpApi.make("api").add(
|
|
2234
|
+
// Mark the group as top-level
|
|
2235
|
+
HttpApiGroup.make("group", { topLevel: true }).add(
|
|
2236
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
613
2237
|
)
|
|
614
2238
|
)
|
|
2239
|
+
|
|
2240
|
+
const program = Effect.gen(function* () {
|
|
2241
|
+
const client = yield* HttpApiClient.make(api, {
|
|
2242
|
+
baseUrl: "http://localhost:3000"
|
|
2243
|
+
})
|
|
2244
|
+
// The `get` method is not nested under the "group" name
|
|
2245
|
+
const user = yield* client.get()
|
|
2246
|
+
console.log(user)
|
|
2247
|
+
})
|
|
615
2248
|
```
|
|
616
2249
|
|
|
617
|
-
##
|
|
2250
|
+
## Converting to a Web Handler
|
|
618
2251
|
|
|
619
|
-
|
|
620
|
-
the server.
|
|
2252
|
+
You can convert your `HttpApi` implementation into a web handler using the `HttpApiBuilder.toWebHandler` API. This approach enables you to serve your API through a custom server setup.
|
|
621
2253
|
|
|
622
|
-
|
|
2254
|
+
**Example** (Creating and Serving a Web Handler)
|
|
623
2255
|
|
|
624
2256
|
```ts
|
|
625
|
-
import {
|
|
2257
|
+
import {
|
|
2258
|
+
HttpApi,
|
|
2259
|
+
HttpApiBuilder,
|
|
2260
|
+
HttpApiEndpoint,
|
|
2261
|
+
HttpApiGroup,
|
|
2262
|
+
HttpApiSwagger,
|
|
2263
|
+
HttpServer
|
|
2264
|
+
} from "@effect/platform"
|
|
2265
|
+
import { Effect, Layer, Schema } from "effect"
|
|
2266
|
+
import * as http from "node:http"
|
|
626
2267
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
2268
|
+
const api = HttpApi.make("myApi").add(
|
|
2269
|
+
HttpApiGroup.make("group").add(
|
|
2270
|
+
HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
|
|
2271
|
+
)
|
|
2272
|
+
)
|
|
2273
|
+
|
|
2274
|
+
const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
|
|
2275
|
+
handlers.handle("get", () => Effect.succeed("Hello, world!"))
|
|
2276
|
+
)
|
|
2277
|
+
|
|
2278
|
+
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))
|
|
2279
|
+
|
|
2280
|
+
const SwaggerLayer = HttpApiSwagger.layer().pipe(Layer.provide(MyApiLive))
|
|
2281
|
+
|
|
2282
|
+
// Convert the API to a web handler
|
|
2283
|
+
const { dispose, handler } = HttpApiBuilder.toWebHandler(
|
|
2284
|
+
Layer.mergeAll(MyApiLive, SwaggerLayer, HttpServer.layerContext)
|
|
2285
|
+
)
|
|
2286
|
+
|
|
2287
|
+
// Serving the handler using a custom HTTP server
|
|
2288
|
+
http
|
|
2289
|
+
.createServer(async (req, res) => {
|
|
2290
|
+
const url = `http://${req.headers.host}${req.url}`
|
|
2291
|
+
const init: RequestInit = {
|
|
2292
|
+
method: req.method!
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const response = await handler(new Request(url, init))
|
|
2296
|
+
|
|
2297
|
+
res.writeHead(
|
|
2298
|
+
response.status,
|
|
2299
|
+
response.statusText,
|
|
2300
|
+
Object.fromEntries(response.headers.entries())
|
|
2301
|
+
)
|
|
2302
|
+
const responseBody = await response.arrayBuffer()
|
|
2303
|
+
res.end(Buffer.from(responseBody))
|
|
2304
|
+
})
|
|
2305
|
+
.listen(3000, () => {
|
|
2306
|
+
console.log("Server running at http://localhost:3000/")
|
|
2307
|
+
})
|
|
2308
|
+
.on("close", () => {
|
|
2309
|
+
dispose()
|
|
632
2310
|
})
|
|
633
|
-
const user = yield* client.users.findById({ path: { userId: 1 } })
|
|
634
|
-
yield* Effect.log(user)
|
|
635
|
-
})
|
|
636
2311
|
```
|
|
637
2312
|
|
|
638
2313
|
# HTTP Client
|