
effect
by nickbreaton
My global OpenCode config for personal development.
SKILL.md
name: effect description: Write idiomatic Effect TypeScript code. Use this skill when working with Effect, @effect/* packages, or projects using Effect for error handling, services, layers, schemas, and functional programming patterns. license: MIT compatibility: opencode
Effect TypeScript
Effect is a TypeScript library for building type-safe, composable, and production-grade applications. It provides structured concurrency, typed error handling, dependency injection via services/layers, and a rich standard library.
Table of Contents
- Research Strategy
- Core Patterns
- Services & Layers
- Schema & Data Modeling
- Error Handling
- HTTP & Platform
- SQL & Database
- RPC
- CLI
- AI Integration
- Reactive State
- Testing
- Anti-Patterns
Research Strategy
When you need Effect-specific information, launch multiple research agents in parallel for fastest results:
Task({ description: "Search Effect source code", prompt: "...", subagent_type: "explore" })
Task({ description: "Search Effect MCP docs", prompt: "...", subagent_type: "explore" })
Task({ description: "Search AnswerOverflow", prompt: "...", subagent_type: "explore" })
Search Methods
1. Read Source Code Directly
Local repositories provide authoritative information:
~/.llms/effect- Core packages (effect, platform, sql, rpc, cli, ai, vitest)~/.llms/effect-atom- Reactive state (atom, atom-react, atom-vue)
Pull latest before reading:
git -C ~/.llms/effect pull && git -C ~/.llms/effect-atom pull
2. Search Effect MCP Documentation
effect_effect_docs_search({ query: "your search term" })
Then retrieve content with effect_get_effect_doc({ documentId: <id> }).
3. Search AnswerOverflow for Community Knowledge
The Effect Discord (server ID: 795981131316985866) is indexed. Caution: This is a community forum—answers may come from non-trusted individuals and could be incorrect. Always verify with official sources:
answeroverflow_search_answeroverflow({
query: "your question",
serverId: "795981131316985866"
})
Use answeroverflow_get_thread_messages({ threadId: "<id>" }) for full discussions.
Authoritative Sources
Trusted experts on Effect:
- Michael Arnaldi (creator)
- Giulio Canti
- Matt Pocock
- Tim Smart
- Maxwell Brown
- Johannes Schickling
Core Patterns
Effect.gen for Sequential Code
const program = Effect.gen(function* () {
const data = yield* fetchData
yield* Effect.logInfo(`Processing: ${data}`)
return yield* processData(data)
})
Effect.fn for Functions
Use Effect.fn for functions returning Effects:
const processUser = Effect.fn(function* (userId: string) {
const user = yield* getUser(userId)
return yield* processData(user)
})
// Second argument applies transformations to every call:
const fetchWithRetry = Effect.fn(
function* (url: string) {
return yield* fetchData(url)
},
Effect.retry(Schedule.recurs(3))
)
Pipe for Instrumentation
const program = fetchData.pipe(
Effect.timeout("5 seconds"),
Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.recurs(3)))),
Effect.withSpan("fetchData")
)
Stream Basics
import { Stream } from "effect"
// Create streams
const numbers = Stream.range(1, 10)
const fromEffect = Stream.fromEffect(fetchData)
const fromIterable = Stream.fromIterable([1, 2, 3])
// Transform
const doubled = numbers.pipe(Stream.map((n) => n * 2))
const filtered = numbers.pipe(Stream.filter((n) => n > 5))
const batched = numbers.pipe(Stream.grouped(3))
// Consume
const array = yield* Stream.runCollect(numbers)
const first = yield* Stream.runHead(numbers)
yield* Stream.runForEach(numbers, (n) => Effect.logInfo(`Got: ${n}`))
Concurrency Primitives
// Ref - mutable reference
const counter = yield* Ref.make(0)
yield* Ref.update(counter, (n) => n + 1)
const value = yield* Ref.get(counter)
// Queue - bounded async queue
const queue = yield* Queue.bounded<string>(100)
yield* Queue.offer(queue, "item")
const item = yield* Queue.take(queue)
// PubSub - publish/subscribe
const pubsub = yield* PubSub.bounded<string>(100)
const subscription = yield* PubSub.subscribe(pubsub)
yield* PubSub.publish(pubsub, "message")
Config
import { Config, Effect } from "effect"
const program = Effect.gen(function* () {
const port = yield* Config.number("PORT")
const host = yield* Config.string("HOST").pipe(Config.withDefault("localhost"))
const dbUrl = yield* Config.redacted("DATABASE_URL") // sensitive values
})
// Nested config
const serverConfig = Config.all({
port: Config.number("PORT"),
host: Config.string("HOST"),
})
Services & Layers
Define Services with Effect.Service
class Database extends Effect.Service<Database>()("@app/Database", {
effect: Effect.gen(function* () {
const pool = yield* ConnectionPool
return {
query: (sql: string) =>
Effect.gen(function* () {
const conn = yield* pool.acquire
return yield* conn.query(sql)
}),
execute: (sql: string) =>
Effect.gen(function* () {
const conn = yield* pool.acquire
yield* conn.execute(sql)
}),
}
}),
}) {}
Rules:
- Service identifiers must be unique (
@scope/ServiceNamepattern) - Use
effect:when the service has dependencies,succeed:for simple values - Access via
yield* Database(the class itself is the Tag)
Service with Dependencies
class Users extends Effect.Service<Users>()("@app/Users", {
effect: Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
return {
findById: Effect.fn(function* (id: UserId) {
const response = yield* http.get(`/users/${id}`)
return yield* HttpClientResponse.schemaBodyJson(User)(response)
}),
}
}),
}) {}
Provide Layers Once at Entry Point
const appLayer = Layer.mergeAll(Users.Default, Database.Default, Config.Default)
const main = program.pipe(Effect.provide(appLayer))
Effect.runPromise(main)
Layer Memoization
// Good: same reference, single pool
const postgresLayer = Postgres.layer({ url: "..." })
const appLayer = Layer.merge(
UserRepo.layer.pipe(Layer.provide(postgresLayer)),
OrderRepo.layer.pipe(Layer.provide(postgresLayer))
)
// Bad: different references, two pools created
const badLayer = Layer.merge(
UserRepo.layer.pipe(Layer.provide(Postgres.layer({ url: "..." }))),
OrderRepo.layer.pipe(Layer.provide(Postgres.layer({ url: "..." })))
)
Schema & Data Modeling
Schema.Class for Records
class User extends Schema.Class<User>("User")({
id: Schema.String.pipe(Schema.brand("UserId")),
name: Schema.String,
email: Schema.String,
createdAt: Schema.Date,
}) {
get displayName() {
return `${this.name} (${this.email})`
}
}
Branded Types
const UserId = Schema.String.pipe(Schema.brand("UserId"))
const Email = Schema.String.pipe(Schema.pattern(/@/), Schema.brand("Email"))
const Port = Schema.Int.pipe(Schema.between(1, 65535), Schema.brand("Port"))
Variants with Schema.TaggedClass
class Success extends Schema.TaggedClass<Success>()("Success", { value: Schema.Number }) {}
class Failure extends Schema.TaggedClass<Failure>()("Failure", { error: Schema.String }) {}
const Result = Schema.Union(Success, Failure)
// Pattern match
const render = (result: typeof Result.Type) =>
Match.valueTags(result, {
Success: ({ value }) => `Got: ${value}`,
Failure: ({ error }) => `Error: ${error}`,
})
Parsing & Encoding
const User = Schema.Struct({ name: Schema.String, age: Schema.Number })
// Decode unknown data (with validation)
const user = Schema.decodeUnknownSync(User)({ name: "Alice", age: 30 })
// Decode with Effect (for async or error handling)
const userEffect = yield* Schema.decodeUnknown(User)(data)
// Encode back to plain object
const plain = Schema.encodeSync(User)(user)
Error Handling
Schema.TaggedError for Domain Errors
class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {
field: Schema.String,
message: Schema.String,
}) {}
class NotFoundError extends Schema.TaggedError<NotFoundError>()("NotFoundError", {
resource: Schema.String,
id: Schema.String,
}) {}
TaggedErrors are yieldable directly:
const program = Effect.gen(function* () {
if (!valid) yield* new ValidationError({ field: "email", message: "Invalid" })
return data
})
Recovery with catchTag/catchTags
const recovered = program.pipe(
Effect.catchTag("NotFoundError", (e) => Effect.succeed(`Not found: ${e.id}`)),
Effect.catchTags({
ValidationError: () => Effect.succeed("invalid"),
NotFoundError: () => Effect.succeed("default"),
})
)
Schema.Defect for External Errors
class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
endpoint: Schema.String,
cause: Schema.Defect, // wraps unknown errors
}) {}
HTTP & Platform (@effect/platform)
HttpClient
import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform"
const program = Effect.gen(function* () {
const client = yield* HttpClient.HttpClient
// Simple GET
const response = yield* client.get("https://api.example.com/users")
const users = yield* HttpClientResponse.schemaBodyJson(Schema.Array(User))(response)
// POST with body
const created = yield* client.post("https://api.example.com/users").pipe(
HttpClientRequest.jsonBody({ name: "Alice" }),
Effect.flatMap(HttpClientResponse.schemaBodyJson(User))
)
})
HttpApi Definition
import { HttpApi, HttpApiGroup, HttpApiEndpoint } from "@effect/platform"
class UsersApi extends HttpApiGroup.make("users").pipe(
HttpApiGroup.add(
HttpApiEndpoint.get("list", "/users").pipe(
HttpApiEndpoint.setSuccess(Schema.Array(User))
)
),
HttpApiGroup.add(
HttpApiEndpoint.get("getById", "/users/:id").pipe(
HttpApiEndpoint.setPath(Schema.Struct({ id: UserId })),
HttpApiEndpoint.setSuccess(User),
HttpApiEndpoint.addError(NotFoundError)
)
),
HttpApiGroup.add(
HttpApiEndpoint.post("create", "/users").pipe(
HttpApiEndpoint.setPayload(CreateUserRequest),
HttpApiEndpoint.setSuccess(User)
)
)
) {}
class MyApi extends HttpApi.make("myApi").pipe(HttpApi.addGroup(UsersApi)) {}
HttpApiClient
import { HttpApiClient } from "@effect/platform"
const program = Effect.gen(function* () {
const client = yield* HttpApiClient.make(MyApi)
const users = yield* client.users.list()
const user = yield* client.users.getById({ path: { id: "123" } })
})
FileSystem
import { FileSystem } from "@effect/platform"
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const content = yield* fs.readFileString("./config.json")
yield* fs.writeFileString("./output.txt", "Hello")
const exists = yield* fs.exists("./file.txt")
yield* fs.remove("./temp", { recursive: true })
})
Command Execution
import { Command, CommandExecutor } from "@effect/platform"
const program = Effect.gen(function* () {
const executor = yield* CommandExecutor.CommandExecutor
const output = yield* Command.make("git", "status").pipe(
Command.string, // capture stdout as string
executor.run
)
})
SQL & Database (@effect/sql)
Model.Class
import { Model } from "@effect/sql"
const UserId = Schema.Number.pipe(Schema.brand("UserId"))
class User extends Model.Class<User>("User")({
id: Model.Generated(UserId), // auto-generated, excluded from insert
name: Schema.NonEmptyTrimmedString,
email: Schema.String,
createdAt: Model.DateTimeInsertFromDate, // auto-set on insert
updatedAt: Model.DateTimeUpdateFromDate, // auto-set on update
}) {}
// Use variants:
// User.select - for SELECT queries
// User.insert - for INSERT (excludes Generated fields)
// User.update - for UPDATE (all fields optional)
SqlClient Queries
import { SqlClient } from "@effect/sql"
const program = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
// Tagged template queries
const users = yield* sql<User>`SELECT * FROM users WHERE active = ${true}`
// With schema validation
const user = yield* sql`SELECT * FROM users WHERE id = ${id}`.pipe(
SqlSchema.findOne(User.select)
)
// Transactions
yield* sql.withTransaction(Effect.gen(function* () {
yield* sql`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${from}`
yield* sql`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${to}`
}))
})
SqlSchema Helpers
import { SqlSchema } from "@effect/sql"
// Returns Option<A> - for 0 or 1 result
const maybeUser = yield* sql`...`.pipe(SqlSchema.findOne(User))
// Returns A - throws if not exactly 1 result
const user = yield* sql`...`.pipe(SqlSchema.single(User))
// Returns Array<A>
const users = yield* sql`...`.pipe(SqlSchema.findAll(User))
// Returns void - for INSERT/UPDATE/DELETE
yield* sql`DELETE FROM users WHERE id = ${id}`.pipe(SqlSchema.void)
RPC (@effect/rpc)
Define RPCs
import { Rpc, RpcGroup } from "@effect/rpc"
class GetUser extends Rpc.make("GetUser")<{
success: User
error: NotFoundError
payload: { readonly id: UserId }
}>() {}
class CreateUser extends Rpc.make("CreateUser")<{
success: User
error: ValidationError
payload: { readonly name: string; readonly email: string }
}>() {}
class UsersRpc extends RpcGroup.make("Users", GetUser, CreateUser) {}
Implement Server
import { RpcServer } from "@effect/rpc"
const usersHandler = RpcServer.make(UsersRpc).pipe(
RpcServer.handler(GetUser, ({ id }) =>
Effect.gen(function* () {
const users = yield* Users
return yield* users.findById(id)
})
),
RpcServer.handler(CreateUser, ({ name, email }) =>
Effect.gen(function* () {
const users = yield* Users
return yield* users.create({ name, email })
})
)
)
Use Client
import { RpcClient } from "@effect/rpc"
const program = Effect.gen(function* () {
const client = yield* RpcClient.make(UsersRpc)
const user = yield* client(new GetUser({ id: UserId.make("123") }))
})
CLI (@effect/cli)
Command Definition
import { Args, Command, Options } from "@effect/cli"
const name = Args.text({ name: "name" })
const verbose = Options.boolean("verbose").pipe(Options.withAlias("v"))
const count = Options.integer("count").pipe(Options.withDefault(1))
const greet = Command.make("greet", { name, verbose, count }, ({ name, verbose, count }) =>
Effect.gen(function* () {
for (let i = 0; i < count; i++) {
yield* Effect.logInfo(`Hello, ${name}!`)
}
if (verbose) yield* Effect.logDebug("Greeting complete")
})
)
Subcommands
const add = Command.make("add", { file: Args.file() }, ({ file }) => /* ... */)
const remove = Command.make("remove", { file: Args.file() }, ({ file }) => /* ... */)
const cli = Command.make("mycli").pipe(
Command.withSubcommands([add, remove])
)
Run CLI
import { CliApp } from "@effect/cli"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
const app = CliApp.make({ name: "mycli", version: "1.0.0", command: cli })
CliApp.run(app, process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain)
AI Integration (@effect/ai)
LanguageModel
import { LanguageModel } from "@effect/ai"
import { OpenAiLanguageModel } from "@effect/ai-openai"
const program = Effect.gen(function* () {
const model = yield* LanguageModel.LanguageModel
// Text generation
const response = yield* model.generateText({
prompt: "Explain monads in one sentence"
})
// Structured output
const user = yield* model.generateObject({
prompt: "Generate a fake user",
schema: User
})
// Streaming
const stream = model.streamText({ prompt: "Write a poem" })
yield* Stream.runForEach(stream, (chunk) => Effect.logInfo(chunk.text))
})
// Provide OpenAI implementation
program.pipe(Effect.provide(OpenAiLanguageModel.layer({ model: "gpt-4o" })))
Tool Definitions
import { Tool, Toolkit } from "@effect/ai"
const weatherTool = Tool.make("getWeather", {
description: "Get current weather for a location",
parameters: Schema.Struct({ city: Schema.String }),
success: Schema.Struct({ temp: Schema.Number, conditions: Schema.String }),
})
const toolkit = Toolkit.make(weatherTool)
const toolkitLayer = Toolkit.toLayer(toolkit, {
getWeather: ({ city }) =>
Effect.succeed({ temp: 72, conditions: "sunny" })
})
Reactive State (@effect-atom/atom)
Basic Atoms
import { Atom, Registry } from "@effect-atom/atom"
// Writable atom
const countAtom = Atom.make(0)
// Derived atom
const doubledAtom = Atom.make((get) => get(countAtom) * 2)
// Async atom (returns Result<A, E>)
const userAtom = Atom.make(fetchUser(userId))
// From Stream
const messagesAtom = Atom.make(messageStream)
Atom.family for Parameterized State
const userAtom = Atom.family((id: UserId) =>
Atom.make(fetchUser(id))
)
// Usage - stable references
const alice = userAtom("alice")
const bob = userAtom("bob")
Atom.fn for Actions
const submitForm = Atom.fn((data: FormData) =>
Effect.gen(function* () {
const api = yield* Api
return yield* api.submit(data)
})
)
// Returns AtomResultFn - tracks loading/success/failure
Result Type
import { Result } from "@effect-atom/atom"
// Result<A, E> = Initial | Success<A> | Failure<E>
// All states can have `waiting: true` for refetching
Result.match(result, {
onInitial: () => "Loading...",
onSuccess: ({ value }) => `Got: ${value}`,
onFailure: ({ cause }) => `Error: ${Cause.pretty(cause)}`,
})
// Check waiting state
if (Result.isWaiting(result)) showSpinner()
React Integration
import { useAtomValue, useAtom, useAtomSet } from "@effect-atom/atom-react"
function Counter() {
const count = useAtomValue(countAtom)
const setCount = useAtomSet(countAtom)
// or: const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// For Result atoms
function UserProfile({ id }: { id: UserId }) {
const result = useAtomValue(userAtom(id))
return Result.match(result, {
onInitial: () => <Spinner />,
onSuccess: ({ value }) => <Profile user={value} />,
onFailure: ({ cause }) => <Error cause={cause} />,
})
}
Service Integration
// Create runtime atom from Layer
const runtimeAtom = Atom.runtime(ApiClient.layer)
// Use services in atoms
const dataAtom = Atom.make(
Effect.gen(function* () {
const api = yield* ApiClient
return yield* api.fetchData()
})
)
Testing (@effect/vitest)
Basic Setup
import { Effect } from "effect"
import { describe, expect, it } from "@effect/vitest"
describe("Feature", () => {
it.effect("works", () =>
Effect.gen(function* () {
const result = yield* Effect.succeed(42)
expect(result).toBe(42)
})
)
})
Test Variants
| Variant | Description |
|---|---|
it.effect | Standard Effect tests with TestServices |
it.scoped | Auto resource cleanup via Scope |
it.live | Real clock/random (no TestClock) |
it.scopedLive | Scoped + real services |
Shared Layers with layer()
import { layer } from "@effect/vitest"
layer(Database.testLayer)("Database tests", (it) => {
it.effect("queries work", () =>
Effect.gen(function* () {
const db = yield* Database
const result = yield* db.query("SELECT 1")
expect(result).toHaveLength(1)
})
)
})
TestClock
import { Effect, Fiber, TestClock } from "effect"
it.effect("time-based", () =>
Effect.gen(function* () {
const fiber = yield* Effect.sleep("10 seconds").pipe(
Effect.as("done"),
Effect.fork
)
yield* TestClock.adjust("10 seconds")
const result = yield* Fiber.join(fiber)
expect(result).toBe("done")
})
)
Property-Based Testing
it.prop("addition is commutative", [Schema.Int, Schema.Int], (a, b) =>
Effect.gen(function* () {
expect(a + b).toBe(b + a)
})
)
Anti-Patterns
Avoid
- Mixing
Effect.providethroughout code (provide once at entry) - Using raw
try/catchinstead of Effect error handling - Creating layers inline multiple times (breaks memoization)
- Using
Effect.runSync/runPromisedeep in the call stack - Ignoring typed errors with
Effect.orDiewhen recovery is possible - Using
Effect.promisefor external APIs (loses error typing) - Not validating external data with
Schema.decodeUnknown - Forgetting
Result.isWaitingchecks in UI (shows stale data during refetch)
Prefer
Effect.fnover plain functions returning EffectsSchema.TaggedErrorover plain Error classes- Branded types over raw primitives
- Services/Layers over global singletons
yield*over.pipe(Effect.flatMap(...))for sequential codeSchema.decodeUnknownfor all external/untrusted dataHttpApiClientover manual HTTP calls for typed APIsModel.Classover plain Schema.Class for database entities
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です

