effect-ts-patterns

star 2

Patterns and best practices for Effect-TS in the Nuclom codebase. Use when writing API routes, services, or any code using Effect-TS. Covers Effect.gen, error handling, services, layers, and common pitfalls.

sferarc By sferarc schedule Updated 2/17/2026

name: effect-ts-patterns description: Patterns and best practices for Effect-TS in the Nuclom codebase. Use when writing API routes, services, or any code using Effect-TS. Covers Effect.gen, error handling, services, layers, and common pitfalls.

Effect-TS Patterns for Nuclom

This skill documents the Effect-TS patterns used throughout the Nuclom codebase. Effect-TS provides type-safe error handling and dependency injection.

Quick Reference

API Route Template

import { Effect } from "effect";
import type { NextRequest } from "next/server";
import { Auth, handleEffectExit, runApiEffect } from "@nuclom/lib/api-handler";
import { VideoLayer } from "@nuclom/lib/effect/layers/video";
import { VideoRepository } from "@nuclom/lib/effect/services/video-repository";

export async function GET(request: NextRequest) {
  const effect = Effect.gen(function* () {
    // 1. Authenticate
    const auth = yield* Auth;
    const { user } = yield* auth.getSession(request.headers);

    // 2. Use repository services
    const repo = yield* VideoRepository;
    return yield* repo.findByUserId(user.id);
  });

  // 3. Run with domain layer (layer first, effect second)
  const exit = await runApiEffect(VideoLayer, effect);

  // 4. Handle exit
  return handleEffectExit(exit);
}

Domain Layers

Each route imports only the layer(s) it needs from @nuclom/lib/effect/layers/*:

Layer Services Used by
VideoLayer Video CRUD, clips, collections, speakers, shares, storage videos/*, clips/*, collections/*, share/*
BillingLayer Billing, subscriptions, Stripe, email notifications billing/*, webhooks/stripe
OrganizationLayer Orgs, members, preferences, notifications, presence organizations/*, notifications/*
SearchLayer Search, semantic search, unified search, embeddings search/*, videos/[id]/embeddings
KnowledgeLayer Knowledge graph, AI insights, discovery, chat KB knowledge/*, ai/*, discovery/*
ChatLayer Chat conversations and messages chat/conversations/*
ContentSourceLayer Content sources (GitHub, Notion, Slack adapters) content/*
IntegrationLayer OAuth integrations (Google, Zoom, Slack, Teams) integrations/*/callback
MonitoringLayer Slack monitoring for alerts beta-access, contact, support

Routes needing multiple domains merge layers:

import { Layer } from "effect";
import { VideoLayer } from "@nuclom/lib/effect/layers/video";
import { BillingLayer } from "@nuclom/lib/effect/layers/billing";

const exit = await runApiEffect(Layer.mergeAll(VideoLayer, BillingLayer), effect);

Core Patterns

1. Effect.gen for Sequential Code

Always use Effect.gen with yield* for sequential async operations:

const program = Effect.gen(function* () {
  const user = yield* getUser(id);
  const videos = yield* getVideosForUser(user.id);
  return { user, videos };
});

2. Error Handling with Data.TaggedError

import { Data, Effect } from "effect";

class NotFoundError extends Data.TaggedError("NotFoundError")<{
  readonly message: string;
  readonly entity: string;
}> {}

const findVideo = (id: string) => Effect.gen(function* () {
  const video = yield* repo.findById(id);
  if (!video) {
    return yield* Effect.fail(new NotFoundError({
      message: `Video ${id} not found`,
      entity: "video"
    }));
  }
  return video;
});

3. Error Recovery with catchTag

Handle errors close to where they occur:

const program = Effect.gen(function* () {
  const video = yield* findVideo(id);
  return video;
}).pipe(
  Effect.catchTag("NotFoundError", () => Effect.succeed(null)),
  Effect.catchTag("ValidationError", (e) =>
    Effect.fail(new BadRequestError({ message: e.message }))
  )
);

4. Repository Services

Always use repository services, never direct db access:

const effect = Effect.gen(function* () {
  const repo = yield* VideoRepository;
  return yield* repo.findById(id);
});

5. Layer Composition

Use standalone Layer.provide (not .pipe) for composing layers:

// Compose service with its dependencies
const VideoRepositoryWithDeps = Layer.provide(VideoRepositoryLive, DatabaseLive);

// Merge all services into a domain layer
export const VideoLayer = Layer.mergeAll(
  DatabaseLive,
  StorageLive,
  Layer.provide(VideoRepositoryLive, Layer.merge(DatabaseLive, StorageLive)),
  Layer.provide(ClipRepositoryLive, DatabaseLive),
);

Common Pitfalls

Never await inside Effect.gen

// WRONG
const program = Effect.gen(function* () {
  const result = await Effect.runPromise(someEffect); // NO!
});

// CORRECT
const program = Effect.gen(function* () {
  const result = yield* someEffect;
});

Never use try/catch around Effect.runPromise

// WRONG
try {
  const result = await Effect.runPromise(Effect.provide(effect, layer));
} catch (error) { ... }

// CORRECT: Use Effect.runPromiseExit + Exit.match
const exit = await Effect.runPromiseExit(Effect.provide(effect, layer));
return Exit.match(exit, {
  onFailure: (cause) => { /* handle error */ },
  onSuccess: (value) => { /* handle success */ },
});

Use Effect.forkDaemon for fire-and-forget

// WRONG
Effect.runPromise(backgroundEffect).catch(console.error);

// CORRECT
yield* Effect.forkDaemon(
  backgroundEffect.pipe(
    Effect.catchAll((err) => Effect.sync(() => { logger.error(err); })),
  ),
);

API Route Checklist

Before completing an API route, verify:

  • Uses Effect.gen for business logic
  • Authenticates with Auth service (if needed)
  • Uses repository services (not direct db)
  • Custom errors extend Data.TaggedError
  • Errors handled with catchTag
  • Effect executed with runApiEffect(layer, effect) or runPublicApiEffect(layer, effect)
  • Uses the appropriate domain layer(s)
  • Exit handled with handleEffectExit()
  • No await Effect.runPromise() inside Effect.gen
  • No as type casts on Effect types

Service Pattern

import { Context, Effect, Layer } from "effect";

// Define service interface
interface VideoService {
  findById(id: string): Effect.Effect<Video | null, NotFoundError>;
  create(data: CreateVideoInput): Effect.Effect<Video, ValidationError>;
}

// Create tag
export class VideoService extends Context.Tag("VideoService")<
  VideoService,
  VideoService
>() {}

// Implement service
export const VideoServiceLive = Layer.succeed(VideoService, {
  findById: (id) => Effect.gen(function* () {
    // implementation
  }),
  create: (data) => Effect.gen(function* () {
    // implementation
  }),
});

See Also

  • CLAUDE.md - Project-wide patterns
  • content/docs/internal/architecture/effect-best-practices.mdx - Full documentation
  • packages/lib/src/effect/services/ - Existing service implementations
  • packages/lib/src/effect/layers/ - Domain-specific layer compositions
Install via CLI
npx skills add https://github.com/sferarc/nuclom --skill effect-ts-patterns
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator