npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

effect-orpc

v0.2.0

Published

A type-safe integration between [oRPC](https://orpc.dev/) and [Effect](https://effect.website/), enabling Effect-native procedures with full service injection support, OpenTelemetry tracing support and typesafe Effect errors support.

Readme

effect-orpc

A type-safe integration between oRPC and Effect, enabling Effect-native procedures with full service injection support, OpenTelemetry tracing support and typesafe Effect errors support.

Inspired by effect-trpc.

Features

  • Effect-native procedures - Write oRPC procedures using generators with yield* syntax
  • Type-safe service injection - Use ManagedRuntime<R> to provide services to procedures with compile-time safety
  • Tagged errors - Create Effect-native error classes with ORPCTaggedError that integrate with oRPC's error handling
  • Full oRPC compatibility - Mix Effect procedures with standard oRPC procedures in the same router
  • Telemetry support with automatic tracing - Procedures are automatically traced with OpenTelemetry-compatible spans. Customize span names with .traced().
  • Builder pattern preserved - oRPC builder methods (.errors(), .meta(), .route(), .input(), .output(), .use()) work seamlessly

Installation

npm install effect-orpc
# or
pnpm add effect-orpc
# or
bun add effect-orpc

Runnable demos live in the repository's examples/ directory.

Demo

import { os } from "@orpc/server";
import { Effect, ManagedRuntime } from "effect";
import { makeEffectORPC, ORPCTaggedError } from "effect-orpc";

interface User {
  id: number;
  name: string;
}

let users: User[] = [
  { id: 1, name: "John Doe" },
  { id: 2, name: "Jane Doe" },
  { id: 3, name: "James Dane" },
];

// Authenticated os with initial context & errors set
const authedOs = os
  .errors({ UNAUTHORIZED: { status: 401 } })
  .$context<{ userId?: number }>()
  .use(({ context, errors, next }) => {
    if (context.userId === undefined) throw errors.UNAUTHORIZED();
    return next({ context: { ...context, userId: context.userId } });
  });

// Define your services
class UsersRepo extends Effect.Service<UsersRepo>()("UsersRepo", {
  accessors: true,
  sync: () => ({
    get: (id: number) => users.find((u) => u.id === id),
  }),
}) {}

// Special yieldable oRPC error class
class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
  status: 404,
}) {}

// Create runtime with your services
const runtime = ManagedRuntime.make(UsersRepo.Default);
// Create Effect-aware oRPC builder from an other (optional) base oRPC builder and provide tagged errors
const effectOs = makeEffectORPC(runtime, authedOs).errors({
  UserNotFoundError,
});

// Create the router with mixed procedures
export const router = {
  health: os.handler(() => "ok"),
  users: {
    me: effectOs.effect(function* ({ context: { userId } }) {
      const user = yield* UsersRepo.get(userId);
      if (!user) {
        return yield* new UserNotFoundError();
      }
      return user;
    }),
  },
};

export type Router = typeof router;

Type Safety

The wrapper enforces that Effect procedures only use services provided by the ManagedRuntime. If you try to use a service that isn't in the runtime, you'll get a compile-time error:

import { Context, Effect, Layer, ManagedRuntime } from "effect";
import { makeEffectORPC } from "effect-orpc";

class ProvidedService extends Context.Tag("ProvidedService")<
  ProvidedService,
  { doSomething: () => Effect.Effect<string> }
>() {}

class MissingService extends Context.Tag("MissingService")<
  MissingService,
  { doSomething: () => Effect.Effect<string> }
>() {}

const runtime = ManagedRuntime.make(
  Layer.succeed(ProvidedService, {
    doSomething: () => Effect.succeed("ok"),
  }),
);

const effectOs = makeEffectORPC(runtime);

// ✅ This compiles - ProvidedService is in the runtime
const works = effectOs.effect(function* () {
  const service = yield* ProvidedService;
  return yield* service.doSomething();
});

// ❌ This fails to compile - MissingService is not in the runtime
const fails = effectOs.effect(function* () {
  const service = yield* MissingService; // Type error!
  return yield* service.doSomething();
});

Error Handling

ORPCTaggedError lets you create Effect-native error classes that integrate seamlessly with oRPC. These errors:

  • Can be yielded in Effect generators (yield* new MyError() or yield* Effect.fail(errors.MyError))
  • Can be used in Effect builder's .errors() maps for type-safe error handling alongside regular oRPC errors
  • Automatically convert to ORPCError when thrown

Make sure the tagged error class is passed to the effect .errors() to be able to yield the error class directly and make the client recognize it as defined.

