Skip to content

Effect Integration

Effect integration lets you seamlessly use Effect's powerful features, such as its effect system, concurrency model, and schema library, within oRPC.

WARNING

This guide assumes familiarity with Effect. Review the official documentation if needed.

Installation

sh
npm install @orpc/experimental-effect@beta effect@beta
sh
yarn add @orpc/experimental-effect@beta effect@beta
sh
pnpm add @orpc/experimental-effect@beta effect@beta
sh
bun add @orpc/experimental-effect@beta effect@beta
sh
deno add npm:@orpc/experimental-effect@beta npm:effect@beta

Effectful 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.

ts
import { 
handlerGen
} from '@orpc/experimental-effect'
import {
Effect
} from 'effect'
const
procedure
=
os
.
handler
(
handlerGen
(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
}))

.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.

ts
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
})
ts
import '@orpc/experimental-effect/extensions/effect'

import { os } from '@orpc/server'

export const base = os

Effect Services

You can provide Effect services through the oRPC context in a typesafe way with WithEffectContext and ~effect/context:

ts
import { 
handlerGen
,
WithEffectContext
} from '@orpc/experimental-effect'
import {
Context
,
Effect
} from 'effect'
class
Random
extends
Context
.
Tag
('MyRandomService')<
Random
,
{ readonly
next
:
Effect
.
Effect
<number>
} >() {} interface ServerContext extends
WithEffectContext
<
Random
> {}
const
procedure
=
os
.
$context
<ServerContext>()
.
handler
(
handlerGen
(function* ({
input
,
context
}) {
const
random
= yield*
Random
const
result
= yield*
random
.
next
return
result
})) const
random
= await
call
(
procedure
,
undefined
, {
context
: {
'~effect/context':
Context
.
empty
().
pipe
(
Context
.
add
(
Random
, {
next
:
Effect
.
succeed
(
Math
.
random
()),
}), ) } })

INFO

You can also extend the Effect context with middleware:

ts
const procedure = os
  .$context<ServerContext>()
  .use(({ context, next }) => {
    return next({
      context: {
        '~effect/context': context['~effect/context'].pipe(
          Context.add(AdditionService, {}),
        )
      }
    })
  })
  .handler(handlerGen(function* ({ input, context }) {
    const additionService = yield* AdditionService
  }))

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:

ts
import { Context, Effect } from 'effect'

interface ServerContext extends WithEffectContext<never> {}

export async function fetch(request: Request) {
  const { response } = await handler.fetch(request, {
    context: {
      '~effect/context': Context.empty(),
      '~effect/wrap': (effect, opts) => effect.pipe(
        Effect.catchAllCause((cause) => {

        })
      ),
    }
  })

  return response ?? new Response('Not Found', { status: 404 })
}

INFO

For app level error handling, we recommend middleware or interceptors.

Typesafe Errors

When you yield* Effect.fail(new ORPCError(...)) or return new ORPCError(...), oRPC treats it as a returned ORPCError. On the client, you can handle these errors in a typesafe way:

ts
const procedure = os.handler(handlerGen(function* ({ errors }) {
  if (resourceNotFound) {
    yield* Effect.fail(new ORPCError('NOT_FOUND', {
      message: 'The resource you are looking for does not exist',
    }))
    // -- or -
    return new ORPCError('NOT_FOUND', {
      message: 'The resource you are looking for does not exist',
    })
  }

  return 'Success'
}))

const [error, result] = await call(procedure)

if (isInferableError(error)) {
  // typesafe error handling
}

Effect Schema

oRPC natively supports Standard Schema, and Effect Schema implements that spec through Schema.standardSchemaV1:

ts
import { Schema } from 'effect'

const procedure = os
  .input(Schema.standardSchemaV1(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:

ts
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}!` }
  }))
ts
import '@orpc/experimental-effect/extensions/input-output'

import { os } from '@orpc/server'

export const base = os

INFO

You can also use these extensions with the contract builder.

JSON Schema Converter

This integration also provides EffectSchemaToJsonSchemaConverter, built on top of Effect Schema to JSON Schema. You can use it with tools such as the OpenAPI Generator:

ts
import { EffectSchemaToJsonSchemaConverter } from '@orpc/experimental-effect'

const generator = new OpenAPIGenerator({
  converters: [new EffectSchemaToJsonSchemaConverter()],
})

OpenTelemetry Integration

First, set up the oRPC OpenTelemetry integration. Then instrument your Effect to work seamlessly with OpenTelemetry by providing TracingLive through ~effect/wrap in the context. This makes Effect tracing equivalent to OpenTelemetry tracing:

ts
import { Resource, Tracer } from '@effect/opentelemetry'
import { Context, Effect, Layer } from 'effect'

interface ServerContext extends WithEffectContext<never> {}

const TracingLive = Tracer.layerGlobal.pipe(
  Layer.provide(Resource.layerFromEnv()),
)

export async function fetch(request: Request) {
  const { response } = await handler.fetch(request, {
    context: {
      '~effect/context': Context.empty(),
      '~effect/wrap': (effect, opts) => effect.pipe(Effect.provide(TracingLive)),
    }
  })

  return response ?? new Response('Not Found', { status: 404 })
}

Released under the MIT License.