1# Overview
2
3The `HttpApi` modules let you describe your HTTP API once and use that description to run a server, generate documentation, and create a type-safe client.
4
5An API is built from three building blocks:
6
7- **HttpEndpoint** — a single route (path + HTTP method) with schemas for its request and response.
8- **HttpApiGroup** — a collection of related endpoints (e.g., all user-related routes).
9- **HttpApi** — the top-level object that combines groups into a complete API.
10
11```
12HttpApi
13├── HttpGroup
14│ ├── HttpEndpoint
15│ └── HttpEndpoint
16└── HttpGroup
17 ├── HttpEndpoint
18 ├── HttpEndpoint
19 └── HttpEndpoint
20```
21
22From one API definition you can:
23
24- **Start a server** that implements and serves every endpoint.
25- **Generate documentation** (Scalar or Swagger) automatically.
26- **Derive a client** with a typed method for each endpoint.
27
28One definition powers the server, docs, and client — change it once and everything stays in sync.
29
30# Getting Started
31
32## Defining and Implementing an API
33
34Let's build a minimal API with one endpoint that returns `"Hello, World!"`. You'll define what the endpoint looks like, implement it, and start a server.
35
36```
37HttpApi ("MyApi")
38└── HttpGroup ("Greetings")
39 └── HttpEndpoint ("hello-world")
40```
41
42**Example** (Hello World)
43
44```ts
45import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
46import { Effect, Layer, Schema } from "effect"
47import { HttpRouter } from "effect/http"
48import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
49import { createServer } from "node:http"
50
51// Definition
52const Api = HttpApi.make("MyApi").add(
53 // Define the API group
54 HttpApiGroup.make("Greetings").add(
55 // Define the endpoint
56 HttpApiEndpoint.get("hello", "/", {
57 // Define the success schema
58 success: Schema.String
59 })
60 )
61)
62
63// Implementation
64const GroupLayer = HttpApiBuilder.group(
65 Api,
66 "Greetings", // The name of the group to handle
67 (handlers) =>
68 handlers.handle(
69 "hello", // The name of the endpoint to handle
70 () => Effect.succeed("Hello, World!") // The handler function
71 )
72)
73
74// Server
75const ApiLayer = HttpApiBuilder.layer(Api).pipe(
76 Layer.provide(GroupLayer),
77 HttpRouter.serve,
78 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
79)
80
81// Launch
82Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
83```
84
85After running the code, open a browser and navigate to http://localhost:3000. The server will respond with:
86
87```
88Hello, World!
89```
90
91## Serving The Auto Generated OpenAPI Documentation
92
93Adding a documentation layer gives you an interactive page where you (and your API consumers) can explore endpoints, try requests, and see response shapes — all generated automatically from your API definition. You can choose between the `HttpApiScalar` module (Scalar UI) or the `HttpApiSwagger` module (Swagger UI); both do the same job.
94
95**Example** (Serving Scalar Documentation)
96
97To include Scalar in your server setup, provide the `HttpApiScalar.layer` when configuring the server.
98
99```ts
100const ApiLayer = HttpApiBuilder.layer(Api).pipe(
101 // Provide the Scalar layer so clients can access auto-generated docs
102 Layer.provide(GroupLayer),
103 Layer.provide(HttpApiScalar.layer(Api)),
104 HttpRouter.serve,
105 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
106)
107```
108
109After running the server, open your browser and navigate to http://localhost:3000/docs.
110
111This URL will display the Scalar documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively.
112
113**Example** (Serving Swagger Documentation)
114
115To include Swagger in your server setup, provide the `HttpApiSwagger.layer` when configuring the server.
116
117```ts
118const ApiLayer = HttpApiBuilder.layer(Api).pipe(
119 Layer.provide(GroupLayer),
120 // Provide the Swagger layer so clients can access auto-generated docs
121 Layer.provide(HttpApiSwagger.layer(Api)), // "/docs" is the default path.
122 // or Layer.provide(HttpApiScalar.layer(Api)),
123 HttpRouter.serve,
124 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
125)
126```
127
128After running the server, open your browser and navigate to http://localhost:3000/docs.
129
130This URL will display the Swagger documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively.
131
132## Adding Annotations to Schemas
133
134Annotations attach extra information to your schemas — like a human-readable description or an identifier shown in the docs UI. They don't change runtime behavior; they enrich the generated documentation.
135
136```ts
137const User = Schema.Struct({
138 id: Schema.Int,
139 name: Schema.String
140}).annotate({
141 description: "A user", // The description of the user
142 identifier: "User" // Used in the Scalar UI under the Model section
143})
144```
145
146## Deriving a Client
147
148Once you've defined an API, you can generate a fully typed client from it using the `HttpApiClient` module. The client gives you a method for every endpoint, so calling your API feels like calling a local function — with full type safety and no manual HTTP handling.
149
150**Example** (Deriving and Using a Client)
151
152```ts
153import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
154import { Effect, Layer, Schema } from "effect"
155import { HttpRouter } from "effect/http"
156import { FetchHttpClient } from "effect/http"
157import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
158import { createServer } from "node:http"
159
160const Api = HttpApi.make("MyApi")
161 .add(
162 HttpApiGroup.make("Greetings")
163 .add(
164 HttpApiEndpoint.get("hello", "/", {
165 success: Schema.String
166 })
167 )
168 )
169
170const GroupLayer = HttpApiBuilder.group(
171 Api,
172 "Greetings",
173 (handlers) => handlers.handle("hello", () => Effect.succeed("Hello, World!"))
174)
175
176const ApiLayer = HttpApiBuilder.layer(Api).pipe(
177 Layer.provide(GroupLayer),
178 HttpRouter.serve,
179 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
180)
181
182Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
183
184// Create a program that derives and uses the client
185const program = Effect.gen(function*() {
186 // Derive the client
187 const client = yield* HttpApiClient.make(Api, {
188 baseUrl: "http://localhost:3000"
189 })
190 // Call the "hello-world" endpoint
191 const hello = yield* client.Greetings.hello()
192 console.log(hello)
193})
194
195// Provide a Fetch-based HTTP client and run the program
196Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
197/*
198Output:
199[18:55:26.051] INFO (#2): Listening on http://0.0.0.0:3000
200[18:55:26.057] INFO (#12) http.span=2ms: Sent HTTP response { 'http.method': 'GET', 'http.url': '/', 'http.status': 200 }
201Hello, World!
202*/
203```
204
205# Design Principles
206
207- **Schemas first**: Every piece of data flowing in or out of an endpoint — path params, query strings, headers, payloads, responses, and errors — is described by a schema. The framework uses these schemas to validate requests, serialize responses, generate docs, and type the client.
208- **Metadata lives on schemas**: Configuration like HTTP status codes and content types is attached directly to the schema via annotations, not to the endpoint. This keeps all the information about a data shape in one place.
209
210In particular:
211
212- **Request**
213 - **Payload encoding / content type** is controlled with `HttpApiSchema.as*` helpers:
214 - `asJson` — parse the body as JSON (default)
215 - `asFormUrlEncoded` — parse the body as URL-encoded form data
216 - `asText` — parse the body as plain text
217 - `asUint8Array` — parse the body as raw binary data
218 - `asMultipart` — parse the body as a multipart form (for file uploads)
219 - `asMultipartStream` — parse the body as a streaming multipart form
220- **Response**
221 - **Status code** is set via the `HttpApiSchema.status` API (or `httpApiStatus` annotation)
222 - **Encoding / content type** is controlled with `HttpApiSchema.as*` helpers:
223 - `asJson` — send the body as JSON (default)
224 - `asFormUrlEncoded` — send the body as URL-encoded form data
225 - `asText` — send the body as plain text
226 - `asUint8Array` — send the body as raw binary data
227
228## Anatomy of an Endpoint
229
230An endpoint definition describes everything the framework needs to know about a single HTTP route: which URL parameters it expects, what query strings and headers it reads, what the request body looks like, and what it can respond with (both successes and errors). All of these are optional.
231
232`HttpApiEndpoint` automatically coerces request and response schemas by default. Path / query / header schemas use `Schema.toCodecStringTree`, while JSON payload / success / error schemas use `Schema.toCodecJson`. This means you can define schemas in their natural domain types (for example `Schema.Int`), without manually adding string / JSON transformations.
233
234```ts
235const User = Schema.Struct({
236 id: Schema.String,
237 name: Schema.String
238})
239
240// ┌─── Endpoint name (used in the client as the method name)
241// │ ┌─── Endpoint path
242// ▼ ▼
243HttpApiEndpoint.patch("updateUser", "/user/:id", {
244 // Parameters from the route pattern (e.g. /user/:id).
245 // Can be a record of fields or a full schema.
246 params: {
247 // ┌─── Schema for the "id" parameter.
248 // ▼
249 id: Schema.String
250 },
251
252 // (optional) Query string parameters (e.g. ?mode=merge).
253 // Can be a record of fields or a full schema.
254 query: {
255 // ┌─── Schema for the "mode" query parameter
256 // ▼
257 mode: Schema.Literals(["merge", "replace"])
258 },
259
260 // (optional) Request headers.
261 // Can be a record of fields or a full schema.
262 headers: {
263 "x-api-key": Schema.String,
264 "x-request-id": Schema.String
265 },
266
267 // The request payload can be a single schema or an array of schemas.
268 // - Default encoding is JSON.
269 // - Default status for success is 200.
270 // For GET requests, the payload must be a record of schemas.
271 payload: [
272 // JSON payload (default encoding).
273 Schema.Struct({
274 name: Schema.String
275 }),
276 // text/plain payload.
277 Schema.String.pipe(HttpApiSchema.asText())
278 ],
279
280 // Possible success responses.
281 // Default is 200 OK with no content if omitted.
282 success: [
283 // JSON response (default encoding).
284 User,
285 // text/plain response with a custom status code.
286 Schema.String
287 .pipe(
288 HttpApiSchema.status(206),
289 HttpApiSchema.asText()
290 )
291 ],
292
293 // Possible error responses.
294 error: [
295 // Default is 500 Internal Server Error with JSON encoding.
296 Schema.Finite,
297
298 // text/plain error with a custom status code.
299 Schema.String
300 .pipe(
301 HttpApiSchema.status(404),
302 HttpApiSchema.asText()
303 ),
304
305 // Any schema that encodes to `Schema.Void` is treated as "no content".
306 // Here it uses a custom status code.
307 Schema.Void
308 .pipe(HttpApiSchema.status(401))
309 ]
310})
311```
312
313# Routing
314
315This section walks through defining endpoints for common HTTP methods — GET, POST, DELETE, and PATCH — using a user-management API as a running example:
316
317- `GET /users` — retrieve all users.
318- `GET /users/:userId` — retrieve a specific user by ID.
319- `POST /users` — create a new user.
320- `DELETE /users/:userId` — delete a user by ID.
321- `PATCH /users/:userId` — update a user by ID.
322
323## GET
324
325Use `HttpApiEndpoint.get` to create a GET endpoint. Provide a name (used as the method name in generated clients), a path, and optionally a `success` schema describing what the endpoint returns. Without a success schema the default response is `204 No Content`.
326
327**Example** (Defining a GET Endpoint to Retrieve All Users)
328
329```ts
330import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
331import { Effect, Layer, Schema } from "effect"
332import { HttpRouter } from "effect/http"
333import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
334import { createServer } from "node:http"
335
336// Define a schema representing a User entity
337const User = Schema.Struct({
338 id: Schema.Int,
339 name: Schema.String
340})
341
342const Api = HttpApi.make("MyApi")
343 .add(
344 HttpApiGroup.make("Users")
345 .add(
346 // Define the "getUsers" endpoint, returning a list of users
347 // ┌─── Endpoint name (used in the client as the method name)
348 // │ ┌─── Endpoint path
349 // ▼ ▼
350 HttpApiEndpoint.get("getUsers", "/users", {
351 // ┌─── success schema
352 // │
353 // ▼
354 success: Schema.Array(User)
355 })
356 )
357 )
358
359const GroupLayer = HttpApiBuilder.group(
360 Api,
361 "Users",
362 (handlers) =>
363 handlers.handle("getUsers", () =>
364 Effect.succeed(
365 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
366 ))
367)
368
369const ApiLayer = HttpApiBuilder.layer(Api).pipe(
370 Layer.provide(GroupLayer),
371 Layer.provide(HttpApiScalar.layer(Api)),
372 HttpRouter.serve,
373 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
374)
375
376Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
377```
378
379## POST
380
381Use `HttpApiEndpoint.post` to create an endpoint that accepts data. The `payload` option describes the shape of the request body, and `success` describes what the endpoint returns.
382
383When a group has more than one endpoint, use `.handleAll` to register all the handlers in a single call, keyed by endpoint name. Single endpoints can still be registered one at a time with `.handle`.
384
385**Example** (Defining a POST Endpoint with Payload and Success Schemas)
386
387```ts
388import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
389import { Effect, Layer, Schema } from "effect"
390import { HttpRouter } from "effect/http"
391import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
392import { createServer } from "node:http"
393
394const User = Schema.Struct({
395 id: Schema.Int,
396 name: Schema.String
397})
398
399const Api = HttpApi.make("MyApi")
400 .add(
401 HttpApiGroup.make("Users")
402 .add(
403 HttpApiEndpoint.get("getUsers", "/users", {
404 success: Schema.Array(User)
405 }),
406 HttpApiEndpoint.get("getUser", "/user/:id", {
407 params: {
408 id: Schema.Int
409 },
410 success: User
411 }),
412 // Define a POST endpoint for creating a new user
413 HttpApiEndpoint.post("createUser", "/user", {
414 // Define the request body schema (payload)
415 payload: User,
416 // Define the schema for a successful response
417 success: User
418 })
419 )
420 )
421
422const GroupLayer = HttpApiBuilder.group(
423 Api,
424 "Users",
425 (handlers) =>
426 handlers.handleAll({
427 getUsers: () =>
428 Effect.succeed(
429 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
430 ),
431 getUser: (ctx) => {
432 const id = ctx.params.id
433 return Effect.succeed({ id, name: `User ${id}` })
434 },
435 createUser: (ctx) => {
436 // ┌─── User
437 // ▼
438 const user = ctx.payload
439 return Effect.succeed(user)
440 }
441 })
442)
443
444const ApiLayer = HttpApiBuilder.layer(Api).pipe(
445 Layer.provide(GroupLayer),
446 Layer.provide(HttpApiScalar.layer(Api)),
447 HttpRouter.serve,
448 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
449)
450
451Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
452```
453
454## DELETE
455
456Use `HttpApiEndpoint.delete` to create an endpoint that removes a resource.
457
458**Example** (Defining a DELETE Endpoint with Parameters)
459
460```ts
461import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
462import { Effect, Layer, Schema } from "effect"
463import { HttpRouter } from "effect/http"
464import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
465import { createServer } from "node:http"
466
467const User = Schema.Struct({
468 id: Schema.Int,
469 name: Schema.String
470})
471
472const IdParam = Schema.Int
473
474const Api = HttpApi.make("MyApi")
475 .add(
476 HttpApiGroup.make("Users")
477 .add(
478 HttpApiEndpoint.get("getUsers", "/users", {
479 success: Schema.Array(User)
480 }),
481 HttpApiEndpoint.get("getUser", "/user/:id", {
482 params: {
483 id: IdParam
484 },
485 success: User
486 }),
487 HttpApiEndpoint.post("createUser", "/user", {
488 payload: User,
489 success: User
490 }),
491 HttpApiEndpoint.delete("deleteUser", "/user/:id", {
492 params: {
493 id: IdParam
494 }
495 })
496 )
497 )
498
499const GroupLayer = HttpApiBuilder.group(
500 Api,
501 "Users",
502 (handlers) =>
503 handlers.handleAll({
504 getUsers: () =>
505 Effect.succeed(
506 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
507 ),
508 getUser: (ctx) => {
509 const id = ctx.params.id
510 return Effect.succeed({ id, name: `User ${id}` })
511 },
512 createUser: (ctx) => {
513 const user = ctx.payload
514 return Effect.succeed(user)
515 },
516 deleteUser: (ctx) => {
517 const id = ctx.params.id
518 return Effect.log(`Deleting user ${id}`)
519 }
520 })
521)
522
523const ApiLayer = HttpApiBuilder.layer(Api).pipe(
524 Layer.provide(GroupLayer),
525 Layer.provide(HttpApiScalar.layer(Api)),
526 HttpRouter.serve,
527 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
528)
529
530Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
531```
532
533## PATCH
534
535Use `HttpApiEndpoint.patch` to create an endpoint that partially updates a resource. Like POST, you can define `payload` (the fields to update) and `success` (the response after the update).
536
537**Example** (Defining a PATCH Endpoint for Updating a User)
538
539```ts
540import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
541import { Effect, Layer, Schema } from "effect"
542import { HttpRouter } from "effect/http"
543import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
544import { createServer } from "node:http"
545
546const User = Schema.Struct({
547 id: Schema.Int,
548 name: Schema.String
549})
550
551const IdParam = Schema.Int
552
553const Api = HttpApi.make("MyApi")
554 .add(
555 HttpApiGroup.make("Users")
556 .add(
557 HttpApiEndpoint.get("getUsers", "/users", {
558 success: Schema.Array(User)
559 }),
560 HttpApiEndpoint.get("getUser", "/user/:id", {
561 params: {
562 id: IdParam
563 },
564 success: User
565 }),
566 HttpApiEndpoint.post("createUser", "/user", {
567 payload: User,
568 success: User
569 }),
570 HttpApiEndpoint.delete("deleteUser", "/user/:id", {
571 params: {
572 id: IdParam
573 }
574 }),
575 HttpApiEndpoint.patch("updateUser", "/user/:id", {
576 params: {
577 id: IdParam
578 },
579 // Specify the schema for the request payload
580 payload: Schema.Struct({
581 name: Schema.String // Only the name can be updated
582 }),
583 // Specify the schema for a successful response
584 success: User
585 })
586 )
587 )
588
589const GroupLayer = HttpApiBuilder.group(
590 Api,
591 "Users",
592 (handlers) =>
593 handlers.handleAll({
594 getUsers: () =>
595 Effect.succeed(
596 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
597 ),
598 getUser: (ctx) => {
599 const id = ctx.params.id
600 return Effect.succeed({ id, name: `User ${id}` })
601 },
602 createUser: (ctx) => {
603 const user = ctx.payload
604 return Effect.succeed(user)
605 },
606 deleteUser: (ctx) => {
607 const id = ctx.params.id
608 return Effect.log(`Deleting user ${id}`)
609 },
610 updateUser: (ctx) => {
611 const id = ctx.params.id
612 return Effect.succeed({ id, name: `User ${id}` })
613 }
614 })
615)
616
617const ApiLayer = HttpApiBuilder.layer(Api).pipe(
618 Layer.provide(GroupLayer),
619 Layer.provide(HttpApiScalar.layer(Api)),
620 HttpRouter.serve,
621 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
622)
623
624Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
625```
626
627## Parameters
628
629Path parameters let you capture dynamic values from the URL. For example, `/user/:id` extracts the `id` segment. Use the `params` option to declare a record of fields or a full schema — the framework will parse and validate the value before your handler runs.
630
631**Example** (Defining a GET Endpoint to Retrieve a User by ID)
632
633```ts
634import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
635import { Effect, Layer, Schema } from "effect"
636import { HttpRouter } from "effect/http"
637import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
638import { createServer } from "node:http"
639
640const User = Schema.Struct({
641 id: Schema.Int,
642 name: Schema.String
643})
644
645const Api = HttpApi.make("MyApi")
646 .add(
647 HttpApiGroup.make("Users")
648 .add(
649 HttpApiEndpoint.get("getUsers", "/users", {
650 success: Schema.Array(User)
651 }),
652 // a GET endpoint with a parameter ":id"
653 HttpApiEndpoint.get("getUser", "/user/:id", {
654 params: {
655 // ┌─── schema for the "id" parameter
656 // ▼
657 id: Schema.Int
658 },
659 success: User
660 })
661 )
662 )
663
664const GroupLayer = HttpApiBuilder.group(
665 Api,
666 "Users",
667 (handlers) =>
668 handlers.handleAll({
669 getUsers: () =>
670 Effect.succeed(
671 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
672 ),
673 getUser: (ctx) => {
674 // ┌─── number
675 // ▼
676 const id = ctx.params.id
677 return Effect.succeed({ id, name: `User ${id}` })
678 }
679 })
680)
681
682const ApiLayer = HttpApiBuilder.layer(Api).pipe(
683 Layer.provide(GroupLayer),
684 Layer.provide(HttpApiScalar.layer(Api)),
685 HttpRouter.serve,
686 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
687)
688
689Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
690```
691
692## Catch-All Endpoints
693
694Set the path to `"*"` to match any URL that no other endpoint handles. This is useful for custom "not found" pages or fallback responses.
695
696**Example** (Defining a Catch-All Endpoint)
697
698```ts
699import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
700import { Effect, Layer, Schema } from "effect"
701import { HttpRouter } from "effect/http"
702import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
703import { createServer } from "node:http"
704
705const User = Schema.Struct({
706 id: Schema.Int,
707 name: Schema.String
708})
709
710const IdParam = Schema.Int
711
712const Api = HttpApi.make("MyApi")
713 .add(
714 HttpApiGroup.make("Users")
715 .add(
716 HttpApiEndpoint.get("getUsers", "/users", {
717 success: Schema.Array(User)
718 }),
719 HttpApiEndpoint.get("getUser", "/user/:id", {
720 params: {
721 id: IdParam
722 },
723 success: User
724 }),
725 HttpApiEndpoint.post("createUser", "/user", {
726 payload: User,
727 success: User
728 }),
729 HttpApiEndpoint.delete("deleteUser", "/user/:id", {
730 params: {
731 id: IdParam
732 }
733 }),
734 HttpApiEndpoint.patch("updateUser", "/user/:id", {
735 params: {
736 id: IdParam
737 },
738 payload: Schema.Struct({
739 name: Schema.String
740 }),
741 success: User
742 }),
743 // catch-all endpoint
744 HttpApiEndpoint.get("catchAll", "*", {
745 success: Schema.String
746 })
747 )
748 )
749
750const GroupLayer = HttpApiBuilder.group(
751 Api,
752 "Users",
753 (handlers) =>
754 handlers.handleAll({
755 getUsers: () =>
756 Effect.succeed(
757 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
758 ),
759 getUser: (ctx) => {
760 const id = ctx.params.id
761 return Effect.succeed({ id, name: `User ${id}` })
762 },
763 createUser: (ctx) => {
764 const user = ctx.payload
765 return Effect.succeed(user)
766 },
767 deleteUser: (ctx) => {
768 const id = ctx.params.id
769 return Effect.log(`Deleting user ${id}`)
770 },
771 updateUser: (ctx) => {
772 const id = ctx.params.id
773 return Effect.succeed({ id, name: `User ${id}` })
774 },
775 catchAll: () => {
776 return Effect.succeed("Not found")
777 }
778 })
779)
780
781const ApiLayer = HttpApiBuilder.layer(Api).pipe(
782 Layer.provide(GroupLayer),
783 Layer.provide(HttpApiScalar.layer(Api)),
784 HttpRouter.serve,
785 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
786)
787
788Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
789```
790
791> [!IMPORTANT]
792> The catch-all endpoint must be the last endpoint in the group.
793
794> [!IMPORTANT]
795> (OpenAPI). A catch-all endpoint is not included in the OpenAPI specification because can't be represented as a path.
796
797## Prefixing
798
799Prefixes let you prepend a common path segment to endpoints, groups, or an entire API. This avoids repeating the same base path on every endpoint.
800
801**Example** (Using Prefixes for Common Path Management)
802
803```ts
804import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
805import { Effect, Layer, Schema } from "effect"
806import { HttpRouter } from "effect/http"
807import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
808import { createServer } from "node:http"
809
810const Api = HttpApi.make("MyApi")
811 .add(
812 HttpApiGroup.make("group")
813 .add(
814 HttpApiEndpoint.get("endpointA", "/a", {
815 success: Schema.String
816 })
817 // Prefix for this endpoint
818 .prefix("/endpointPrefix"),
819 HttpApiEndpoint.get("endpointB", "/b", {
820 success: Schema.String
821 })
822 )
823 // Prefix for all endpoints in the group
824 .prefix("/groupPrefix")
825 )
826 // Prefix for the entire API
827 .prefix("/apiPrefix")
828
829const GroupLayer = HttpApiBuilder.group(
830 Api,
831 "group",
832 (handlers) =>
833 handlers.handleAll({
834 endpointA: () => Effect.succeed("Endpoint A"),
835 endpointB: () => Effect.succeed("Endpoint B")
836 })
837)
838
839const ApiLayer = HttpApiBuilder.layer(Api).pipe(
840 Layer.provide(GroupLayer),
841 Layer.provide(HttpApiScalar.layer(Api)),
842 HttpRouter.serve,
843 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
844)
845
846Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
847```
848
849You can test this endpoint using a GET request. For example:
850
851```sh
852curl http://localhost:3000/apiPrefix/groupPrefix/endpointPrefix/a # Returns 200 OK
853curl http://localhost:3000/apiPrefix/groupPrefix/b # Returns 200 OK
854```
855
856# Request
857
858## Query Parameters
859
860Query parameters are the `?key=value` pairs appended to a URL. Use the `query` option to declare a record of fields or a full schema — the framework will parse, validate, and type them for you.
861
862**Example** (Defining Query Parameters with Metadata)
863
864```ts
865import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
866import { Effect, Layer, Schema } from "effect"
867import { HttpRouter } from "effect/http"
868import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
869import { createServer } from "node:http"
870
871const User = Schema.Struct({
872 id: Schema.Int,
873 name: Schema.String
874})
875
876const Page = Schema.Int.check(Schema.isGreaterThan(0))
877
878const Api = HttpApi.make("MyApi")
879 .add(
880 HttpApiGroup.make("Users")
881 .add(
882 HttpApiEndpoint.get("getUsers", "/users", {
883 success: Schema.Array(User),
884 // Specify a schema for each query parameter
885 query: {
886 // Parameter "page" for pagination
887 page: Schema.optionalKey(Page),
888 // Parameter "sort" for sorting options
889 sort: Schema.optionalKey(Schema.Literals(["id", "name"]))
890 }
891 })
892 )
893 )
894
895const GroupLayer = HttpApiBuilder.group(
896 Api,
897 "Users",
898 (handlers) =>
899 handlers
900 .handle("getUsers", (ctx) => {
901 const { page, sort } = ctx.query
902 console.log(`Getting users with page ${page} and sort ${sort}`)
903 return Effect.succeed(
904 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
905 )
906 })
907)
908
909const ApiLayer = HttpApiBuilder.layer(Api).pipe(
910 Layer.provide(GroupLayer),
911 Layer.provide(HttpApiScalar.layer(Api)),
912 HttpRouter.serve,
913 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
914)
915
916Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
917```
918
919#### Defining an Array of Values for a Query Parameter
920
921A single query parameter can carry multiple values (e.g., `?a=1&a=2`). Wrap the parameter's schema in `Schema.Array` to accept an array of values.
922
923**Example** (Defining an Array of String Values for a Query Parameter)
924
925```ts
926import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
927import { Effect, Layer, Schema } from "effect"
928import { HttpRouter } from "effect/http"
929import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
930import { createServer } from "node:http"
931
932const User = Schema.Struct({
933 id: Schema.Int,
934 name: Schema.String
935})
936
937const Api = HttpApi.make("MyApi")
938 .add(
939 HttpApiGroup.make("Users")
940 .add(
941 HttpApiEndpoint.get("getUsers", "/users", {
942 success: Schema.Array(User),
943 query: {
944 a: Schema.optionalKey(Schema.Array(Schema.String))
945 }
946 })
947 )
948 )
949
950const GroupLayer = HttpApiBuilder.group(
951 Api,
952 "Users",
953 (handlers) =>
954 handlers
955 .handle("getUsers", (ctx) => {
956 console.log(ctx.query)
957 return Effect.succeed(
958 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
959 )
960 })
961)
962
963const ApiLayer = HttpApiBuilder.layer(Api).pipe(
964 Layer.provide(GroupLayer),
965 Layer.provide(HttpApiScalar.layer(Api)),
966 HttpRouter.serve,
967 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
968)
969
970Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
971```
972
973You can test this endpoint by passing an array of values in the query string. For example:
974
975```sh
976curl "http://localhost:3000/users?a=1&a=2" # Two values for the `a` parameter
977```
978
979The query string sends two values (`1` and `2`) for the `a` parameter. The server will process and validate these values according to the schema.
980
981Both the following requests will be valid:
982
983```sh
984curl "http://localhost:3000/users" # No values for the `a` parameter
985curl "http://localhost:3000/users?a=1" # One value for the `a` parameter
986```
987
988## Request Headers
989
990Use the `headers` option to declare a record of fields or a full schema for the request headers the endpoint expects.
991
992> [!IMPORTANT]
993> All headers are normalized to lowercase. Always use lowercase keys for the headers.
994
995**Example** (Describe and validate custom headers)
996
997```ts
998import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
999import { Effect, Layer, Schema } from "effect"
1000import { HttpRouter } from "effect/http"
1001import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
1002import { createServer } from "node:http"
1003
1004const User = Schema.Struct({
1005 id: Schema.Int,
1006 name: Schema.String
1007})
1008
1009const Api = HttpApi.make("MyApi")
1010 .add(
1011 HttpApiGroup.make("Users")
1012 .add(
1013 HttpApiEndpoint.get("getUsers", "/users", {
1014 // Always use lowercase keys for the headers
1015 headers: {
1016 "x-api-key": Schema.String,
1017 "x-request-id": Schema.String
1018 },
1019 success: Schema.Array(User)
1020 })
1021 )
1022 )
1023
1024const GroupLayer = HttpApiBuilder.group(
1025 Api,
1026 "Users",
1027 (handlers) =>
1028 handlers.handle("getUsers", () =>
1029 Effect.succeed(
1030 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
1031 ))
1032)
1033
1034const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1035 Layer.provide(GroupLayer),
1036 Layer.provide(HttpApiScalar.layer(Api)),
1037 HttpRouter.serve,
1038 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1039)
1040
1041Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1042```
1043
1044You can test the endpoint by sending the headers:
1045
1046```sh
1047curl -H "X-API-Key: 1234567890" -H "X-Request-ID: 1234567890" http://localhost:3000/users
1048```
1049
1050The server validates these headers against the declared schema before handling the request.
1051
1052## Handling Multipart Requests
1053
1054To accept file uploads, mark the payload as multipart with `HttpApiSchema.asMultipart`. Use `Multipart.FilesSchema` for the file fields — uploaded files will be persisted to disk automatically.
1055
1056**Example** (Defining an Endpoint for File Uploads)
1057
1058```ts
1059import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1060import { Effect, Layer, Schema } from "effect"
1061import { HttpRouter, Multipart } from "effect/http"
1062import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar, HttpApiSchema } from "effect/http-api"
1063import { createServer } from "node:http"
1064
1065const Api = HttpApi.make("MyApi")
1066 .add(
1067 HttpApiGroup.make("Users")
1068 .add(
1069 HttpApiEndpoint.post("upload", "/users/upload", {
1070 // Specify that the payload is a multipart request
1071 payload: HttpApiSchema.asMultipart(
1072 Schema.Struct({
1073 // Define a "files" field to handle file uploads
1074 files: Multipart.FilesSchema
1075 })
1076 ),
1077 success: Schema.String
1078 })
1079 )
1080 )
1081
1082const GroupLayer = HttpApiBuilder.group(
1083 Api,
1084 "Users",
1085 (handlers) =>
1086 handlers
1087 .handle("upload", (ctx) => {
1088 // ┌─── readonly Multipart.PersistedFile[]
1089 // ▼
1090 const { files } = ctx.payload
1091 console.log(files)
1092 return Effect.succeed("Uploaded")
1093 })
1094)
1095
1096const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1097 Layer.provide(GroupLayer),
1098 Layer.provide(HttpApiScalar.layer(Api)),
1099 HttpRouter.serve,
1100 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1101)
1102
1103Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1104```
1105
1106You can test this endpoint by sending a multipart request with a file upload. For example:
1107
1108```sh
1109echo "Sample file content" | curl -X POST -F "files=@-" http://localhost:3000/users/upload
1110```
1111
1112## Changing the Request Encoding
1113
1114By default, request bodies are JSON. To accept a different format — like form-urlencoded data — pipe the payload schema through the appropriate `HttpApiSchema.as*` helper.
1115
1116**Example** (Customizing Request Encoding)
1117
1118```ts
1119import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1120import { Effect, Layer, Schema } from "effect"
1121import { HttpRouter } from "effect/http"
1122import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar, HttpApiSchema } from "effect/http-api"
1123import { createServer } from "node:http"
1124
1125const User = Schema.Struct({
1126 id: Schema.Int,
1127 name: Schema.String
1128})
1129
1130const Api = HttpApi.make("MyApi")
1131 .add(
1132 HttpApiGroup.make("Users")
1133 .add(
1134 HttpApiEndpoint.post("createUser", "/user", {
1135 // Set the request payload as a string encoded with query parameters
1136 payload: Schema.Struct({
1137 id: Schema.Int,
1138 name: Schema.String
1139 })
1140 // Specify the encoding as form url encoded
1141 .pipe(HttpApiSchema.asFormUrlEncoded()),
1142 success: User
1143 })
1144 )
1145 )
1146
1147const GroupLayer = HttpApiBuilder.group(
1148 Api,
1149 "Users",
1150 (handlers) =>
1151 handlers
1152 .handle("createUser", (ctx) => {
1153 const user = ctx.payload
1154 return Effect.succeed(user)
1155 })
1156)
1157
1158const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1159 Layer.provide(GroupLayer),
1160 Layer.provide(HttpApiScalar.layer(Api)),
1161 HttpRouter.serve,
1162 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1163)
1164
1165Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1166```
1167
1168You can test this endpoint using a URL-encoded request body. For example:
1169
1170```sh
1171curl http://localhost:3000/user \
1172 --request POST \
1173 --header 'Accept: */*' \
1174 --header 'Content-Type: application/x-www-form-urlencoded' \
1175 --data-urlencode 'id=1' \
1176 --data-urlencode 'name=John'
1177```
1178
1179## Accessing the HttpServerRequest
1180
1181Inside a handler, `ctx.request` gives you access to the raw incoming HTTP request. Use this when you need low-level details not covered by the endpoint schema (e.g., the HTTP method or raw URL).
1182
1183**Example** (Accessing the Request Object in a GET Endpoint)
1184
1185```ts
1186import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1187import { Effect, Layer, Schema } from "effect"
1188import { HttpRouter } from "effect/http"
1189import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
1190import { createServer } from "node:http"
1191
1192const Api = HttpApi.make("MyApi").add(
1193 HttpApiGroup.make("Greetings").add(
1194 HttpApiEndpoint.get("hello", "/", {
1195 success: Schema.String
1196 })
1197 )
1198)
1199
1200const GroupLayer = HttpApiBuilder.group(
1201 Api,
1202 "Greetings",
1203 (handlers) =>
1204 handlers.handle(
1205 "hello",
1206 (ctx) => {
1207 // ┌─── HttpServerRequest
1208 // ▼
1209 const req = ctx.request
1210 // Access the request method
1211 console.log(req.method)
1212 return Effect.succeed("Hello, World!")
1213 }
1214 )
1215)
1216
1217const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1218 Layer.provide(GroupLayer),
1219 HttpRouter.serve,
1220 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1221)
1222
1223Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1224```
1225
1226## Validating Request Cookies
1227
1228There is no `cookies` option on endpoints. Instead, validated cookie access goes through the security middleware system: define an `HttpApiSecurity.apiKey` with `in: "cookie"` and attach it to a middleware. The cookie value is decoded and handed to your security handler as a `Redacted` credential.
1229
1230**Example** (Validating a Session Cookie)
1231
1232```ts
1233import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1234import { Context, Effect, Layer, Redacted, Schema } from "effect"
1235import { HttpRouter } from "effect/http"
1236import {
1237 HttpApi,
1238 HttpApiBuilder,
1239 HttpApiEndpoint,
1240 HttpApiGroup,
1241 HttpApiMiddleware,
1242 HttpApiScalar,
1243 HttpApiSecurity
1244} from "effect/http-api"
1245import { createServer } from "node:http"
1246
1247// Define the service providing the current user
1248class CurrentUser
1249 extends Context.Service<CurrentUser, { readonly id: number; readonly name: string }>()("CurrentUser")
1250{}
1251
1252// Define the security scheme: read the "session" cookie
1253const sessionCookie = HttpApiSecurity.apiKey({ in: "cookie", key: "session" })
1254
1255class Auth extends HttpApiMiddleware.Service<Auth, {
1256 provides: CurrentUser
1257}>()("Auth", {
1258 error: Schema.String.annotate({
1259 httpApiStatus: 401,
1260 description: "Auth error"
1261 }),
1262 security: { session: sessionCookie }
1263}) {}
1264
1265const Api = HttpApi.make("api").add(
1266 HttpApiGroup.make("group")
1267 .add(
1268 HttpApiEndpoint.get("me", "/me", {
1269 success: Schema.Struct({ id: Schema.Finite })
1270 })
1271 )
1272 .middleware(Auth)
1273)
1274
1275const AuthLayer = Layer.succeed(
1276 Auth,
1277 {
1278 session: (effect, opts) =>
1279 Effect.provideServiceEffect(
1280 effect,
1281 CurrentUser,
1282 Effect.gen(function*() {
1283 const value = Redacted.value(opts.credential)
1284 if (value !== "valid-session") {
1285 return yield* Effect.fail("Invalid session")
1286 }
1287 return { id: 1, name: "John Doe" }
1288 })
1289 )
1290 }
1291)
1292
1293const GroupLayer = HttpApiBuilder.group(
1294 Api,
1295 "group",
1296 (handlers) =>
1297 handlers.handle("me", () =>
1298 Effect.gen(function*() {
1299 const user = yield* CurrentUser
1300 return { id: user.id }
1301 }))
1302).pipe(Layer.provide(AuthLayer))
1303
1304const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1305 Layer.provide(GroupLayer),
1306 Layer.provide(HttpApiScalar.layer(Api)),
1307 HttpRouter.serve,
1308 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1309)
1310
1311Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1312
1313// Valid session:
1314// curl "http://localhost:3000/me" --cookie "session=valid-session"
1315// {"id":1}
1316//
1317// Invalid session:
1318// curl "http://localhost:3000/me" --cookie "session=wrong"
1319// "Invalid session"
1320```
1321
1322For quick, unvalidated access you can read cookies directly from `ctx.request.cookies` inside any handler. These cookies won't appear in the OpenAPI spec.
1323
1324**Example** (Reading Cookies Directly in a Handler)
1325
1326```ts
1327import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1328import { Effect, Layer, Schema } from "effect"
1329import { HttpRouter } from "effect/http"
1330import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
1331import { createServer } from "node:http"
1332
1333const Api = HttpApi.make("api").add(
1334 HttpApiGroup.make("group").add(
1335 HttpApiEndpoint.get("me", "/me", {
1336 success: Schema.String
1337 })
1338 )
1339)
1340
1341const GroupLayer = HttpApiBuilder.group(
1342 Api,
1343 "group",
1344 (handlers) =>
1345 handlers.handle("me", (ctx) => {
1346 const lang = ctx.request.cookies.lang ?? "en"
1347 return Effect.succeed(`Language: ${lang}`)
1348 })
1349)
1350
1351const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1352 Layer.provide(GroupLayer),
1353 HttpRouter.serve,
1354 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1355)
1356
1357Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1358
1359// curl "http://localhost:3000/me" --cookie "lang=it"
1360// "Language: it"
1361```
1362
1363## Streaming Requests
1364
1365To receive large or continuous data from the client, register the endpoint with `.handleRaw`, which opts out of automatic payload decoding and exposes the raw `HttpServerRequest`. The request body is then available as a `Stream` of `Uint8Array` chunks through `request.stream`, so the handler can consume it incrementally instead of buffering it in memory.
1366
1367The payload schema still describes the endpoint in the generated documentation, but with `.handleRaw` the handler decides how the body is consumed. Note that the declared content type is no longer enforced at runtime: requests with a different `Content-Type` header are not rejected with `415`, so any such validation is up to the handler.
1368
1369**Example** (Handling Streaming Requests)
1370
1371```ts
1372import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1373import { Effect, Layer, Schema, Stream } from "effect"
1374import { HttpRouter } from "effect/http"
1375import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api"
1376import { createServer } from "node:http"
1377
1378const Api = HttpApi.make("myApi").add(
1379 HttpApiGroup.make("group").add(
1380 HttpApiEndpoint.post("acceptStream", "/stream", {
1381 // Define the payload as a Uint8Array with a specific encoding
1382 payload: Schema.Uint8Array.pipe(
1383 HttpApiSchema.asUint8Array() // default content type: application/octet-stream
1384 ),
1385 success: Schema.String
1386 })
1387 )
1388)
1389
1390const GroupLayer = HttpApiBuilder.group(
1391 Api,
1392 "group",
1393 (handlers) =>
1394 // Use `handleRaw` to opt out of payload decoding and access the raw request
1395 handlers.handleRaw("acceptStream", (ctx) =>
1396 // Consume the request body as a stream of Uint8Array chunks
1397 ctx.request.stream.pipe(
1398 // Fold over the chunks as they arrive, without buffering the body
1399 Stream.runFold(() => 0, (total, chunk) => total + chunk.length),
1400 Effect.map((total) => `received ${total} bytes`),
1401 // `request.stream` fails with `HttpServerError`, which the endpoint
1402 // does not declare, so treat it as a defect
1403 Effect.orDie
1404 ))
1405)
1406
1407const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1408 Layer.provide(GroupLayer),
1409 HttpRouter.serve,
1410 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1411)
1412
1413Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1414```
1415
1416You can test the streaming request using `curl` or any tool that supports sending binary data. For example:
1417
1418```sh
1419printf 'abc' | curl -X POST 'http://localhost:3000/stream' --data-binary @- -H "Content-Type: application/octet-stream"
1420# Output: "received 3 bytes"
1421```
1422
1423# Response
1424
1425## Status Codes
1426
1427Success responses default to `200 OK`. To use a different status code, annotate the success schema with `HttpApiSchema.status(code)` or set the `httpApiStatus` annotation.
1428
1429**Example** (Defining a GET Endpoint with a custom status code)
1430
1431```ts
1432import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1433import { Effect, Layer, Schema } from "effect"
1434import { HttpRouter } from "effect/http"
1435import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar, HttpApiSchema } from "effect/http-api"
1436import { createServer } from "node:http"
1437
1438const User = Schema.Struct({
1439 id: Schema.Int,
1440 name: Schema.String
1441})
1442
1443const Api = HttpApi.make("MyApi")
1444 .add(
1445 HttpApiGroup.make("Users")
1446 .add(
1447 HttpApiEndpoint.get("getUsers", "/users", {
1448 success: Schema.Array(User)
1449 .pipe(HttpApiSchema.status(206))
1450 })
1451 )
1452 )
1453
1454const GroupLayer = HttpApiBuilder.group(
1455 Api,
1456 "Users",
1457 (handlers) =>
1458 handlers
1459 .handle("getUsers", () => {
1460 return Effect.succeed(
1461 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
1462 )
1463 })
1464)
1465
1466const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1467 Layer.provide(GroupLayer),
1468 Layer.provide(HttpApiScalar.layer(Api)),
1469 HttpRouter.serve,
1470 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1471)
1472
1473Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1474```
1475
1476## Changing the Response Encoding
1477
1478Responses default to JSON. To return a different format — like CSV or plain text — pipe the success schema through the matching `HttpApiSchema.as*` helper and, optionally, set a custom `contentType`.
1479
1480**Example** (Returning Data as `text/csv`)
1481
1482```ts
1483import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1484import { Effect, Layer, Schema } from "effect"
1485import { HttpRouter } from "effect/http"
1486import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar, HttpApiSchema } from "effect/http-api"
1487import { createServer } from "node:http"
1488
1489const Api = HttpApi.make("MyApi")
1490 .add(
1491 HttpApiGroup.make("Users")
1492 .add(
1493 HttpApiEndpoint.get("csv", "/users/csv", {
1494 success: Schema.String.pipe(
1495 // Set the success response as a string with CSV encoding
1496 HttpApiSchema.asText({
1497 // Define the content type as text/csv
1498 contentType: "text/csv"
1499 })
1500 )
1501 })
1502 )
1503 )
1504
1505const GroupLayer = HttpApiBuilder.group(
1506 Api,
1507 "Users",
1508 (handlers) =>
1509 handlers
1510 .handle("csv", (ctx) => {
1511 return Effect.succeed("id,name\n1,John\n2,Jane")
1512 })
1513)
1514
1515const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1516 Layer.provide(GroupLayer),
1517 Layer.provide(HttpApiScalar.layer(Api)),
1518 HttpRouter.serve,
1519 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1520)
1521
1522Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1523```
1524
1525You can test this endpoint using a GET request. For example:
1526
1527```sh
1528curl http://localhost:3000/users/csv
1529```
1530
1531The following encodings are supported:
1532
1533- `Json` the default encoding (default content type: `application/json`)
1534- `Uint8Array` the encoding for binary data (default content type: `application/octet-stream`)
1535- `Text` the encoding for text data (default content type: `text/plain`)
1536
1537## Setting Response Headers
1538
1539Response headers can be declared in the endpoint's schemas, so they are type-checked on the server, rendered in the OpenAPI documentation, and decoded by the derived client. Two mechanisms are available:
1540
1541- `HttpApiSchema.WithHeaders(schema, headers)` wraps a response schema together with a headers schema. Handlers return the body and headers as a pair. Recommended for success responses, including streams.
1542- `HttpApiSchema.encodeToWithHeaders` folds headers into an opaque domain type such as an error class, so handlers keep working with plain domain values.
1543
1544Only one response schema carrying headers may be declared for each status, though plain responses with different content types may share that status.
1545
1546For headers that are not part of the API contract, `HttpEffect.appendPreResponseHandler` remains available as an untyped escape hatch.
1547
1548### Declaring Response Headers with WithHeaders
1549
1550Wrap the success schema with `HttpApiSchema.WithHeaders(schema, headers)`. The headers argument accepts a fields shorthand (as below) or any schema, mirroring the request-side `headers` option. The handler then returns a value built with `HttpApiSchema.withHeaders({ body, headers })`.
1551
1552**Example** (Declaring a Response Header on a Success Schema)
1553
1554```ts
1555import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1556import { Effect, Layer, Schema } from "effect"
1557import { HttpRouter } from "effect/http"
1558import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api"
1559import { createServer } from "node:http"
1560
1561const User = Schema.Struct({
1562 id: Schema.Int,
1563 name: Schema.String
1564})
1565
1566const Api = HttpApi.make("MyApi").add(
1567 HttpApiGroup.make("Users").add(
1568 HttpApiEndpoint.get("getUsers", "/users", {
1569 // Wrap the success schema with a response headers schema
1570 success: HttpApiSchema.WithHeaders(Schema.Array(User), {
1571 "x-total-count": Schema.Int
1572 })
1573 })
1574 )
1575)
1576
1577const GroupLayer = HttpApiBuilder.group(
1578 Api,
1579 "Users",
1580 (handlers) =>
1581 handlers.handle("getUsers", () =>
1582 // Return the body together with the declared headers
1583 Effect.succeed(HttpApiSchema.withHeaders({
1584 body: [{ id: 1, name: "John" }],
1585 headers: { "x-total-count": 1 }
1586 })))
1587)
1588
1589const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1590 Layer.provide(GroupLayer),
1591 HttpRouter.serve,
1592 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1593)
1594
1595Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1596
1597// curl -v "http://localhost:3000/users" 2>&1 | grep -i "x-total-count"
1598// < x-total-count: 1
1599```
1600
1601The derived client detects the wrapper and returns the same shape, with the headers decoded through the headers schema:
1602
1603```ts
1604const users = yield * client.Users.getUsers()
1605users.body // => [{ id: 1, name: "John" }]
1606users.headers // => { "x-total-count": 1 }
1607```
1608
1609Things to know:
1610
1611- Header values are converted to strings at the HTTP boundary, the same way as request headers, params, and query. `Schema.Int` goes out as `"1"` and decodes back to `1` on the client. `undefined` values are omitted from the response.
1612- Status and encoding annotations resolve from the wrapper first, then fall through to the inner schema, so `HttpApiSchema.WithHeaders(User.pipe(HttpApiSchema.status(201)), ...)` responds with `201`.
1613- Declared headers are applied after the body is encoded and override headers set by the encoding on collision, including `content-type`.
1614- Stream success schemas (`HttpApiSchema.StreamSse`, `HttpApiSchema.StreamUint8Array`) can be wrapped too. Headers are encoded before the response starts streaming, and the client resolves to a value whose `body` is the stream.
1615- `WithHeaders` is also allowed on error schemas, in which case the handler fails with the wrapped value. For errors, `encodeToWithHeaders` (below) is usually more convenient because handlers can fail with the domain error directly.
1616
1617### Folding Headers into Domain Types with encodeToWithHeaders
1618
1619`HttpApiSchema.encodeToWithHeaders` encodes a schema as a `{ body, headers }` pair while its Type stays unchanged. This lets an error class carry data that travels in a response header: handlers fail with plain error instances, and the client receives the same class with the header folded back in.
1620
1621The body schema is authoritative for everything wire-level: status, content type, and response encoding resolve from the body schema's annotations. A status annotation on the source schema stops mattering once wrapped, so spell the status on the body — `HttpApiSchema.Empty(404)` declares an empty body with status 404.
1622
1623**Example** (Returning an Error With a Response Header)
1624
1625```ts
1626import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1627import { Effect, Layer, Schema } from "effect"
1628import { HttpRouter } from "effect/http"
1629import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api"
1630import { createServer } from "node:http"
1631
1632class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
1633 userId: Schema.Int
1634}) {}
1635
1636const UserNotFoundWithHeaders = UserNotFound.pipe(
1637 HttpApiSchema.encodeToWithHeaders({
1638 // The body schema is authoritative for status and content type
1639 body: HttpApiSchema.Empty(404),
1640 headers: {
1641 "x-user-id": Schema.Int
1642 }
1643 }, {
1644 // Pure mappings between the domain type and the { body, headers } pair
1645 decode: ({ headers }) => new UserNotFound({ userId: headers["x-user-id"] }),
1646 encode: (error) => ({
1647 headers: { "x-user-id": error.userId },
1648 body: undefined
1649 })
1650 })
1651)
1652
1653const User = Schema.Struct({
1654 id: Schema.Int,
1655 name: Schema.String
1656})
1657
1658const Api = HttpApi.make("MyApi").add(
1659 HttpApiGroup.make("Users").add(
1660 HttpApiEndpoint.get("getUser", "/user/:id", {
1661 params: {
1662 id: Schema.Int
1663 },
1664 success: User,
1665 error: UserNotFoundWithHeaders
1666 })
1667 )
1668)
1669
1670const GroupLayer = HttpApiBuilder.group(
1671 Api,
1672 "Users",
1673 (handlers) =>
1674 handlers.handle("getUser", (ctx) => {
1675 const id = ctx.params.id
1676 if (id === 1) {
1677 // Fail with the plain error instance
1678 return Effect.fail(new UserNotFound({ userId: id }))
1679 }
1680 return Effect.succeed({ id, name: `User ${id}` })
1681 })
1682)
1683
1684const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1685 Layer.provide(GroupLayer),
1686 HttpRouter.serve,
1687 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1688)
1689
1690Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1691
1692// curl -v "http://localhost:3000/user/1" 2>&1 | grep -i "x-user-id"
1693// < x-user-id: 1
1694```
1695
1696The `decode`/`encode` mappings are pure total functions: validation lives in the body and headers schemas, the mappings only reshape valid data. The error channel is unchanged — a client calling this endpoint fails with a `UserNotFound` instance whose `userId` was decoded from the header.
1697
1698`encodeToWithHeaders` also works on custom success types, but avoid burying stream schemas in it; wrap streams with `WithHeaders` instead so the generated client keeps the stream's error channel.
1699
1700### Untyped Response Headers
1701
1702For headers that should not appear in the API contract, call `HttpEffect.appendPreResponseHandler` inside your handler. The callback receives the request and response objects and must return the updated response. These headers bypass the schemas, the OpenAPI documentation, and the derived client.
1703
1704**Example** (Adding a Custom Response Header)
1705
1706```ts
1707import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1708import { Effect, Layer, Schema } from "effect"
1709import { HttpEffect, HttpRouter, HttpServerResponse } from "effect/http"
1710import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
1711import { createServer } from "node:http"
1712
1713const Api = HttpApi.make("api").add(
1714 HttpApiGroup.make("group").add(
1715 HttpApiEndpoint.get("hello", "/hello", {
1716 success: Schema.String
1717 })
1718 )
1719)
1720
1721const GroupLayer = HttpApiBuilder.group(
1722 Api,
1723 "group",
1724 (handlers) =>
1725 handlers.handle("hello", () =>
1726 Effect.gen(function*() {
1727 yield* HttpEffect.appendPreResponseHandler((_req, response) =>
1728 Effect.succeed(HttpServerResponse.setHeader(response, "x-custom", "hello"))
1729 )
1730 return "Hello, World!"
1731 }))
1732)
1733
1734const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1735 Layer.provide(GroupLayer),
1736 HttpRouter.serve,
1737 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1738)
1739
1740Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1741
1742// curl -v "http://localhost:3000/hello" 2>&1 | grep -i "x-custom"
1743// < x-custom: hello
1744```
1745
1746## Setting Response Cookies
1747
1748Set cookies on the response using `HttpEffect.appendPreResponseHandler` together with `HttpServerResponse.setCookie`. For cookies tied to an `HttpApiSecurity.apiKey`, use the shortcut `HttpApiBuilder.securitySetCookie` instead.
1749
1750**Example** (Setting a Response Cookie)
1751
1752```ts
1753import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1754import { Effect, Layer, Schema } from "effect"
1755import { HttpEffect, HttpRouter, HttpServerResponse } from "effect/http"
1756import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
1757import { createServer } from "node:http"
1758
1759const Api = HttpApi.make("api").add(
1760 HttpApiGroup.make("group").add(
1761 HttpApiEndpoint.get("hello", "/hello", {
1762 success: Schema.String
1763 })
1764 )
1765)
1766
1767const GroupLayer = HttpApiBuilder.group(
1768 Api,
1769 "group",
1770 (handlers) =>
1771 handlers.handle("hello", () =>
1772 Effect.gen(function*() {
1773 yield* HttpEffect.appendPreResponseHandler((_req, response) =>
1774 Effect.succeed(HttpServerResponse.setCookieUnsafe(response, "my-cookie", "my-value", {
1775 httpOnly: true,
1776 secure: true,
1777 path: "/"
1778 }))
1779 )
1780 return "Hello, World!"
1781 }))
1782)
1783
1784const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1785 Layer.provide(GroupLayer),
1786 HttpRouter.serve,
1787 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1788)
1789
1790Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1791
1792// curl -v "http://localhost:3000/hello" 2>&1 | grep -i "set-cookie"
1793// < set-cookie: my-cookie=my-value; Path=/; HttpOnly; Secure
1794```
1795
1796## Redirects
1797
1798To redirect the client to a different URL, return an `HttpServerResponse.redirect` from the handler. The redirect is not modeled in the schema — the endpoint definition stays as "no content".
1799
1800```ts
1801import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1802import { Effect, Layer, Schema } from "effect"
1803import { HttpRouter } from "effect/http"
1804import { HttpServerResponse } from "effect/http"
1805import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
1806import { createServer } from "node:http"
1807
1808const Api = HttpApi.make("MyApi").add(
1809 HttpApiGroup.make("group").add(
1810 HttpApiEndpoint.get("newPage", "/new", {
1811 success: Schema.String
1812 }),
1813 // Schema-wise this is just "no content" (redirect headers aren't modeled here)
1814 HttpApiEndpoint.get("oldPage", "/old")
1815 )
1816)
1817
1818const GroupLayer = HttpApiBuilder.group(
1819 Api,
1820 "group",
1821 (handlers) =>
1822 handlers.handleAll({
1823 newPage: () => Effect.succeed("You are on /new"),
1824 oldPage: () =>
1825 Effect.succeed(
1826 HttpServerResponse.redirect("/new", { status: 302 })
1827 )
1828 })
1829)
1830
1831const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1832 Layer.provide(GroupLayer),
1833 Layer.provide(HttpApiScalar.layer(Api)),
1834 HttpRouter.serve,
1835 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1836)
1837
1838Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1839
1840// curl "http://localhost:3000/old" -L
1841```
1842
1843## Streaming Responses
1844
1845To stream data to the client over time, declare the success schema with one of the streaming constructors:
1846
1847- `HttpApiSchema.StreamUint8Array()` streams raw bytes (default content type: `application/octet-stream`)
1848- `HttpApiSchema.StreamSse(options)` streams typed Server-Sent Events (default content type: `text/event-stream`)
1849
1850With a streaming success schema, the handler returns a `Stream` directly, and the derived client resolves to a typed `Stream` on the consuming side. The stream declaration is also rendered in the OpenAPI documentation.
1851
1852### Streaming Raw Bytes
1853
1854`HttpApiSchema.StreamUint8Array` declares a binary streaming response. The handler returns a `Stream<Uint8Array>` that emits chunks at whatever pace you choose.
1855
1856**Example** (Implementing a Binary Streaming Endpoint)
1857
1858```ts
1859import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1860import { Effect, Layer, Schedule, Stream } from "effect"
1861import { HttpRouter } from "effect/http"
1862import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api"
1863import { createServer } from "node:http"
1864
1865const Api = HttpApi.make("myApi").add(
1866 HttpApiGroup.make("group").add(
1867 HttpApiEndpoint.get("getStream", "/stream", {
1868 // Declare a streaming success response
1869 // (default content type: application/octet-stream)
1870 success: HttpApiSchema.StreamUint8Array()
1871 })
1872 )
1873)
1874
1875// Simulate a stream of data
1876const stream = Stream.make("a", "b", "c").pipe(
1877 Stream.schedule(Schedule.spaced("500 millis")),
1878 Stream.map((s) => new TextEncoder().encode(s))
1879)
1880
1881const GroupLayer = HttpApiBuilder.group(
1882 Api,
1883 "group",
1884 (handlers) =>
1885 handlers.handle(
1886 "getStream",
1887 // Return the stream directly from the handler
1888 () => Effect.succeed(stream)
1889 )
1890)
1891
1892const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1893 Layer.provide(GroupLayer),
1894 HttpRouter.serve,
1895 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1896)
1897
1898Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1899```
1900
1901You can test the streaming response using `curl` or any similar HTTP client that supports streaming:
1902
1903```sh
1904curl 'http://localhost:3000/stream' --no-buffer
1905```
1906
1907The response will stream data (`a`, `b`, `c`) with a 500ms interval between each item.
1908
1909### Streaming Server-Sent Events
1910
1911`HttpApiSchema.StreamSse` declares a Server-Sent Events response. Pass a `data` schema to stream plain values: each stream element is JSON-encoded into the `data` field of an SSE event on the way out, and the derived client decodes it back to the value.
1912
1913**Example** (Implementing an SSE Endpoint)
1914
1915```ts
1916import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
1917import { Effect, Layer, Schedule, Schema, Stream } from "effect"
1918import { HttpRouter } from "effect/http"
1919import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api"
1920import { createServer } from "node:http"
1921
1922const Message = Schema.Struct({
1923 text: Schema.String
1924})
1925
1926const Api = HttpApi.make("myApi").add(
1927 HttpApiGroup.make("group").add(
1928 HttpApiEndpoint.get("getEvents", "/events", {
1929 // Each stream element is JSON-encoded into the `data` field of an SSE
1930 // event (default content type: text/event-stream)
1931 success: HttpApiSchema.StreamSse({ data: Message })
1932 })
1933 )
1934)
1935
1936const GroupLayer = HttpApiBuilder.group(
1937 Api,
1938 "group",
1939 (handlers) =>
1940 handlers.handle("getEvents", () =>
1941 Effect.succeed(
1942 Stream.make({ text: "one" }, { text: "two" }, { text: "three" }).pipe(
1943 Stream.schedule(Schedule.spaced("500 millis"))
1944 )
1945 ))
1946)
1947
1948const ApiLayer = HttpApiBuilder.layer(Api).pipe(
1949 Layer.provide(GroupLayer),
1950 HttpRouter.serve,
1951 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
1952)
1953
1954Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
1955
1956// curl 'http://localhost:3000/events' --no-buffer
1957// data: {"text":"one"}
1958//
1959// data: {"text":"two"}
1960//
1961// data: {"text":"three"}
1962```
1963
1964To control the full event shape instead, pass an `events` schema whose Encoded side matches the SSE wire format: a required `data` string plus optional `id` and `event` fields. The handler then emits whole events.
1965
1966### Consuming Streams with the Derived Client
1967
1968For both constructors, calling the endpoint on the derived client resolves to a typed `Stream`:
1969
1970```ts
1971const events = yield * client.group.getEvents()
1972// events: Stream<{ readonly text: string }, ...>
1973
1974yield * Stream.runForEach(events, (message) => Console.log(message.text))
1975```
1976
1977### Typed Stream Failures
1978
1979`HttpApiSchema.StreamSse` accepts an `error` schema describing typed stream failures. Since the HTTP status is already sent when the stream fails, the failure cannot travel as a normal error response. Instead, the server encodes the full `Cause` into a reserved `effect/http-api/stream/failure` event, and the derived client decodes that event and fails the stream with the original cause.
1980
1981**Example** (Declaring a Typed Stream Failure)
1982
1983```ts
1984class StreamError extends Schema.TaggedError<StreamError>()("StreamError", {
1985 reason: Schema.String
1986}) {}
1987
1988const Api = HttpApi.make("myApi").add(
1989 HttpApiGroup.make("group").add(
1990 HttpApiEndpoint.get("getEvents", "/events", {
1991 // The handler may return a Stream that fails with StreamError,
1992 // and the client's stream fails with StreamError too
1993 success: HttpApiSchema.StreamSse({ data: Message, error: StreamError })
1994 })
1995 )
1996)
1997```
1998
1999Things to know:
2000
2001- Set a custom status or content type with `HttpApiSchema.status(code)` and the `contentType` option, for example `HttpApiSchema.status(206)(HttpApiSchema.StreamUint8Array({ contentType: "application/custom-bytes" }))`.
2002- Wrap a stream schema with `HttpApiSchema.WithHeaders` to declare typed response headers (see [Setting Response Headers](#setting-response-headers)). The handler returns `HttpApiSchema.withHeaders({ body, headers })` with the stream as `body`, and the client resolves to the same shape.
2003- An endpoint may declare at most one streaming success schema, `HEAD` endpoints cannot declare one, and streaming schemas are not supported in error responses.
2004- The `effect/http-api/stream/failure` event name is reserved: `events` schemas may not declare it, and the client treats such an event as a stream failure only when its `data` decodes to a `Cause`.
2005
2006# Error Handling
2007
2008## Adding Custom Error Responses
2009
2010Endpoints can declare the errors they may return. Each error is a schema annotated with an HTTP status code via `HttpApiSchema.status(code)`. The status is set once on the schema and reused wherever that schema appears. When your handler fails with a matching error, the framework serializes it and responds with the declared status.
2011
2012**Example** (Defining Error Responses for an Endpoint)
2013
2014```ts
2015import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
2016import { Effect, Layer, Schema } from "effect"
2017import { HttpRouter } from "effect/http"
2018import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar, HttpApiSchema } from "effect/http-api"
2019import { createServer } from "node:http"
2020
2021const User = Schema.Struct({
2022 id: Schema.Int,
2023 name: Schema.String
2024})
2025
2026const UserNotFound = Schema.Struct({
2027 _tag: Schema.tag("UserNotFound"),
2028 message: Schema.String
2029}).pipe(HttpApiSchema.status(404))
2030
2031const Unauthorized = Schema.Struct({
2032 _tag: Schema.tag("Unauthorized")
2033}).pipe(HttpApiSchema.status(401))
2034
2035const Api = HttpApi.make("MyApi")
2036 .add(
2037 HttpApiGroup.make("Users")
2038 .add(
2039 HttpApiEndpoint.get("getUser", "/user/:id", {
2040 params: {
2041 id: Schema.Int
2042 },
2043 success: User,
2044 error: [UserNotFound, Unauthorized /** etc. */]
2045 })
2046 )
2047 .add(
2048 HttpApiEndpoint.delete("deleteUser", "/user/:id", {
2049 params: {
2050 id: Schema.Int
2051 },
2052 error: [UserNotFound, Unauthorized /** etc. */]
2053 })
2054 )
2055 )
2056
2057const GroupLayer = HttpApiBuilder.group(
2058 Api,
2059 "Users",
2060 (handlers) =>
2061 handlers.handleAll({
2062 getUser: (ctx) => {
2063 const id = ctx.params.id
2064 if (id === 1) {
2065 return Effect.fail(UserNotFound.make({ message: "User not found" }))
2066 }
2067 return Effect.succeed({ id, name: `User ${id}` })
2068 },
2069 deleteUser: (ctx) => {
2070 const id = ctx.params.id
2071 if (id === 1) {
2072 return Effect.fail(UserNotFound.make({ message: "User not found" }))
2073 }
2074 return Effect.succeed(void 0)
2075 }
2076 })
2077)
2078
2079const ApiLayer = HttpApiBuilder.layer(Api).pipe(
2080 Layer.provide(GroupLayer),
2081 Layer.provide(HttpApiScalar.layer(Api)),
2082 HttpRouter.serve,
2083 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
2084)
2085
2086Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
2087```
2088
2089You can test these endpoints. For example:
2090
2091```sh
2092curl http://localhost:3000/user/1 # Returns 404 Not Found
2093curl http://localhost:3000/user/2 # Returns 200 OK
2094curl -X DELETE http://localhost:3000/user/1 # Returns 404 Not Found
2095curl -X DELETE http://localhost:3000/user/2 # Returns 200 OK
2096```
2097
2098## Predefined Error Types
2099
2100The `HttpApiError` module provides ready-made error schemas for common HTTP status codes (404, 401, etc.). Using these saves you from defining boilerplate error types and keeps error handling consistent across your API.
2101
2102**Example** (Adding a Predefined Error to an Endpoint)
2103
2104```ts
2105import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
2106import { Effect, Layer, Schema } from "effect"
2107import { HttpRouter } from "effect/http"
2108import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiScalar } from "effect/http-api"
2109import { createServer } from "node:http"
2110
2111const User = Schema.Struct({
2112 id: Schema.Int,
2113 name: Schema.String
2114})
2115
2116const Api = HttpApi.make("MyApi")
2117 .add(
2118 HttpApiGroup.make("Users")
2119 .add(
2120 HttpApiEndpoint.get("getUser", "/user/:id", {
2121 params: {
2122 id: Schema.Int
2123 },
2124 success: User,
2125 error: [
2126 // Add a 404 error JSON response for this endpoint
2127 HttpApiError.NotFound,
2128 // Add a 401 error JSON response for unauthorized access
2129 HttpApiError.Unauthorized
2130 ]
2131 })
2132 )
2133 )
2134
2135const GroupLayer = HttpApiBuilder.group(
2136 Api,
2137 "Users",
2138 (handlers) =>
2139 handlers
2140 .handle("getUser", (ctx) => {
2141 const id = ctx.params.id
2142 if (id === 1) {
2143 return Effect.fail(new HttpApiError.NotFound({}))
2144 }
2145 return Effect.succeed({ id, name: `User ${id}` })
2146 })
2147)
2148
2149const ApiLayer = HttpApiBuilder.layer(Api).pipe(
2150 Layer.provide(GroupLayer),
2151 Layer.provide(HttpApiScalar.layer(Api)),
2152 HttpRouter.serve,
2153 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
2154)
2155
2156Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
2157```
2158
2159| Name | Status | Description |
2160| --------------------- | ------ | -------------------------------------------------------------------------------------------------- |
2161| `HttpApiDecodeError` | 400 | Represents an error where the request did not match the expected schema. Includes detailed issues. |
2162| `BadRequest` | 400 | Indicates that the request was malformed or invalid. |
2163| `Unauthorized` | 401 | Indicates that authentication is required but missing or invalid. |
2164| `Forbidden` | 403 | Indicates that the client does not have permission to access the requested resource. |
2165| `NotFound` | 404 | Indicates that the requested resource could not be found. |
2166| `MethodNotAllowed` | 405 | Indicates that the HTTP method used is not allowed for the requested resource. |
2167| `NotAcceptable` | 406 | Indicates that the requested resource cannot be delivered in a format acceptable to the client. |
2168| `RequestTimeout` | 408 | Indicates that the server timed out waiting for the client request. |
2169| `Conflict` | 409 | Indicates a conflict in the request, such as conflicting data. |
2170| `Gone` | 410 | Indicates that the requested resource is no longer available and will not return. |
2171| `InternalServerError` | 500 | Indicates an unexpected server error occurred. |
2172| `NotImplemented` | 501 | Indicates that the requested functionality is not implemented on the server. |
2173| `ServiceUnavailable` | 503 | Indicates that the server is temporarily unavailable, often due to maintenance or overload. |
2174
2175#### Predefined NoContent Error Types
2176
2177Each predefined error also has a `NoContent` variant that responds with the status code but no body.
2178
2179**Example** (Using a Predefined NoContent Error Type)
2180
2181```ts
2182import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
2183import { Effect, Layer, Schema } from "effect"
2184import { HttpRouter } from "effect/http"
2185import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiScalar } from "effect/http-api"
2186import { createServer } from "node:http"
2187
2188const User = Schema.Struct({
2189 id: Schema.Int,
2190 name: Schema.String
2191})
2192
2193const Api = HttpApi.make("MyApi")
2194 .add(
2195 HttpApiGroup.make("Users")
2196 .add(
2197 HttpApiEndpoint.get("getUser", "/user/:id", {
2198 params: {
2199 id: Schema.Int
2200 },
2201 success: User,
2202 error: [
2203 // Add a 404 error no-content response for this endpoint
2204 HttpApiError.NotFoundNoContent,
2205 // Add a 401 error no-content response for unauthorized access
2206 HttpApiError.UnauthorizedNoContent
2207 ]
2208 })
2209 )
2210 )
2211
2212const GroupLayer = HttpApiBuilder.group(
2213 Api,
2214 "Users",
2215 (handlers) =>
2216 handlers
2217 .handle("getUser", (ctx) => {
2218 const id = ctx.params.id
2219 if (id === 1) {
2220 return Effect.fail(new HttpApiError.NotFound({}))
2221 }
2222 return Effect.succeed({ id, name: `User ${id}` })
2223 })
2224)
2225
2226const ApiLayer = HttpApiBuilder.layer(Api).pipe(
2227 Layer.provide(GroupLayer),
2228 Layer.provide(HttpApiScalar.layer(Api)),
2229 HttpRouter.serve,
2230 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
2231)
2232
2233Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
2234```
2235
2236## Customizing Schema Error Responses
2237
2238By default, when a request fails schema validation (e.g., an invalid query parameter or a malformed path parameter), the framework responds with an empty `400 Bad Request`. If you want to replace that response with a custom error, use `HttpApiMiddleware.layerSchemaErrorTransform`.
2239
2240This function creates a [middleware](#middlewares) layer that intercepts any `SchemaError` thrown during request decoding and lets you return your own error instead.
2241
2242**Example** (Returning a Custom Error on Validation Failure)
2243
2244In this example, if a client sends a non-integer `id` query parameter, the API responds with a `422` status and a JSON body describing the problem, instead of the default empty `400`.
2245
2246```ts
2247import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
2248import { Effect, Layer, Schema } from "effect"
2249import { HttpRouter } from "effect/http"
2250import {
2251 HttpApi,
2252 HttpApiBuilder,
2253 HttpApiEndpoint,
2254 HttpApiGroup,
2255 HttpApiMiddleware,
2256 HttpApiScalar,
2257 HttpApiSchema
2258} from "effect/http-api"
2259import { createServer } from "node:http"
2260
2261// Define a custom error for validation failures
2262class ValidationError extends Schema.TaggedError<ValidationError>()(
2263 "ValidationError",
2264 {
2265 message: Schema.String
2266 }
2267) {}
2268
2269// Define the middleware service, declaring the error it can produce
2270class SchemaErrorHandler extends HttpApiMiddleware.Service<SchemaErrorHandler>()(
2271 "api/SchemaErrorHandler",
2272 {
2273 error: ValidationError.pipe(HttpApiSchema.status(422))
2274 }
2275) {}
2276
2277// Implement the middleware layer
2278const SchemaErrorHandlerLayer = HttpApiMiddleware.layerSchemaErrorTransform(
2279 SchemaErrorHandler,
2280 (schemaError) =>
2281 Effect.fail(
2282 new ValidationError({
2283 message: `Invalid request: ${schemaError.message}`
2284 })
2285 )
2286)
2287
2288const User = Schema.Struct({
2289 id: Schema.Int,
2290 name: Schema.String
2291})
2292
2293const Api = HttpApi.make("MyApi").add(
2294 HttpApiGroup.make("Users").add(
2295 HttpApiEndpoint.get("getUser", "/user", {
2296 query: {
2297 id: Schema.Int
2298 },
2299 success: User
2300 })
2301 // Attach the middleware to this endpoint only
2302 .middleware(SchemaErrorHandler)
2303 )
2304)
2305
2306const GroupLayer = HttpApiBuilder.group(
2307 Api,
2308 "Users",
2309 (handlers) => handlers.handle("getUser", (ctx) => Effect.succeed({ id: ctx.query.id, name: `User ${ctx.query.id}` }))
2310)
2311
2312const ApiLayer = HttpApiBuilder.layer(Api).pipe(
2313 Layer.provide(GroupLayer),
2314 Layer.provide(SchemaErrorHandlerLayer),
2315 Layer.provide(HttpApiScalar.layer(Api)),
2316 HttpRouter.serve,
2317 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
2318)
2319
2320Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
2321
2322// Test:
2323// curl "http://localhost:3000/user?id=1" # 200 OK
2324// curl "http://localhost:3000/user?id=abc" # 422 with ValidationError JSON
2325```
2326
2327The middleware can be attached at different scopes:
2328
2329- **Endpoint**: `.middleware(SchemaErrorHandler)` on a single endpoint (as shown above).
2330- **Group**: `.middleware(SchemaErrorHandler)` on a group to cover all its endpoints.
2331- **API**: `.middleware(SchemaErrorHandler)` on the API to cover every endpoint.
2332
2333# Middlewares
2334
2335Middleware lets you run shared logic — like logging or authentication — before (or around) your handlers. Define a middleware as a class extending `HttpApiMiddleware.Service`, implement it as a `Layer`, and attach it to an endpoint, a group, or the entire API.
2336
2337**Example** (Defining a Logger Middleware)
2338
2339```ts
2340import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
2341import { Effect, Layer, Schema } from "effect"
2342import { HttpRouter, HttpServerRequest } from "effect/http"
2343import {
2344 HttpApi,
2345 HttpApiBuilder,
2346 HttpApiEndpoint,
2347 HttpApiGroup,
2348 HttpApiMiddleware,
2349 HttpApiScalar,
2350 HttpApiSchema
2351} from "effect/http-api"
2352import { createServer } from "node:http"
2353
2354class Logger extends HttpApiMiddleware.Service<Logger>()("Http/Logger", {
2355 // default is 500 Internal Server Error with JSON encoding
2356 error: Schema.String
2357 .pipe(
2358 HttpApiSchema.status(405), // override default status code
2359 HttpApiSchema.asText() // override default encoding
2360 )
2361}) {}
2362
2363const User = Schema.Struct({
2364 id: Schema.Finite,
2365 name: Schema.String
2366})
2367
2368const Api = HttpApi.make("api").add(
2369 HttpApiGroup.make("group").add(
2370 HttpApiEndpoint.get("getUser", "/user/:id", {
2371 params: {
2372 id: Schema.Int
2373 },
2374 success: User
2375 })
2376 // Apply the middleware to a single endpoint
2377 .middleware(Logger)
2378 )
2379 // Or apply the middleware to the entire group
2380 .middleware(Logger)
2381)
2382const GroupLayer = HttpApiBuilder.group(
2383 Api,
2384 "group",
2385 (handlers) =>
2386 handlers.handle("getUser", (ctx) => {
2387 const id = ctx.params.id
2388 return Effect.succeed({ id, name: `User ${id}` })
2389 })
2390)
2391
2392const LoggerLayer = Layer.effect(
2393 Logger,
2394 Effect.gen(function*() {
2395 yield* Effect.log("creating Logger middleware")
2396
2397 return (res) =>
2398 Effect.gen(function*() {
2399 const request = yield* HttpServerRequest.HttpServerRequest
2400 yield* Effect.log(`Request: ${request.method} ${request.url}`)
2401 return yield* res
2402 })
2403 })
2404)
2405
2406const ApiLayer = HttpApiBuilder.layer(Api).pipe(
2407 Layer.provide(GroupLayer),
2408 Layer.provide(HttpApiScalar.layer(Api)),
2409 Layer.provide(LoggerLayer),
2410 HttpRouter.serve,
2411 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
2412)
2413
2414Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
2415
2416// Test this with this curl command:
2417// curl "http://localhost:3000/user/1"
2418```
2419
2420## Interdependent Middleware
2421
2422Middleware can depend on services provided by other middleware. Declare the dependency with `requires` and declare the produced service with `provides`.
2423
2424When you attach interdependent middleware to an endpoint, group, or API, the middleware that uses `requires` must come **BEFORE** the middleware that provides that service. The earlier middleware wraps the later middleware, so it can consume the service that the later middleware adds to the request effect.
2425
2426**Example** (Middleware that consumes another middleware's output)
2427
2428```ts
2429import { Context, Effect, Layer, Schema } from "effect"
2430import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effect/http-api"
2431
2432class AuthInfo extends Context.Service<AuthInfo, {
2433 readonly userId: string
2434}>()("AuthInfo") {}
2435
2436class LoadAuth extends HttpApiMiddleware.Service<LoadAuth, {
2437 readonly provides: AuthInfo
2438}>()("LoadAuth") {}
2439
2440class RequireAuth extends HttpApiMiddleware.Service<RequireAuth, {
2441 readonly requires: AuthInfo
2442}>()("RequireAuth") {}
2443
2444const Api = HttpApi.make("api").add(
2445 HttpApiGroup.make("users").add(
2446 HttpApiEndpoint.get("me", "/me", {
2447 success: Schema.String
2448 })
2449 // RequireAuth reads AuthInfo, so it must appear first.
2450 .middleware(RequireAuth)
2451 // LoadAuth provides AuthInfo to middleware that appears before it.
2452 .middleware(LoadAuth)
2453 )
2454)
2455
2456const LoadAuthLayer = Layer.effect(
2457 LoadAuth,
2458 Effect.succeed((effect) =>
2459 Effect.provideService(effect, AuthInfo, {
2460 userId: "user-1"
2461 })
2462 )
2463)
2464
2465const RequireAuthLayer = Layer.effect(
2466 RequireAuth,
2467 Effect.succeed(
2468 Effect.fnUntraced(function*(effect) {
2469 const authInfo = yield* AuthInfo
2470 yield* Effect.log(`authenticated user ${authInfo.userId}`)
2471 return yield* effect
2472 })
2473 )
2474)
2475```
2476
2477# Security
2478
2479The `HttpApiSecurity` module lets you declare how an endpoint is protected. These declarations show up in the generated OpenAPI spec and are enforced at runtime through middleware.
2480
2481Supported authorization types:
2482
2483| Authorization Type | Description |
2484| ------------------------ | ---------------------------------------------------------------- |
2485| `HttpApiSecurity.apiKey` | API key authorization via headers, query parameters, or cookies. |
2486| `HttpApiSecurity.basic` | HTTP Basic authentication. |
2487| `HttpApiSecurity.bearer` | Bearer token authentication. |
2488
2489Attach a security scheme to an endpoint, group, or the entire API via `HttpApiMiddleware`.
2490
2491**Example** (Defining Security Middleware)
2492
2493```ts
2494import { Context, Schema } from "effect"
2495import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity } from "effect/http-api"
2496
2497// Define a schema for the "User"
2498class User extends Schema.Class<User>("User")({ id: Schema.Finite }) {}
2499
2500// Define a schema for the "Unauthorized" error
2501class Unauthorized extends Schema.TaggedError<Unauthorized>()(
2502 "Unauthorized",
2503 {},
2504 // Specify the HTTP status code for unauthorized errors
2505 { httpApiStatus: 401 }
2506) {}
2507
2508// Define a Context.Tag for the authenticated user
2509class CurrentUser extends Context.Service<CurrentUser, User>()("CurrentUser") {}
2510
2511// Create the Authorization middleware
2512class Authorization extends HttpApiMiddleware.Service<Authorization, {
2513 // Specify the resource this middleware will provide
2514 provides: CurrentUser
2515}>()(
2516 "Authorization",
2517 {
2518 // Define the error schema for unauthorized access
2519 error: Unauthorized,
2520 // Add security definitions
2521 security: {
2522 // ┌─── Custom name for the security definition
2523 // ▼
2524 myBearer: HttpApiSecurity.bearer
2525 // Additional security definitions can be added here.
2526 // They will attempt to be resolved in the order they are defined.
2527 }
2528 }
2529) {}
2530
2531const api = HttpApi.make("api")
2532 .add(
2533 HttpApiGroup.make("group")
2534 .add(
2535 HttpApiEndpoint.get("get", "/", {
2536 success: Schema.String
2537 })
2538 // Apply the middleware to a single endpoint
2539 .middleware(Authorization)
2540 )
2541 // Or apply the middleware to the entire group
2542 .middleware(Authorization)
2543 )
2544 // Or apply the middleware to the entire API
2545 .middleware(Authorization)
2546```
2547
2548## Implementing HttpApiSecurity middleware
2549
2550To enforce a security scheme, implement its middleware as a `Layer`. The layer returns an object with a handler for each security definition. Each handler receives the credential (e.g., a Bearer token as a `Redacted` value) and must return the resource the middleware provides (e.g., the current user).
2551
2552**Example** (Implementing Bearer Token Authentication Middleware)
2553
2554```ts
2555import { Context, Effect, Layer, Redacted, Schema } from "effect"
2556import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api"
2557
2558class User extends Schema.Class<User>("User")({ id: Schema.Finite }) {}
2559
2560class Unauthorized extends Schema.TaggedError<Unauthorized>()(
2561 "Unauthorized",
2562 {},
2563 // Specify the HTTP status code for unauthorized errors
2564 { httpApiStatus: 401 }
2565) {}
2566
2567class CurrentUser extends Context.Service<CurrentUser, User>()("CurrentUser") {}
2568
2569class Authorization extends HttpApiMiddleware.Service<Authorization, {
2570 provides: CurrentUser
2571}>()(
2572 "Authorization",
2573 {
2574 error: Unauthorized,
2575 security: {
2576 myBearer: HttpApiSecurity.bearer
2577 }
2578 }
2579) {}
2580
2581const AuthorizationLayer = Layer.succeed(
2582 Authorization,
2583 // Return the security handlers for the middleware
2584 {
2585 // Define the handler for the Bearer token
2586 // The Bearer token is redacted for security
2587 myBearer: (effect, opts) =>
2588 Effect.provideServiceEffect(
2589 effect,
2590 CurrentUser,
2591 Effect.gen(function*() {
2592 yield* Effect.log(
2593 "checking bearer token",
2594 Redacted.value(opts.credential)
2595 )
2596 // Return a mock User object as the CurrentUser
2597 return new User({ id: 1 })
2598 })
2599 )
2600 }
2601)
2602```
2603
2604## Adding Descriptions to Security Definitions
2605
2606Use `HttpApiSecurity.annotate` to attach metadata — like a description — to a security definition. This metadata appears in the generated docs.
2607
2608**Example** (Adding a Description to a Bearer Token Security Definition)
2609
2610```ts
2611import { Context, Schema } from "effect"
2612import { HttpApiMiddleware, HttpApiSecurity, OpenApi } from "effect/http-api"
2613
2614class User extends Schema.Class<User>("User")({ id: Schema.Finite }) {}
2615
2616class Unauthorized extends Schema.TaggedError<Unauthorized>()(
2617 "Unauthorized",
2618 {},
2619 // Specify the HTTP status code for unauthorized errors
2620 { httpApiStatus: 401 }
2621) {}
2622
2623class CurrentUser extends Context.Service<CurrentUser, User>()("CurrentUser") {}
2624
2625class Authorization extends HttpApiMiddleware.Service<Authorization, {
2626 provides: CurrentUser
2627}>()(
2628 "Authorization",
2629 {
2630 error: Unauthorized,
2631 security: {
2632 myBearer: HttpApiSecurity.bearer.pipe(
2633 // Add a description to the security definition
2634 HttpApiSecurity.annotate(OpenApi.Description, "my description")
2635 )
2636 }
2637 }
2638) {}
2639```
2640
2641## Setting HttpApiSecurity cookies
2642
2643Use `HttpApiBuilder.securitySetCookie` to set a security cookie from a handler. The cookie is created with `HttpOnly` and `Secure` flags by default.
2644
2645**Example** (Setting a Security Cookie in a Login Handler)
2646
2647```ts
2648import { Redacted, Schema } from "effect"
2649import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSecurity } from "effect/http-api"
2650
2651const Api = HttpApi.make("MyApi")
2652 .add(
2653 HttpApiGroup.make("Users")
2654 .add(
2655 HttpApiEndpoint.get("login", "/login", {
2656 params: {
2657 success: Schema.String
2658 }
2659 })
2660 )
2661 )
2662
2663// Define the security configuration for an API key stored in a cookie
2664const security = HttpApiSecurity.apiKey({
2665 // Specify that the API key is stored in a cookie
2666 in: "cookie",
2667 // Define the cookie name,
2668 key: "token"
2669})
2670
2671const UsersApiLayer = HttpApiBuilder.group(Api, "Users", (handlers) =>
2672 handlers.handle("login", () =>
2673 // Set the security cookie with a redacted value
2674 HttpApiBuilder.securitySetCookie(security, Redacted.make("keep me secret"))))
2675```
2676
2677# Using Services Inside a HttpApiEndpoint
2678
2679Handlers can access any Effect service. Because `HttpApiBuilder.group` returns an `Effect`, you can `yield*` services directly inside your handler logic.
2680
2681**Example** (Using Services in a Endpoint Implementation)
2682
2683```ts
2684import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
2685import { Context, Effect, Layer, Schema } from "effect"
2686import { HttpRouter } from "effect/http"
2687import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
2688import { createServer } from "node:http"
2689
2690const User = Schema.Struct({
2691 id: Schema.Int,
2692 name: Schema.String
2693})
2694
2695// Define the UsersRepository service
2696class UsersRepository extends Context.Service<UsersRepository, {
2697 readonly findById: (id: number) => Effect.Effect<typeof User.Type>
2698}>()("UsersRepository") {}
2699
2700const Api = HttpApi.make("MyApi")
2701 .add(
2702 HttpApiGroup.make("Users")
2703 .add(
2704 HttpApiEndpoint.get("getUser", "/user/:id", {
2705 params: {
2706 id: Schema.Int
2707 },
2708 success: User
2709 })
2710 )
2711 )
2712
2713const GroupLayer = HttpApiBuilder.group(
2714 Api,
2715 "Users",
2716 (handlers) =>
2717 handlers
2718 .handle("getUser", (ctx) => {
2719 const id = ctx.params.id
2720 return Effect.gen(function*() {
2721 // Access the UsersRepository service
2722 const repository = yield* UsersRepository
2723 return yield* repository.findById(id)
2724 })
2725 })
2726)
2727
2728const ApiLayer = HttpApiBuilder.layer(Api).pipe(
2729 Layer.provide(GroupLayer),
2730 Layer.provide(HttpApiScalar.layer(Api)),
2731 Layer.provide(
2732 Layer.succeed(UsersRepository, {
2733 findById: (id) => Effect.succeed({ id, name: `User ${id}` })
2734 })
2735 ),
2736 HttpRouter.serve,
2737 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
2738)
2739
2740Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
2741```
2742
2743# OpenAPI Documentation
2744
2745Add interactive API documentation with `HttpApiScalar` (Scalar UI) or `HttpApiSwagger` (Swagger UI). Both read your API definition and generate a browsable docs page at `/docs`.
2746
2747**Example** (Adding Scalar Documentation to an API)
2748
2749```ts
2750import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
2751import { Effect, Layer, Schema } from "effect"
2752import { HttpRouter } from "effect/http"
2753import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
2754import { createServer } from "node:http"
2755
2756const User = Schema.Struct({
2757 id: Schema.Int,
2758 name: Schema.String
2759})
2760
2761const IdParam = Schema.Int
2762
2763const Api = HttpApi.make("MyApi")
2764 .add(
2765 HttpApiGroup.make("Users")
2766 .add(
2767 HttpApiEndpoint.get("getUsers", "/users", {
2768 success: Schema.Array(User)
2769 }),
2770 HttpApiEndpoint.get("getUser", "/user/:id", {
2771 params: {
2772 id: IdParam
2773 },
2774 success: User
2775 }),
2776 HttpApiEndpoint.post("createUser", "/user", {
2777 payload: User,
2778 success: User
2779 }),
2780 HttpApiEndpoint.delete("deleteUser", "/user/:id", {
2781 params: {
2782 id: IdParam
2783 }
2784 }),
2785 HttpApiEndpoint.patch("updateUser", "/user/:id", {
2786 params: {
2787 id: IdParam
2788 },
2789 // Specify the schema for the request payload
2790 payload: Schema.Struct({
2791 name: Schema.String // Only the name can be updated
2792 }),
2793 // Specify the schema for a successful response
2794 success: User
2795 })
2796 )
2797 )
2798
2799const GroupLayer = HttpApiBuilder.group(
2800 Api,
2801 "Users",
2802 (handlers) =>
2803 handlers.handleAll({
2804 getUsers: () =>
2805 Effect.succeed(
2806 [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }]
2807 ),
2808 getUser: (ctx) => {
2809 const id = ctx.params.id
2810 return Effect.succeed({ id, name: `User ${id}` })
2811 },
2812 createUser: (ctx) => {
2813 const user = ctx.payload
2814 return Effect.succeed(user)
2815 },
2816 deleteUser: (ctx) => {
2817 const id = ctx.params.id
2818 return Effect.log(`Deleting user ${id}`)
2819 },
2820 updateUser: (ctx) => {
2821 const id = ctx.params.id
2822 return Effect.succeed({ id, name: `User ${id}` })
2823 }
2824 })
2825)
2826
2827const ApiLayer = HttpApiBuilder.layer(Api).pipe(
2828 Layer.provide(GroupLayer),
2829 Layer.provide(HttpApiScalar.layer(Api)), // "/docs" is the default path.
2830 HttpRouter.serve,
2831 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
2832)
2833
2834Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
2835```
2836
2837After running the server, open your browser and navigate to http://localhost:3000/docs.
2838
2839This URL will display the Scalar documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively.
2840
2841## Adding OpenAPI Annotations
2842
2843Annotations let you enrich the generated OpenAPI spec with titles, descriptions, server URLs, and more. They are added via the `.annotate` method on `HttpApi`, `HttpApiGroup`, or `HttpApiEndpoint`.
2844
2845#### HttpApi
2846
2847Below is a list of available annotations for a top-level `HttpApi`. They can be added using the `.annotate` method:
2848
2849| Annotation | Description |
2850| --------------------------- | ------------------------------------------------------------------------------------------------------------------ |
2851| `HttpApi.AdditionalSchemas` | Adds custom schemas to the final OpenAPI specification. Only schemas with an `identifier` annotation are included. |
2852| `OpenApi.Description` | Sets a general description for the API. |
2853| `OpenApi.Title` | Sets the title of the API. |
2854| `OpenApi.Version` | Sets the version of the API. |
2855| `OpenApi.License` | Defines the license used by the API. |
2856| `OpenApi.Summary` | Provides a brief summary of the API. |
2857| `OpenApi.Servers` | Lists server URLs and optional metadata such as variables. |
2858| `OpenApi.Override` | Merges the supplied fields into the resulting specification. |
2859| `OpenApi.Transform` | Allows you to modify the final specification with a custom function. |
2860
2861**Example** (Annotating the Top-Level API)
2862
2863```ts
2864import { Schema } from "effect"
2865import { HttpApi, OpenApi } from "effect/http-api"
2866
2867const api = HttpApi.make("api")
2868 // Provide additional schemas
2869 .annotate(HttpApi.AdditionalSchemas, [
2870 Schema.String.annotate({ identifier: "MyString" })
2871 ])
2872 // Add a description
2873 .annotate(OpenApi.Description, "my description")
2874 // Set license information
2875 .annotate(OpenApi.License, { name: "MIT", url: "http://example.com" })
2876 // Provide a summary
2877 .annotate(OpenApi.Summary, "my summary")
2878 // Define servers
2879 .annotate(OpenApi.Servers, [
2880 {
2881 url: "http://example.com",
2882 description: "example",
2883 variables: { a: { default: "b", enum: ["c"], description: "d" } }
2884 }
2885 ])
2886 // Override parts of the generated specification
2887 .annotate(OpenApi.Override, {
2888 tags: [{ name: "a", description: "a-description" }]
2889 })
2890 // Apply a transform function to the final specification
2891 .annotate(OpenApi.Transform, (spec) => ({
2892 ...spec,
2893 tags: [...spec.tags, { name: "b", description: "b-description" }]
2894 }))
2895
2896// Generate the OpenAPI specification from the annotated API
2897const spec = OpenApi.fromApi(api)
2898
2899console.log(JSON.stringify(spec, null, 2))
2900/*
2901Output:
2902{
2903 "openapi": "3.1.0",
2904 "info": {
2905 "title": "Api",
2906 "version": "0.0.1",
2907 "description": "my description",
2908 "license": {
2909 "name": "MIT",
2910 "url": "http://example.com"
2911 },
2912 "summary": "my summary"
2913 },
2914 "paths": {},
2915 "components": {
2916 "schemas": {
2917 "MyString": {
2918 "type": "string"
2919 }
2920 },
2921 "securitySchemes": {}
2922 },
2923 "security": [],
2924 "tags": [
2925 {
2926 "name": "a",
2927 "description": "a-description"
2928 },
2929 {
2930 "name": "b",
2931 "description": "b-description"
2932 }
2933 ],
2934 "servers": [
2935 {
2936 "url": "http://example.com",
2937 "description": "example",
2938 "variables": {
2939 "a": {
2940 "default": "b",
2941 "enum": [
2942 "c"
2943 ],
2944 "description": "d"
2945 }
2946 }
2947 }
2948 ]
2949}
2950*/
2951```
2952
2953#### HttpApiGroup
2954
2955The following annotations can be added to an `HttpApiGroup`:
2956
2957| Annotation | Description |
2958| ---------------------- | --------------------------------------------------------------------- |
2959| `OpenApi.Description` | Sets a description for this group. |
2960| `OpenApi.ExternalDocs` | Provides external documentation links for the group. |
2961| `OpenApi.Override` | Merges specified fields into the resulting specification. |
2962| `OpenApi.Transform` | Lets you modify the final group specification with a custom function. |
2963| `OpenApi.Exclude` | Excludes the group from the final OpenAPI specification. |
2964
2965**Example** (Annotating a Group)
2966
2967```ts
2968import { HttpApi, HttpApiGroup, OpenApi } from "effect/http-api"
2969
2970const api = HttpApi.make("api")
2971 .add(
2972 HttpApiGroup.make("group")
2973 // Add a description for the group
2974 .annotate(OpenApi.Description, "my description")
2975 // Provide external documentation links
2976 .annotate(OpenApi.ExternalDocs, {
2977 url: "http://example.com",
2978 description: "example"
2979 })
2980 // Override parts of the final output
2981 .annotate(OpenApi.Override, { name: "my name" })
2982 // Transform the final specification for this group
2983 .annotate(OpenApi.Transform, (spec) => ({
2984 ...spec,
2985 name: spec.name + "-transformed"
2986 }))
2987 )
2988 .add(
2989 HttpApiGroup.make("excluded")
2990 // Exclude the group from the final specification
2991 .annotate(OpenApi.Exclude, true)
2992 )
2993
2994// Generate the OpenAPI spec
2995const spec = OpenApi.fromApi(api)
2996
2997console.log(JSON.stringify(spec, null, 2))
2998/*
2999Output:
3000{
3001 "openapi": "3.1.0",
3002 "info": {
3003 "title": "Api",
3004 "version": "0.0.1"
3005 },
3006 "paths": {},
3007 "components": {
3008 "schemas": {},
3009 "securitySchemes": {}
3010 },
3011 "security": [],
3012 "tags": [
3013 {
3014 "name": "my name-transformed",
3015 "description": "my description",
3016 "externalDocs": {
3017 "url": "http://example.com",
3018 "description": "example"
3019 }
3020 }
3021 ]
3022}
3023*/
3024```
3025
3026#### HttpApiEndpoint
3027
3028For an `HttpApiEndpoint`, you can use the following annotations:
3029
3030| Annotation | Description |
3031| ---------------------- | --------------------------------------------------------------------------- |
3032| `OpenApi.Description` | Adds a description for this endpoint. |
3033| `OpenApi.Summary` | Provides a short summary of the endpoint's purpose. |
3034| `OpenApi.Deprecated` | Marks the endpoint as deprecated. |
3035| `OpenApi.ExternalDocs` | Supplies external documentation links for the endpoint. |
3036| `OpenApi.Override` | Merges specified fields into the resulting specification for this endpoint. |
3037| `OpenApi.Transform` | Lets you modify the final endpoint specification with a custom function. |
3038| `OpenApi.Exclude` | Excludes the endpoint from the final OpenAPI specification. |
3039
3040**Example** (Annotating an Endpoint)
3041
3042```ts
3043import { Schema } from "effect"
3044import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api"
3045
3046const api = HttpApi.make("api").add(
3047 HttpApiGroup.make("group")
3048 .add(
3049 HttpApiEndpoint.get("get", "/", {
3050 success: Schema.String
3051 })
3052 // Add a description
3053 .annotate(OpenApi.Description, "my description")
3054 // Provide a summary
3055 .annotate(OpenApi.Summary, "my summary")
3056 // Mark the endpoint as deprecated
3057 .annotate(OpenApi.Deprecated, true)
3058 // Provide external documentation
3059 .annotate(OpenApi.ExternalDocs, {
3060 url: "http://example.com",
3061 description: "example"
3062 })
3063 )
3064 .add(
3065 HttpApiEndpoint.get("excluded", "/excluded", {
3066 success: Schema.String
3067 })
3068 // Exclude this endpoint from the final specification
3069 .annotate(OpenApi.Exclude, true)
3070 )
3071)
3072
3073// Generate the OpenAPI spec
3074const spec = OpenApi.fromApi(api)
3075
3076console.log(JSON.stringify(spec, null, 2))
3077/*
3078Output:
3079{
3080 "openapi": "3.1.0",
3081 "info": {
3082 "title": "Api",
3083 "version": "0.0.1"
3084 },
3085 "paths": {
3086 "/": {
3087 "get": {
3088 "tags": [
3089 "group"
3090 ],
3091 "operationId": "group.get",
3092 "parameters": [],
3093 "security": [],
3094 "responses": {
3095 "200": {
3096 "description": "Success",
3097 "content": {
3098 "application/json": {
3099 "schema": {
3100 "$ref": "#/components/schemas/String_"
3101 }
3102 }
3103 }
3104 },
3105 "400": {
3106 "description": "The request or response did not match the expected schema",
3107 "content": {
3108 "application/json": {
3109 "schema": {
3110 "type": "object",
3111 "properties": {
3112 "_tag": {
3113 "type": "string",
3114 "enum": [
3115 "HttpApiSchemaError"
3116 ]
3117 },
3118 "message": {
3119 "$ref": "#/components/schemas/String_"
3120 }
3121 },
3122 "required": [
3123 "_tag",
3124 "message"
3125 ],
3126 "additionalProperties": false
3127 }
3128 }
3129 }
3130 }
3131 },
3132 "description": "my description",
3133 "summary": "my summary",
3134 "deprecated": true,
3135 "externalDocs": {
3136 "url": "http://example.com",
3137 "description": "example"
3138 }
3139 }
3140 }
3141 },
3142 "components": {
3143 "schemas": {
3144 "String_": {
3145 "type": "string"
3146 }
3147 },
3148 "securitySchemes": {}
3149 },
3150 "security": [],
3151 "tags": [
3152 {
3153 "name": "group"
3154 }
3155 ]
3156}
3157*/
3158```
3159
3160The default response description is "Success". You can override this by annotating the schema.
3161
3162**Example** (Defining a custom response description)
3163
3164```ts
3165import { Schema } from "effect"
3166import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api"
3167
3168const User = Schema.Struct({
3169 id: Schema.Finite,
3170 name: Schema.String
3171}).annotate({ identifier: "User" })
3172
3173const api = HttpApi.make("api").add(
3174 HttpApiGroup.make("group").add(
3175 HttpApiEndpoint.get("getUsers", "/users", {
3176 success: Schema.Array(User).annotate({
3177 description: "Returns an array of users"
3178 })
3179 })
3180 )
3181)
3182
3183const spec = OpenApi.fromApi(api)
3184
3185console.log(JSON.stringify(spec.paths, null, 2))
3186/*
3187Output:
3188{
3189 "/users": {
3190 "get": {
3191 "tags": [
3192 "group"
3193 ],
3194 "operationId": "group.getUsers",
3195 "parameters": [],
3196 "security": [],
3197 "responses": {
3198 "200": {
3199 "description": "Returns an array of users",
3200 "content": {
3201 "application/json": {
3202 "schema": {
3203 "type": "array",
3204 "items": {
3205 "type": "object",
3206 "properties": {
3207 "id": {
3208 "type": "number"
3209 },
3210 "name": {
3211 "$ref": "#/components/schemas/String_"
3212 }
3213 },
3214 "required": [
3215 "id",
3216 "name"
3217 ],
3218 "additionalProperties": false
3219 },
3220 "description": "Returns an array of users"
3221 }
3222 }
3223 }
3224 },
3225 "400": {
3226 "description": "The request or response did not match the expected schema",
3227 "content": {
3228 "application/json": {
3229 "schema": {
3230 "type": "object",
3231 "properties": {
3232 "_tag": {
3233 "type": "string",
3234 "enum": [
3235 "HttpApiSchemaError"
3236 ]
3237 },
3238 "message": {
3239 "$ref": "#/components/schemas/String_"
3240 }
3241 },
3242 "required": [
3243 "_tag",
3244 "message"
3245 ],
3246 "additionalProperties": false
3247 }
3248 }
3249 }
3250 }
3251 }
3252 }
3253 }
3254}
3255*/
3256```
3257
3258## Top Level Groups
3259
3260When a group is `topLevel`, its name is not prepended to operation IDs in the OpenAPI spec. Use this when the group is just for tagging and you want shorter, cleaner operation IDs.
3261
3262**Example** (Using a Top-Level Group)
3263
3264```ts
3265import { Schema } from "effect"
3266import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api"
3267
3268const api = HttpApi.make("api").add(
3269 // Mark the group as top-level
3270 HttpApiGroup.make("group", { topLevel: true }).add(
3271 HttpApiEndpoint.get("get", "/", {
3272 success: Schema.String
3273 })
3274 )
3275)
3276
3277// Generate the OpenAPI spec
3278const spec = OpenApi.fromApi(api)
3279
3280console.log(JSON.stringify(spec.paths, null, 2))
3281/*
3282Output:
3283{
3284 "/": {
3285 "get": { // The operation ID is not prefixed with "group"
3286 "tags": [
3287 "group"
3288 ],
3289 "operationId": "get",
3290 "parameters": [],
3291 "security": [],
3292 "responses": {
3293 "200": {
3294 "description": "Success",
3295 "content": {
3296 "application/json": {
3297 "schema": {
3298 "$ref": "#/components/schemas/String_"
3299 }
3300 }
3301 }
3302 },
3303 "400": {
3304 "description": "The request or response did not match the expected schema",
3305 "content": {
3306 "application/json": {
3307 "schema": {
3308 "type": "object",
3309 "properties": {
3310 "_tag": {
3311 "type": "string",
3312 "enum": [
3313 "HttpApiSchemaError"
3314 ]
3315 },
3316 "message": {
3317 "$ref": "#/components/schemas/String_"
3318 }
3319 },
3320 "required": [
3321 "_tag",
3322 "message"
3323 ],
3324 "additionalProperties": false
3325 }
3326 }
3327 }
3328 }
3329 }
3330 }
3331 }
3332}
3333*/
3334```
3335
3336# Deriving a Client
3337
3338The `HttpApiClient` module generates a fully typed client from your API definition. Each endpoint becomes a method — grouped by `HttpApiGroup` name — so calling your API is as simple as calling a function.
3339
3340**Example** (Deriving and Using a Client)
3341
3342```ts
3343import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
3344import { Effect, Layer, Schema } from "effect"
3345import { HttpRouter } from "effect/http"
3346import { FetchHttpClient } from "effect/http"
3347import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
3348import { createServer } from "node:http"
3349
3350const Api = HttpApi.make("MyApi")
3351 .add(
3352 HttpApiGroup.make("Greetings")
3353 .add(
3354 HttpApiEndpoint.get("hello", "/", {
3355 success: Schema.String
3356 })
3357 )
3358 )
3359
3360const GroupLayer = HttpApiBuilder.group(
3361 Api,
3362 "Greetings",
3363 (handlers) => handlers.handle("hello", () => Effect.succeed("Hello, World!"))
3364)
3365
3366const ApiLayer = HttpApiBuilder.layer(Api).pipe(
3367 Layer.provide(GroupLayer),
3368 HttpRouter.serve,
3369 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
3370)
3371
3372Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
3373
3374// Create a program that derives and uses the client
3375const program = Effect.gen(function*() {
3376 // Derive the client
3377 const client = yield* HttpApiClient.make(Api, {
3378 baseUrl: "http://localhost:3000"
3379 })
3380 // Call the "hello-world" endpoint
3381 const hello = yield* client.Greetings.hello()
3382 console.log(hello)
3383})
3384
3385// Provide a Fetch-based HTTP client and run the program
3386Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
3387/*
3388Output:
3389[18:55:26.051] INFO (#2): Listening on http://0.0.0.0:3000
3390[18:55:26.057] INFO (#12) http.span=2ms: Sent HTTP response { 'http.method': 'GET', 'http.url': '/', 'http.status': 200 }
3391Hello, World!
3392*/
3393```
3394
3395## Top Level Groups
3396
3397When a group is `topLevel`, its endpoints are exposed as top-level methods on the client instead of being nested under the group name.
3398
3399**Example** (Using a Top-Level Group in the Client)
3400
3401```ts
3402import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
3403import { Effect, Layer, Schema } from "effect"
3404import { HttpRouter } from "effect/http"
3405import { FetchHttpClient } from "effect/http"
3406import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
3407import { createServer } from "node:http"
3408
3409const Api = HttpApi.make("MyApi")
3410 .add(
3411 HttpApiGroup.make("Greetings", { topLevel: true })
3412 .add(
3413 HttpApiEndpoint.get("hello", "/", {
3414 success: Schema.String
3415 })
3416 )
3417 )
3418
3419const GroupLayer = HttpApiBuilder.group(
3420 Api,
3421 "Greetings",
3422 (handlers) => handlers.handle("hello", () => Effect.succeed("Hello, World!"))
3423)
3424
3425const ApiLayer = HttpApiBuilder.layer(Api).pipe(
3426 Layer.provide(GroupLayer),
3427 HttpRouter.serve,
3428 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
3429)
3430
3431Layer.launch(ApiLayer).pipe(NodeRuntime.runMain)
3432
3433const program = Effect.gen(function*() {
3434 const client = yield* HttpApiClient.make(Api, {
3435 baseUrl: "http://localhost:3000"
3436 })
3437 // The `hello` method is not nested under the "group" name
3438 const hello = yield* client.hello()
3439 console.log(hello)
3440})
3441
3442Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
3443```
3444
3445# Converting to a Web Handler
3446
3447If you need to plug your API into an existing HTTP server (instead of using `NodeHttpServer`), convert it to a standard web handler with `HttpApiBuilder.toWebHandler`. The returned `handler` function takes a `Request` and returns a `Response`.
3448
3449**Example** (Creating and Serving a Web Handler)
3450
3451```ts
3452import { Effect, Layer, Schema } from "effect"
3453import { HttpRouter, HttpServer } from "effect/http"
3454import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/http-api"
3455import * as http from "node:http"
3456
3457const Api = HttpApi.make("myApi").add(
3458 HttpApiGroup.make("group").add(
3459 HttpApiEndpoint.get("get", "/", {
3460 success: Schema.String
3461 })
3462 )
3463)
3464
3465const GroupLayer = HttpApiBuilder.group(
3466 Api,
3467 "group",
3468 (handlers) => handlers.handle("get", () => Effect.succeed("Hello, world!"))
3469)
3470
3471const ApiLayer = HttpApiBuilder.layer(Api).pipe(
3472 Layer.provide(GroupLayer),
3473 Layer.provide(HttpApiScalar.layer(Api)),
3474 Layer.provide(HttpServer.layerServices)
3475)
3476
3477// Convert the API to a web handler
3478const { dispose, handler } = HttpRouter.toWebHandler(
3479 Layer.mergeAll(ApiLayer)
3480)
3481
3482// Serving the handler using a custom HTTP server
3483http
3484 .createServer(async (req, res) => {
3485 const url = `http://${req.headers.host}${req.url}`
3486 const init: RequestInit = {
3487 method: req.method!
3488 }
3489
3490 const response = await handler(new Request(url, init))
3491
3492 res.writeHead(
3493 response.status,
3494 response.statusText,
3495 Object.fromEntries(response.headers.entries())
3496 )
3497 const responseBody = await response.arrayBuffer()
3498 res.end(Buffer.from(responseBody))
3499 })
3500 .listen(3000, () => {
3501 console.log("Server running at http://localhost:3000/")
3502 })
3503 .on("close", () => {
3504 dispose()
3505 })
3506```