Our objective is to organize our codebase around "abstract interfaces", each defining only the signature of our API.
Then based on the environment (testing, development, production) we build and provide concrete implementations.
This pattern is called Dependency Injection.
Effect services are designed for dependency injection. Not only that, but effect makes injecting dependencies completely type-safe as well.
Services in effect: Context
A generic service is defined using Context
.
Context
is implemented as a globalMap
that stores service implementations by key.When we provide a concrete implementation effect extracts the service from this global
Map
(all type-safe).
It's common practice to define each service in its own file with the same name of the service. We therefore create a new PokeApi.ts
file for our new service.
I personally name each service using Pascal Case, in this example
PokeApi
We first define the signature of our API using a simple interface
. This contains all the methods that the service provides:
import { Effect } from "effect";
import type { ParseResult } from "@effect/schema";
import type { ConfigError } from "effect/ConfigError";
import type { FetchError, JsonError } from "./errors";
import type { Pokemon } from "./schemas";
export interface PokeApi {
readonly getPokemon: Effect.Effect<
Pokemon,
FetchError | JsonError | ParseResult.ParseError | ConfigError
>;
}
Defining an
interface
manually is verbose. Don't worry, there is a leaner way, bare with me on this for now.
Based on this interface
we create a service using Context.GenericTag
:
- We pass a key
string
used to identify the service in the globalMap
("PokeApi"
)
import { Effect, Context } from "effect";
export interface PokeApi {
readonly getPokemon: Effect.Effect<
typeof Pokemon.Type,
FetchError | JsonError | ParseResult.ParseError | ConfigError
>;
}
export const PokeApi = Context.GenericTag<PokeApi>("PokeApi");
It's convenient to give the same name to the
interface
andContext
service (PokeApi
). This allows to export and use the service both as type and value with a single import.
This is all you need to define a service. As you can notice we didn't define any concrete implementation yet. Nonetheless, we can already start using the service. The implementation can be defined later. That's the power of composability and dependency injection!