Dependencies on the service level

One thing missing: why PokeApiLive doesn't have a dependency on PokemonCollection and BuildPokeApiUrl?

const make = {
  getPokemon: Effect.gen(function* () {
    const pokemonCollection = yield* PokemonCollection;
    const buildPokeApiUrl = yield* BuildPokeApiUrl;

    const requestUrl = buildPokeApiUrl({ name: pokemonCollection[0] });

    const response = yield* Effect.tryPromise({
      try: () => fetch(requestUrl),
      catch: () => new FetchError(),
    });

    if (!response.ok) {
      return yield* new FetchError();
    }

    const json = yield* Effect.tryPromise({
      try: () => response.json(),
      catch: () => new JsonError(),
    });

    return yield* Schema.decodeUnknown(Pokemon)(json);
  }),
};

export class PokeApi extends Context.Tag("PokeApi")<PokeApi, typeof make>() {
  static readonly Live = Layer.succeed(this, make);
}

Despite using PokemonCollection and BuildPokeApiUrl, PokeApi.Live has no dependency on them (third type parameter of Layer is never)
Despite using PokemonCollection and BuildPokeApiUrl, PokeApi.Live has no dependency on them (third type parameter of Layer is never)

This comes back to what we discussed previously: the dependency is not on PokeApi, but instead on the getPokemon function.

In practice it means that only when we use getPokemon these dependencies will be required. It's not a requirement for the service itself.

In our example MainLayer provides both PokemonCollection and BuildPokeApiUrl, therefore everything works as expected.

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

Dependencies on the function level are useful when only 1 function needs them. In practice is common to raise the dependency on the service since multiple functions will use the same dependency:

  1. Extract PokemonCollection and BuildPokeApiUrl outside of getPokemon
  2. Change the definition of the service Context to Effect.Effect.Success<PokeApi>
  3. Since make is now an Effect we need to use Layer.effect instead of Layer.succeed
PokeApi.ts
const make = Effect.gen(function* () {
  /// 1️⃣ Extract `PokemonCollection` and `BuildPokeApiUrl` outside of `getPokemon` 
  const pokemonCollection = yield* PokemonCollection;
  const buildPokeApiUrl = yield* BuildPokeApiUrl;

  return {
    getPokemon: Effect.gen(function* () {
      const requestUrl = buildPokeApiUrl({ name: pokemonCollection[0] });

      const response = yield* Effect.tryPromise({
        try: () => fetch(requestUrl),
        catch: () => new FetchError(),
      });

      if (!response.ok) {
        return yield* new FetchError();
      }

      const json = yield* Effect.tryPromise({
        try: () => response.json(),
        catch: () => new JsonError(),
      });

      return yield* Schema.decodeUnknown(Pokemon)(json);
    }),
  };
});

export class PokeApi extends Context.Tag("PokeApi")<
  PokeApi,
  /// 2️⃣ Change the definition of the service to `Effect.Effect.Success<typeof make>`
  Effect.Effect.Success<typeof make>
>() {
  /// 3️⃣ Use `Layer.effect` instead of `Layer.succeed`
  static readonly Live = Layer.effect(this, make);
}

Effect.Effect.Success allows to extract the success type from any Effect (the first type parameter).

This is needed because we want the service to contain the success methods, not the Effect used to create it.

Last step is providing the required dependencies to PokeApiLive:

PokeApi.ts
export class PokeApi extends Context.Tag("PokeApi")<
  PokeApi,
  Effect.Effect.Success<typeof make>
>() {
  static readonly Live = Layer.effect(this, make).pipe(
    // 👇 Remember: provide dependencies directly inside `Live`
    Layer.provide(Layer.mergeAll(PokemonCollection.Live, BuildPokeApiUrl.Live))
  );
}

Furthermore, PokemonCollection and BuildPokeApiUrl are provided from PokeApi, and PokeApiUrl is provided from BuildPokeApiUrl:

export class PokeApi extends Context.Tag("PokeApi")<
  PokeApi,
  Effect.Effect.Success<typeof make>
>() {
  static readonly Live = Layer.effect(this, make).pipe(
    // 👇 `PokemonCollection` and `BuildPokeApiUrl` are provided from `PokeApi`
    Layer.provide(Layer.mergeAll(PokemonCollection.Live, BuildPokeApiUrl.Live))
  );
}
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}`;
    })
  ).pipe(
    // 👇 `PokeApiUrl` is provided from `BuildPokeApiUrl`
    Layer.provide(PokeApiUrl.Live)
  );
}

This means that we don't need to provide them in MainLayer:

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

MainLayer only requires PokeApi since it is used directly inside program. This is the final result:

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

const MainLayer = Layer.mergeAll(PokeApi.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);

Done! Now we organized all services, layers, and their dependencies.

Every service implementation has its own dependencies provided directly when Layer is created.

This allows to have a flat MainLayer with a single mergeAll. Each dependency is defined and provided in separate files, so that we can focus on one service at the time before composing everything together.

All type safe!