1/**
2 * @title Getting started with HttpApi
3 *
4 * Define a schema-first API, implement handlers, secure endpoints with
5 * middleware, serve it over HTTP, and call it using a generated typed client.
6 */
7import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
8import { Context, Effect, flow, Layer, Schedule } from "effect"
9import { FetchHttpClient, HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/http"
10import { HttpApiBuilder, HttpApiClient, HttpApiMiddleware, HttpApiScalar } from "effect/http-api"
11import { createServer } from "node:http"
12// Api definitions should **always** be seperate from the server implementation,
13// so that they can be shared between the server and client without leaking
14// server code into clients.
15// Ideally, the would use a seperate package in a monorepo.
16import { Api } from "./fixtures/api/Api.ts"
17import { Authorization } from "./fixtures/api/Authorization.ts"
18import { UsersApiHandlers } from "./fixtures/server/Users/http.ts"
19
20// This walkthrough focuses on runtime wiring and typed client usage.
21// See the fixture files for the API schemas, endpoint definitions and handlers:
22
23const SystemApiHandlers = HttpApiBuilder.group(
24 Api,
25 "system",
26 Effect.fn(function*(handlers) {
27 return handlers.handleAll({
28 health: () => Effect.void
29 })
30 })
31)
32
33const ApiRoutes = HttpApiBuilder.layer(Api, {
34 openapiPath: "/openapi.json"
35}).pipe(
36 // Provide all the handler Layers for the API.
37 Layer.provide([UsersApiHandlers, SystemApiHandlers])
38)
39
40// Define a /docs route that serves scalar documentation
41const DocsRoute = HttpApiScalar.layer(Api, {
42 path: "/docs"
43})
44
45// Merge all the http routes together
46const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute)
47
48// Create an HTTP server Layer that serves the API routes.
49//
50// Here we are using the NodeHttpServer, but you could also use the
51// BunHttpServer
52export const HttpServerLayer = HttpRouter.serve(AllRoutes).pipe(
53 Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
54)
55
56// Then run the server using Layer.launch
57Layer.launch(HttpServerLayer).pipe(
58 NodeRuntime.runMain
59)
60
61// Or create a web handler, which can be used in serverless environments
62export const { handler, dispose } = HttpRouter.toWebHandler(AllRoutes.pipe(
63 Layer.provide(HttpServer.layerServices)
64))
65
66// -----------------
67// Client side setup
68// -----------------
69
70export const AuthorizationClient = HttpApiMiddleware.layerClient(
71 Authorization,
72 Effect.fn(function*({ next, request }) {
73 // Here you can modify the request and pass it down the middleware chain.
74 // This is where you would add authentication tokens, custom headers, etc.
75 // For this example, we just add a hardcoded bearer token to all requests.
76 return yield* next(HttpClientRequest.bearerToken(request, "dev-token"))
77 })
78)
79
80// Define the HttpApiClient service, which will be used to make requests to the
81// API.
82export class ApiClient extends Context.Service<ApiClient, HttpApiClient.ForApi<typeof Api>>()("acme/ApiClient") {
83 static readonly layer = Layer.effect(
84 ApiClient,
85 HttpApiClient.make(Api, {
86 // Use transformClient to apply middleware to the generated client. This
87 // is useful for settings the base url and applying retry policies.
88 transformClient: (client) =>
89 client.pipe(
90 HttpClient.mapRequest(flow(
91 HttpClientRequest.prependUrl("http://localhost:3000")
92 )),
93 HttpClient.retryTransient({
94 schedule: Schedule.exponential(100),
95 times: 3
96 })
97 )
98 })
99 ).pipe(
100 // Provide the client implementation of the Authorization middleware, which
101 // is required.
102 Layer.provide(AuthorizationClient),
103 // Supply a HttpClient implementation to use for making requests. Here we
104 // use the FetchHttpClient, but you could also use the NodeHttpClient or
105 // BunHttpClient.
106 Layer.provide(FetchHttpClient.layer)
107 )
108}
109
110// The generated client mirrors your API definition, so renames and schema
111// changes are checked end-to-end at compile time.
112export const callApi = Effect.gen(function*() {
113 const client = yield* ApiClient
114
115 yield* client.health()
116}).pipe(
117 Effect.provide(ApiClient.layer)
118)