Skip to content

Instantly share code, notes, and snippets.

@rjhilgefort
Last active June 11, 2025 23:08
Show Gist options
  • Save rjhilgefort/46df7098b0a8ee6c9dc47d1ed41c4d7a to your computer and use it in GitHub Desktop.
Save rjhilgefort/46df7098b0a8ee6c9dc47d1ed41c4d7a to your computer and use it in GitHub Desktop.

Effect Services

Folder Structure of a repository/service

|- 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

"Repositories"

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);
}

"Services"

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

@andrewdhazlett
Copy link

how do you feel about extended filetypes here, e.g. foo-api.service.ts. that is one of the things I kinda like from nest

@rjhilgefort
Copy link
Author

how do you feel about extended filetypes here, e.g. foo-api.service.ts. that is one of the things I kinda like from nest

Down to try it out!

@andrewdhazlett
Copy link

also I think services should be named after a domain concept, not an implementation detail like an API

@rjhilgefort
Copy link
Author

rjhilgefort commented Jun 11, 2025

also I think services should be named after a domain concept, not an implementation detail like an API

Can you comment with the names you'd rather see here? You mean just drop the Api bit from all the names, yeah?

@andrewdhazlett
Copy link

so I think in a perfect world there's also a layer of decoupling/details hiding. eg if we have FooApi that we use for managing Bars, the implementation would be called FooApiBarRepository, the DI tag would be BarRepository, and the service would be BarService (it doesn't know about FooApi at all).

an example in our domain could be a LinkService that depends on LinkRepository and UrlWrapperRepository, which are implemented concretely as PrismaLinkRepository (or MySqlLinkRepository) and BranchUrlWrapperRepository.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment