@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 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` family of modules provide a declarative way to define HTTP APIs.
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
- After you have defined your API, you can use it to implement a server or derive
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
- ## Defining an API
16
+ Collections of endpoints are grouped in an `HttpApiGroup`, and multiple groups can be merged into a complete `HttpApi`.
20
17
 
21
- To define an API, you need to create a set of endpoints. Each endpoint is
22
- defined by a path, a method, and a set of schemas that define the request and
23
- response types.
18
+ ```
19
+ HttpApi
20
+ ├── HttpGroup
21
+ │ ├── HttpEndpoint
22
+ │ └── HttpEndpoint
23
+ └── HttpGroup
24
+ ├── HttpEndpoint
25
+ ├── HttpEndpoint
26
+ └── HttpEndpoint
27
+ ```
24
28
 
25
- Each set of endpoints is added to an `HttpApiGroup`, which can be combined with
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
- ### Your first `HttpApiGroup`
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
- Let's define a simple CRUD API for managing users. First, we need to make an
31
- `HttpApiGroup` that contains our endpoints.
35
+ Benefits of a Single API Definition:
32
36
 
33
- ```ts
34
- import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
35
- import { Schema } from "effect"
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
- // Our domain "User" Schema
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
- // Our user id path parameter schema
45
- const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
43
+ ### Defining and Implementing an API
46
44
 
47
- const usersApi = HttpApiGroup.make("users")
48
- .add(
49
- // each endpoint has a name and a path
50
- // You can use a template string to define path parameter schemas
51
- HttpApiEndpoint.get("findById")`/users/${UserIdParam}`
52
- // the endpoint can have a Schema for a successful response
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
- You can also extend the `HttpApiGroup` with a class to gain an opaque type.
84
- We will use this API style in the following examples:
53
+ **Example** (Hello World Definition)
85
54
 
86
55
  ```ts
87
- class UsersApi extends HttpApiGroup.make("users").add(
88
- HttpApiEndpoint.get("findById")`/users/${UserIdParam}`
89
- // ... same as above
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
- ### Creating the top level `HttpApi`
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
- Once you have defined your groups, you can combine them into a single `HttpApi`.
78
+ // Provide the implementation for the API
79
+ const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))
96
80
 
97
- ```ts
98
- import { HttpApi } from "@effect/platform"
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
- class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
87
+ // Launch the server
88
+ Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
101
89
  ```
102
90
 
103
- Or with the non-opaque style:
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
- ### Adding OpenApi annotations
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
- You can add OpenApi annotations to your API by using the `OpenApi` module.
101
+ To include Swagger in your server setup, provide the `HttpApiSwagger.layer` when configuring the server.
112
102
 
113
- Let's add a title to our `UsersApi` group:
103
+ **Example** (Serving Swagger Documentation)
114
104
 
115
105
  ```ts
116
- import { OpenApi } from "@effect/platform"
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
- class UsersApi extends HttpApiGroup.make("users")
119
- .add(
120
- HttpApiEndpoint.get("findById")`/users/${UserIdParam}`
121
- // ... same as above
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
- Now when you generate OpenApi documentation, the title and description will be
136
- included.
123
+ const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
124
+ handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
125
+ )
137
126
 
138
- You can also add OpenApi annotations to the top-level `HttpApi`:
127
+ const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))
139
128
 
