effect-orpc 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,9 +10,8 @@ Inspired by [effect-trpc](https://github.com/mikearnaldi/effect-trpc).
10
10
  - **Type-safe service injection** - Use `ManagedRuntime<R>` to provide services to procedures with compile-time safety
11
11
  - **Tagged errors** - Create Effect-native error classes with `ORPCTaggedError` that integrate with oRPC's error handling
12
12
  - **Full oRPC compatibility** - Mix Effect procedures with standard oRPC procedures in the same router
13
- - **Builder pattern preserved** - All oRPC builder methods (`.errors()`, `.meta()`, `.route()`, `.input()`, `.output()`, `.use()`) work seamlessly
14
- - **Callable procedures** - Make procedures directly invocable while preserving Effect types
15
- - **Server actions support** - Full compatibility with framework server actions
13
+ - **Telemetry support with automatic tracing** - Procedures are automatically traced with OpenTelemetry-compatible spans. Customize span names with `.traced()`.
14
+ - **Builder pattern preserved** - oRPC builder methods (`.errors()`, `.meta()`, `.route()`, `.input()`, `.output()`, `.use()`) work seamlessly
16
15
 
17
16
  ## Installation
18
17
 
@@ -24,71 +23,68 @@ pnpm add effect-orpc
24
23
  bun add effect-orpc
25
24
  ```
26
25
 
27
- ## Quick Start
26
+ ## Demo of the features
28
27
 
29
28
  ```ts
30
- import { makeEffectORPC } from 'effect-orpc'
31
- import { os } from '@orpc/server'
32
- import { Context, Effect, Layer, ManagedRuntime } from 'effect'
33
- import { z } from 'zod'
29
+ import { os } from "@orpc/server";
30
+ import { Effect, ManagedRuntime } from "effect";
31
+ import { makeEffectORPC, ORPCTaggedError } from "effect-orpc";
34
32
 
35
- // Define your services
36
- class UserService extends Context.Tag('UserService')<
37
- UserService,
38
- {
39
- findById: (id: string) => Effect.Effect<User | undefined>
40
- findAll: () => Effect.Effect<User[]>
41
- create: (name: string) => Effect.Effect<User>
42
- }
43
- >() {}
44
-
45
- // Create service implementation
46
- const UserServiceLive = Layer.succeed(UserService, {
47
- findById: id => Effect.succeed(users.find(u => u.id === id)),
48
- findAll: () => Effect.succeed(users),
49
- create: name => Effect.succeed({ id: crypto.randomUUID(), name })
50
- })
51
-
52
- // Create runtime with your services
53
- const runtime = ManagedRuntime.make(UserServiceLive)
54
-
55
- // Create Effect-aware oRPC builder
56
- const effectOs = makeEffectORPC(runtime)
57
-
58
- // Define your procedures
59
- const getUser = effectOs
60
- .input(z.object({ id: z.string() }))
61
- .effect(function* ({ input }) {
62
- const userService = yield* UserService
63
- return yield* userService.findById(input.id)
64
- })
65
-
66
- const listUsers = effectOs.effect(function* () {
67
- const userService = yield* UserService
68
- return yield* userService.findAll()
69
- })
33
+ interface User {
34
+ id: number;
35
+ name: string;
36
+ }
70
37
 
71
- const createUser = effectOs
72
- .input(z.object({ name: z.string() }))
73
- .effect(function* ({ input }) {
74
- const userService = yield* UserService
75
- return yield* userService.create(input.name)
76
- })
38
+ let users: User[] = [
39
+ { id: 1, name: "John Doe" },
40
+ { id: 2, name: "Jane Doe" },
41
+ { id: 3, name: "James Dane" },
42
+ ];
77
43
 
78
- // Create router with mixed procedures
79
- const router = os.router({
80
- // Standard oRPC procedure
81
- health: os.handler(() => 'ok'),
44
+ // Authenticated os with initial context & errors set
45
+ const authedOs = os
46
+ .errors({ UNAUTHORIZED: { status: 401 } })
47
+ .$context<{ userId?: number }>()
48
+ .use(({ context, errors, next }) => {
49
+ if (context.userId === undefined) throw errors.UNAUTHORIZED();
50
+ return next({ context: { ...context, userId: context.userId } });
51
+ });
82
52
 
83
- // Effect procedures
84
- users: os.router({
85
- get: getUser,
86
- list: listUsers,
87
- create: createUser,
88
- })
89
- })
53
+ // Define your services
54
+ class UsersRepo extends Effect.Service<UsersRepo>()("UserService", {
55
+ accessors: true,
56
+ sync: () => ({
57
+ get: (id: number) => users.find((u) => u.id === id),
58
+ }),
59
+ }) {}
60
+
61
+ // Special yieldable oRPC error class
62
+ class UserNotFoundError extends ORPCTaggedError()("UserNotFoundError", {
63
+ status: 404,
64
+ }) {}
90
65
 
91
- export type Router = typeof router
66
+ // Create runtime with your services
67
+ const runtime = ManagedRuntime.make(UsersRepo.Default);
68
+ // Create Effect-aware oRPC builder from an other (optional) base oRPC builder
69
+ const effectOs = makeEffectORPC(runtime, authedOs).errors({
70
+ UserNotFoundError,
71
+ });
72
+
73
+ // Create the router with mixed procedures
74
+ export const router = {
75
+ health: os.handler(() => "ok"),
76
+ users: {
77
+ me: effectOs.effect(function* ({ context: { userId } }) {
78
+ const user = yield* UsersRepo.get(userId);
79
+ if (!user) {
80
+ return yield* new UserNotFoundError();
81
+ }
82
+ return user;
83
+ }),
84
+ },
85
+ };
86
+
87
+ export type Router = typeof router;
92
88
  ```
93
89
 
94
90
  ## Type Safety
@@ -96,397 +92,131 @@ export type Router = typeof router
96
92
  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:
97
93
 
98
94
  ```ts
99
- class ProvidedService extends Context.Tag('ProvidedService')<
95
+ import { Context, Effect, Layer, ManagedRuntime } from "effect";
96
+ import { makeEffectORPC } from "effect-orpc";
97
+
98
+ class ProvidedService extends Context.Tag("ProvidedService")<
100
99
  ProvidedService,
101
100
  { doSomething: () => Effect.Effect<string> }
102
101
  >() {}
103
102
 
104
- class MissingService extends Context.Tag('MissingService')<
103
+ class MissingService extends Context.Tag("MissingService")<
105
104
  MissingService,
106
105
  { doSomething: () => Effect.Effect<string> }
107
106
  >() {}
108
107
 
109
- const runtime = ManagedRuntime.make(Layer.succeed(ProvidedService, {
110
- doSomething: () => Effect.succeed('ok')
111
- }))
108
+ const runtime = ManagedRuntime.make(
109
+ Layer.succeed(ProvidedService, {
110
+ doSomething: () => Effect.succeed("ok"),
111
+ }),
112
+ );
112
113
 
113
- const effectOs = makeEffectORPC(runtime)
114
+ const effectOs = makeEffectORPC(runtime);
114
115
 
115
116
  // ✅ This compiles - ProvidedService is in the runtime
116
117
  const works = effectOs.effect(function* () {
117
- const service = yield* ProvidedService
118
- return yield* service.doSomething()
119
- })
118
+ const service = yield* ProvidedService;
119
+ return yield* service.doSomething();
120
+ });
120
121
 
