Claude Code Plugins

Community-maintained marketplace

Feedback

Ports & Adapters with Effect Context.Tag and Layer

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name hexagonal
description Ports & Adapters with Effect Context.Tag and Layer
allowed-tools Read, Write, Edit, Grep
token-budget 400

hexagonal

Core Concept

Hexagonal Architecture isolates business logic from infrastructure:

  • Port = Context.Tag (interface definition)
  • Adapter = Layer (implementation)
  • Live adapters for production, Test adapters for tests

Define a Port

import { Context, Effect } from "effect";

class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError>;
    readonly save: (user: User) => Effect.Effect<void, DatabaseError>;
  }
>() {}

Create Adapters

import { Layer } from "effect";

// Live adapter - real database
const UserRepositoryLive = Layer.effect(UserRepository,
  Effect.gen(function* () {
    const db = yield* Database;
    return {
      findById: (id) => db.query("SELECT * FROM users WHERE id = ?", [id]),
      save: (user) => db.execute("INSERT INTO users ...", [user]),
    };
  })
);

// Test adapter - in-memory
const UserRepositoryTest = Layer.succeed(UserRepository, {
  findById: () => Effect.succeed(testUser),
  save: () => Effect.succeed(undefined),
});

Testing Without Mocks

describe("createUser", () => {
  it("saves user", async () => {
    const TestLayer = Layer.mergeAll(UserRepositoryTest, ClockTest);

    const result = await Effect.runPromise(
      createUser({ name: "Test" }).pipe(Effect.provide(TestLayer))
    );

    expect(result.name).toBe("Test");
  });
});

No mocks needed - swap layers for different behaviors.