Skip to main content

Define a service and error

users.ts
import { Context, Effect, Layer } from "effect";
import { ORPCTaggedError } from "effect-orpc";
import * as z from "zod";

export const User = z.object({
  id: z.string(),
  name: z.string(),
});

const users: Array<z.infer<typeof User>> = [
  { id: "1", name: "Ada Lovelace" },
  { id: "2", name: "Grace Hopper" },
];

export class UsersRepo extends Context.Service<
  UsersRepo,
  {
    readonly findById: (
      id: string,
    ) => Effect.Effect<z.infer<typeof User> | undefined>;
  }
>()("UsersRepo") {}

export const UsersRepoLive = Layer.succeed(UsersRepo, {
  findById: (id) => Effect.succeed(users.find((user) => user.id === id)),
});

export class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
  status: 404,
  message: "User not found",
  schema: z.object({ id: z.string() }),
}) {}

Create the router

router.ts
import { os } from "@orpc/server";
import { eos } from "effect-orpc";
import * as z from "zod";

import { User, UserNotFoundError, UsersRepo, UsersRepoLive } from "./users";

const effectProcedure = eos
  .provide(UsersRepoLive)
  .errors({ UserNotFoundError });

export const router = {
  health: os.handler(() => "ok"),

  users: {
    get: effectProcedure
      .input(z.object({ id: z.string() }))
      .output(User)
      .effect(function* ({ input }) {
        const usersRepo = yield* UsersRepo;
        const user = yield* usersRepo.findById(input.id);

        if (!user) {
          return yield* new UserNotFoundError({
            data: { id: input.id },
          });
        }

        return user;
      }),
  },
};

export type Router = typeof router;

Call it!

Use .callable() when you want to invoke a procedure directly in tests, scripts, or local examples.
main.ts
import { router } from "./router";

const getUser = router.users.get.callable();

console.log(await getUser({ id: "1" }));
// { id: "1", name: "Ada Lovelace" }

What happened

1

`eos` wrapped an oRPC builder

eos exposes familiar oRPC builder methods and adds Effect-aware methods such as .provide(...), .effect(...), and .traced(...).
2

The service layer satisfied Effect requirements

UsersRepoLive was provided before the handler, so yield* UsersRepo is available at compile-time.
3

The tagged error became an oRPC error

UserNotFoundError is yieldable in Effect code and serializable as an oRPC error.

Next steps

Mental model

Learn how builders, layers, runtimes, and request boundaries fit together.

Effect procedures

Focus on .effect(...) handlers and their typed handler options.
Last modified on June 15, 2026