Refactoring effect code

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 to getPokemon
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.