Express Integration
On this page
In this guide, we'll explore how to integrate Effect with Express, 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" found in the Express documentation.
Setup Steps
-
Create a new directory for your project and navigate to it using your terminal:
Terminalbashmkdir express-effect-integrationcd express-effect-integrationTerminalbashmkdir express-effect-integrationcd express-effect-integration -
Initialize your project with npm. This will create a
package.json
file:Terminalbashnpm init -yTerminalbashnpm init -y -
Install the necessary dependencies:
Terminalbashnpm install effect expressTerminalbashnpm install effect expressInstall the necessary dev dependencies:
Terminalbashnpm install typescript @types/express --save-devTerminalbashnpm install typescript @types/express --save-devNow, initialize TypeScript:
Terminalbashnpx tsc --initTerminalbashnpx tsc --init -
Create a new file, for example,
hello-world.ts
, and add the following code:hello-world.tstsimport {Context ,Layer ,Effect ,Runtime } from "effect"importexpress from "express"// Define Express as a serviceclassExpress extendsContext .Tag ("Express")<Express ,ReturnType <typeofexpress >>() {}// Define the main route, IndexRouteLive, as a LayerconstIndexRouteLive =Layer .effectDiscard (Effect .gen (function* (_ ) {constapp = yield*_ (Express )construnFork =Runtime .runFork (yield*_ (Effect .runtime <never>()))app .get ("/", (_ ,res ) => {runFork (Effect .sync (() =>res .send ("Hello World!")))})}))// Server SetupconstServerLive =Layer .scopedDiscard (Effect .gen (function* (_ ) {constport = 3001constapp = 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 ExpressconstExpressLive =Layer .sync (Express , () =>express ())// Combine the layersconstAppLive =ServerLive .pipe (Layer .provide (IndexRouteLive ),Layer .provide (ExpressLive ))// Run the programEffect .runFork (Layer .launch (AppLive ))hello-world.tstsimport {Context ,Layer ,Effect ,Runtime } from "effect"importexpress from "express"// Define Express as a serviceclassExpress extendsContext .Tag ("Express")<Express ,ReturnType <typeofexpress >>() {}// Define the main route, IndexRouteLive, as a LayerconstIndexRouteLive =Layer .effectDiscard (Effect .gen (function* (_ ) {constapp = yield*_ (Express )construnFork =Runtime .runFork (yield*_ (Effect .runtime <never>()))app .get ("/", (_ ,res ) => {runFork (Effect .sync (() =>res .send ("Hello World!")))})}))// Server SetupconstServerLive =Layer .scopedDiscard (Effect .gen (function* (_ ) {constport = 3001constapp = 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 ExpressconstExpressLive =Layer .sync (Express , () =>express ())// Combine the layersconstAppLive =ServerLive .pipe (Layer .provide (IndexRouteLive ),Layer .provide (ExpressLive ))// Run the programEffect .runFork (Layer .launch (AppLive )) -
Run your Express server. If you have
ts-node
installed, run the following command in the terminal:Terminalbashts-node hello-world.tsTerminalbashts-node hello-world.tsVisit http://localhost:3001 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.ts// Define Express as a serviceclass Express extends Context.Tag("Express")<Express,ReturnType<typeof express>>() {}ts// Define Express as a serviceclass Express extends Context.Tag("Express")<Express,ReturnType<typeof express>>() {} -
Main Route. The main route,
IndexRouteLive
, is defined as a Layer.ts// Define the main route, IndexRouteLive, as a Layerconst 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!")))})}))ts// Define the main route, IndexRouteLive, as a Layerconst 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.ts// Server Setupconst ServerLive = Layer.scopedDiscard(Effect.gen(function* (_) {const port = 3001const app = yield* _(Express)yield* _(Effect.acquireRelease(Effect.sync(() =>app.listen(port, () =>console.log(`Example app listening on port ${port}`))),(server) => Effect.sync(() => server.close())))}))ts// Server Setupconst ServerLive = Layer.scopedDiscard(Effect.gen(function* (_) {const port = 3001const 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
tsconst AppLive = ServerLive.pipe(Layer.provide(IndexRouteLive),Layer.provide(ExpressLive))tsconst AppLive = ServerLive.pipe(Layer.provide(IndexRouteLive),Layer.provide(ExpressLive))and providing the necessary dependency to the Express app
tsconst ExpressLive = Layer.sync(Express, () => express())// Combine the layersconst AppLive = ServerLive.pipe(Layer.provide(IndexRouteLive),Layer.provide(ExpressLive))tsconst ExpressLive = Layer.sync(Express, () => express())// Combine the layersconst 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.
ts
import { Context, Effect, FiberSet, Layer } from "effect"import express from "express"//// Express//// NB: this is an example of an integration to a third party lib, not the suggested way of integrating express//// Define Express as a serviceclass Express extends Context.Tag("Express")<Express,ReturnType<typeof express>>() {}const get = <A, E, R>(path: string,body: (req: express.Request,res: express.Response) => Effect.Effect<A, E, R>) =>Effect.gen(function* (_) {const app = yield* _(Express)const run = yield* _(FiberSet.makeRuntime<R>())app.get(path, (req, res) => run(body(req, res)))})// Server Setupconst ServerLive = Layer.scopedDiscard(Effect.gen(function* (_) {const port = 3001const 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 Expressconst ExpressLive = Layer.sync(Express, () => express())//// Domain//interface Todo {readonly id: numberreadonly title: stringreadonly completed: boolean}// Define the repository as a serviceclass TodoRepository extends Context.Tag("TodoRepository")<TodoRepository,{readonly getTodos: Effect.Effect<Array<Todo>>readonly getTodo: (id: number) => Effect.Effect<Todo | null>}>() {}//// App//// Define a main route that returns all Todosconst IndexRouteLive = Layer.scopedDiscard(Effect.gen(function* (_) {const repo = yield* _(TodoRepository)yield* _(get("/", (_, res) =>Effect.gen(function* (_) {const todos = yield* _(repo.getTodos)res.json(todos)})))}))// Define a route that returns a Todo by its IDconst TodoByIdRouteLive = Layer.scopedDiscard(Effect.gen(function* (_) {const repo = yield* _(TodoRepository)yield* _(get("/todo/:id", (req, res) =>Effect.gen(function* (_) {const id = req.params.idconst todo = yield* _(repo.getTodo(Number(id)))res.json(todo)})))}))// Merge routes into a single layerconst RouterLive = Layer.mergeAll(IndexRouteLive, TodoByIdRouteLive)// Combine all layers to create the final application layerconst AppLive = ServerLive.pipe(Layer.provide(RouterLive),Layer.provide(ExpressLive))// Test Data for TodoRepositoryconst 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 dataconst TodoRepositoryTest = Layer.succeed(TodoRepository, {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))
ts
import { Context, Effect, FiberSet, Layer } from "effect"import express from "express"//// Express//// NB: this is an example of an integration to a third party lib, not the suggested way of integrating express//// Define Express as a serviceclass Express extends Context.Tag("Express")<Express,ReturnType<typeof express>>() {}const get = <A, E, R>(path: string,body: (req: express.Request,res: express.Response) => Effect.Effect<A, E, R>) =>Effect.gen(function* (_) {const app = yield* _(Express)const run = yield* _(FiberSet.makeRuntime<R>())app.get(path, (req, res) => run(body(req, res)))})// Server Setupconst ServerLive = Layer.scopedDiscard(Effect.gen(function* (_) {const port = 3001const 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 Expressconst ExpressLive = Layer.sync(Express, () => express())//// Domain//interface Todo {readonly id: numberreadonly title: stringreadonly completed: boolean}// Define the repository as a serviceclass TodoRepository extends Context.Tag("TodoRepository")<TodoRepository,{readonly getTodos: Effect.Effect<Array<Todo>>readonly getTodo: (id: number) => Effect.Effect<Todo | null>}>() {}//// App//// Define a main route that returns all Todosconst IndexRouteLive = Layer.scopedDiscard(Effect.gen(function* (_) {const repo = yield* _(TodoRepository)yield* _(get("/", (_, res) =>Effect.gen(function* (_) {const todos = yield* _(repo.getTodos)res.json(todos)})))}))// Define a route that returns a Todo by its IDconst TodoByIdRouteLive = Layer.scopedDiscard(Effect.gen(function* (_) {const repo = yield* _(TodoRepository)yield* _(get("/todo/:id", (req, res) =>Effect.gen(function* (_) {const id = req.params.idconst todo = yield* _(repo.getTodo(Number(id)))res.json(todo)})))}))// Merge routes into a single layerconst RouterLive = Layer.mergeAll(IndexRouteLive, TodoByIdRouteLive)// Combine all layers to create the final application layerconst AppLive = ServerLive.pipe(Layer.provide(RouterLive),Layer.provide(ExpressLive))// Test Data for TodoRepositoryconst 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 dataconst TodoRepositoryTest = Layer.succeed(TodoRepository, {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))