We now have multiple service required to run the final program.
Without Layer
we are required to provide each service one by one:
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 anEffect
.In our example
PokeApiUrlLive
andBuildPokeApiUrlLive
are defined usingEffect.gen
, therefore we need to useprovideServiceEffect
to extract the service fromEffect
.
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
:
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",
]);
}
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
andBuildPokeApiUrl.of
anymore, the type is inferred fromLayer
.
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`;
})
);
}
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
:
const MainLayer = Layer.mergeAll(
PokeApi.Live,
PokemonCollection.Live,
BuildPokeApiUrl.Live,
PokeApiUrl.Live
);
Notice how easy is to reference
Live
implementations by making them astatic
attribute on theclass
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
:
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 singleEffect.provide
insideindex.ts
.
index.ts
now doesn't contain any implementation details, but instead it defines how to compose layers and run the app:
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!
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 👇