Getting Started
Install better-effect and build a complete typed workflow.
Install the two building blocks
better-effect builds on better-result, so install both packages:
bun add better-effect better-resultThe public TypeScript peer range starts at 6.0. The repository uses the latest
Bun release by default and the current Node.js LTS for interoperability smoke
tests. better-effect also uses the standard
Symbol.dispose and Symbol.asyncDispose declarations for disposable
resources, so your tsconfig should include a modern JavaScript library such as
ESNext.Disposable.
A tiny complete application
This example has one dependency and one program. The type of program records
that it needs GreetingService; the Runtime is accepted only when its Layer
provides that Service.
import { Result } from 'better-result'
import { Effect, Layer, Runtime, Service } from 'better-effect'
class GreetingService extends Service<GreetingService>()('GreetingService') {
greet(name: string) {
return `Hello, ${name}!`
}
}
const GreetingLive = Layer.make(GreetingService)
const greet = Effect.fn(async function* () {
const greeting = yield* GreetingService
return Result.ok(greeting.greet('Ada'))
})
await using runtime = await Runtime.make(GreetingLive)
const result = await runtime.run(greet)
if (Result.isOk(result)) {
console.log(result.value) // Hello, Ada!
}Effect.gen evaluates immediately and remains useful when the resolver and
Scope are already active. Effect.fn is the recommended Runtime boundary: it
captures the generator without running it and returns a nominal Program, so
Service resolution starts inside runtime.run. The callback form
runtime.run(() => effect) remains supported for existing code.
A Layer's public type is Layer<Provided, Required>: Provided is the tagged
Service instance union it creates, and Required is the external dependency
union left after composition. Prefer inferred Layers or satisfies Layer<..., ...>
when naming a composition root.
MapLayerBackend is the built-in backend and is used by default. Choose a
custom backend through Runtime options when integrating a container:
import { ItiLayerBackend } from 'better-effect/adapters/iti'
const runtime = await Runtime.make(GreetingLive, {
backend: new ItiLayerBackend()
})MemoryLayerBackend remains an alias of MapLayerBackend from
better-effect/testing for compatibility.
Project shape
Keep behavior in Services and keep construction in Layers. Your application entry point should be the place where a custom backend, when needed, and the Runtime are selected.
Next steps
- Learn how Services become typed dependency tokens.
- Compose implementations with Layers.
- Understand execution and shutdown in Runtime.
- Add ownership to connections and files with Scope and Resource.
- Integrate request handlers with Hono.
- Call APIs with the HTTP client, including typed responses and streaming boundaries.
- Adapt a Kysely database with the Kysely integration.
- Adapt a server-side Better Auth instance with Better Auth.