121
122
  // ❌ This fails to compile - MissingService is not in the runtime
122
123
  const fails = effectOs.effect(function* () {
123
- const service = yield* MissingService // Type error!
124
- return yield* service.doSomething()
125
- })
126
- ```
127
-
128
- ## Wrapping a Customized Builder
129
-
130
- You can pass a customized oRPC builder as the second argument to inherit middleware, errors, and configuration:
131
-
132
- ```ts
133
- import { makeEffectORPC } from 'effect-orpc'
134
- import { ORPCError, os } from '@orpc/server'
135
- import { Effect } from 'effect'
136
-
137
- // Create a customized base builder with auth middleware
138
- const authedOs = os
139
- .errors({
140
- UNAUTHORIZED: { message: 'Not authenticated' },
141
- FORBIDDEN: { message: 'Access denied' },
142
- })
143
- .use(async ({ context, next, errors }) => {
144
- if (!context.user) {
145
- throw errors.UNAUTHORIZED()
146
- }
147
- return next({ context: { ...context, userId: context.user.id } })
148
- })
149
-
150
- // Wrap the customized builder with Effect support
151
- const effectAuthedOs = makeEffectORPC(runtime, authedOs)
152
-
153
- // All procedures inherit the auth middleware and error definitions
154
- const getProfile = effectAuthedOs.effect(function* ({ context }) {
155
- const userService = yield* UserService
156
- return yield* userService.findById(context.userId)
157
- })
158
-
159
- const updateProfile = effectAuthedOs
160
- .input(z.object({ name: z.string() }))
161
- .effect(function* ({ context, input }) {
162
- const userService = yield* UserService
163
- return yield* userService.update(context.userId, input)
164
- })
165
- ```
166
-
167
- ## Chaining Builder Methods
168
-
169
- The `EffectBuilder` supports all standard oRPC builder methods:
170
-
171
- ```ts
172
- const createPost = effectOs
173
- // Add custom errors
174
- .errors({
175
- NOT_FOUND: { message: 'User not found' },
176
- VALIDATION_ERROR: {
177
- message: 'Invalid input',
178
- data: z.object({ field: z.string(), issue: z.string() })
179
- },
180
- })
181
- // Add metadata
182
- .meta({ auth: true, rateLimit: 100 })
183
- // Configure route for OpenAPI
184
- .route({ method: 'POST', path: '/posts', tags: ['posts'] })
185
- // Define input schema
186
- .input(z.object({
187
- title: z.string().min(1).max(200),
188
- content: z.string(),
189
- authorId: z.string(),
190
- }))
191
- // Define output schema
192
- .output(z.object({
193
- id: z.string(),
194
- title: z.string(),
195
- content: z.string(),
196
- createdAt: z.date(),
197
- }))
198
- // Define Effect handler
199
- .effect(function* ({ input, errors }) {
200
- const userService = yield* UserService
201
- const user = yield* userService.findById(input.authorId)
202
-
203
- if (!user) {
204
- return yield Effect.Fail(errors.NOT_FOUND())
205
- }
206
-
207
- const postService = yield* PostService
208
- return yield* postService.create({
209
- title: input.title,
210
- content: input.content,
211
- authorId: input.authorId,
212
- })
213
- })
214
- ```
215
-
216
- ## Making Procedures Callable
217
-
218
- Use `.callable()` to make procedures directly invocable:
219
-
220
- ```ts
221
- const greet = effectOs
222
- .input(z.object({ name: z.string() }))
223
- .effect(function* ({ input }) {
224
- return `Hello, ${input.name}!`
225
- })
226
- .callable()
227
-
228
- // Can be called directly as a function
229
- const result = await greet({ name: 'World' })
230
- // => "Hello, World!"
231
-
232
- // Still a valid procedure for routers
233
- const router = os.router({ greet })
124
+ const service = yield* MissingService; // Type error!
125
+ return yield* service.doSomething();
126
+ });
234
127
  ```
235
128
 
236
- ## Server Actions Support
237
-
238
- Use `.actionable()` for framework server actions (Next.js, etc.):
129
+ ## Error Handling
239
130
 
240
- ```tsx
241
- const createTodo = effectOs
242
- .input(z.object({ title: z.string() }))
243
- .effect(function* ({ input }) {
244
- const todoService = yield* TodoService
245
- return yield* todoService.create(input.title)
246
- })
247
- .actionable({ context: async () => ({ user: await getSession() }) })
248
-
249
- // Use in React Server Components
250
- export async function TodoForm() {
251
- return (
252
- <form action={createTodo}>
253
- <input name="title" />
254
- <button type="submit">Add Todo</button>
255
- </form>
256
- )
257
- }
258
- ```
131
+ `ORPCTaggedError` lets you create Effect-native error classes that integrate seamlessly with oRPC. These errors:
259
132
 
260
- ## Error Handling
133
+ - Can be yielded in Effect generators (`yield* new MyError()` or `yield* Effect.fail(errors.MyError)`)
134
+ - Can be used in Effect builder's `.errors()` maps for type-safe error handling alongside regular oRPC errors
135
+ - Automatically convert to ORPCError when thrown
261
136
 
262
- Effect errors are properly propagated through oRPC's error handling. You can use the `errors` object passed to your handler:
137
+ 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.
263
138
 
264
139
  ```ts
