effect-patterns
Effect TypeScript patterns including services, layers, schema validation, and Elysia integration.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Effect TypeScript Patterns
Type-safe error handling and composable services using Effect as the core type system for Node.js/Bun backends.
Core Stack
| Component | Library | Purpose |
|---|---|---|
| Runtime | Bun 1.1+ | Fastest Node.js-compatible runtime |
| Framework | Elysia 1.x | End-to-end type safety |
| Type System | Effect 3.x | TypeScript's missing standard library |
| Validation | Effect Schema | Type-safe validation (replaces Zod) |
| HTTP Client | @effect/platform | HTTP operations |
| Database | Drizzle ORM | TypeScript ORM |
| Testing | Vitest, bun:test | Test framework |
Context7 Library IDs
Query these libraries for current best practices:
-
Effect:
/effect-ts/effect -
Elysia:
elysiajs -
Drizzle:
/drizzle-team/drizzle-orm
Effect Documentation
Before implementing Effect code, consult:
-
AI Documentation:
https://effect.website/llms.txt -
Main Docs:
https://effect.website/docs
Typed Errors
Define errors as classes extending Schema.TaggedError:
import { Schema } from "effect"
class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>("UserNotFoundError")({
userId: Schema.String,
}) {}
class ValidationError extends Schema.TaggedError<ValidationError>("ValidationError")({
message: Schema.String,
field: Schema.String,
}) {}
class DatabaseError extends Schema.TaggedError<DatabaseError>("DatabaseError")({
message: Schema.String,
}) {}
Services with Context.Tag
Define services with typed interfaces:
import { Effect, Context, Layer } from "effect"
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
findById: (id: string) => Effect.Effect<User, UserNotFoundError>
create: (data: CreateUser) => Effect.Effect<User, ValidationError>
}
>() {}
class DatabaseService extends Context.Tag("DatabaseService")<
DatabaseService,
{ query: <T>(sql: string) => Effect.Effect<T[], DatabaseError> }
>() {}
Layer Implementation
Implement services with Layer:
const UserRepositoryLive = Layer.succeed(
UserRepository,
UserRepository.of({
findById: (id) => Effect.tryPromise({
try: () => db.findUser(id),
catch: () => new UserNotFoundError({ userId: id })
}),
create: (data) => Effect.tryPromise({
try: () => db.createUser(data),
catch: (e) => new ValidationError({ message: String(e), field: "unknown" })
})
})
)
const DatabaseServiceLive = Layer.succeed(
DatabaseService,
DatabaseService.of({
query: (sql) => Effect.tryPromise({
try: () => db.query(sql),
catch: (e) => new DatabaseError({ message: String(e) })
})
})
)
Effect.gen for Composition
Use generator syntax for composable logic:
const getUser = (id: string) =>
Effect.gen(function* () {
const repo = yield* UserRepository
const user = yield* repo.findById(id)
return user
})
const createUserWithAudit = (data: CreateUser) =>
Effect.gen(function* () {
const repo = yield* UserRepository
const user = yield* repo.create(data)
yield* Effect.log(`Created user: ${user.id}`)
return user
})
Schema Validation
Use Effect Schema for validation:
import { Schema } from "effect"
const CreateUserSchema = Schema.Struct({
name: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100)),
email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/)),
age: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.positive())),
})
type CreateUser = Schema.Schema.Type<typeof CreateUserSchema>
// Validate unknown data
const validateUser = Schema.decodeUnknown(CreateUserSchema)
Error Handling with catchTags
Pattern match on typed errors:
const result = await Effect.runPromise(
program.pipe(
Effect.catchTags({
UserNotFoundError: (e) => Effect.succeed({ error: `User ${e.userId} not found` }),
ValidationError: (e) => Effect.succeed({ error: `Invalid ${e.field}: ${e.message}` }),
DatabaseError: (e) => Effect.succeed({ error: `Database error: ${e.message}` }),
})
)
)
Retry with Schedule
import { Effect, Schedule } from "effect"
const fetchWithRetry = Effect.retry(
fetchExternalApi,
Schedule.exponential("1 second").pipe(Schedule.compose(Schedule.recurs(3)))
)
Elysia + Effect Integration
import { Elysia, t } from "elysia"
import { Effect } from "effect"
const app = new Elysia()
.post("/api/users", async ({ body }) => {
const program = Effect.gen(function* () {
const validated = yield* Schema.decodeUnknown(CreateUserSchema)(body)
const repo = yield* UserRepository
return yield* repo.create(validated)
})
return Effect.runPromise(
program.pipe(
Effect.provide(UserRepositoryLive),
Effect.catchAll((e) => Effect.succeed({ error: e.message }))
)
)
}, {
body: t.Object({
name: t.String(),
email: t.String({ format: "email" }),
})
})
Testing with Effect
import { Effect, Layer } from "effect"
const TestUserRepository = Layer.succeed(
UserRepository,
UserRepository.of({
findById: (id) =>
id === "123"
? Effect.succeed({ id: "123", name: "Test User" })
: Effect.fail(new UserNotFoundError({ userId: id })),
create: (data) => Effect.succeed({ id: "new", ...data })
})
)
// In tests
const result = await Effect.runPromise(
getUser("123").pipe(Effect.provide(TestUserRepository))
)
Validation Commands
bun tsc --noEmit
bun eslint src/
bun test
bun build src/index.ts --outdir=dist
Guidelines
- Always define typed errors with Schema.TaggedError
- Use Context.Tag for all services
- Implement services with Layer.succeed
- Compose logic with Effect.gen
- Use Schema for all validation
- Pattern match errors with catchTags
- Provide layers at the edge of the application
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-03-03
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!