better-effect
Integrations and adapters

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 situationAPI
A route factory is already inside a better-effect RuntimeNextEffect.fromCurrent
Next.js owns module/request/process lifecycleNextEffect.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 next

next is an optional peer dependency.

On this page