265
140
  const getUser = effectOs
141
+ // Mixed error maps
266
142
  .errors({
143
+ // Regular oRPC error
267
144
  NOT_FOUND: {
268
- message: 'User not found',
269
- data: z.object({ id: z.string() })
145
+ message: "User not found",
146
+ data: z.object({ id: z.string() }),
270
147
  },
148
+ // Effect oRPC tagged error
149
+ UserNotFoundError,
150
+ // Note: The key of an oRPC error is not used as the error code
151
+ // So the following will only change the key of the error when accessing it
152
+ // from the errors object passed to the handler, but not the actual error code itself.
153
+ // To change the error's code, please see the next section on creating tagged errors.
154
+ USER_NOT_FOUND: UserNotFoundError,
155
+ // ^^^ same code as the `UserNotFoundError` error key, defined at the class level
271
156
  })
272
- .input(z.object({ id: z.string() }))
273
157
  .effect(function* ({ input, errors }) {
274
- const userService = yield* UserService
275
- const user = yield* userService.findById(input.id)
276
-
158
+ const user = yield* UsersRepo.findById(input.id);
277
159
  if (!user) {
278
- // Use oRPC's type-safe errors
279
- yield* Effect.fail(errors.NOT_FOUND({ id: input.id }))
160
+ return yield* new UserNotFoundError();
161
+ // or return `yield* Effect.fail(errors.USER_NOT_FOUND())`
280
162
  }
281
-
282
- return user
283
- })
163
+ return user;
164
+ });
284
165
  ```
285
166
 
286
- ## Tagged Errors
287
-
288
- `ORPCTaggedError` lets you create Effect-native error classes that integrate seamlessly with oRPC. These errors:
289
-
290
- - Can be yielded in Effect generators (`yield* new MyError()` or `yield* Effect.fail(errors.MyError)`)
291
- - Have all ORPCError properties (code, status, data, defined)
292
- - Can be used in `.errors()` maps for type-safe error handling
293
- - Automatically convert to ORPCError when thrown
294
-
295
167
  ### Creating Tagged Errors
296
168
 
297
169
  ```ts
