Effect Integration
Write effectful oRPC handlers with Effect generators, call oRPC clients as effects, provide services through context, and use Effect Schema for input and output validation.
Installation
npm install @orpc/experimental-effect@beta effectpnpm add @orpc/experimental-effect@beta effectyarn add @orpc/experimental-effect@beta effectbun add @orpc/experimental-effect@beta effectnub add @orpc/experimental-effect@beta effectaube add @orpc/experimental-effect@beta effectEffectful Handlers
handlerGen allows you to write effectful handlers using generator functions. Inside the generator, you can yield Effect operations, and handlerGen will handle the execution and error handling for you.
import { function handlerGen<TCurrentContext extends Context, TInput, TErrorConstructorMap extends ORPCErrorConstructorMap<any>, TYield extends Effect.Effect<any, any, InferEffectServices<TCurrentContext>>, TReturn>(handler: HandlerGen<TCurrentContext, TInput, TYield, TReturn, TErrorConstructorMap>): ProcedureHandler<TCurrentContext, TInput, TReturn, TErrorConstructorMap>Creates a procedure handler from an Effect generator function.
Inside the generator you can yield Effect operations, and `handlerGen`
handles the execution and error handling for you.handlerGen } from '@orpc/experimental-effect'
import { import EffectEffect } from 'effect'
const const procedure: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>procedure = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os.Builder<DefaultInitialContext & object, Record<never, never>>.handler<number>(handler: ProcedureHandler<DefaultInitialContext & object, unknown, number, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>Defines the function that implements the procedure and completes the
chain, returning a callable procedure.handler(handlerGen<DefaultInitialContext & object, unknown, ORPCErrorConstructorMap<Record<never, never>>, Effect.Effect<number, never, never>, number>(handler: HandlerGen<DefaultInitialContext & object, unknown, Effect.Effect<number, never, never>, number, ORPCErrorConstructorMap<Record<never, never>>>): ProcedureHandler<DefaultInitialContext & object, unknown, number, ORPCErrorConstructorMap<Record<never, never>>>Creates a procedure handler from an Effect generator function.
Inside the generator you can yield Effect operations, and `handlerGen`
handles the execution and error handling for you.handlerGen(function* ({ input: unknowninput, context: DefaultInitialContext & objectcontext }) {
// You can use Effect's features here, such as concurrency, error handling, etc.
const const result: numberresult = yield* import EffectEffect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>Creates an `Effect` that represents an asynchronous computation guaranteed to
succeed.
**When to use**
Use to convert a `Promise` into an `Effect` when the async operation is
guaranteed to succeed and will not reject.
**Details**
An optional `AbortSignal` can be provided to allow for interruption of the
wrapped `Promise` API.
**Gotchas**
The `Promise` must not reject. If it rejects, the rejection is treated as a
defect, not as a typed failure. Use `tryPromise` when rejection is expected.
Interruption aborts the provided `AbortSignal`, but the underlying
asynchronous operation only stops if it observes that signal.
**Example** (Wrapping a non-rejecting Promise)
```ts import.meta.vitest
import { Effect } from "effect"
const succeedAsync = (message: string) =>
Effect.promise<string>(() => Promise.resolve(message))
// ┌─── Effect<string, never, never>
// ▼
const program = succeedAsync("Async operation completed successfully!")
await Effect.runPromise(program) // => "Async operation completed successfully!"
```promise(() => var Promise: PromiseConstructorRepresents the completion of an asynchronous operationPromise.PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)Creates a new resolved promise for the provided value.resolve(5))
return const result: numberresult
}))
.effect extension
Import @orpc/experimental-effect/extensions/effect from a module that always runs during initialization, such as the file where you define your base builder or create your server. This adds an .effect method to the builder so you can write effectful handlers directly.
const procedure = base.effect(function* ({ input, context }) {
// You can use Effect's features here, such as concurrency, error handling, etc.
const result = yield* Effect.promise(() => Promise.resolve(5))
return result
})import '@orpc/experimental-effect/extensions/effect'
import { os } from '@orpc/server'
export const base = os.effect also works when implementing a contract:
const findPlanet = implement(contract).planet.find.effect(function* ({ input }) {
return yield* Effect.promise(() => db.planet.find(input.id))
})
Effect Services
You can provide Effect services through the oRPC context in a typesafe way with WithEffectContext and effect/context:
import { function handlerGen<TCurrentContext extends Context, TInput, TErrorConstructorMap extends ORPCErrorConstructorMap<any>, TYield extends Effect.Effect<any, any, InferEffectServices<TCurrentContext>>, TReturn>(handler: HandlerGen<TCurrentContext, TInput, TYield, TReturn, TErrorConstructorMap>): ProcedureHandler<TCurrentContext, TInput, TReturn, TErrorConstructorMap>Creates a procedure handler from an Effect generator function.
Inside the generator you can yield Effect operations, and `handlerGen`
handles the execution and error handling for you.handlerGen, interface WithEffectContext<Services>A context shape that provides Effect services to effectful handlers
through the oRPC context in a typesafe way.WithEffectContext } from '@orpc/experimental-effect'
import { import ContextContext, import EffectEffect } from 'effect'
class class RandomRandom extends import ContextContext.const Service: <Random, {
readonly next: Effect.Effect<number>;
}>() => <Identifier, E, R, Args>(id: Identifier, options?: {
readonly make?: ((...args: Args) => Effect.Effect<{
readonly next: Effect.Effect<number>;
}, E, R>) | Effect.Effect<{
readonly next: Effect.Effect<number>;
}, E, R> | undefined;
} | undefined) => Context.ServiceClass<Random, Identifier, {
readonly next: Effect.Effect<number>;
}> & ([unassigned] extends [R] ? unknown : {
...;
}) (+2 overloads)
Creates a `Context` service key.
**When to use**
Use when you need to define a context service key for a dependency that must
be provided by the surrounding context.
**Details**
Call `Context.Service("Key")` for a function-style key, or use the two-stage
form `Context.Service<Self, Shape>()("Key")` for class-style service
declarations. The returned key can be yielded as an Effect and passed to
`Context.make`, `Context.add`, and the Context getter functions.
**Gotchas**
The string key is the runtime identity of the service. Reusing the same key
string for unrelated services makes them occupy the same slot in a
`Context`.
**Example** (Creating service keys)
```ts import.meta.vitest
import { Context } from "effect"
// Create a simple service
const Database = Context.Service<{
query: (sql: string) => string
}>("Database")
// Create a service class
class Config extends Context.Service<Config, {
port: number
}>()("Config") {}
// Use the services to create contexts
const db = Context.make(Database, {
query: (sql) => `Result: ${sql}`
})
const config = Context.make(Config, { port: 8080 })
Context.get(db, Database).query("SELECT 1") // => "Result: SELECT 1"
Context.get(config, Config).port // => 8080
```Service<
class RandomRandom,
{
readonly next: Effect.Effect<number, never, never>next: import EffectEffect.interface Effect<out A, out E = never, out R = never>The `Effect` interface defines a value that lazily describes a workflow or
job. The workflow requires some context `R`, and may fail with an error of
type `E`, or succeed with a value of type `A`.
**When to use**
Use when you need to represent a lazy, composable workflow that can require
services, fail with a typed error, or succeed with a typed value.
**Details**
`Effect` values model resourceful interaction with the outside world,
including synchronous, asynchronous, concurrent, and parallel interaction.
They use a fiber-based concurrency model, with built-in support for
scheduling, fine-grained interruption, structured concurrency, and high
scalability.
To run an `Effect` value, you need a `Runtime`, which is a type that is
capable of executing `Effect` values.Effect<number>
}
>()('app/Random') {}
interface ServerContext extends interface WithEffectContext<Services>A context shape that provides Effect services to effectful handlers
through the oRPC context in a typesafe way.WithEffectContext<class RandomRandom> {}
const const procedure: DecoratedProcedure<ServerContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>procedure = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os
.Builder<DefaultInitialContext & object, Record<never, never>>.$context<ServerContext>(): Builder<ServerContext & object, Record<never, never>>Declares the initial context type that must be provided when executing
procedures built from this builder.$context<ServerContext>()
.Builder<ServerContext & object, Record<never, never>>.handler<number>(handler: ProcedureHandler<ServerContext & object, unknown, number, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<ServerContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>Defines the function that implements the procedure and completes the
chain, returning a callable procedure.handler(handlerGen<ServerContext & object, unknown, ORPCErrorConstructorMap<Record<never, never>>, Effect.Effect<number, never, never> | Effect.Effect<{
readonly next: Effect.Effect<number>;
}, never, Random>, number>(handler: HandlerGen<ServerContext & object, unknown, Effect.Effect<number, never, never> | Effect.Effect<{
readonly next: Effect.Effect<number>;
}, never, Random>, number, ORPCErrorConstructorMap<Record<never, never>>>): ProcedureHandler<...>
Creates a procedure handler from an Effect generator function.
Inside the generator you can yield Effect operations, and `handlerGen`
handles the execution and error handling for you.handlerGen(function* ({ input: unknowninput, context: ServerContext & objectcontext }) {
const const random: {
readonly next: Effect.Effect<number>;
}
random = yield* class RandomRandom
const const result: numberresult = yield* const random: {
readonly next: Effect.Effect<number>;
}
random.next: Effect.Effect<number, never, never>next
return const result: numberresult
}))
const const random: numberrandom = await call<{
'effect/context': Context.Context<Random>;
}, InitialInputSchema, Schema<number>, Record<never, never>>(lazyableProcedure: Lazyable<Procedure<{
'effect/context': Context.Context<Random>;
}, any, InitialInputSchema, Schema<number>, Record<never, never>>>, input: void, options: CallOptions<{
'effect/context': Context.Context<Random>;
}, Schema<number>, Record<never, never>>): PromiseWithError<...>
Quickly call a procedure without creating a client.call(const procedure: DecoratedProcedure<ServerContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>procedure, var undefinedundefined, {
context: Value<Promisable<{
'effect/context': Context.Context<Random>;
}>, [clientContext: object]>
context: {
'effect/context': import ContextContext.const empty: () => Context.Context<never>Returns an empty `Context`.
**Example** (Creating an empty context)
```ts import.meta.vitest
import { Context } from "effect"
Context.empty().mapUnsafe.size // => 0
```empty().Pipeable.pipe<Context.Context<never>, Context.Context<Random>>(this: Context.Context<never>, ab: (_: Context.Context<never>) => Context.Context<Random>): Context.Context<Random> (+21 overloads)pipe(
import ContextContext.const add: <Random, {
readonly next: Effect.Effect<number>;
}>(key: Context.Key<Random, {
readonly next: Effect.Effect<number>;
}>, service: {
readonly next: Effect.Effect<number>;
}) => <Services>(self: Context.Context<Services>) => Context.Context<Random | Services> (+1 overload)
Adds a service to a given `Context`.
**When to use**
Use when you need to store a known service value in a `Context`.
**Details**
If the context already contains the same service key, the new service
replaces the previous one.
**Example** (Adding a service to a context)
```ts import.meta.vitest
import { Context, pipe } from "effect"
const Port = Context.Service<{ PORT: number }>("Port")
const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout")
const someContext = Context.make(Port, { PORT: 8080 })
const context = pipe(
someContext,
Context.add(Timeout, { TIMEOUT: 5000 })
)
const values = [Context.get(context, Port).PORT, Context.get(context, Timeout).TIMEOUT]
values // => [8080, 5000]
```add(class RandomRandom, {
next: Effect.Effect<number, never, never>next: import EffectEffect.const sync: <number>(thunk: LazyArg<number>) => Effect.Effect<number, never, never>Creates an `Effect` that represents a synchronous side-effectful computation.
**When to use**
Use when you need to wrap a synchronous side-effectful operation that is not
expected to throw.
**Details**
The provided function is evaluated lazily when the effect runs.
**Gotchas**
The function must not throw. If it throws, the thrown value is treated as a
defect, not as a typed failure. Use `try` when throwing is expected.
**Example** (Capturing synchronous logging in an Effect)
```ts import.meta.vitest
import { Effect } from "effect"
const output: Array<unknown> = []
const log = (message: string) =>
Effect.sync(() => {
void output.push(message) // side effect
})
// ┌─── Effect<void, never, never>
// ▼
const program = log("Hello, World!")
Effect.runSync(program)
output // => ["Hello, World!"]
```sync(() => var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): numberReturns a pseudorandom number between 0 and 1.random()),
}),
)
}
})
Error Handling
This integration preserves the original error whenever possible. If you call Effect.fail(error), the error is forwarded to middleware and interceptors, just like a regular thrown error.
To customize this behavior, wrap the effect before execution using effect/wrap in the context:
import { class ORPCError<TCode extends ORPCErrorCode, TData>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError } from '@orpc/server'
import { import CauseCause, import ContextContext, import DataData, import EffectEffect } from 'effect'
class class NotFoundErrorNotFoundError extends import DataData.const TaggedError: <"NotFoundError">(tag: "NotFoundError") => new <A>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
readonly _tag: "NotFoundError";
} & Readonly<A>
Creates a tagged error class with a `_tag` discriminator.
**When to use**
Use when you need domain errors with discriminated-union handling.
**Details**
Like
{@link
Error
}
, but instances also carry a `readonly _tag` property,
enabling `Effect.catchTag` and `Effect.catchTags` for tag-based recovery.
The `_tag` is excluded from the constructor argument. Yielding an instance
inside `Effect.gen` fails the effect with this error.
**Example** (Recovering by tag)
```ts import.meta.vitest
import { Data, Effect } from "effect"
class NotFound extends Data.TaggedError("NotFound")<{
readonly resource: string
}> {}
class Forbidden extends Data.TaggedError("Forbidden")<{
readonly reason: string
}> {}
const program = Effect.gen(function*() {
return yield* new NotFound({ resource: "/users/42" })
})
const recovered = program.pipe(
Effect.catchTag("NotFound", (e) =>
Effect.succeed(`missing: ${e.resource}`))
)
await Effect.runPromise(recovered) // => "missing: /users/42"
```TaggedError('NotFoundError')<{
readonly resource: stringresource: string
}> {}
export async function function fetch(request: Request): Promise<Response>fetch(request: Requestrequest: Request) {
const { const matched: booleanmatched, const response: Response | undefinedresponse } = await const handler: RPCHandler<ServerContext>handler.FetchHandler<ServerContext>.handle(request: Request, options: FriendlyStandardHandlerHandleOptions<ServerContext>): Promise<FetchHandlerHandleResult>handle(request: Requestrequest, {
context: Value<Promisable<ServerContext>, [request: StandardLazyRequest]>context: {
'effect/context': import ContextContext.const empty: () => Context.Context<never>Returns an empty `Context`.
**Example** (Creating an empty context)
```ts import.meta.vitest
import { Context } from "effect"
Context.empty().mapUnsafe.size // => 0
```empty(),
'effect/wrap': effect: Effect.Effect<A, unknown, never>effect => effect: Effect.Effect<A, unknown, never>effect.Pipeable.pipe<Effect.Effect<A, unknown, never>, Effect.Effect<A, unknown, never>>(this: Effect.Effect<A, unknown, never>, ab: (_: Effect.Effect<A, unknown, never>) => Effect.Effect<A, unknown, never>): Effect.Effect<A, unknown, never> (+21 overloads)pipe(
import EffectEffect.const catchCause: <unknown, never, unknown, never>(f: (cause: Cause.Cause<unknown>) => Effect.Effect<never, unknown, never>) => <A, R>(self: Effect.Effect<A, unknown, R>) => Effect.Effect<A, unknown, R> (+1 overload)Handles both recoverable and unrecoverable errors by providing a recovery
effect.
**When to use**
Use when you need to recover from an `Effect` by inspecting the full `Cause`,
including recoverable failures, defects, and interruptions, instead of only
the typed error value.
**Details**
When to Recover from Defects:
Defects are unexpected errors that typically shouldn't be recovered from, as
they often indicate serious issues. However, in some cases, such as
dynamically loaded plugins, controlled recovery might be needed.
**Example** (Recovering from full failure causes)
```ts import.meta.vitest
import { Cause, Effect } from "effect"
const output: Array<unknown> = []
// An effect that might fail in different ways
const program = Effect.die("Something went wrong")
// Recover from any cause (including defects)
const recovered = Effect.catchCause(program, (cause) => {
if (Cause.hasDies(cause)) {
return Effect.sync(() => { output.push("Caught defect") }).pipe(
Effect.as("Recovered from defect")
)
}
return Effect.succeed("Unknown error")
})
void output.push(Effect.runSync(recovered))
output // => ["Caught defect", "Recovered from defect"]
```catchCause((cause: Cause.Cause<unknown>cause) => {
const const error: unknownerror = import CauseCause.const squash: <unknown>(self: Cause.Cause<unknown>) => unknownCollapses a `Cause` into a single `unknown` value, picking the "most
important" failure in this order:
**When to use**
Use to collapse a structured cause to the single value that synchronous and
promise runners would throw.
**Details**
1. First `Fail` error (the `E` value)
2. First `Die` defect
3. A generic `Error("All fibers interrupted without error")` for interrupt-only causes
4. A generic `Error("Empty cause")` for `empty`
This is the function used by `Effect.runPromise` and `Effect.runSync` to
decide what to throw.
**Gotchas**
This function is lossy. Use
{@link
prettyErrors
}
or iterate `cause.reasons`
when you need all failures.
**Example** (Squashing a cause)
```ts import.meta.vitest
import { Cause } from "effect"
Cause.squash(Cause.fail("error")) // => "error"
Cause.squash(Cause.die("defect")) // => "defect"
```squash(cause: Cause.Cause<unknown>cause)
if (const error: unknownerror instanceof class NotFoundErrorNotFoundError) {
return import EffectEffect.const fail: <ORPCError<"NOT_FOUND", unknown>>(error: ORPCError<"NOT_FOUND", unknown>) => Effect.Effect<never, ORPCError<"NOT_FOUND", unknown>, never>Creates an `Effect` that represents a recoverable error.
**When to use**
Use to explicitly signal a recoverable error in an `Effect`.
**Details**
The error keeps propagating unless it is handled. You can handle tagged
errors with functions like
{@link
catchTag
}
or
{@link
catchTags
}
.
**Example** (Creating a failed effect)
```ts import.meta.vitest
import { Data, Effect } from "effect"
class OperationFailedError extends Data.TaggedError("OperationFailedError")<{}> {}
// ┌─── Effect<never, OperationFailedError, never>
// ▼
const failure = Effect.fail(
new OperationFailedError()
)
Effect.runSync(Effect.flip(failure))._tag // => "OperationFailedError"
```fail(new new ORPCError<"NOT_FOUND", unknown>(code: "NOT_FOUND", options?: ORPCErrorOptions<unknown> | undefined): ORPCError<"NOT_FOUND", unknown>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError('NOT_FOUND', {
message?: string | undefinedmessage: `${const error: NotFoundErrorerror.resource: stringresource} not found`,
ErrorOptions.cause?: unknowncause: const error: NotFoundErrorerror,
}))
}
return import EffectEffect.const failCause: <unknown>(cause: Cause.Cause<unknown>) => Effect.Effect<never, unknown, never>Creates an `Effect` that represents a failure with a specific `Cause`.
**When to use**
Use when you already have a full `Cause` and need to preserve defects,
interruptions, annotations, or combined failures in the effect's failure
channel.
**Details**
This function allows you to create effects that fail with complex error
structures, including multiple errors, defects, interruptions, and more.
**Example** (Failing with a full Cause)
```ts import.meta.vitest
import { Cause, Effect } from "effect"
const program = Effect.failCause(
Cause.fail("Network error")
)
Effect.runSync(Effect.flip(program)) // => "Network error"
```failCause(cause: Cause.Cause<unknown>cause)
}),
),
},
})
if (const matched: booleanmatched) {
return const response: Responseresponse
}
return new var Response: new (body?: BodyInit | null, init?: ResponseInit) => ResponseThe **`Response`** interface of the Fetch API represents the response to a request.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)Response('Not Found', { ResponseInit.status?: number | undefinedstatus: 404 })
}
The wrap runs with effect/context, once per handlerGen and middlewareGen (see opts.kind). Keep it idempotent, since failures from inner layers pass through it again.
Typesafe Errors
An ORPCError that fails the effect, such as return yield* Effect.fail(new ORPCError(...)), is thrown from the handler exactly like a thrown ORPCError in a regular handler. Define your errors with .errors and fail with errors.X(...) to make them typesafe on the client:
const procedure = os
.errors({
NOT_FOUND: {
message: 'The resource you are looking for does not exist',
},
})
.handler(handlerGen(function* ({ errors }) {
if (resourceNotFound) {
return yield* Effect.fail(errors.NOT_FOUND())
}
return 'Success'
}))
const [error, result] = await safe(call(procedure))
if (isDefinedError(error)) {
// typesafe error handling
}
Catching ORPCErrors
Use catchORPCError to recover from every ORPCError failure in the error channel of an effect, or catchORPCErrorCode and catchORPCErrorCodes to recover from specific codes only. Recovered errors are excluded from the resulting effect, and other failures re-fail with their original cause:
import { catchORPCError, catchORPCErrorCode, catchORPCErrorCodes } from '@orpc/experimental-effect'
import { Effect } from 'effect'
const recovered = program.pipe(
catchORPCError(error => Effect.succeed(`caught ${error.code}`)),
)
const fallback = program.pipe(
catchORPCErrorCode('NOT_FOUND', error => Effect.succeed(null)),
)
const handled = program.pipe(
catchORPCErrorCodes({
NOT_FOUND: error => Effect.succeed(null),
CONFLICT: error => Effect.succeed(error.message),
}),
)
error.data is typed only when the error channel holds nothing but declared ORPCErrors. Otherwise, as with client calls, it is unknown.
Interruption
When a request is aborted, handlerGen and middlewareGen fibers are interrupted, so finalizers such as Effect.ensuring and Effect.onInterrupt run. Handlers have no Scope, so wrap Effect.acquireRelease in Effect.scoped.
Effectful Middleware
middlewareGen allows you to write effectful middleware using generator functions. Inside the generator, you can yield Effect operations and yield* next() to continue the chain. Downstream failures land in the error channel of next, so you can recover from them with Effect.
import { function middlewareGen<TInContext extends Context, TOutContext extends Context = object, TInput = unknown, TOutput = any, TErrorMap extends ErrorMap = Record<never, never>, TYield extends Effect<any, any, InferEffectServices<TInContext>> = Effect<any, any, InferEffectServices<TInContext>>>(middleware: MiddlewareGen<TInContext, TOutContext, TInput, TOutput, TYield, ORPCErrorConstructorMap<TErrorMap>>): Middleware<TInContext, TOutContext, TInput, TOutput, TErrorMap>Creates an oRPC middleware from an Effect generator function.
Inside the generator you can yield Effect operations and `yield* next()`
to continue the chain, and `middlewareGen` handles the execution and
error handling for you.middlewareGen } from '@orpc/experimental-effect'
import { import ClockClock } from 'effect'
const const procedure: DecoratedProcedure<{
auth: boolean;
} & object, {
startedAt: number;
}, InitialInputSchema, Schema<number>, Record<never, never>>
procedure = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os
.Builder<DefaultInitialContext & object, Record<never, never>>.$context<{
auth: boolean;
}>(): Builder<{
auth: boolean;
} & object, Record<never, never>>
Declares the initial context type that must be provided when executing
procedures built from this builder.$context<{ auth: booleanauth: boolean }>()
.Builder<{ auth: boolean; } & object, Record<never, never>>.use<{
startedAt: number;
}, {
auth: boolean;
} & object, Record<never, never>>(middleware: Middleware<{
auth: boolean;
} & object, {
startedAt: number;
}, unknown, unknown, Record<never, never>>): BuilderWithMiddlewares<{
auth: boolean;
} & object, {
startedAt: number;
}, Record<never, never>>
Applies a middleware that runs before the handler of every procedure
built from this builder.use(middlewareGen<{
auth: boolean;
} & object, {
startedAt: number;
}, unknown, unknown, Record<never, never>, Effect<number, never, never> | Effect<{
output: unknown;
context: {
startedAt: number;
};
}, unknown, never>>(middleware: MiddlewareGen<{
auth: boolean;
} & object, {
startedAt: number;
}, unknown, unknown, Effect<number, never, never> | Effect<{
output: unknown;
context: {
startedAt: number;
};
}, unknown, never>, ORPCErrorConstructorMap<Record<never, never>>>): Middleware<{
auth: boolean;
} & object, {
startedAt: number;
}, unknown, unknown, Record<...>>
Creates an oRPC middleware from an Effect generator function.
Inside the generator you can yield Effect operations and `yield* next()`
to continue the chain, and `middlewareGen` handles the execution and
error handling for you.middlewareGen(function* ({ context: {
auth: boolean;
} & object
context, next: MiddlewareGenNext<unknown>next }) {
const const startedAt: numberstartedAt = yield* import ClockClock.const currentTimeMillis: Effect<number, never, never>Returns an Effect that succeeds with the current Unix time in milliseconds.
**When to use**
Use to create wall-clock timestamps from the active `Clock` service with
millisecond precision.
**Gotchas**
The value can move backward or forward when the system wall clock is
corrected, so it is not suitable for measuring elapsed time.
**Example** (Reading milliseconds)
```ts import.meta.vitest
import { Clock, Effect } from "effect"
const testClock: Clock.Clock = {
currentTimeMillisUnsafe: () => 1_000,
currentTimeMillis: Effect.succeed(1_000),
monotonicTimeNanosUnsafe: () => 1_000_000_000n,
monotonicTimeNanos: Effect.succeed(1_000_000_000n),
currentTimeNanosUnsafe: () => 1_000_000_000n,
currentTimeNanos: Effect.succeed(1_000_000_000n),
sleep: () => Effect.void
}
await Effect.runPromise(Effect.provideService(Clock.currentTimeMillis, Clock.Clock, testClock)) // => 1_000
```currentTimeMillis
return yield* next: MiddlewareGenNext
<{
startedAt: number;
}>(options: {
context: {
startedAt: number;
};
}) => Effect<{
output: unknown;
context: {
startedAt: number;
};
}, unknown, never>
next({ context: {
startedAt: number;
}
context: { startedAt: numberstartedAt } })
}))
.BuilderWithMiddlewares<{ auth: boolean; } & object, { startedAt: number; }, Record<never, never>>['handler']<number>(handler: ProcedureHandler<DistributiveOmit<{
auth: boolean;
} & object, "startedAt"> & {
startedAt: number;
}, unknown, number, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<{
auth: boolean;
} & object, {
startedAt: number;
}, InitialInputSchema, Schema<number>, Record<never, never>>
Defines the function that implements the procedure and completes the
chain, returning a callable procedure.handler(({ context: DistributiveOmit<{
auth: boolean;
} & object, "startedAt"> & {
startedAt: number;
}
context }) => context: DistributiveOmit<{
auth: boolean;
} & object, "startedAt"> & {
startedAt: number;
}
context.startedAt: numberstartedAt)
Client Calls
createEffectClient wraps any oRPC client, whether server-side or client-side, so every procedure returns a lazy Effect instead of a promise, ready to yield* inside Effect generators. The output becomes the success value, and errors land in the error channel with their original types preserved, ready for utilities like catchORPCErrorCodes:
import { catchORPCErrorCodes, createEffectClient } from '@orpc/experimental-effect'
import { Effect } from 'effect'
const effectClient = createEffectClient(client)
const program = Effect.gen(function* () {
const planet = yield* effectClient.planet.find({ id: 1 })
return planet.name
}).pipe(
catchORPCErrorCodes({
NOT_FOUND: error => Effect.succeed('unknown'),
}),
)
For typed error.data, combine Effect.catchIf with isDefinedError:
import { isDefinedError } from '@orpc/client'
import { Effect } from 'effect'
const recovered = effectClient.planet.find({ id: 1 }).pipe(
Effect.catchIf(isDefinedError, (error) => {
// error and error.data are fully typed here
return Effect.succeed(null)
}),
)
Effect Schema
oRPC natively supports Standard Schema, and Effect Schema implements that spec through Schema.toStandardSchemaV1:
import { Schema } from 'effect'
const procedure = os
.input(Schema.toStandardSchemaV1(Schema.Struct({ name: Schema.String })))
.handler(handlerGen(function* ({ input, context }) {
return `Hello ${input.name}!`
}))
.input and .output Extensions
Import @orpc/experimental-effect/extensions/input-output from a module that always runs during initialization, such as the file where you define your base builder or create your server. This lets you define .input and .output directly with Effect Schema:
const procedure = base
.input(Schema.Struct({ name: Schema.String }))
.output(Schema.Struct({ greeting: Schema.String }))
.handler(handlerGen(function* ({ input, context }) {
return { greeting: `Hello ${input.name}!` }
}))import '@orpc/experimental-effect/extensions/input-output'
import { os } from '@orpc/server'
export const base = osJSON Schema Converter
This integration also provides EffectSchemaToJsonSchemaConverter, which wraps Effect’s built-in Schema.toJsonSchemaDocument. Use it with tools such as the OpenAPI Generator. It accepts the same options as Schema.toJsonSchemaDocument, such as onExcessProperty, generateDescriptions, includeAnnotationKey, and referencePolicy:
import { EffectSchemaToJsonSchemaConverter } from '@orpc/experimental-effect'
const generator = new OpenAPIGenerator({
converters: [
new EffectSchemaToJsonSchemaConverter({
generateDescriptions: true,
}),
],
})
Schemas with an identifier annotation, including Schema.Class and recursive schemas, are preserved in $defs, which OpenAPIGenerator can then hoist into components.schemas.
OpenTelemetry Integration
After setting up the oRPC OpenTelemetry integration, add OtelTracer.layerGlobal from @effect/opentelemetry to the layer behind effect/context, so Effect spans nest under oRPC’s:
npm install @effect/opentelemetrypnpm add @effect/opentelemetryyarn add @effect/opentelemetrybun add @effect/opentelemetrynub add @effect/opentelemetryaube add @effect/opentelemetryimport { OtelTracer } from '@effect/opentelemetry'
import { Layer, ManagedRuntime } from 'effect'
const runtime = ManagedRuntime.make(AppLayer.pipe(Layer.provideMerge(OtelTracer.layerGlobal)))
// pass `await runtime.context()` as `effect/context` per request
Cloudflare Workers
Effect recommends a sync scheduler on Workers. Add it to the layer behind effect/context, and create the runtime at module scope:
import { Layer, ManagedRuntime, Scheduler } from 'effect'
const SyncScheduler = Layer.succeed(Scheduler.Scheduler, new Scheduler.MixedScheduler('sync'))
const runtime = ManagedRuntime.make(AppLayer.pipe(Layer.provideMerge(SyncScheduler)))
const effectContext = runtime.context() // pass `await effectContext` per request
Next.js
If you catch defects, for example with Effect.catchCause, call unstable_rethrow first so redirect() and notFound() still reach Next.js:
import { Cause, Effect } from 'effect'
import { unstable_rethrow } from 'next/navigation'
const safeDashboard = loadDashboard.pipe(
Effect.catchCause((cause) => {
unstable_rethrow(Cause.squash(cause))
return Effect.succeed(fallbackDashboard)
}),
)