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

  1. Create a new directory for your project and navigate to it using your terminal:

    Terminal
    bash
    mkdir express-effect-integration
    cd express-effect-integration
    Terminal
    bash
    mkdir express-effect-integration
    cd express-effect-integration
  2. Initialize your project with npm. This will create a package.json file:

    Terminal
    bash
    npm init -y
    Terminal
    bash
    npm init -y
  3. Install the necessary dependencies:

    Terminal
    bash
    npm install effect express
    Terminal
    bash
    npm install effect express

    Install the necessary dev dependencies:

    Terminal
    bash
    npm install typescript @types/express --save-dev
    Terminal
    bash
    npm install typescript @types/express --save-dev

    Now, initialize TypeScript:

    Terminal
    bash
    npx tsc --init
    Terminal
    bash
    npx tsc --init
  4. Create a new file, for example, hello-world.ts, and add the following code:

    hello-world.ts
    ts
    import { Context, Layer, Effect, Runtime } from "effect"
    import express from "express"
     
    // Define Express as a service
    class Express extends Context.Tag("Express")<
    Express,
    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))
    hello-world.ts
    ts
    import { Context, Layer, Effect, Runtime } from "effect"
    import express from "express"
     
    // Define Express as a service
    class Express extends Context.Tag("Express")<
    Express,
    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))
  5. Run your Express server. If you have ts-node installed, run the following command in the terminal:

    Terminal
    bash
    ts-node hello-world.ts
    Terminal
    bash
    ts-node hello-world.ts

    Visit 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 service
    class Express extends Context.Tag("Express")<
    Express,
    ReturnType<typeof express>
    >() {}
    ts
    // Define Express as a service
    class 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 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!")))
    })
    })
    )
    ts
    // 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 use Layer.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 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())
    )
    })
    )
    ts
    // 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 use Layer.scopedDiscard to discard its output.

  • Mounting. Finally, we mount the server by adding our route

    ts
    const AppLive = ServerLive.pipe(
    Layer.provide(IndexRouteLive),
    Layer.provide(ExpressLive)
    )
    ts
    const AppLive = ServerLive.pipe(
    Layer.provide(IndexRouteLive),
    Layer.provide(ExpressLive)
    )

    and providing the necessary dependency to the Express app

    ts
    const ExpressLive = Layer.sync(Express, () => express())
    // Combine the layers
    const AppLive = ServerLive.pipe(
    Layer.provide(IndexRouteLive),
    Layer.provide(ExpressLive)
    )
    ts
    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.

basic-routing.ts
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 service
class 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 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())
//
// Domain
//
interface Todo {
readonly id: number
readonly title: string
readonly completed: boolean
}
// Define the repository as a service
class 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 Todos
const 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 ID
const TodoByIdRouteLive = Layer.scopedDiscard(
Effect.gen(function* () {
const repo = yield* TodoRepository
yield* get("/todo/:id", (req, res) =>
Effect.gen(function* () {
const id = req.params.id
const todo = yield* repo.getTodo(Number(id))
res.json(todo)
})
)
})
)
// Merge routes into a single layer
const RouterLive = Layer.mergeAll(IndexRouteLive, TodoByIdRouteLive)
// Combine all layers to create the final application layer
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
const 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))
basic-routing.ts
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 service
class 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 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())
//
// Domain
//
interface Todo {
readonly id: number
readonly title: string
readonly completed: boolean
}
// Define the repository as a service
class 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 Todos
const 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 ID
const TodoByIdRouteLive = Layer.scopedDiscard(
Effect.gen(function* () {
const repo = yield* TodoRepository
yield* get("/todo/:id", (req, res) =>
Effect.gen(function* () {
const id = req.params.id
const todo = yield* repo.getTodo(Number(id))
res.json(todo)
})
)
})
)
// Merge routes into a single layer
const RouterLive = Layer.mergeAll(IndexRouteLive, TodoByIdRouteLive)
// Combine all layers to create the final application layer
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
const 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))