298
- import { ORPCTaggedError } from 'effect-orpc'
170
+ import { ORPCTaggedError } from "effect-orpc";
299
171
 
300
172
  // Basic tagged error - code defaults to 'USER_NOT_FOUND' (CONSTANT_CASE of tag)
301
- class UserNotFound extends ORPCTaggedError<UserNotFound>()('UserNotFound') {}
173
+ class UserNotFound extends ORPCTaggedError()("UserNotFound") {}
302
174
 
303
175
  // With explicit code
304
- class NotFound extends ORPCTaggedError<NotFound>()('NotFound', 'NOT_FOUND') {}
176
+ class NotFound extends ORPCTaggedError()("NotFound", "NOT_FOUND") {}
305
177
 
306
178
  // With default options (code defaults to 'VALIDATION_ERROR') (CONSTANT_CASE of tag)
307
- class ValidationError extends ORPCTaggedError<ValidationError>()(
308
- 'ValidationError',
309
- { status: 400, message: 'Validation failed' }
310
- ) {}
179
+ class ValidationError extends ORPCTaggedError()("ValidationError", {
180
+ status: 400,
181
+ message: "Validation failed",
182
+ }) {}
311
183
 
312
184
  // With explicit code and options
313
- class Forbidden extends ORPCTaggedError<Forbidden>()(
314
- 'Forbidden',
315
- 'FORBIDDEN',
316
- { status: 403, message: 'Access denied' }
317
- ) {}
318
-
319
- // With typed data
320
- class UserNotFoundWithData extends ORPCTaggedError<
321
- UserNotFoundWithData,
322
- { userId: string }
323
- >()('UserNotFoundWithData') {}
324
- ```
325
-
326
- ### Using Tagged Errors in Procedures
327
-
328
- Tagged errors can be yielded directly in Effect generators:
329
-
330
- ```ts
331
- class UserNotFound extends ORPCTaggedError<
332
- UserNotFound,
333
- { userId: string }
334
- >()('UserNotFound', { status: 404, message: 'User not found' })
335
-
336
- const getUser = effectOs
337
- .input(z.object({ id: z.string() }))
338
- .effect(function* ({ input }) {
339
- const userService = yield* UserService
340
- const user = yield* userService.findById(input.id)
341
-
342
- if (!user) {
343
- // Yield the error - it will be converted to ORPCError automatically
344
- return yield* new UserNotFound({ data: { userId: input.id } })
345
- }
346
-
347
- return user
348
- })
349
- ```
350
-
351
- ### Using Tagged Errors in Error Maps
352
-
353
- Tagged error classes can be passed directly to `.errors()`:
354
-
355
- ```ts
356
- class UserNotFound extends ORPCTaggedError<
357
- UserNotFound,
358
- { userId: string }
359
- >()('UserNotFound', 'NOT_FOUND', { status: 404, message: 'User not found' })
360
-
361
- class InvalidInput extends ORPCTaggedError<
362
- InvalidInput,
363
- { field: string }
364
- >()('InvalidInput', 'BAD_REQUEST', { status: 400 })
365
-
366
- const getUser = effectOs
367
- .errors({
368
- // Tagged error class - use the class directly
369
- // The only difference is that the code is defined by the constant version of the tag
370
- // Or when defined explicitely like in the Forbidden tagged error above
371
- UserNotFound,
372
- INVALID_INPUT: InvalidInput,
373
- // Traditional format still works, and can be colocated
374
- INTERNAL_ERROR: { status: 500, message: 'Something went wrong' },
375
- })
376
- .input(z.object({ id: z.string() }))
377
- .effect(function* ({ input, errors }) {
378
- if (!input.id) {
379
- // errors.BAD_REQUEST is the InvalidInput class
380
- return yield* new errors.INVALID_INPUT({ data: { field: 'id' } })
381
- }
382
-
383
- const userService = yield* UserService
384
- const user = yield* userService.findById(input.id)
385
-
386
- if (!user) {
387
- // errors.UserNotFound is the UserNotFound class
388
- // with the code USER_NOT_FOUND (defined at the class level)
389
- return yield* new errors.UserNotFound({ data: { userId: input.id } })
390
- }
391
-
392
- return user
393
- })
185
+ class Forbidden extends ORPCTaggedError()("Forbidden", "FORBIDDEN", {
186
+ status: 403,
187
+ message: "Access denied",
188
+ }) {}
189
+
190
+ // With typed data using Standard Schema
191
+ class UserNotFoundWithData extends ORPCTaggedError(
192
+ z.object({ userId: z.string() }),
193
+ )("UserNotFoundWithData") {}
394
194
  ```
395
195
 
396
- ### Converting Tagged Errors
397
-
398
- Use `toORPCError` to convert a tagged error to a plain ORPCError:
399
-
400
- ```ts
401
- import { toORPCError, ORPCTaggedError } from 'effect-orpc'
402
- import { Effect } from 'effect'
403
-
404
- class MyError extends ORPCTaggedError<MyError>()('MyError', 'BAD_REQUEST') {}
405
-
406
- const procedure = effectOs.effect(function* () {
407
- const result = yield* someOperation.pipe(
408
- Effect.catchTag('MyError', (e) =>
409
- // Convert to plain ORPCError if needed
410
- Effect.fail(toORPCError(e))
411
- )
412
- )
413
- return result
414
- })
415
- ```
416
-
417
- ### Tagged Error Properties
418
-
419
- Tagged errors have all the properties of ORPCError plus Effect integration:
420
-
421
- ```ts
422
- const error = new UserNotFound({
423
- data: { userId: '123' },
424
- message: 'Custom message', // Override default message
425
- cause: originalError, // Attach cause for debugging
426
- })
427
-
428
- error._tag // 'UserNotFound' - for Effect's catchTag
429
- error.code // 'USER_NOT_FOUND' - ORPCError code
430
- error.status // 404 - HTTP status
431
- error.data // { userId: '123' } - typed data
432
- error.message // 'Custom message'
433
- error.defined // true - whether error is defined in error map
434
-
435
- // Convert to plain ORPCError
436
- const orpcError = error.toORPCError()
437
-
438
- // Serialize to JSON
439
- const json = error.toJSON()
440
- // { _tag: 'UserNotFound', code: 'USER_NOT_FOUND', status: 404, ... }
441
- ```
442
-
443
- ## Generator Syntax
444
-
445
- Pass a generator function directly to `.effect()` — no need to wrap it with `Effect.fn()` or `Effect.gen()`:
446
-
447
- ```ts
448
- // Recommended: Pass generator function directly
449
- const procedureWithGen = effectOs
450
- .input(z.object({ id: z.string() }))
451
- .effect(function* ({ input }) {
452
- const service = yield* MyService
453
- return yield* service.doSomething(input.id)
454
- })
455
-
456
- // Simple procedures without yield*
457
- const simpleProcedure = effectOs
458
- .input(z.object({ name: z.string() }))
459
- .effect(function* ({ input }) {
460
- return `Hello, ${input.name}!`
461
- })
462
- ```
463
-
464
- The handler receives `{ context, input, path, procedure, signal, lastEventId, errors }` as its argument, giving you full access to the oRPC procedure context.
465
-
466
196
  ## Traceable Spans
467
197
 
468
198
  All Effect procedures are automatically traced with `Effect.withSpan`. By default, the span name is the procedure path (e.g., `users.getUser`):
469
199
 
470
200
  ```ts
