@effect/platform 0.72.1 → 0.72.2

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