Finally, we can refactor also PokeApi
:
const make = Effect.gen(function* () {
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,
Effect.Effect.Success<typeof make>
>() {
static readonly Live = Layer.effect(this, make).pipe(
Layer.provide(Layer.mergeAll(PokemonCollection.Live, BuildPokeApiUrl.Live))
);
}
Since the service is defined as Effect
(using Layer.effect
) we use effect
:
export class PokeApi extends Effect.Service<PokeApi>()("PokeApi", {
effect: Effect.gen(function* () {
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);
}),
};
}),
}) {}
We then also add dependencies
to provide the required services.
A class
defined with Effect.Service
has a Default
attribute that contains the default layer with all the dependencies provided.
We use it from both PokemonCollection
and BuildPokeApiUrl
:
export class PokeApi extends Effect.Service<PokeApi>()("PokeApi", {
effect: Effect.gen(function* () {
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);
}),
};
}),
dependencies: [PokemonCollection.Default, BuildPokeApiUrl.Default],
}) {}
Effect.Service
also has aDefaultWithoutDependencies
attribute that contains the default layer without the dependencies (e.g.BuildPokeApiUrl.DefaultWithoutDependencies
).
We extract PokeApi.Default
also inside MainLayer
to complete the refactoring to Effect.Service
:
import { Effect, Layer } from "effect";
import { PokeApi } from "./PokeApi";
const MainLayer = Layer.mergeAll(PokeApi.Default);
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);
Now the app works the same as before, but using a more expressive API with fewer lines of code.
What are the advantages of all these Layer
and Config
?
This implementation makes all the services composable. We are going to see composability in action in the next module about testing 🚀