471
201
  // Router structure determines span names automatically
472
- const router = os.router({
473
- users: os.router({
202
+ const router = {
203
+ users: {
474
204
  // Span name: "users.get"
475
- get: effectOs
476
- .input(z.object({ id: z.string() }))
477
- .effect(function* ({ input }) {
478
- const userService = yield* UserService
479
- return yield* userService.findById(input.id)
480
- }),
205
+ get: effectOs.input(z.object({ id: z.string() })).effect(function* ({
206
+ input,
207
+ }) {
208
+ const userService = yield* UserService;
209
+ return yield* userService.findById(input.id);
210
+ }),
481
211
  // Span name: "users.create"
482
- create: effectOs
483
- .input(z.object({ name: z.string() }))
484
- .effect(function* ({ input }) {
485
- const userService = yield* UserService
486
- return yield* userService.create(input.name)
487
- }),
488
- })
489
- })
212
+ create: effectOs.input(z.object({ name: z.string() })).effect(function* ({
213
+ input,
214
+ }) {
215
+ const userService = yield* UserService;
216
+ return yield* userService.create(input.name);
217
+ }),
218
+ },
219
+ };
490
220
  ```
491
221
 
492
222
  Use `.traced()` to override the default span name:
@@ -494,11 +224,11 @@ Use `.traced()` to override the default span name:
494
224
  ```ts
