Todo API Walkthrough
See every primitive working together in the executable example.
The project includes an executable Bun API example. It combines Services, Layers, Effects, Runtime, Scope, better-result, Bun SQL and the ITI adapter. Use it as a reference for organizing a real application; in your own project, the example files can live wherever your application structure calls for them.
Dependency graph
AuthService ──────────────┐
├── UserRepository ─────┤
├── SessionRepository ──┤── Database (Layer.scoped)
└── PasswordHasher │
TodoService ──────────────┘
└── TodoRepository ─────┘The Services contain behavior and yield the dependencies they need. The Layer file is the composition root:
const DatabaseLive = Layer.scoped(
Database,
async () => {
const database = new Database(new SQL(':memory:', { adapter: 'sqlite' }))
await database.initialize()
return database
},
(database) => database.close()
)
const RepositoriesLive = Layer.merge(
Layer.make(UserRepository),
Layer.make(SessionRepository),
Layer.make(TodoRepository)
)
const ServicesLive = Layer.merge(
Layer.make(PasswordHasher),
Layer.make(AuthService),
Layer.make(TodoService)
)
export const AppLive = Layer.merge(DatabaseLive, RepositoriesLive, ServicesLive)Request lifecycle
Each HTTP handler runs a program in the shared Runtime:
const result = await runtime.run(() =>
Effect.gen(async function* () {
const { userId } = yield* Result.await(requireUser(request))
const todos = yield* TodoService
const items = yield* Result.await(todos.list(userId))
return Result.ok(items)
})
)The request gets an execution child Scope. The SQLite client belongs to the
root Scope and remains available across requests. A request-local file, lease
or stream can use Effect.acquireRelease and be cleaned up as soon as that
handler settles.
Run a project based on this example
bun run src/index.tsThe server listens on http://localhost:3333 and seeds
demo@example.com / demo1234 in the reference implementation. In a new
application, use your own entry point and configuration. The important part is
that the entry point creates one Runtime, passes it to request handlers, and
calls runtime.dispose() during shutdown.
Why this shape scales
- The HTTP layer knows only the Runtime and domain Results.
- Services express behavior and dependencies without ITI imports.
- Layers make production and test environments explicit.
- Runtime owns graceful shutdown.
better-resultkeeps error unions typed from database to HTTP response.