Express Integration
In this guide, we'll explore how to integrate Effect with Express (opens in a new tab), a popular web framework for Node.js.
Hello World Example
Let's start with a simple example that creates an Express server responding with "Hello World!" for requests to the root URL (/). This mirrors the classic "Hello world example" (opens in a new tab) found in the Express documentation.
Setup Steps
-
Create a new directory for your project and navigate to it using your terminal:
Terminalmkdir express-effect-integration cd express-effect-integration
-
Initialize your project with npm. This will create a
package.json
file:Terminalnpm init -y
-
Install the necessary dependencies:
Terminalnpm install effect express
Install the necessary dev dependencies:
npm install typescript @types/express --save-dev
Now, initialize TypeScript:
npx tsc --init
-
Create a new file, for example,
hello-world.ts
, and add the following code:hello-world.tsimport { Context, Layer, Effect, Runtime } from "effect" import express from "express" // Define Express as a service const Express = Context.Tag<ReturnType<typeof express>>() // Define the main route, IndexRouteLive, as a Layer const IndexRouteLive = Layer.effectDiscard( Effect.gen(function* (_) { const app = yield* _(Express) const runFork = Runtime.runFork(yield* _(Effect.runtime<never>())) app.get("/", (_, res) => { runFork(Effect.sync(() => res.send("Hello World!"))) }) }) ) // Server Setup const ServerLive = Layer.scopedDiscard( Effect.gen(function* (_) { const port = 3001 const app = yield* _(Express) yield* _( Effect.acquireRelease( Effect.sync(() => app.listen(port, () => console.log(`Example app listening on port ${port}`) ) ), (server) => Effect.sync(() => server.close()) ) ) }) ) // Setting Up Express const ExpressLive = Layer.sync(Express, () => express()) // Combine the layers const AppLive = ServerLive.pipe( Layer.provide(IndexRouteLive), Layer.provide(ExpressLive) ) // Run the program Effect.runFork(Layer.launch(AppLive))
-
Run your Express server. If you have
ts-node
(opens in a new tab) installed, run the following command in the terminal:Terminalts-node hello-world.ts
Visit http://localhost:3001 (opens in a new tab) in your web browser, and you should see "Hello World!".
Code Breakdown
Here's a breakdown of what's happening:
-
Express Service. We define an
Express
service to retrieve the Express app later on.// Define Express as a service const Express = Context.Tag<ReturnType<typeof express>>()
-
Main Route. The main route,
IndexRouteLive
, is defined as a Layer.// Define the main route, IndexRouteLive, as a Layer const IndexRouteLive = Layer.effectDiscard( Effect.gen(function* (_) { const app = yield* _(Express) const runFork = Runtime.runFork(yield* _(Effect.runtime<never>())) app.get("/", (_, res) => { runFork(Effect.sync(() => res.send("Hello World!"))) }) }) )
We access the runtime (
Effect.runtime
), which can be used to execute tasks within our route (runFork
). Since we don't need to produce any service in the output, we useLayer.effectDiscard
to discard its output. -
Server Setup. The server is created in a layer (
ServerLive
) and mounted at the end of our program.// Server Setup const ServerLive = Layer.scopedDiscard( Effect.gen(function* (_) { const port = 3001 const app = yield* _(Express) yield* _( Effect.acquireRelease( Effect.sync(() => app.listen(port, () => console.log(`Example app listening on port ${port}`) ) ), (server) => Effect.sync(() => server.close()) ) ) }) )
We use
Effect.acquireRelease
to create the server, allowing automatic management of the scope. Again, as we don't need to produce any service in the output, we useLayer.scopedDiscard
to discard its output. -
Mounting. Finally, we mount the server by adding our route
const AppLive = ServerLive.pipe( Layer.provide(IndexRouteLive), Layer.provide(ExpressLive) )
and providing the necessary dependency to the Express app
const ExpressLive = Layer.sync(Express, () => express()) // Combine the layers const AppLive = ServerLive.pipe( Layer.provide(IndexRouteLive), Layer.provide(ExpressLive) )
Basic routing
In this example, we'll explore the basics of routing with Effect and Express. The goal is to create a simple web server with two routes: one that returns all todos and another that returns a todo by its ID.
import { Context, Layer, Effect, Runtime } from "effect"
import express from "express"
// Define Express as a service
const Express = Context.Tag<ReturnType<typeof express>>()
interface Todo {
readonly id: number
readonly title: string
readonly completed: boolean
}
// Define a interface with methods to get all todos and a todo by ID
interface TodoRepository {
readonly getTodos: Effect.Effect<never, never, Array<Todo>>
readonly getTodo: (id: number) => Effect.Effect<never, never, Todo | null>
}
// Define the repository as a service
const TodoRepository = Context.Tag<TodoRepository>()
// Define a main route that returns all Todos
// $ExpectType Layer<Express | TodoRepository, never, never>
const IndexRouteLive = Layer.effectDiscard(
Effect.gen(function* (_) {
const app = yield* _(Express)
const runFork = Runtime.runFork(yield* _(Effect.runtime<TodoRepository>()))
app.get("/", (_, res) => {
runFork(
Effect.gen(function* (_) {
const repo = yield* _(TodoRepository)
const todos = yield* _(repo.getTodos)
res.json(todos)
})
)
})
})
)
// Define a route that returns a Todo by its ID
// $ExpectType Layer<Express | TodoRepository, never, never>
const TodoByIdRouteLive = Layer.effectDiscard(
Effect.gen(function* (_) {
const app = yield* _(Express)
const runFork = Runtime.runFork(yield* _(Effect.runtime<TodoRepository>()))
app.get("/todo/:id", (req, res) => {
const id = req.params.id
runFork(
Effect.gen(function* (_) {
const repo = yield* _(TodoRepository)
const todo = yield* _(repo.getTodo(Number(id)))
res.json(todo)
})
)
})
})
)
// Server Setup
// $ExpectType Layer<Express, never, never>
const ServerLive = Layer.scopedDiscard(
Effect.gen(function* (_) {
const port = 3001
const app = yield* _(Express)
yield* _(
Effect.acquireRelease(
Effect.sync(() =>
app.listen(port, () =>
console.log(`Example app listening on port ${port}`)
)
),
(server) => Effect.sync(() => server.close())
)
)
})
)
// Setting Up Express
// $ExpectType Layer<never, never, Express>
const ExpressLive = Layer.sync(Express, () => express())
// Merge routes into a single layer
// $ExpectType Layer<Express | TodoRepository, never, never>
const RouterLive = Layer.mergeAll(IndexRouteLive, TodoByIdRouteLive)
// Combine all layers to create the final application layer
// $ExpectType Layer<TodoRepository, never, never>
const AppLive = ServerLive.pipe(Layer.provide(RouterLive), Layer.provide(ExpressLive))
// Test Data for TodoRepository
const testData = [
{
id: 1,
title: "delectus aut autem",
completed: false
},
{
id: 2,
title: "quis ut nam facilis et officia qui",
completed: false
},
{
id: 3,
title: "fugiat veniam minus",
completed: false
}
]
// Create a layer with test data
// $ExpectType Layer<never, never, TodoRepository>
const TodoRepositoryTest = Layer.succeed(
TodoRepository,
TodoRepository.of({
getTodos: Effect.succeed(testData),
getTodo: (id) =>
Effect.succeed(testData.find((todo) => todo.id === id) || null)
})
)
const Test = AppLive.pipe(Layer.provide(TodoRepositoryTest))
Effect.runFork(Layer.launch(Test))