@effect/platform 0.72.1 → 0.73.0

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