Next.js App Router
Run better-effect Programs inside typed Next.js Route Handlers.
The optional better-effect/next entry point adapts the framework-neutral
WebEffect boundary to native Next.js App Router Route Handlers. It does not
import Next.js from the core or main better-effect entry point.
Choose the ownership mode explicitly:
| Host situation | API |
|---|---|
| A route factory is already inside a better-effect Runtime | NextEffect.fromCurrent |
| Next.js owns module/request/process lifecycle | NextEffect.managed(layer) |
Host-owned App Router Runtime
managed accepts a complete Layer and creates its Runtime lazily. Concurrent
first requests share one initialization Promise; root resources remain alive
until dispose():
import { Result } from 'better-result'
import { Layer, Service } from 'better-effect'
import { NextEffect } from 'better-effect/next'
class UserService extends Service<UserService>()('UserService') {
find(id: string) {
return { id, name: 'Ada' }
}
}
export const AppNext = NextEffect.managed(Layer.make(UserService), {
runtime: { warmup: true }
})Use the manager in a route module. The returned value is the native
(request, context) => Promise<Response> shape expected by Next:
import { Result } from 'better-result'
import { AppNext } from '@/app/runtime'
export const GET = AppNext.gen(async function* (
_request,
context: RouteContext<'/api/users/[id]'>
) {
const { id } = await context.params
const users = yield* UserService
return Result.ok(users.find(id))
})Call initialize() from an application startup/instrumentation hook when eager
warmup is useful, and call dispose() from a host lifecycle hook when one is
available. The manager does not install process signal handlers, call
process.exit, or promise automatic HMR cleanup. It also does not make Edge
runtime or serverless long-lived resource ownership assumptions for you.
Embedded Runtime
fromCurrent is inert: it creates no Runtime and owns no Layer. Its handler
and gen methods return yieldable route materialization operations. Route
Program requirements are therefore requirements of the surrounding Program:
const next = NextEffect.fromCurrent()
const makeRoutes = Effect.fn(async function* () {
const GET = yield* next.gen(async function* (_request, context) {
const { id } = await context.params
const users = yield* UserService
return Result.ok(users.find(id))
})
return Result.ok(GET)
})The operation captures the non-owning Runtime.Executor when yielded inside a
Runtime. Yielding it outside an active Runtime fails explicitly.
Request context and lifecycle
Each request uses exactly one WebEffect.handleWith execution and child Scope.
CurrentRequest and CurrentAbortSignal are available to the Program, custom
request Layers are built once per request, and request resources are released
before the handler Promise resolves. Root Layer resources belong to a managed
manager until disposal; they are never released after an individual request.
Request Layers may depend on Services from the managed root Layer:
const AppNext = NextEffect.managed(AppLive, {
requestLayer: (request, context) => CurrentSession.requestLayer(request, context)
})The Layer passed to managed should represent Services needed by requests, not
another HTTP server competing with Next. Worker/background ownership and
deployment-specific lifecycle remain the host application's responsibility.
Responses and failures
Successful values default to { data: value } JSON. A successful Response is
passed through unchanged. A route may choose one success policy: serialize
for a JSON-safe transformation, respond for complete Response control, or a
route-level onSuccess policy. Shared onSuccess and onFailure policies
receive the native request and route context. Typed Result.err failures use
onFailure; thrown defects remain rejected. Policies may be asynchronous but
must return a standards-compatible Web Response.
Install Next.js only when this subpath is used:
bun add better-effect better-result nextnext is an optional peer dependency.