Building and composing layers

We now have multiple service required to run the final program.

Without Layer we are required to provide each service one by one:

index.ts
export const program = Effect.gen(function* () {
  const pokeApi = yield* PokeApi;
  return yield* pokeApi.getPokemon;
});

const runnable = program.pipe(
  Effect.provideService(PokeApi, PokeApi.Live),
  Effect.provideService(PokemonCollection, PokemonCollection.Live),
  Effect.provideServiceEffect(BuildPokeApiUrl, BuildPokeApiUrl.Live),
  Effect.provideServiceEffect(PokeApiUrl, PokeApiUrl.Live)
);

provideServiceEffect is used to extract the service from an Effect.

In our example PokeApiUrlLive and BuildPokeApiUrlLive are defined using Effect.gen, therefore we need to use provideServiceEffect to extract the service from Effect.

This strategy quickly becomes hard to maintain and read. Furthermore, if a service depends on another (interdependency), this will become even worst.

What Layer allows us to do instead is to organize all the dependencies and provide them only once.

A Layer for a service can be created using Layer.succeed:

PokemonCollection.ts
import { Context, Layer, type Array } from "effect";

export class PokemonCollection extends Context.Tag("PokemonCollection")<
  PokemonCollection,
  Array.NonEmptyArray<string>
>() {
  static readonly Live = Layer.succeed(this, [
    "staryu",
    "perrserker",
    "flaaffy",
  ]);
}
PokeApi.ts
export class PokeApi extends Context.Tag("PokeApi")<PokeApi, typeof make>() {
  static readonly Live = Layer.succeed(this, make);
}

When a service is derived from an Effect instead we use Layer.effect:

Notice how we don't need PokeApiUrl.of and BuildPokeApiUrl.of anymore, the type is inferred from Layer.

PokeApiUrl.ts
export class PokeApiUrl extends Context.Tag("PokeApiUrl")<
  PokeApiUrl,
  string
>() {
  static readonly Live = Layer.effect(
    this,
    Effect.gen(function* () {
      const baseUrl = yield* Config.string("BASE_URL");
      return `${baseUrl}/api/v2/pokemon`;
    })
  );
}
BuildPokeApiUrl.ts
export class BuildPokeApiUrl extends Context.Tag("BuildPokeApiUrl")<
  BuildPokeApiUrl,
  ({ name }: { name: string }) => string
>() {
  static readonly Live = Layer.effect(
    this,
    Effect.gen(function* () {
      const pokeApiUrl = yield* PokeApiUrl;
      return ({ name }) => `${pokeApiUrl}/${name}`;
    })
  );
}

Each Layer is then composed using Layer.mergeAll:

index.ts
const MainLayer = Layer.mergeAll(
  PokeApi.Live,
  PokemonCollection.Live,
  BuildPokeApiUrl.Live,
  PokeApiUrl.Live
);

Notice how easy is to reference Live implementations by making them a static attribute on the class

MainLayer is now a new Layer that contains all the dependencies defined inside mergeAll.

Finally, we can provide this single layer to run the final program using Effect.provide:

index.ts
const MainLayer = Layer.mergeAll(
  PokeApi.Live,
  PokemonCollection.Live,
  BuildPokeApiUrl.Live,
  PokeApiUrl.Live
);

export const program = Effect.gen(function* () {
  const pokeApi = yield* PokeApi;
  return yield* pokeApi.getPokemon;
});

const runnable = program.pipe(Effect.provide(MainLayer));

This allows to separate how we organize services (Layer) from how we provide them (Effect.provide).

We can use Layer to manage each dependency while keeping a single Effect.provide inside index.ts.

index.ts now doesn't contain any implementation details, but instead it defines how to compose layers and run the app:

index.ts
import { Effect, Layer } from "effect";
import { BuildPokeApiUrl } from "./BuildPokeApiUrl";
import { PokeApi } from "./PokeApi";
import { PokeApiUrl } from "./PokeApiUrl";
import { PokemonCollection } from "./PokemonCollection";

const MainLayer = Layer.mergeAll(
  PokeApi.Live,
  PokemonCollection.Live,
  BuildPokeApiUrl.Live,
  PokeApiUrl.Live
);

export const program = Effect.gen(function* () {
  const pokeApi = yield* PokeApi;
  return yield* pokeApi.getPokemon;
});

const runnable = program.pipe(Effect.provide(MainLayer));

const main = runnable.pipe(
  Effect.catchTags({
    FetchError: () => Effect.succeed("Fetch error"),
    JsonError: () => Effect.succeed("Json error"),
    ParseError: () => Effect.succeed("Parse error"),
  })
);

Effect.runPromise(main).then(console.log);

We are not done yet. There is a type error!

We still cannot run main, since a dependency seems to be missing!
We still cannot run main, since a dependency seems to be missing!

Type 'PokeApiUrl' is not assignable to type 'never'.

This is a missing dependency. What's happening here? Didn't we already provide PokeApiUrl to Layer.mergeAll?

const MainLayer = Layer.mergeAll(
  PokeApi.Live,
  PokemonCollection.Live,
  BuildPokeApiUrl.Live,
  PokeApiUrl.Live
);

PokeApiUrl is there, but we missed something else: dependencies between layers. Let's fix this in the next lesson 👇