|- services/
|- foo-api/
|- index.ts # entry point for the repo
|- foo-api.ts
|- foo-api-service/
|- index.ts # entry point for the service, exports all
|- foo-api-service.ts
|- some-method.ts
A repository is an abstraction over a service (api, db, etc...).
example, simple API Service
// ./services/foo-api/index.ts
export * from './foo-api';
// ./services/foo-api/foo-api.ts
import axios from 'axios';
import {Context, Effect, Layer, Config, LogLevel, pipe} from 'effect';
export const FOO_API_BASE_URL = pipe(
Config.nonEmptyString('FOO_API_BASE_URL'),
// same host used for all environments
Config.withDefault('https://api.foo.com'),
);
export const FOO_API_TOKEN = Config.nonEmptyString('FOO_API_TOKEN');
export const createFooApi = Effect.gen(function* () {
const baseURL = yield* FOO_API_BASE_URL;
const token = yield* FOO_API_TOKEN;
return axios.create({baseURL, headers: {'X-FOO-TOKEN': token}});
});
export class FooApi extends Context.Tag('FooApi')<
FooApi,
Effect.Effect.Success<typeof createFooApi>
>() {
static Live = Layer.effect(this, createFooApi);
}A service is built on top of a repository (or multiple repositories) and provides Mavely specific functionality.
// ./services/foo-api-service/index.ts
export * from './foo-api-service';
// ./services/foo-api-service/some-method.ts
import {Array, Effect, Schema, Struct, pipe} from 'effect';
import {FooApi} from '../foo-api';
export const someMethod = (entityId: string) =>
Effect.gen(function* () {
const prisma = yield* Prisma;
const fooApi = yield* FooApi;
return yield* pipe(
Effect.tryPromise(() => fooApi.get(`/some-endpoint/${entityId}`)),
Effect.map(Struct.get('data')),
Effect.flatMap(
Schema.decodeUnknown(Schema.Struct({result: Struct.String})),
),
Effect.map(Struct.get('result')),
Effect.flatMap(result => prisma.user.findUnique({where: {id: result}})),
// do more stuff
);
});
// ./services/foo-api-service/foo-api-service.ts
import {Context, Layer} from 'effect';
import {someMethod} from './some-method';
export const createFooApiService = () => ({someMethod});
export class FooApiService extends Context.Tag('FooApiService')<
FooApiService,
ReturnType<typeof createFooApiService>
>() {
static Live = Layer.succeed(this, createFooApiService());
}TODO: Show using this repo/service from a top level program
so I think in a perfect world there's also a layer of decoupling/details hiding. eg if we have
FooApithat we use for managingBars, the implementation would be calledFooApiBarRepository, the DI tag would beBarRepository, and the service would beBarService(it doesn't know aboutFooApiat all).an example in our domain could be a
LinkServicethat depends onLinkRepositoryandUrlWrapperRepository, which are implemented concretely asPrismaLinkRepository(orMySqlLinkRepository) andBranchUrlWrapperRepository.as an aside I would usually call a wrapped external API also a
Service, so in my example(Brandch)UrlWrapperService. you're probably more right though