140
- ```ts
141
- class MyApi extends HttpApi.make("myApi")
142
- .add(UsersApi)
143
- .annotate(OpenApi.Title, "My API") {}
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
- ### Adding errors
139
+ After running the server, open your browser and navigate to http://localhost:3000/docs.
147
140
 
148
- You can add error responses to your endpoints using the following apis:
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
- - `HttpApiEndpoint.addError` - add an error response for a single endpoint
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
+ ![Swagger Documentation](./images/swagger-hello-world.png)
153
144
 
154
- The group & api level errors are useful for adding common error responses that
155
- can be used in middleware.
145
+ ### Deriving a Client
156
146
 
157
- Here is an example of adding a 404 error to the `UsersApi` group:
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
- ```ts
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
- class Unauthorized extends Schema.TaggedError<Unauthorized>()(
167
- "Unauthorized",
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
- class UsersApi extends HttpApiGroup.make("users")
172
- .add(
173
- HttpApiEndpoint.get("findById")`/users/${UserIdParam}`
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
- // or we could add an error to the group
179
- .addError(Unauthorized, { status: 401 }) {}
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
- It is worth noting that you can add multiple error responses to an endpoint,
183
- just by calling `HttpApiEndpoint.addError` multiple times.
201
+ ## Defining a HttpApiEndpoint
184
202
 
185
- ### Multipart requests
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
- If you need to handle file uploads, you can use the `HttpApiSchema.Multipart`
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
- You can then use the schemas from the `Multipart` module to define the expected
191
- shape of the multipart request.
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 { HttpApiSchema, Multipart } from "@effect/platform"
222
+ import { HttpApiEndpoint } from "@effect/platform"
223
+ import { Schema } from "effect"
195
224
 
196
- class UsersApi extends HttpApiGroup.make("users").add(
197
- HttpApiEndpoint.post("upload")`/users/upload`.setPayload(
198
- HttpApiSchema.Multipart(
199
- Schema.Struct({
200
- // add a "files" field to the schema
201
- files: Multipart.FilesSchema
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
- ### Changing the response encoding
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
- By default, the response is encoded as JSON. You can change the encoding using
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
- Here is an example of changing the encoding to text/csv:
251
+ **Example** (Defining Parameters with setPath)
214
252
 
215
253
  ```ts
216
- class UsersApi extends HttpApiGroup.make("users").add(
217
- HttpApiEndpoint.get("csv")`/users/csv`.addSuccess(
218
- Schema.String.pipe(
219
- HttpApiSchema.withEncoding({
220
- kind: "Text",
221
- contentType: "text/csv"
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
- ## Implementing a server
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
- Now that you have defined your API, you can implement a server that serves the
231
- endpoints.
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
- The `HttpApiBuilder` module provides all the apis you need to implement your
234
- server.
290
+ // Create a path parameter using HttpApiSchema.param
291
+ const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
235
292
 
236
- ### Implementing a `HttpApiGroup`
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
- First up, let's implement an `UsersApi` group with a single `findById` endpoint.
299
+ ### POST
239
300
 
240
- The `HttpApiBuilder.group` api takes the `HttpApi` definition, the group name,
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
- Each endpoint is implemented using the `HttpApiBuilder.handle` api.
303
+ **Example** (Defining a POST Endpoint with Payload and Success Schemas)
244
304
 
245
305
  ```ts
246
- import {
247
- HttpApi,
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
- // here is our api definition
256
- class User extends Schema.Class<User>("User")({
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
- // Our user id path parameter schema
263
- const UserIdParam = HttpApiSchema.param("userId", Schema.NumberFromString)
328
+ ### DELETE
264
329
 
265
- class UsersApi extends HttpApiGroup.make("users").add(
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
- class MyApi extends HttpApi.make("myApi").add(UsersApi) {}
332
+ **Example** (Defining a DELETE Endpoint with Path Parameters)
270
333
 
271
- // --------------------------------------------
272
- // Implementation
273
- // --------------------------------------------
334
+ ```ts
335
+ import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
336
+ import { Schema } from "effect"
274
337
 
275
- // the `HttpApiBuilder.group` api returns a `Layer`
276
- const UsersApiLive: Layer.Layer<HttpApiGroup.ApiGroup<"users">> =
277
- HttpApiBuilder.group(MyApi, "users", (handlers) =>
278
- handlers
279
- // the parameters & payload are passed to the handler function.
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
- ### Using services inside a `HttpApiGroup`
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
- If you need to use services inside your handlers, you can return an
295
- `Effect` from the `HttpApiBuilder.group` api.
349
+ **Example** (Defining a PATCH Endpoint for Updating a User)
296
350
 
297
351
  ```ts
298
- class UsersRepository extends Context.Tag("UsersRepository")<
299
- UsersRepository,
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
- // the dependencies will show up in the resulting `Layer`
306
- const UsersApiLive: Layer.Layer<
307
- HttpApiGroup.ApiGroup<"users">,
308
- never,
309
- UsersRepository
310
- > = HttpApiBuilder.group(MyApi, "users", (handlers) =>
311
- // we can return an Effect that creates our handlers
312
- Effect.gen(function* () {
313
- const repository = yield* UsersRepository
314
- return handlers.handle("findById", ({ path: { userId } }) =>
315
- repository.findById(userId)
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
- ### Implementing a `HttpApi`
377
+ ### Catch-All Endpoints
322
378
 
323
- Once all your groups are implemented, you can implement the top-level `HttpApi`.
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
- This is done using the `HttpApiBuilder.api` api, and then using `Layer.provide`
326
- to add all the group implementations.
381
+ **Example** (Defining a Catch-All Endpoint)
327
382
 
328
383
  ```ts
329
- const MyApiLive: Layer.Layer<HttpApi.Api> = HttpApiBuilder.api(MyApi).pipe(
330
- Layer.provide(UsersApiLive)
331
- )
384
+ import { HttpApiEndpoint } from "@effect/platform"
385
+
386
+ const catchAll = HttpApiEndpoint.get("catchAll", "*")
332
387
  ```
333
388
 
334
- ### Serving the API
389
+ ### Setting URL Parameters
335
390
 
336
- Finally, you can serve the API using the `HttpApiBuilder.serve` api.
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
- You can also add middleware to the server using the `HttpMiddleware` module, or
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 { HttpMiddleware, HttpServer } from "@effect/platform"
343
- import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
344
- import { createServer } from "node:http"
396
+ import { HttpApiEndpoint } from "@effect/platform"
397
+ import { Schema } from "effect"
345
398
 
346
- // use the `HttpApiBuilder.serve` function to register our API with the HTTP
347
- // server
348
- const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
349
- // Add CORS middleware
350
- Layer.provide(HttpApiBuilder.middlewareCors()),
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
- // run the server
360
- Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
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
- ### Serving Swagger documentation
420
+ #### Defining an Array of Values for a URL Parameter
364
421
 
365
- You can add Swagger documentation to your API using the `HttpApiSwagger` module.
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
- You just need to provide the `HttpApiSwagger.layer` to your server
368
- implementation:
424
+ **Example** (Defining an Array of String Values for a URL Parameter)
369
425
 
370
426
  ```ts
371
- import { HttpApiSwagger } from "@effect/platform"
427
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
428
+ import { Schema } from "effect"
372
429
 
373
- const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
374
- // add the swagger documentation layer
375
- Layer.provide(
376
- HttpApiSwagger.layer({
377
- // "/docs" is the default path for the swagger documentation
378
- path: "/docs"
379
- })
380
- ),
381
- Layer.provide(HttpApiBuilder.middlewareCors()),
382
- Layer.provide(MyApiLive),
383
- Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
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
- ## Adding middleware
444
+ You can test this endpoint by passing an array of values in the query string. For example:
388
445
 
389
- ### Defining middleware
446
+ ```sh
447
+ curl "http://localhost:3000/?a=1&a=2"
448
+ ```
390
449
 
391
- The `HttpApiMiddleware` module provides a way to add middleware to your API.
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
- You can create a `HttpApiMiddleware.Tag` that represents your middleware, which
394
- allows you to set:
452
+ ### Status Codes
395
453
 
396
- - `failure` - a Schema for any errors that the middleware can return
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
- Here is an example of defining a simple logger middleware:
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
- class LoggerError extends Schema.TaggedError<LoggerError>()(
415
- "LoggerError",
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
- // apply the middleware to an `HttpApiGroup`
426
- class UsersApi extends HttpApiGroup.make("users")
427
- .add(
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
- ### Defining security middleware
437
-
438
- The `HttpApiSecurity` module provides a way to add security annotations to your
439
- API.
473
+ ### Handling Multipart Requests
440
474
 
441
- It offers the following authorization types:
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
- - `HttpApiSecurity.apiKey` - API key authorization through headers, query
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
- You can then use these security annotations in combination with `HttpApiMiddleware`
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
- HttpApiGroup,
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
- class Unauthorized extends Schema.TaggedError<Unauthorized>()(
464
- "Unauthorized",
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
- class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
1675
+ const api = HttpApi.make("myApi").add(usersGroup)
470
1676
 
471
- // first extend the HttpApiMiddleware.Tag class
472
- class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
473
- "Authorization",
474
- {
475
- // add your error schema
476
- failure: Unauthorized,
477
- // add the Context.Tag that the middleware will provide
478
- provides: CurrentUser,
479
- // add the security definitions
480
- security: {
481
- // the object key is a custom name for the security definition
482
- myBearer: HttpApiSecurity.bearer
483
- // You can add more security definitions here.
484
- // They will attempt to be resolved in the order they are defined
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
+ ![Swagger Documentation](./images/swagger-myapi.png)
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
- // apply the middleware to an `HttpApiGroup`
490
- class UsersApi extends HttpApiGroup.make("users")
1827
+ ```ts
1828
+ import { HttpApi, HttpApiGroup, OpenApi } from "@effect/platform"
1829
+
1830
+ const api = HttpApi.make("api")
491
1831
  .add(
492
- HttpApiEndpoint.get("findById")`/${Schema.NumberFromString}`
493
- // apply the middleware to a single endpoint
494
- .middleware(Authorization)
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
- // or apply the middleware to the group
497
- .middleware(Authorization) {}
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
- ### Implementing `HttpApiMiddleware`
1886
+ #### HttpApiEndpoint
501
1887
 
502
- Once your `HttpApiMiddleware` is defined, you can use the
503
- `HttpApiMiddleware.Tag` definition to implement your middleware.
1888
+ For an `HttpApiEndpoint`, you can use the following annotations:
504
1889
 
505
- By using the `Layer` apis, you can create a Layer that implements your
506
- middleware.
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
- Here is an example:
1900
+ **Example** (Annotating an Endpoint)
509
1901
 
510
1902
  ```ts
511
- import { HttpApiMiddleware, HttpServerRequest } from "@effect/platform"
512
- import { Effect, Layer } from "effect"
1903
+ import {
1904
+ HttpApi,
1905
+ HttpApiEndpoint,
1906
+ HttpApiGroup,
1907
+ OpenApi
1908
+ } from "@effect/platform"
1909
+ import { Schema } from "effect"
513
1910
 
514
- class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger") {}
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
- const LoggerLive = Layer.effect(
517
- Logger,
518
- Effect.gen(function* () {
519
- yield* Effect.log("creating Logger middleware")
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
- // standard middleware is just an Effect, that can access the `HttpRouter`
522
- // context.
523
- return Logger.of(
524
- Effect.gen(function* () {
525
- const request = yield* HttpServerRequest.HttpServerRequest
526
- yield* Effect.log(`Request: ${request.method} ${request.url}`)
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
- When the `Layer` is created, you can then provide it to your group layers:
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
- const UsersApiLive = HttpApiBuilder.group(...).pipe(
537
- Layer.provide(LoggerLive)
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
- ### Implementing `HttpApiSecurity` middleware
2134
+ ## Deriving a Client
542
2135
 
543
- If you are using `HttpApiSecurity` in your middleware, implementing the `Layer`
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
- Here is an example of implementing a `HttpApiSecurity.bearer` middleware:
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
- HttpApiMiddleware,
2144
+ FetchHttpClient,
2145
+ HttpApi,
2146
+ HttpApiBuilder,
2147
+ HttpApiClient,
2148
+ HttpApiEndpoint,
2149
+ HttpApiGroup,
551
2150
  HttpApiSchema,
552
- HttpApiSecurity
2151
+ HttpApiSwagger,
2152
+ HttpMiddleware,
2153
+ HttpServer
553
2154
  } from "@effect/platform"
554
- import { Context, Effect, Layer, Redacted, Schema } from "effect"
555
-
556
- class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}
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
- class Unauthorized extends Schema.TaggedError<Unauthorized>()(
559
- "Unauthorized",
560
- {},
561
- HttpApiSchema.annotations({ status: 401 })
562
- ) {}
2159
+ const User = Schema.Struct({
2160
+ id: Schema.Number,
2161
+ name: Schema.String,
2162
+ createdAt: Schema.DateTimeUtc
2163
+ })
563
2164
 
564
- class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
2165
+ const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
565
2166
 
566
- class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
567
- "Authorization",
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 AuthorizationLive = Layer.effect(
576
- Authorization,
577
- Effect.gen(function* () {
578
- yield* Effect.log("creating Authorization middleware")
2171
+ const api = HttpApi.make("myApi").add(usersGroup)
579
2172
 
580
- // return the security handlers
581
- return Authorization.of({
582
- myBearer: (bearerToken) =>
583
- Effect.gen(function* () {
584
- yield* Effect.log(
585
- "checking bearer token",
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
- ### Setting `HttpApiSecurity` cookies
2218
+ ### Top Level Groups
597
2219
 
598
- If you need to set the security cookie from within a handler, you can use the
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
- By default, the cookie will be set with the `HttpOnly` and `Secure` flags.
2222
+ **Example** (Using a Top-Level Group in the Client)
602
2223
 
603
2224
  ```ts
604
- const security = HttpApiSecurity.apiKey({
605
- in: "cookie",
606
- key: "token"
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 UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
610
- handlers.handle("login", () =>
611
- // set the security cookie
612
- HttpApiBuilder.securitySetCookie(security, Redacted.make("keep me secret"))
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
- ## Deriving a client
2250
+ ## Converting to a Web Handler
618
2251
 
619
- Once you have defined your API, you can derive a client that can interact with
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
- The `HttpApiClient` module provides all the apis you need to derive a client.
2254
+ **Example** (Creating and Serving a Web Handler)
623
2255
 
624
2256
  ```ts
625
- import { HttpApiClient } from "@effect/platform"
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
- Effect.gen(function* () {
628
- const client = yield* HttpApiClient.make(MyApi, {
629
- baseUrl: "http://localhost:3000"
630
- // You can transform the HttpClient to add things like authentication
631
- // transformClient: ....
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