Until now we implemented everything in a single file index.ts
.
This single file starts to look complex:
index.ts
import { Config, Data, Effect, Schema } from "effect";
class Pokemon extends Schema.Class<Pokemon>("Pokemon")({
id: Schema.Number,
order: Schema.Number,
name: Schema.String,
height: Schema.Number,
weight: Schema.Number,
}) {}
class FetchError extends Data.TaggedError("FetchError")<{}> {}
class JsonError extends Data.TaggedError("JsonError")<{}> {}
const config = Config.string("BASE_URL");
const fetchRequest = (baseUrl: string) =>
Effect.tryPromise({
try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`),
catch: () => new FetchError(),
});
const jsonResponse = (response: Response) =>
Effect.tryPromise({
try: () => response.json(),
catch: () => new JsonError(),
});
const decodePokemon = Schema.decodeUnknown(Pokemon);
const program = Effect.gen(function* () {
const baseUrl = yield* config;
const response = yield* fetchRequest(baseUrl);
if (!response.ok) {
return yield* new FetchError();
}
const json = yield* jsonResponse(response);
return yield* decodePokemon(json);
});
const main = program.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 want to better organize our code to make it easier to maintain and test. That's the truth power of effect: composability.
We do this by creating what are called services.
Refactor: make the code more concise
Before moving to services it's a good idea to refactor the code a little (it will come handy to understand services later):
- Instead of having multiple separate functions we can collect all together inside
.gen
- Rename
program
togetPokemon
const getPokemon = Effect.gen(function* () {
const baseUrl = yield* Config.string("BASE_URL");
const response = yield* Effect.tryPromise({
try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`),
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);
});
It's clear now that this single Effect
is not our complete program but only a single API in our codebase.
Let's also move errors and schemas in their own separate files:
errors.ts
import { Data } from "effect";
export class FetchError extends Data.TaggedError("FetchError")<{}> {}
export class JsonError extends Data.TaggedError("JsonError")<{}> {}
schemas.ts
import { Schema } from "effect";
export class Pokemon extends Schema.Class<Pokemon>("Pokemon")({
id: Schema.Number,
order: Schema.Number,
name: Schema.String,
height: Schema.Number,
weight: Schema.Number,
}) {}
We are now going to organize the Pokémon API inside a service.