Skip to main content

effect-best-practices

Enforces Effect-TS patterns for services, errors, layers, and atoms. Use when writing code with Effect.Service, Schema.TaggedError, Layer composition, or effect-atom React components.

Zur Installation springen

Quellinformationen

Repository
millionco/expect
Letzte Quellaktivität
17. März 2026 um 02:22
Erkannte Sprache von SKILL.md
Englisch
Sterne
3.553
Forks
157

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
9 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
effect-best-practices
description
Enforces Effect-TS patterns for services, errors, layers, and atoms. Use when writing code with Effect.Service, Schema.TaggedError, Layer composition, or effect-atom React components.
version
1.0.0
# Effect-TS Best Practices This skill enforces opinionated, consistent patterns for Effect-TS codebases. These patterns optimize for type safety, testability, observability, and maintainability. ## Quick Reference: Critical Rules | Category | DO | DON'T | | ----------------- | ------------------------------------------------- | ------------------------------------------ | | Services | `Effect.Service` with `accessors: true` | `Context.Tag` for business logic | | Dependencies | `dependencies: [Dep.Default]` in service | Manual `Layer.provide` at usage sites | | Errors | `Schema.TaggedError` with `message` field | Plain classes or generic Error | | Error Specificity | `UserNotFoundError`, `SessionExpiredError` | Generic `NotFoundError`, `BadRequestError` | | Error Handling | `catchTag`/`catchTags` | `catchAll` or `mapError` | | IDs | `Schema.UUID.pipe(Schema.brand("@App/EntityId"))` | Plain `string` for entity IDs | | Functions | `Effect.fn("Service.method")` | Anonymous generators | | Logging | `Effect.log` with structured data | `console.log` | | Config | `Config.*` with validation | `process.env` directly | | Options | `Option.match` with both cases | `Option.getOrThrow` | | Nullability | `Option<T>` in domain types | `null`/`undefined` | | Atoms | `Atom.make` outside components | Creating atoms inside render | | Atom State | `Atom.keepAlive` for global state | Forgetting keepAlive for persistent state | | Atom Updates | `useAtomSet` in React components | `Atom.update` imperatively from React | | Atom Cleanup | `get.addFinalizer()` for side effects | Missing cleanup for event listeners | | Atom Results | `Result.builder` with `onErrorTag` | Ignoring loading/error states | ## Service Definition Pattern **Always use `Effect.Service`** for business logic services. This provides automatic accessors, built-in `Default` layer, and proper dependency declaration. ```typescript import { Effect } from "effect"; export class UserService extends Effect.Service<UserService>()("UserService", { accessors: true, dependencies: [UserRepo.Default, CacheService.Default], effect: Effect.gen(function* () { const repo = yield* UserRepo; const cache = yield* CacheService; const findById = Effect.fn("UserService.findById")(function* (id: UserId) { const cached = yield* cache.get(id); if (Option.isSome(cached)) return cached.value; const user = yield* repo.findById(id); yield* cache.set(id, user); return user; }); const create = Effect.fn("UserService.create")(function* (data: CreateUserInput) { const user = yield* repo.create(data); yield* Effect.log("User created", { userId: user.id }); return user; }); return { findById, create }; }), }) {} // Usage - dependencies are already wired const program = Effect.gen(function* () { const user = yield* UserService.findById(userId); return user; }); // At app root const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default); ``` **When `Context.Tag` is acceptable:** - Infrastructure with runtime injection (Cloudflare KV, worker bindings) - Factory patterns where resources are provided externally See `references/service-patterns.md` for detailed patterns. ## Error Definition Pattern **Always use `Schema.TaggedError`** for errors. This makes them serializable (required for RPC) and provides consistent structure. ```typescript import { Schema } from "effect"; import { HttpApiSchema } from "@effect/platform"; export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()( "UserNotFoundError", { userId: UserId, message: Schema.String, }, HttpApiSchema.annotations({ status: 404 }), ) {} export class UserCreateError extends Schema.TaggedError<UserCreateError>()( "UserCreateError", { message: Schema.String, cause: Schema.optional(Schema.String), }, HttpApiSchema.annotations({ status: 400 }), ) {} ``` **Error handling - use `catchTag`/`catchTags`:** ```typescript // CORRECT - preserves type information yield * repo.findById(id).pipe( Effect.catchTag("DatabaseError", (err) => Effect.fail(new UserNotFoundError({ userId: id, message: "Lookup failed" })), ), Effect.catchTag("ConnectionError", (err) => Effect.fail(new ServiceUnavailableError({ message: "Database unreachable" })), ), ); // CORRECT - multiple tags at once yield * effect.pipe( Effect.catchTags({ DatabaseError: (err) => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })), ValidationError: (err) => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message })), }), ); ``` ### Prefer Explicit Over Generic Errors **Every distinct failure reason deserves its own error type.** Don't collapse multiple failure modes into generic HTTP errors. ```typescript // WRONG - Generic errors lose information export class NotFoundError extends Schema.TaggedError<NotFoundError>()( "NotFoundError", { message: Schema.String }, HttpApiSchema.annotations({ status: 404 }), ) {} // Then mapping everything to it: Effect.catchTags({ UserNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })), ChannelNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })), MessageNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })), }); // Frontend gets useless: { _tag: "NotFoundError", message: "Not found" } // Which resource? User? Channel? Message? Can't tell! ``` ```typescript // CORRECT - Explicit domain errors with rich context export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()( "UserNotFoundError", { userId: UserId, message: Schema.String }, HttpApiSchema.annotations({ status: 404 }), ) {} export class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()( "ChannelNotFoundError", { channelId: ChannelId, message: Schema.String }, HttpApiSchema.annotations({ status: 404 }), ) {} export class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()( "SessionExpiredError", { sessionId: SessionId, expiredAt: Schema.DateTimeUtc, message: Schema.String, }, HttpApiSchema.annotations({ status: 401 }), ) {} // Frontend can now show specific UI: // - UserNotFoundError → "User doesn't exist" // - ChannelNotFoundError → "Channel was deleted" // - SessionExpiredError → "Your session expired. Please log in again." ``` See `references/error-patterns.md` for error remapping and retry patterns. ## Schema & Branded Types Pattern **Brand all entity IDs** for type safety across service boundaries: ```typescript import { Schema } from "effect"; // Entity IDs - always branded export const UserId = Schema.UUID.pipe(Schema.brand("@App/UserId")); export type UserId = Schema.Schema.Type<typeof UserId>; export const OrganizationId = Schema.UUID.pipe(Schema.brand("@App/OrganizationId")); export type OrganizationId = Schema.Schema.Type<typeof OrganizationId>; // Domain types - use Schema.Struct export const User = Schema.Struct({ id: UserId, email: Schema.String, name: Schema.String, organizationId: OrganizationId, createdAt: Schema.DateTimeUtc, }); export type User = Schema.Schema.Type<typeof User>; // Input types for mutations export const CreateUserInput = Schema.Struct({ email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), name: Schema.String.pipe(Schema.minLength(1)), organizationId: OrganizationId, }); export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>; ``` **When NOT to brand:** - Simple strings that don't cross service boundaries (URLs, file paths) - Primitive config values See `references/schema-patterns.md` for transforms and advanced patterns. ## Function Pattern with Effect.fn **Always use `Effect.fn`** for service methods. This provides automatic tracing with proper span names: ```typescript // CORRECT - Effect.fn with descriptive name const findById = Effect.fn("UserService.findById")(function* (id: UserId) { yield* Effect.annotateCurrentSpan("userId", id); const user = yield* repo.findById(id); return user; }); // CORRECT - Effect.fn with multiple parameters const transfer = Effect.fn("AccountService.transfer")(function* ( fromId: AccountId, toId: AccountId, amount: number, ) { yield* Effect.annotateCurrentSpan("fromId", fromId); yield* Effect.annotateCurrentSpan("toId", toId); yield* Effect.annotateCurrentSpan("amount", amount); // ... }); ``` ## Layer Composition **Declare dependencies in the service**, not at usage sites: ```typescript // CORRECT - dependencies in service definition export class OrderService extends Effect.Service<OrderService>()("OrderService", { accessors: true, dependencies: [UserService.Default, ProductService.Default, PaymentService.Default], effect: Effect.gen(function* () { const users = yield* UserService; const products = yield* ProductService; const payments = yield* PaymentService; // ... }), }) {} // At app root - simple merge const AppLive = Layer.mergeAll( OrderService.Default, // Infrastructure layers (intentionally not in dependencies) DatabaseLive, RedisLive, ); ``` See `references/layer-patterns.md` for testing layers and config-dependent layers. ## Option Handling **Never use `Option.getOrThrow`**. Always handle both cases explicitly: ```typescript // CORRECT - explicit handling yield * Option.match(maybeUser, { onNone: () => Effect.fail(new UserNotFoundError({ userId, message: "Not found" })), onSome: (user) => Effect.succeed(user), }); // CORRECT - with getOrElse for defaults const name = Option.getOrElse(maybeName, () => "Anonymous"); // CORRECT - Option.map for transformations const upperName = Option.map(maybeName, (n) => n.toUpperCase()); ``` ## Effect Atom (Frontend State) Effect Atom provides reactive state management for React with Effect integration. ### Basic Atoms ```typescript import { Atom } from "@effect-atom/atom-react"; // Define atoms OUTSIDE components const countAtom = Atom.make(0); // Use keepAlive for global state that should persist const userPrefsAtom = Atom.make({ theme: "dark" }).pipe(Atom.keepAlive); // Atom families for per-entity state const modalAtomFamily = Atom.family((type: string) => Atom.make({ isOpen: false }).pipe(Atom.keepAlive), ); ``` ### React Integration ```typescript import { useAtomValue, useAtomSet, useAtom, useAtomMount } from "@effect-atom/atom-react" function Counter() { const count = useAtomValue(countAtom) // Read only const setCount = useAtomSet(countAtom) // Write only const [value, setValue] = useAtom(countAtom) // Read + write return <button onClick={() => setCount((c) => c + 1)}>{count}</button> } // Mount side-effect atoms without reading value function App() { useAtomMount(keyboardShortcutsAtom) return <>{children}</> } ``` ### Handling Results with Result.builder **Use `Result.builder`** for rendering effectful atom results. It provides chainable error handling with `onErrorTag`: ```typescript import { Result } from "@effect-atom/atom-react" function UserProfile() { const userResult = useAtomValue(userAtom) // Result<User, Error> return Result.builder(userResult) .onInitial(() => <div>Loading...</div>) .onErrorTag("NotFoundError", () => <div>User not found</div>) .onError((error) => <div>Error: {error.message}</div>)
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen