Application Bootstrapping with fp-ts
Tags: functional, typescript, architecture
Published: 2023-06-23 16:34:12

*This article assumes familiarity with [fp-ts](https://github.com/gcanti/fp-ts) and/or functional programming with algebraic data types.*

**UPDATE: I've released a library that packages the approach described below, and solves the problem mentioned at the end: [fp-ts-bootstrap](https://github.com/Avaq/fp-ts-bootstrap)**

## 🛫 Application Bootstrapping 🛬

I'm using the term *bootstrapping* here to refer to the "start-up phase" (and "shut-down phase", as we'll get to) of a piece of software. This is any of the code that runs once, just after booting a system, to ensure that everything needed for the program to do its job is in place. 🐣

This task commonly involves setting up database connection (pools), file handlers, socket bindings, timing intervals, etc.

Managing the complexity of such a task is no easy feat, especially when you consider that:

- resource acquisition could be asynchronous;
- some resources may depend on others;
- acquisition of a resource may fail; and
- a process may eventually want to de-allocate / dispose the resources after it completed the task that needed them.

There are a lot of solutions out there to help you manage this complexity. Typically, you'll find dependency injection frameworks utilizing some form of [Inversion of Control](https://en.wikipedia.org/wiki/Inversion_of_control). In this space you'll find factories, singletons, decorators, middleware, and any pattern we can throw at it. That's because there are demanding requirements for these tools. Here's my wishlist for a bootstrapping solution:

- 🛬 Resources should be disposed gracefully, so that I don't have to kill my process to restore it to a clean state after consuming a resource.
- 🧑‍🤝‍🧑 My resource disposal logic should live nearby my resource acquisition logic.
- 🚦 It should be easy to manage asynchronous acquisition and disposal of resources.
- 🏗 When one resource depends on another, I want to express that dependency in a convenient way, and the system should take care of sequencing acquisition and disposal between the two resources in the correct order.
- 🪂 When acquisition of a resource fails, the resources it depended on should be neatly disposed.
- 📦 The program that consumes my resources should be defined separately from the bootstrapping logic, so that I can run the program with a custom set of resources (for example mocks).
- 🚚 I want to be able to use a "stack" of resources and expand upon it / use it as a resource within another stack.

## 🧃 Solving it with Monads 😎

