Documentation
Express

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

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

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

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

    Terminal
    npm install effect express

    Install the necessary dev dependencies:

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

    Now, initialize TypeScript:

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

    hello-world.ts
    import { 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))
  5. Run your Express server. If you have ts-node (opens in a new tab) installed, run the following command in the terminal:

    Terminal
    ts-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 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.

    // 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

    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.

basic-routing.ts
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))