495
225
  const getUser = effectOs
496
226
  .input(z.object({ id: z.string() }))
497
- .traced('custom.span.name') // Override the default path-based name
227
+ .traced("custom.span.name") // Override the default path-based name
498
228
  .effect(function* ({ input }) {
499
- const userService = yield* UserService
500
- return yield* userService.findById(input.id)
501
- })
229
+ const userService = yield* UserService;
230
+ return yield* userService.findById(input.id);
231
+ });
502
232
  ```
503
233
 
504
234
  ### Enabling OpenTelemetry
@@ -506,22 +236,21 @@ const getUser = effectOs
506
236
  To enable tracing, include the OpenTelemetry layer in your runtime:
507
237
 
508
238
  ```ts
509
- import { NodeSdk } from '@effect/opentelemetry'
510
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
511
- import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
512
-
513
- const TracingLive = NodeSdk.layer(Effect.sync(() => ({
514
- resource: { serviceName: 'my-service' },
515
- spanProcessor: [new SimpleSpanProcessor(new OTLPTraceExporter())]
516
- })))
517
-
518
- const AppLive = Layer.mergeAll(
519
- UserServiceLive,
520
- TracingLive
521
- )
522
-
523
- const runtime = ManagedRuntime.make(AppLive)
524
- const effectOs = makeEffectORPC(runtime)
239
+ import { NodeSdk } from "@effect/opentelemetry";
240
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
241
+ import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
242
+
243
+ const TracingLive = NodeSdk.layer(
244
+ Effect.sync(() => ({
245
+ resource: { serviceName: "my-service" },
246
+ spanProcessor: [new SimpleSpanProcessor(new OTLPTraceExporter())],
247
+ })),
248
+ );
249
+
250
+ const AppLive = Layer.mergeAll(UserServiceLive, TracingLive);
251
+
252
+ const runtime = ManagedRuntime.make(AppLive);
253
+ const effectOs = makeEffectORPC(runtime);
525
254
  ```
526
255
 
527
256
  ### Error Stack Traces
@@ -547,26 +276,36 @@ Returns an `EffectBuilder` instance.
547
276
 
548
277
  ```ts