const getUser = effectOs
  // Mixed error maps
  .errors({
    // Regular oRPC error
    NOT_FOUND: {
      message: "User not found",
      data: z.object({ id: z.string() }),
    },
    // Effect oRPC tagged error
    UserNotFoundError,
    // Note: The key of an oRPC error is not used as the error code
    // So the following will only change the key of the error when accessing it
    // from the errors object passed to the handler, but not the actual error code itself.
    // To change the error's code, please see the next section on creating tagged errors.
    USER_NOT_FOUND: UserNotFoundError,
    // ^^^ same code as the `UserNotFoundError` error key, defined at the class level
  })
  .effect(function* ({ input, errors }) {
    const user = yield* UsersRepo.findById(input.id);
    if (!user) {
      return yield* new UserNotFoundError();
      // or return `yield* Effect.fail(errors.USER_NOT_FOUND())`
    }
    return user;
  });

Creating Tagged Errors

import { ORPCTaggedError } from "effect-orpc";

// Basic tagged error - code defaults to 'USER_NOT_FOUND' (CONSTANT_CASE of tag)
class UserNotFound extends ORPCTaggedError("UserNotFound") {}

// With explicit code
class NotFound extends ORPCTaggedError("NotFound", { code: "NOT_FOUND" }) {}

// With default options (code defaults to 'VALIDATION_ERROR') (CONSTANT_CASE of tag)
class ValidationError extends ORPCTaggedError("ValidationError", {
  status: 400,
  message: "Validation failed",
}) {}

// With all options
class ForbiddenError extends ORPCTaggedError("ForbiddenError", {
  code: "FORBIDDEN",
  status: 403,
  message: "Access denied",
  schema: z.object({
    reason: z.string(),
  }),
}) {}

// With typed data using Standard Schema
class UserNotFoundWithData extends ORPCTaggedError("UserNotFoundWithData", {
  schema: z.object({ userId: z.string() }),
}) {}

Traceable Spans

All Effect procedures are automatically traced with Effect.withSpan. By default, the span name is the procedure path (e.g., users.getUser):

// Router structure determines span names automatically
const router = {
  users: {
    // Span name: "users.get"
    get: effectOs.input(z.object({ id: z.string() })).effect(function* ({
      input,
    }) {
      const userService = yield* UserService;
      return yield* userService.findById(input.id);
    }),
    // Span name: "users.create"
    create: effectOs.input(z.object({ name: z.string() })).effect(function* ({
      input,
    }) {
      const userService = yield* UserService;
      return yield* userService.create(input.name);
    }),
  },
};

Use .traced() to override the default span name:

const getUser = effectOs
  .input(z.object({ id: z.string() }))
  .traced("custom.span.name") // Override the default path-based name
  .effect(function* ({ input }) {
    const userService = yield* UserService;
    return yield* userService.findById(input.id);
  });

Enabling OpenTelemetry

To enable tracing, include the OpenTelemetry layer in your runtime:

import { NodeSdk } from "@effect/opentelemetry";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";

const TracingLive = NodeSdk.layer(
  Effect.sync(() => ({
    resource: { serviceName: "my-service" },
    spanProcessor: [new SimpleSpanProcessor(new OTLPTraceExporter())],
  })),
);

const AppLive = Layer.mergeAll(UserServiceLive, TracingLive);

const runtime = ManagedRuntime.make(AppLive);
const effectOs = makeEffectORPC(runtime);

Error Stack Traces

When an Effect procedure fails, the span includes a properly formatted stack trace pointing to the definition site:

MyCustomError: Something went wrong
    at <anonymous> (/app/src/procedures.ts:42:28)
    at users.getById (/app/src/procedures.ts:41:35)

Request-Scoped Fiber Context

If you run effect-orpc inside a framework such as Hono, the handler executes through the runtime boundary and will not automatically inherit request-local FiberRef state from outer middleware.

To preserve request-scoped logs, tracing annotations, and other fiber-local state, wrap the framework continuation with withFiberContext from effect-orpc/node.

import { Hono } from "hono";
import { Effect, ManagedRuntime } from "effect";
import { makeEffectORPC } from "effect-orpc";
import { withFiberContext } from "effect-orpc/node";

const runtime = ManagedRuntime.make(AppLive);
const effectOs = makeEffectORPC(runtime);
const app = new Hono();

app.use("*", async (c, next) => {
  await Effect.runPromise(
    Effect.gen(function* () {
      yield* Effect.annotateLogsScoped({
        requestId: c.get("requestId"),
      });

      yield* withFiberContext(() => next());
    }),
  );
});

When a captured fiber context and the ManagedRuntime both provide the same service, effect-orpc prioritizes the captured context. The runtime is treated as the application-wide base layer, while withFiberContext preserves the more specific request-scoped values from outer middleware. This prevents request-local references such as request IDs, logging annotations, tracing context, or scoped overrides from being replaced by runtime defaults when the handler crosses the runtime boundary.

The reason for the separate /node entrypoint is that withFiberContext relies on Node/Bun's AsyncLocalStorage from node:async_hooks to carry Effect FiberRef state across framework async boundaries. The main package stays runtime-agnostic.

If you do not need framework-to-handler fiber propagation, you do not need the /node entrypoint at all.

Contract-First Usage