[fp-ts](https://github.com/gcanti/fp-ts), a library that caters to functional programming in TypeScript, comes with some micro-abstractions that already solve a few of our needs.

- The [Task Monad](https://gcanti.github.io/fp-ts/modules/Task.ts.html) can help us sequence asynchronous operations, like the "acquisition ➡ consumption ➡ disposal" of resources.
- The [Either Monad](https://gcanti.github.io/fp-ts/modules/Either.ts.html) can model failure. Failure to acquire resources, consume resources, or dispose resources.
- The combination of Task and Either give us the [TaskEither Monad](https://gcanti.github.io/fp-ts/modules/TaskEither.ts.html) which we'll get back to because it comes with a very useful utility for what we're trying to do.
- The [Reader Monad](https://gcanti.github.io/fp-ts/modules/Reader.ts.html) is all about managing injected dependencies, so we can use it to model the requirement for service dependencies.
- There's one more Monad we'll use to tie it all together, but I'll keep you in suspense as we build up to it! 👀

### Bracket

We'll start with that useful TaskEither utility I mentioned: I was referring to [`TaskEither.bracket`](https://gcanti.github.io/fp-ts/modules/TaskEither.ts.html#bracket), of course!

```ts
declare const bracket: <E, A, B>(
  acquire: TaskEither<E, A>,
  use: (a: A) => TaskEither<E, B>,
  release: (a: A, e: E.Either<E, B>) => TaskEither<E, void>
) => TaskEither<E, B>
```

The `bracket` function is basically our "acquisition ➡ consumption ➡ disposal" need completely met 💪! Let's look at how we can utilize it. In the example below, I'm wrapping a Node.js HTTP server in a `bracket` call that handles its acquisition and disposal. The `use` function is where we consume the resource, and in this case we're just delaying the program for 30 seconds so that the resource is "held" for a bit before it's disposed.

```ts
import * as HTTP from 'node:http';
import * as E from 'fp-ts/lib/Either';
import * as T from 'fp-ts/lib/Task';
import * as TE from 'fp-ts/lib/TaskEither';

// These are the dependencies of our http server. Eventually,
// we'd want these to live elsewhere in our codebase, but for
// now we'll just define them here.
const port = 3000;
const app = (_: unknown, response: HTTP.ServerResponse) => {
  response.writeHead(200);
  response.end('hello world\n');
};

// We define the acquisition of our http server as a TaskEither.
// That's a lazy Promise that resolves with an Either.
const acquireServer: TE.TaskEither<Error, HTTP.Server> = () => (
  new Promise(res => {
    const server = HTTP.createServer(app);
    server.once('error', e => res(E.left(e)));
    server.listen(port, () => res(E.right(server)));
  })
);

// The consumption of our server is just a TaskEither that delays
// the program for 30 seconds. Note that we're not using the
// server here, but we could!
const consumeServer = () => T.delay(30000)(TE.of('done'));

// To dispose of our server, we unbind event listeners and close the server.
const disposeServer = (server: HTTP.Server): TE.TaskEither<Error, void> => (
  () => new Promise(res => {
    server.removeAllListeners('error');
    server.close((e: unknown) => res(
      e instanceof Error ? E.left(e) : E.right(undefined)
    ));
  })
);

// We can now use bracket to sequence the acquisition,
// consumption, and disposal of our http server. The
// result is a TaskEither that resolves with the result
// of the consumption.
const program = TE.bracket(
  acquireServer,
  consumeServer,
  disposeServer
);

// Since all of our functions are lazy, we need to run
// the program to get a Promise that resolves with the
// result of the consumption. The Promise only rejects
// in case of an unexpected runtime error. Otherwise, it
// resolves with the Either that represents the success or
// failure of the program.
program().then(E.fold(console.error, console.log), console.error);
```

🧑‍💻 *If you want to follow along, you can run the code snippet above by saving it to `example.ts`, installing fp-ts with `npm i fp-ts`, and running it with `npx ts-node example.ts`*

Running the code above will cause our "hello world"-app to run for 30 seconds, during which `curl localhost:3000` should output `hello world`. After 30 seconds, the `server` resource consumption is done, which causes it to be disposed. We know that this cleaned everything up properly because afterwards, the process exits. When there are no remaining resources in use, Node just exits, because there's nothing left to do or to cause any events.

At the end, we see `done` in the terminal because that's the value returned from the consumption, and logged on the last line.

The type of our program is `Task<Either<Error, string>>`. We can handle asynchronous acquisition, consumption, and disposal of resources through the `Task` Monad, handling possible failure along the way with the `Either` Monad.

🌗 We're halfway to a solution, but there's a few things missing:

### Injecting Dependencies

In our example above, the `app` and `port` are baked into the `acquireServer` function. But if they themselves were resources, we'd want to inject them into `acquireServer` instead. We can do that with the Reader Monad:

```ts
import * as R from 'fp-ts/lib/Reader';
import {pipe} from 'fp-ts/lib/function';

// We capture the dependencies from earlier in a type.
type Dependencies = {
  app: HTTP.RequestListener,
  port: number,
};

// And we use the Reader Monad to inject them into our
// acquireServer function. A Reader is really just a standard
// function. We use the Reader type alias to encourage users
// to use the Reader Monad's composition utilities.
const acquireServer: R.Reader<Dependencies, TE.TaskEither<Error, HTTP.Server>> = (
  deps => () => new Promise(res => {
    const server = HTTP.createServer(deps.app);
    server.once('error', e => res(E.left(e)));
    server.listen(deps.port, () => res(E.right(server)));
  })
);

// For example, here we can use Reader.map to defer the
// injection of the dependencies to the caller of the withServer
// function.
const withServer = pipe(
  acquireServer,
  R.map(acquire => TE.bracket(acquire, consumeServer, disposeServer))
);

// Now, to define our program, we first need to inject the
// dependencies, and they'll be threaded along to the
// acquireServer function.
const program = withServer({app, port});
```

The type of `withServer` has become `Reader<Dependencies, Task<Either<Error, string>>>`. We can now inject dependencies, and threading them along is made easy using the `Reader` Monad!

🌖 We're almost there, but there's one more thing missing:

### De-coupling the Consumption

The `bracket` function takes acquire, consume, and dispose all at the same time. If we want to de-couple the consumption from the acquisition and disposal, we can create a curried a version of `bracket` that takes the consume function separately:

```ts
const createService = <E, Resource, Output = unknown>(
  acquire: TE.TaskEither<E, Resource>,
  dispose: (a: Resource) => TE.TaskEither<E, void>
) => (consume: (a: Resource) => TE.TaskEither<E, Output>) => (
  TE.bracket(acquire, consume, dispose)
);
```

We can use `createService` to create a `withServer` function that takes a `consume` function and returns a `program` function that can be run to acquire, consume, and dispose the server:

```ts
const withServer = pipe(
  acquireServer,
  R.map(acquire => createService<Error, HTTP.Server, string>(acquire, disposeServer))
);

const program = withServer({app, port})(consumeServer);
```

You can imagine how one module might export a `withServer` function, and another module might import it to create a `program` function that uses it.

### Tying it Together with the Cont Monad

In the previous step, we curried the `bracket` function and created something that doesn't really look like it'll compose nicely. What if we want to use multiple resources in our program? We will find ourselves in a kind of callback hell, and our consumption function is nested inside again:

```ts
// Imagine we have these functions:
const program = withLogger({transport})(logger => (
  withDatabase({url, logger})(database => (
    withCache({database, logger})(cache => (
      withServer({app, port, logger})(server => (
        consumeServices({database, server, logger, cache})
      ))
    ))
  ))
));
```

But we missed an opportunity when we curried `bracket`! If we look closely at its type, we can see that it actually returns a `Cont` Monad. `fp-ts` doesn't have a `Cont` Monad built in, so we can use [`fp-ts-cont`](https://github.com/joshburgess/fp-ts-cont):

```ts
import * as C from 'fp-ts-cont/lib/Cont';

const createService = <E, Resource, Output = unknown>(
  acquire: TE.TaskEither<E, Resource>,
  dispose: (a: Resource) => TE.TaskEither<E, void>
): C.Cont<TE.TaskEither<E, Output>, Resource> => consume => (
  TE.bracket(acquire, consume, dispose)
);
```

This new `createService` definition has the same type as the old one, but it uses the `Cont` type alias to signal that it returns a `Cont` Monad which we can use to compose our programs. As with the `Reader` type we used earlier, it's just an alias that encourages users to use the `Cont` Monad's composition utilities.

One of these composition utilities is the set of functions used fo Do-notation in FP-TS. Using `bindTo` and `bind`, we can flatten our nested services, combining them into a new service that we'll call `withServices`:

```ts
const withServices = pipe(
  withLogger({transport}),
  C.bindTo('logger'),
  C.bind('database', ({logger}) => withDatabase({url, logger})),
  C.bind('cache', withCache),
  C.bind('server', ({logger}) => withServer({app, port, logger})),
);

const program = withServices(consumeServices);
```

With the new `createService` function, the type of our services is now:

```ts
type Service<Dependencies, Resource, Output = unknown> = (
  Reader<Dependencies, Cont<Task<Either<Error, Output>>, Resource>>
)
```

🌕 Despite being a pretty simple type (a function that takes dependencies and an async callback), it becomes powerful because we've identified a Monad instance for each of its components. We can use those Monad instances to compose our services together, manage their dependencies, and control their asynchronous flow, all using the same interface. Let's check our requirements:

- 🛬 Resources are disposed gracefully after consumption thanks to `bracket`.
- 🧑‍🤝‍🧑 Definition of a service with its acquisition and disposal lives in isolation from its consumption, thanks to `createService`.
- 🚦 Managing asynchronous resources is easy thanks to `TaskEither`.
- 🏗 Building larger services out of several smaller ones is easy thanks to
  `Cont`, and sequencing of acquisition and disposal is handled automatically.
- 🪂 When a service fails to acquire, the whole program fails, and the disposals of all acquired services are run, thanks to how we've built up our program by composing `bracket` calls using `Cont`.
- 📦 The consumption of my final service is fully decoupled, and I could easily swap consumotion functions, or swap service definitions.
- 🚚 The larger services built using `Cont` are themselves services which could be composed into even larger services, and so on.

## Conclusion

I've been using this approach to application bootstrapping in my projects for some time now, and I'm very happy with it. It gives me the flexibility to split up my application into smaller services, and compose them into larger services, all while allowing each service to be used and tested in isolation.

I hope you find this approach useful, and I'd love to hear your feedback!

Here's the full, executable code after making all the changes suggested above:

```ts
import * as HTTP from 'node:http';
import * as E from 'fp-ts/lib/Either';
import * as T from 'fp-ts/lib/Task';
import * as TE from 'fp-ts/lib/TaskEither';
import * as R from 'fp-ts/lib/Reader';
import * as C from 'fp-ts-cont/lib/Cont';
import {pipe} from 'fp-ts/lib/function';

const createService = <E, Resource>(
  acquire: TE.TaskEither<E, Resource>,
  dispose: (a: Resource) => TE.TaskEither<E, void>
) => <T>(consume: (resource: Resource) => TE.TaskEither<E, T>) => (
  TE.bracket(acquire, consume, dispose)
);

type Dependencies = {
  app: HTTP.RequestListener,
  port: number,
};

const acquireServer: R.Reader<Dependencies, TE.TaskEither<Error, HTTP.Server>> = (
  deps => () => new Promise(res => {
    const server = HTTP.createServer(deps.app);
    server.once('error', e => res(E.left(e)));
    server.listen(deps.port, () => res(E.right(server)));
  })
);

const disposeServer = (server: HTTP.Server): TE.TaskEither<Error, void> => (
  () => new Promise(res => {
    server.removeAllListeners('error');
    server.close((e: unknown) => res(
      e instanceof Error ? E.left(e) : E.right(undefined)
    ));
  })
);

const withServer = pipe(
  acquireServer,
  R.map(acquire => createService(acquire, disposeServer))
);

// Possibly in another module:

const port = 3000;
const app = (_: unknown, response: HTTP.ServerResponse) => {
  response.writeHead(200);
  response.end('hello world\n');
};

const program = withServer({app, port})(() => T.delay(30000)(TE.of('done')));

program().then(E.fold(console.error, console.log), console.error);
```

--------------------------------------------------------------------------------

This approach is based on the [booture](https://github.com/fluture-js/booture) library that I created for [Fluture](https://github.com/fluture-js/Fluture) some time ago, with its ideas ported to `fp-ts` and `fp-ts-cont`.

--------------------------------------------------------------------------------

*Hey! You're still here? If you paid a lot of attention, you might have noticed the bit of type-cheating I did. You see, due to the `Output` type being embedded in the `Service` type, our service consumption isn't __truly__ decoupled from our service definition: The service definition is already deciding what the consumption must return. I'm working on some ideas to fix this. In the meantime, I've been just setting `Output` to `unknown`, and not using the output after disposal of the service.*

*UPDATE: [fp-ts-bootstrap](https://github.com/Avaq/fp-ts-bootstrap) was released, which solves this!*