Skip to content

Commit 572532e

Browse files
committed
add section on managing requirements to incremental adoption tutorial
1 parent 85d128d commit 572532e

File tree

8 files changed

+276
-58
lines changed

8 files changed

+276
-58
lines changed

content/tutorials/incremental-adoption/200-dealing-with-errors.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ The team in charge of the `TodoRepository` has continued to refactor the `create
167167
Using what we have learned above, your tasks for this exercise include:
168168

169169
- If creation of a `Todo` results in a `CreateTodoError`
170-
- Set the response status code to `500`
170+
- Set the response status code to `404`
171171
- Return a JSON response that conforms to the following `{ "type": "CreateTodoError", "text": <TODO_TEXT> }`
172172

173173
In the editor to the right, modify the code to restore functionality to our server. Please note that there is no one "correct" answer, as there are multiple ways to achieve the desired outcome.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
title: Handling Requirements
3+
excerpt: Learn how to incrementally adopt Effect into your application
4+
section: Incremental Adoption
5+
workspace: express
6+
---
7+
8+
### Handling Requirements
9+
10+
Utilizing `Effect.runPromise` to interop with your existing application is fine when you are just getting started with adopting Effect. However, it will quickly become apparent that this approach does not scale, especially once you start using Effect to manage the requirements of your application and `Layer`s to compose the dependency graph between services.
11+
12+
<Idea>
13+
For a detailed walkthrough of how to manage requirements within your Effect applications, take a look at the <a href="/docs/guides/context-management" target="_blank">Requirements Management</a> section of the documentation.
14+
</Idea>
15+
16+
### Understanding the Problem
17+
18+
To understand the problem, let's take a look at a simple example where we create a `Layer` which logs `"Hello, World"` when constructed. The layer is then provided to two Effect programs which are executed at two separate execution boundaries.
19+
20+
```ts twoslash
21+
import { Console, Effect, Layer } from "effect"
22+
23+
const HelloWorldLive = Layer.effectDiscard(
24+
Console.log("Hello, World!")
25+
)
26+
27+
async function main() {
28+
// Execution Boundary #1
29+
await Effect.succeed(1).pipe(
30+
Effect.provide(HelloWorldLive),
31+
Effect.runPromise
32+
)
33+
34+
// Execution Boundary #2
35+
await Effect.succeed(2).pipe(
36+
Effect.provide(HelloWorldLive),
37+
Effect.runPromise
38+
)
39+
}
40+
41+
main()
42+
/**
43+
* Output:
44+
* Hello, World!
45+
* Hello, World!
46+
*/
47+
```
48+
49+
As you can see from the output, the message `"Hello, World!"` is logged twice. This is because each call to `Effect.provide` will fully construct the dependency graph specified by the `Layer` and then provide it to the Effect program. This can create problems when your layers are meant to encapsulate logic that is only meant to be executed **once** (for example, creating a database connection pool) or when layer construction is **expensive** (for example, fetching a large number of remote assets and caching them in memory).
50+
51+
To solve this problem, we need some sort of top-level, re-usable Effect `Runtime` which contains our fully constructed dependency graph, and then use that `Runtime` to execute our Effect programs instead of the default `Runtime` used by the `Effect.run*` methods.
52+
53+
### Managed Runtimes
54+
55+
The `ManagedRuntime` data type in Effect allows us to create a top-level, re-usable Effect `Runtime` which encapsulates a fully constructed dependency graph. In addition, `ManagedRuntime` gives us explicit control over when resources acquired by the runtime should be disposed.
56+
57+
```ts twoslash
58+
import { Console, Effect, Layer, ManagedRuntime } from "effect"
59+
60+
const HelloWorldLive = Layer.effectDiscard(
61+
Console.log("Hello, World!")
62+
)
63+
64+
// Create a managed runtime from our layer
65+
const runtime = ManagedRuntime.make(HelloWorldLive)
66+
67+
async function main() {
68+
// Execution Boundary #1
69+
await Effect.succeed(1).pipe(
70+
runtime.runPromise
71+
)
72+
73+
// Execution Boundary #2
74+
await Effect.succeed(2).pipe(
75+
runtime.runPromise
76+
)
77+
78+
// Dispose of resources when no longer needed
79+
await runtime.dispose()
80+
}
81+
82+
main()
83+
/**
84+
* Output:
85+
* Hello, World!
86+
*/
87+
```
88+
89+
Some things to note about the program above include:
90+
91+
- `"Hello, World!"` is only logged to the console once
92+
- We no longer have to provide `HelloWorldLive` to each Effect program
93+
- Resources acquired by the `ManagedRuntime` must be manually disposed of
94+
95+
### Exercise
96+
97+
The team in charge of the `TodoRepository` has been hard at work and has managed to convert the `TodoRepository` into a completely Effect-based service complete with a `Layer` for service construction.
98+
99+
Using what we have learned above, your tasks for this exercise include:
100+
101+
- Create a `ManagedRuntime` which takes in the `TodoRepository` layer
102+
- Use the `ManagedRuntime` to run the Effect programs within the Express route handlers
103+
- For any Effect program which may result in a `TodoNotFoundError`:
104+
- Set the response status code to `404`
105+
- Return a JSON response that conforms to the following `{ "type": "TodoNotFound", "id": <TODO_ID> }`
106+
- **BONUS**: properly dispose of the `ManagedRuntime` when the server shuts down
107+
108+
In the editor to the right, modify the code to restore functionality to our server. Please note that there is no one "correct" answer, as there are multiple ways to achieve the desired outcome.

src/tutorials/common.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export const tutorialWorkspaces: ReadonlyRecord<
2323
name: "express",
2424
dependencies: {
2525
...packageJson.dependencies,
26-
express: "latest"
26+
express: "latest",
27+
"@types/express": "latest"
2728
},
2829
shells: [
2930
new WorkspaceShell({ label: "Server", command: "../run src/main.ts" }),

src/tutorials/incremental-adoption/0/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ app.patch("/todos/:id", (req, res) => {
3232
// Delete Todo
3333
app.delete("/todos/:id", (req, res) => {
3434
const id = Number.parseInt(req.params.id)
35-
repo.delete(id).then((deleted) => res.json(deleted))
35+
repo.delete(id).then((deleted) => res.json({ deleted }))
3636
})
3737

3838
app.listen(3000, () => {

src/tutorials/incremental-adoption/200/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ app.post("/todos", (req, res) => {
1414
Effect.andThen((todo) => res.json(todo)),
1515
Effect.catchTag("CreateTodoError", (error) =>
1616
Effect.sync(() => {
17-
res.status(500).json({
17+
res.status(404).json({
1818
type: error._tag,
19-
text: error.text
19+
text: error.text ?? ""
2020
})
2121
})
2222
),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Express from "express"
2+
import { TodoRepository } from "./repo"
3+
4+
const app = Express()
5+
6+
app.use(Express.json() as Express.NextFunction)
7+
8+
const repo = new TodoRepository()
9+
10+
// Create Todo
11+
app.post("/todos", (req, res) => {
12+
repo.create(req.body.text).then((todo) => res.json(todo))
13+
})
14+
15+
// Read Todo
16+
app.get("/todos/:id", (req, res) => {
17+
const id = Number.parseInt(req.params.id)
18+
repo.get(id).then((todo) => res.json(todo))
19+
})
20+
21+
// Read Todos
22+
app.get("/todos", (_, res) => {
23+
repo.getAll().then((todos) => res.json(todos))
24+
})
25+
26+
// Update Todo
27+
app.patch("/todos/:id", (req, res) => {
28+
const id = Number.parseInt(req.params.id)
29+
repo.update(id, req.body).then((todo) => res.json(todo))
30+
})
31+
32+
// Delete Todo
33+
app.delete("/todos/:id", (req, res) => {
34+
const id = Number.parseInt(req.params.id)
35+
repo.delete(id).then((deleted) => res.json({ deleted }))
36+
})
37+
38+
app.listen(3000, () => {
39+
console.log("Server listing on port 3000...")
40+
})
Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,78 @@
11
import Express from "express"
2-
import { Effect } from "effect"
2+
import { Effect, ManagedRuntime } from "effect"
33
import { TodoRepository } from "./repo"
44

55
const app = Express()
66

77
app.use(Express.json() as Express.NextFunction)
88

9-
const repo = new TodoRepository()
10-
9+
const runtime = ManagedRuntime.make(TodoRepository.Live)
1110

1211
// Create Todo
1312
app.post("/todos", (req, res) => {
14-
repo.create(req.body.text).then((todo) => res.json(todo))
13+
TodoRepository.create(req.body.text).pipe(
14+
Effect.andThen((todo) => res.json(todo)),
15+
runtime.runPromise
16+
)
1517
})
1618

1719
// Read Todo
1820
app.get("/todos/:id", (req, res) => {
1921
const id = Number.parseInt(req.params.id)
20-
repo.get(id).then((todo) => res.json(todo))
22+
TodoRepository.get(id).pipe(
23+
Effect.andThen((todo) => res.json(todo)),
24+
Effect.catchTag("TodoNotFoundError", () =>
25+
Effect.sync(() => {
26+
res.status(404).json({ type: "TodoNotFound", id })
27+
})
28+
),
29+
runtime.runPromise
30+
)
2131
})
2232

2333
// Read Todos
2434
app.get("/todos", (_, res) => {
25-
repo.getAll().then((todos) => res.json(todos))
35+
TodoRepository.getAll.pipe(
36+
Effect.andThen((todos) => res.json(todos)),
37+
runtime.runPromise
38+
)
2639
})
2740

2841
// Update Todo
2942
app.patch("/todos/:id", (req, res) => {
3043
const id = Number.parseInt(req.params.id)
31-
repo.update(id, req.body).then((todo) => res.json(todo))
44+
TodoRepository.update(id, req.body).pipe(
45+
Effect.andThen((todo) => res.json(todo)),
46+
Effect.catchTag("TodoNotFoundError", () =>
47+
Effect.sync(() => {
48+
res.status(404).json({ type: "TodoNotFound", id })
49+
})
50+
),
51+
runtime.runPromise
52+
)
3253
})
3354

3455
// Delete Todo
3556
app.delete("/todos/:id", (req, res) => {
3657
const id = Number.parseInt(req.params.id)
37-
repo.delete(id).then((deleted) => res.json(deleted))
58+
TodoRepository.delete(id).pipe(
59+
Effect.andThen((deleted) => res.json({ deleted })),
60+
runtime.runPromise
61+
)
3862
})
3963

40-
app.listen(3000, () => {
64+
const server = app.listen(3000, () => {
4165
console.log("Server listing on port 3000...")
4266
})
67+
68+
// Graceful Shutdown
69+
process.on("SIGTERM", shutdown)
70+
process.on("SIGINT", shutdown)
71+
72+
function shutdown() {
73+
server.close(() => {
74+
runtime.dispose().then(() => {
75+
process.exit(0)
76+
})
77+
})
78+
}

0 commit comments

Comments
 (0)