1/**
2 * @title Getting started with HttpClient
3 *
4 * Define a service that uses the HttpClient module to fetch data from an external API
5 */
6import { Context, Effect, flow, Layer, Schedule, Schema } from "effect"
7import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"
8
9class Todo extends Schema.Class<Todo>("Todo")({
10 userId: Schema.Int,
11 id: Schema.Int,
12 title: Schema.String,
13 completed: Schema.Boolean
14}) {}
15
16export class JsonPlaceholder extends Context.Service<JsonPlaceholder, {
17 readonly allTodos: Effect.Effect<ReadonlyArray<Todo>, JsonPlaceholderError>
18 getTodo(id: number): Effect.Effect<Todo, JsonPlaceholderError>
19 createTodo(todo: Omit<Todo, "id">): Effect.Effect<Todo, JsonPlaceholderError>
20}>()("app/JsonPlaceholder") {
21 static readonly layer = Layer.effect(
22 JsonPlaceholder,
23 Effect.gen(function*() {
24 // Access the HttpClient service, and apply some common middleware to all
25 // requests:
26 const client = (yield* HttpClient.HttpClient).pipe(
27 // Add a base URL to all requests made with this client, and set the
28 // Accept header to expect JSON responses
29 HttpClient.mapRequest(flow(
30 HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com"),
31 HttpClientRequest.acceptJson
32 )),
33 // Fail if the response status is not 2xx
34 HttpClient.filterStatusOk,
35 // Retry transient errors (network issues, 5xx responses) with an
36 // exponential backoff.
37 //
38 // See the schedule documentation for more complex retry strategies.
39 HttpClient.retryTransient({
40 schedule: Schedule.exponential(100),
41 times: 3
42 })
43 )
44
45 const allTodos = client.get("/todos").pipe(
46 Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Array(Todo))),
47 Effect.mapError((cause) => new JsonPlaceholderError({ cause })),
48 Effect.withSpan("JsonPlaceholder.allTodos")
49 )
50
51 // Use the HttpClient to fetch a todo item by id, and decode the response
52 // using the Todo schema.
53 const getTodo = Effect.fn("JsonPlaceholder.getTodo")(function*(id: number) {
54 // Annotate the current span with the id of the todo being fetched, so
55 // that it shows up in telemetry for this request.
56 yield* Effect.annotateCurrentSpan({ id })
57
58 const todo = yield* client.get(`/todos/${id}`, {
59 // You can pass additional options to individual requests.
60 // There are options for query parameters, request body, headers, and
61 // more.
62 urlParams: { format: "json" }
63 }).pipe(
64 Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)),
65 Effect.mapError((cause) => new JsonPlaceholderError({ cause }))
66 )
67
68 return todo
69 })
70
71 // You can use the HttpClientRequest module to build up more complex
72 // requests:
73 const createTodo = Effect.fn("JsonPlaceholder.createTodo")(function*(todo: Omit<Todo, "id">) {
74 yield* Effect.annotateCurrentSpan({ title: todo.title })
75
76 const createdTodo = yield* HttpClientRequest.post("/todos").pipe(
77 // The HttpClientRequest module has many helper functions for building requests.
78 HttpClientRequest.setUrlParams({ format: "json" }),
79 HttpClientRequest.bodyJsonUnsafe(todo),
80 client.execute,
81 Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)),
82 Effect.mapError((cause) => new JsonPlaceholderError({ cause }))
83 )
84
85 return createdTodo
86 })
87
88 return JsonPlaceholder.of({
89 allTodos,
90 getTodo,
91 createTodo
92 })
93 })
94 ).pipe(
95 // Provide the fetch-based HttpClient implementation
96 Layer.provide(FetchHttpClient.layer)
97 )
98}
99
100export class JsonPlaceholderError extends Schema.TaggedError<JsonPlaceholderError>()("JsonPlaceholderError", {
101 cause: Schema.Defect()
102}) {}