549
278
  // With default builder
550
- const effectOs = makeEffectORPC(runtime)
279
+ const effectOs = makeEffectORPC(runtime);
551
280
 
552
281
  // With customized builder
553
- const effectAuthedOs = makeEffectORPC(runtime, authedBuilder)
282
+ const effectAuthedOs = makeEffectORPC(runtime, authedBuilder);
554
283
  ```
555
284
 
556
285
  ### `EffectBuilder`
557
286
 
558
287
  Wraps an oRPC Builder with Effect support. Available methods:
559
288
 
560
- | Method | Description |
561
- | ------------------ | ------------------------------------------------------------------------------- |
562
- | `.errors(map)` | Add type-safe custom errors |
563
- | `.meta(meta)` | Set procedure metadata |
564
- | `.route(route)` | Configure OpenAPI route |
565
- | `.input(schema)` | Define input validation schema |
566
- | `.output(schema)` | Define output validation schema |
567
- | `.use(middleware)` | Add middleware |
568
- | `.traced(name)` | Add a traceable span for telemetry (optional, defaults to the procedure's path) |
569
- | `.effect(handler)` | Define the Effect handler |
289
+ | Method | Description |
290
+ | ------------------- | ------------------------------------------------------------------------------- |
291
+ | `.$config(config)` | Set or override the builder config |
292
+ | `.$context<U>()` | Set or override the initial context type |
293
+ | `.$meta(meta)` | Set or override the initial metadata |
294
+ | `.$route(route)` | Set or override the initial route configuration |
295
+ | `.$input(schema)` | Set or override the initial input schema |
296
+ | `.errors(map)` | Add type-safe custom errors |
297
+ | `.meta(meta)` | Set procedure metadata (merged with existing) |
298
+ | `.route(route)` | Configure OpenAPI route (merged with existing) |
299
+ | `.input(schema)` | Define input validation schema |
300
+ | `.output(schema)` | Define output validation schema |
301
+ | `.use(middleware)` | Add middleware |
302
+ | `.traced(name)` | Add a traceable span for telemetry (optional, defaults to the procedure's path) |
303
+ | `.handler(handler)` | Define a non-Effect handler (standard oRPC handler) |
304
+ | `.effect(handler)` | Define the Effect handler |
305
+ | `.prefix(prefix)` | Prefix all procedures in the router (for OpenAPI) |
306
+ | `.tag(...tags)` | Add tags to all procedures in the router (for OpenAPI) |
307
+ | `.router(router)` | Apply all options to a router |
308
+ | `.lazy(loader)` | Create and apply options to a lazy-loaded router |
570
309
 
571
310
  ### `EffectDecoratedProcedure`
572
311
 
@@ -575,60 +314,22 @@ The result of calling `.effect()`. Extends standard oRPC `DecoratedProcedure` wi
575
314
  | Method | Description |
576
315
  | ----------------------- | --------------------------------------------- |
577
316
  | `.errors(map)` | Add more custom errors |
578
- | `.meta(meta)` | Update metadata |
579
- | `.route(route)` | Update route configuration |
317
+ | `.meta(meta)` | Update metadata (merged with existing) |
318
+ | `.route(route)` | Update route configuration (merged) |
580
319
  | `.use(middleware)` | Add middleware |
581
320
  | `.callable(options?)` | Make procedure directly invocable |
582
321
  | `.actionable(options?)` | Make procedure compatible with server actions |
583
322
 
584
- ### `ORPCTaggedError<Self, TData>()(tag, codeOrOptions?, defaultOptions?)`
323
+ ### `ORPCTaggedError(schema?)(tag, codeOrOptions?, defaultOptions?)`
585
324
 
586
325
  Factory function to create Effect-native tagged error classes.
326
+ If no code is provided, it defaults to CONSTANT_CASE of the tag (e.g., `UserNotFoundError` → `USER_NOT_FOUND_ERROR`).
587
327
 
588
- - `Self` - The class type itself (for proper typing)
589
- - `TData` - Optional type for the error's data payload
328
+ - `schema` - Optional Standard Schema for the error's data payload (e.g., `z.object({ userId: z.string() })`)
590
329
  - `tag` - Unique tag for discriminated unions (used by Effect's `catchTag`)
591
330
  - `codeOrOptions` - Either an ORPCErrorCode string or `{ status?, message? }` options
592
331
  - `defaultOptions` - Default `{ status?, message? }` when code is provided explicitly
593
332
 
594
- If no code is provided, it defaults to CONSTANT_CASE of the tag (e.g., `UserNotFound` → `USER_NOT_FOUND`).
595
-
596
- ```ts
597
- // Tag only - code defaults to 'MY_ERROR'
598
- class MyError extends ORPCTaggedError<MyError>()('MyError') {}
599
-
600
- // With options - code defaults to 'MY_ERROR'
601
- class MyError extends ORPCTaggedError<MyError>()(
602
- 'MyError',
603
- { status: 400, message: 'Bad request' }
604
- ) {}
605
-
606
- // With explicit code
607
- class MyError extends ORPCTaggedError<MyError>()(
608
- 'MyError',
609
- 'CUSTOM_CODE',
610
- { status: 400 }
611
- ) {}
612
-
613
- // With typed data
614
- class MyError extends ORPCTaggedError<MyError, { field: string }>()(
615
- 'MyError',
616
- 'BAD_REQUEST'
617
- ) {}
618
- ```
619
-
620
- ### `toORPCError(taggedError)`
621
-
622
- Converts an `ORPCTaggedError` instance to a plain `ORPCError`.
623
-
624
- ```ts
625
- import { toORPCError } from 'effect-orpc'
626
-
627
- const taggedError = new UserNotFound({ data: { userId: '123' } })
628
- const orpcError = toORPCError(taggedError)
629
- // => ORPCError { code: 'USER_NOT_FOUND', status: 404, data: { userId: '123' } }
630
- ```
631
-
632
333
  ## License
633
334
 
634
335
  MIT