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.genfor business logic - Authenticates with
Authservice (if needed) - Uses repository services (not direct
db) - Custom errors extend
Data.TaggedError - Errors handled with
catchTag - Effect executed with
runApiEffect(layer, effect)orrunPublicApiEffect(layer, effect) - Uses the appropriate domain layer(s)
- Exit handled with
handleEffectExit() - No
await Effect.runPromise()insideEffect.gen - No
astype 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 patternscontent/docs/internal/architecture/effect-best-practices.mdx- Full documentationpackages/lib/src/effect/services/- Existing service implementationspackages/lib/src/effect/layers/- Domain-specific layer compositions