requirements

Installation
SKILL.md

Requirements Management in Effect

Overview

The third type parameter in Effect<A, E, R> represents requirements - services and dependencies the effect needs to run:

Effect<Success, Error, Requirements>;
//                     ^^^^^^^^^^^^ Services needed

Effect uses a powerful dependency injection system based on Context and Layer.

The primary reason to define services is testability. Every external dependency (API calls, databases, file systems, third-party SDKs) MUST be wrapped in a Context.Tag service so that tests can provide a test implementation instead of hitting real systems. This is how Effect achieves 100% test coverage — business logic depends only on service interfaces, and tests swap in test layers that control all I/O.

Defining Services

Using Effect.Tag (Recommended)

import { Effect, Context } from "effect";

// Define service interface and tag together
class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly findById: (id: string) => Effect.Effect<User, UserNotFound>;
    readonly save: (user: User) => Effect.Effect<void>;
  }
>() {}

// Using the service
const program = Effect.gen(function* () {
  const repo = yield* UserRepository;
  const user = yield* repo.findById("123");
  return user;
});
// Type: Effect<User, UserNotFound, UserRepository>

Alternative: Context.Tag Directly

interface UserRepository {
  readonly findById: (id: string) => Effect.Effect<User, UserNotFound>;
}

const UserRepository = Context.Tag<UserRepository>("UserRepository");

Using Services

const program = Effect.gen(function* () {
  const userRepo = yield* UserRepository;
  const emailService = yield* EmailService;

  const user = yield* userRepo.findById(userId);
  yield* emailService.send(user.email, "Welcome!");
});

Creating Layers

Layers are recipes for building services:

Layer.succeed - Simple Value

const LoggerLive = Layer.succeed(Logger, {
  log: (msg) => Effect.sync(() => console.log(msg)),
});

Layer.effect - Effect-Based Construction

const ConfigLive = Layer.effect(
  Config,
  Effect.gen(function* () {
    const env = yield* Effect.sync(() => process.env);
    return {
      apiUrl: env.API_URL ?? "http://localhost:3000",
      debug: env.DEBUG === "true",
    };
  }),
);

Layer.scoped - Resource with Lifecycle

const DatabaseLive = Layer.scoped(
  Database,
  Effect.gen(function* () {
    const pool = yield* Effect.acquireRelease(createPool(), (pool) => Effect.promise(() => pool.end()));
    return {
      query: (sql) => Effect.promise(() => pool.query(sql)),
    };
  }),
);

Layer.function - From Function

const HttpClientLive = Layer.function(HttpClient, (baseUrl: string) => ({
  get: (path) => Effect.tryPromise(() => fetch(baseUrl + path)),
}));

Providing Dependencies

Effect.provide - Provide Layer

const program = getUserById("123");

const runnable = program.pipe(Effect.provide(AppLive));

await Effect.runPromise(runnable);

Effect.provideService - Provide Single Service

const runnable = program.pipe(
  Effect.provideService(UserRepository, {
    findById: (id) => Effect.succeed(mockUser),
    save: (user) => Effect.void,
  }),
);

Composing Layers

Layer.merge - Combine Independent Layers

const InfraLive = Layer.merge(DatabaseLive, LoggerLive);

Layer.provide - Layer Dependencies

const UserRepositoryLive = Layer.effect(
  UserRepository,
  Effect.gen(function* () {
    const db = yield* Database;
    return {
      findById: (id) => db.query(`SELECT * FROM users WHERE id = ${id}`),
    };
  }),
);

const FullUserRepo = UserRepositoryLive.pipe(Layer.provide(DatabaseLive));

Layer.provideMerge - Provide and Keep

const Combined = UserRepositoryLive.pipe(Layer.provideMerge(DatabaseLive));

Building Application Layers

Typical Pattern

const InfraLive = Layer.mergeAll(DatabaseLive, LoggerLive, HttpClientLive);

const RepositoryLive = Layer.mergeAll(UserRepositoryLive, OrderRepositoryLive).pipe(Layer.provide(InfraLive));

const ServiceLive = Layer.mergeAll(UserServiceLive, OrderServiceLive).pipe(Layer.provide(RepositoryLive));

const AppLive = ServiceLive.pipe(Layer.provide(InfraLive));

Layer Memoization

Layers are memoized by default - each service is created once:

const AppLive = Layer.mergeAll(UserServiceLive, OrderServiceLive).pipe(Layer.provide(DatabaseLive));

Fresh Layers (No Memoization)

const FreshDatabase = Layer.fresh(DatabaseLive);

Default Services

Effect provides default implementations for common services:

const program = Effect.gen(function* () {
  const now = yield* Clock.currentTimeMillis;
  const random = yield* Random.next;
});

Overriding Defaults

import { TestClock } from "effect";

const testProgram = program.pipe(Effect.provide(TestClock.layer));

Testing with Services (CRITICAL for 100% Coverage)

Every service MUST have a test layer. This is how you achieve complete test coverage without hitting real external systems.

Simple Test Layer (Stateless)

Use Layer.succeed for services that don't need to track state:

const EmailServiceTest = Layer.succeed(EmailService, {
  send: (to, subject, body) => Effect.void, // No-op in tests
  sendBulk: (recipients, subject, body) => Effect.void,
});

Stateful Test Layer (Repositories)

Use Layer.effect with Ref for services that need to maintain state:

const UserRepositoryTest = Layer.effect(
  UserRepository,
  Effect.gen(function* () {
    const store = yield* Ref.make<Map<string, User>>(new Map());

    return {
      findById: (id: string) =>
        Effect.gen(function* () {
          const users = yield* Ref.get(store);
          return yield* Option.match(Option.fromNullable(users.get(id)), {
            onNone: () => Effect.fail(new UserNotFound({ userId: id })),
            onSome: Effect.succeed,
          }).pipe(Effect.flatten);
        }),
      save: (user: User) => Ref.update(store, (m) => new Map(m).set(user.id, user)),
    };
  }),
);

Composing Test Layers

const TestEnv = Layer.mergeAll(UserRepositoryTest, EmailServiceTest, PaymentGatewayTest);

Using Test Layers with @effect/vitest

import { it, expect, layer } from "@effect/vitest";
import { Effect } from "effect";

layer(TestEnv)("UserService", (it) => {
  it.effect("should create user and send welcome email", () =>
    Effect.gen(function* () {
      const repo = yield* UserRepository;
      const email = yield* EmailService;

      const user = new User({ id: "1", name: "Alice", email: "alice@test.com" });
      yield* repo.save(user);
      yield* email.send(user.email, "Welcome!", "Hello Alice");

      const found = yield* repo.findById("1");
      expect(found.name).toBe("Alice");
    }),
  );
});

Combining Test Layers with Property Testing

The ultimate testing pattern — service test layers control all I/O, Arbitrary generates all data:

layer(TestEnv)("UserService Properties", (it) => {
  it.effect.prop("should save and retrieve any valid user", [Arbitrary.make(User)], ([user]) =>
    Effect.gen(function* () {
      const repo = yield* UserRepository;
      yield* repo.save(user);
      const found = yield* repo.findById(user.id);
      expect(found).toEqual(user);
    }),
  );
});

Best Practices

  1. Define service interface with Tag - Keeps interface and tag together
  2. ALWAYS create a test layer for every service - This is required, not optional. Without test layers, your code is untestable.
  3. Wrap ALL external dependencies in services - API calls, database queries, file I/O, third-party SDKs, email, caches, queues — everything external MUST go through a service
  4. Use Layer.scoped for resources - Ensures proper cleanup
  5. Compose layers bottom-up - Infrastructure → Repositories → Services
  6. Keep layers focused - One service per layer typically
  7. Name convention - *Live for production layers, *Test for test layers (e.g., UserRepositoryLive, UserRepositoryTest)
  8. Use Layer.effect with Ref for stateful test layers - Repositories and caches need state tracking
  9. Combine test layers with Arbitrary - Services control I/O, Arbitrary generates data — together they enable 100% coverage

Additional Resources

For comprehensive requirements management documentation, consult ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt.

Search for these sections:

  • "Managing Services" for service patterns
  • "Managing Layers" for layer composition
  • "Layer Memoization" for sharing services
  • "Default Services" for built-in services
Related skills

More from andrueandersoncs/claude-skill-effect-ts

Installs
3
GitHub Stars
8
First Seen
Mar 22, 2026