Use implementEffect(contract, runtime) when you already have an oRPC contract and want to keep contract-first enforcement while adding Effect-native handlers. Use makeEffectORPC(runtime, builder?) when you want to build procedures directly from an oRPC builder.

import { Effect, ManagedRuntime } from "effect";
import { eoc, implementEffect } from "effect-orpc";
import z from "zod";

class UsersRepo extends Effect.Service<UsersRepo>()("UsersRepo", {
  accessors: true,
  sync: () => ({
    list: (amount: number) =>
      Array.from({ length: amount }, (_, index) => `user-${index + 1}`),
  }),
}) {}

const contract = {
  users: {
    list: eoc
      .input(z.object({ amount: z.number().int().positive() }))
      .output(z.array(z.string())),
  },
};

const runtime = ManagedRuntime.make(UsersRepo.Default);
const oe = implementEffect(contract, runtime);

export const router = oe.router({
  users: {
    list: oe.users.list.effect(function* ({ input }) {
      return yield* UsersRepo.list(input.amount);
    }),
  },
});

Contract leaves keep the contract-defined input, output, and error surface. They add .effect(...) alongside existing implementer methods such as .handler(...) and .use(...), but do not expose contract-changing builder methods like .input(...) or .output(...).

If your contract declares tagged Effect error classes, prefer eoc.errors(...) instead of raw oc.errors(...) so the error schema and metadata are derived directly from the ORPCTaggedError class.

API Reference

makeEffectORPC(runtime, builder?)

Creates an Effect-aware procedure builder.

  • runtime - A ManagedRuntime<R, E> instance that provides services for Effect procedures
  • builder (optional) - An oRPC Builder instance to wrap. Defaults to os from @orpc/server

Returns an EffectBuilder instance.

// With default builder
const effectOs = makeEffectORPC(runtime);

// With customized builder
const effectAuthedOs = makeEffectORPC(runtime, authedBuilder);

implementEffect(contract, runtime)

Creates an Effect-aware contract implementer.

  • contract - An oRPC contract router built with oc
  • runtime - A ManagedRuntime<R, E> instance that provides services for Effect procedures

Returns a contract-shaped implementer tree whose leaves support .effect(...).

const oe = implementEffect(contract, runtime);

const router = oe.router({
  users: {
    list: oe.users.list.effect(function* ({ input }) {
      return yield* UsersRepo.list(input.amount);
    }),
  },
});

eoc

An Effect-aware wrapper around oRPC's oc contract builder.

Use it when you want contract definitions to accept ORPCTaggedError classes directly in .errors(...) without duplicating the error schema.

class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
  code: "NOT_FOUND",
  schema: z.object({ userId: z.string() }),
}) {}

const contract = {
  users: {
    find: eoc
      .errors({
        NOT_FOUND: UserNotFoundError,
      })
      .input(z.object({ userId: z.string() }))
      .output(z.object({ userId: z.string() })),
  },
};

EffectBuilder

Wraps an oRPC Builder with Effect support. Available methods:

| Method | Description | | ------------------- | ------------------------------------------------------------------------------- | | .$config(config) | Set or override the builder config | | .$context<U>() | Set or override the initial context type | | .$meta(meta) | Set or override the initial metadata | | .$route(route) | Set or override the initial route configuration | | .$input(schema) | Set or override the initial input schema | | .errors(map) | Add type-safe custom errors | | .meta(meta) | Set procedure metadata (merged with existing) | | .route(route) | Configure OpenAPI route (merged with existing) | | .input(schema) | Define input validation schema | | .output(schema) | Define output validation schema | | .use(middleware) | Add middleware | | .traced(name) | Add a traceable span for telemetry (optional, defaults to the procedure's path) | | .handler(handler) | Define a non-Effect handler (standard oRPC handler) | | .effect(handler) | Define the Effect handler | | .prefix(prefix) | Prefix all procedures in the router (for OpenAPI) | | .tag(...tags) | Add tags to all procedures in the router (for OpenAPI) | | .router(router) | Apply all options to a router | | .lazy(loader) | Create and apply options to a lazy-loaded router |

EffectDecoratedProcedure

The result of calling .effect(). Extends standard oRPC DecoratedProcedure with Effect type preservation.

| Method | Description | | ----------------------- | --------------------------------------------- | | .errors(map) | Add more custom errors | | .meta(meta) | Update metadata (merged with existing) | | .route(route) | Update route configuration (merged) | | .use(middleware) | Add middleware | | .callable(options?) | Make procedure directly invocable | | .actionable(options?) | Make procedure compatible with server actions |

ORPCTaggedError(tag, options?)

Factory function to create Effect-native tagged error classes.

The options is an optional object containing:

  • schema? - Optional Standard Schema for the error's data payload (e.g., z.object({ userId: z.string() }))
  • code? - Optional ORPCErrorCode, defaults to CONSTANT_CASE of the tag (e.g., UserNotFoundErrorUSER_NOT_FOUND_ERROR).
  • status? - Sets the default status of the error
  • message - Sets the default message of the error

License

MIT