effect-orpc 0.0.5 → 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
- })
33
+ interface User {
34
+ id: number;
35
+ name: string;
36
+ }
65
37
 
66
- const listUsers = effectOs.effect(function* () {
67
- const userService = yield* UserService
68
- return yield* userService.findAll()
69
- })
38
+ let users: User[] = [
39
+ { id: 1, name: "John Doe" },
40
+ { id: 2, name: "Jane Doe" },
41
+ { id: 3, name: "James Dane" },
42
+ ];
70
43
 
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
- })
77
-
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,460 +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 svc = yield* ProvidedService
118
- return yield* svc.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 svc = yield* MissingService // Type error!
124
- return yield* svc.doSomething()
125
- })
126
- ```
127
-
128
- ## Using Services
129
-
130
- ```ts
131
- import { makeEffectORPC } from 'effect-orpc'
132
- import { Context, Effect, Layer, ManagedRuntime } from 'effect'
133
- import { z } from 'zod'
134
-
135
- // Define services
136
- class DatabaseService extends Context.Tag('DatabaseService')<
137
- DatabaseService,
138
- {
139
- query: <T>(sql: string) => Effect.Effect<T[]>
140
- execute: (sql: string) => Effect.Effect<void>
141
- }
142
- >() {}
143
-
144
- class CacheService extends Context.Tag('CacheService')<
145
- CacheService,
146
- {
147
- get: <T>(key: string) => Effect.Effect<T | undefined>
148
- set: <T>(key: string, value: T, ttl?: number) => Effect.Effect<void>
149
- }
150
- >() {}
151
-
152
- // Create layers
153
- const DatabaseServiceLive = Layer.succeed(DatabaseService, {
154
- query: sql => Effect.succeed([]),
155
- execute: sql => Effect.succeed(undefined),
156
- })
157
-
158
- const CacheServiceLive = Layer.succeed(CacheService, {
159
- get: key => Effect.succeed(undefined),
160
- set: (key, value, ttl) => Effect.succeed(undefined),
161
- })
162
-
163
- // Compose layers
164
- const AppLive = Layer.mergeAll(DatabaseServiceLive, CacheServiceLive)
165
-
166
- // Create runtime with all services
167
- const runtime = ManagedRuntime.make(AppLive)
168
- const effectOs = makeEffectORPC(runtime)
169
-
170
- // Use multiple services in a procedure
171
- const getUserWithCache = effectOs
172
- .input(z.object({ id: z.string() }))
173
- .effect(function* ({ input }) {
174
- const cache = yield* CacheService
175
- const db = yield* DatabaseService
176
-
177
- // Try cache first
178
- const cached = yield* cache.get<User>(`user:${input.id}`)
179
- if (cached)
180
- return cached
181
-
182
- // Fall back to database
183
- const [user] = yield* db.query<User>(`SELECT * FROM users WHERE id = '${input.id}'`)
184
- if (user) {
185
- yield* cache.set(`user:${input.id}`, user, 3600)
186
- }
187
- return user
188
- })
124
+ const service = yield* MissingService; // Type error!
125
+ return yield* service.doSomething();
126
+ });
189
127
  ```
190
128
 
191
- ## Wrapping a Customized Builder
192
-
193
- You can pass a customized oRPC builder as the second argument to inherit middleware, errors, and configuration:
194
-
195
- ```ts
196
- import { makeEffectORPC } from 'effect-orpc'
197
- import { ORPCError, os } from '@orpc/server'
198
- import { Effect } from 'effect'
199
-
200
- // Create a customized base builder with auth middleware
201
- const authedOs = os
202
- .errors({
203
- UNAUTHORIZED: { message: 'Not authenticated' },
204
- FORBIDDEN: { message: 'Access denied' },
205
- })
206
- .use(async ({ context, next, errors }) => {
207
- if (!context.user) {
208
- throw errors.UNAUTHORIZED()
209
- }
210
- return next({ context: { ...context, userId: context.user.id } })
211
- })
212
-
213
- // Wrap the customized builder with Effect support
214
- const effectAuthedOs = makeEffectORPC(runtime, authedOs)
215
-
216
- // All procedures inherit the auth middleware and error definitions
217
- const getProfile = effectAuthedOs.effect(function* ({ context }) {
218
- const userService = yield* UserService
219
- return yield* userService.findById(context.userId)
220
- })
221
-
222
- const updateProfile = effectAuthedOs
223
- .input(z.object({ name: z.string() }))
224
- .effect(function* ({ context, input }) {
225
- const userService = yield* UserService
226
- return yield* userService.update(context.userId, input)
227
- })
228
- ```
229
-
230
- ## Chaining Builder Methods
231
-
232
- The `EffectBuilder` supports all standard oRPC builder methods:
233
-
234
- ```ts
235
- const createPost = effectOs
236
- // Add custom errors
237
- .errors({
238
- NOT_FOUND: { message: 'User not found' },
239
- VALIDATION_ERROR: {
240
- message: 'Invalid input',
241
- data: z.object({ field: z.string(), issue: z.string() })
242
- },
243
- })
244
- // Add metadata
245
- .meta({ auth: true, rateLimit: 100 })
246
- // Configure route for OpenAPI
247
- .route({ method: 'POST', path: '/posts', tags: ['posts'] })
248
- // Define input schema
249
- .input(z.object({
250
- title: z.string().min(1).max(200),
251
- content: z.string(),
252
- authorId: z.string(),
253
- }))
254
- // Define output schema
255
- .output(z.object({
256
- id: z.string(),
257
- title: z.string(),
258
- content: z.string(),
259
- createdAt: z.date(),
260
- }))
261
- // Define Effect handler
262
- .effect(function* ({ input, errors }) {
263
- const userService = yield* UserService
264
- const user = yield* userService.findById(input.authorId)
265
-
266
- if (!user) {
267
- throw errors.NOT_FOUND()
268
- }
269
-
270
- const postService = yield* PostService
271
- return yield* postService.create({
272
- title: input.title,
273
- content: input.content,
274
- authorId: input.authorId,
275
- })
276
- })
277
- ```
278
-
279
- ## Making Procedures Callable
280
-
281
- Use `.callable()` to make procedures directly invocable:
282
-
283
- ```ts
284
- const greet = effectOs
285
- .input(z.object({ name: z.string() }))
286
- .effect(function* ({ input }) {
287
- return `Hello, ${input.name}!`
288
- })
289
- .callable()
290
-
291
- // Can be called directly as a function
292
- const result = await greet({ name: 'World' })
293
- // => "Hello, World!"
294
-
295
- // Still a valid procedure for routers
296
- const router = os.router({ greet })
297
- ```
298
-
299
- ## Server Actions Support
300
-
301
- Use `.actionable()` for framework server actions (Next.js, etc.):
129
+ ## Error Handling
302
130
 
303
- ```tsx
304
- const createTodo = effectOs
305
- .input(z.object({ title: z.string() }))
306
- .effect(function* ({ input }) {
307
- const todoService = yield* TodoService
308
- return yield* todoService.create(input.title)
309
- })
310
- .actionable({ context: async () => ({ user: await getSession() }) })
311
-
312
- // Use in React Server Components
313
- export async function TodoForm() {
314
- return (
315
- <form action={createTodo}>
316
- <input name="title" />
317
- <button type="submit">Add Todo</button>
318
- </form>
319
- )
320
- }
321
- ```
131
+ `ORPCTaggedError` lets you create Effect-native error classes that integrate seamlessly with oRPC. These errors:
322
132
 
323
- ## 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
324
136
 
325
- 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.
326
138
 
327
139
  ```ts
328
140
  const getUser = effectOs
141
+ // Mixed error maps
329
142
  .errors({
143
+ // Regular oRPC error
330
144
  NOT_FOUND: {
331
- message: 'User not found',
332
- data: z.object({ id: z.string() })
145
+ message: "User not found",
146
+ data: z.object({ id: z.string() }),
333
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
334
156
  })
335
- .input(z.object({ id: z.string() }))
336
157
  .effect(function* ({ input, errors }) {
337
- const userService = yield* UserService
338
- const user = yield* userService.findById(input.id)
339
-
158
+ const user = yield* UsersRepo.findById(input.id);
340
159
  if (!user) {
341
- // Use oRPC's type-safe errors
342
- throw errors.NOT_FOUND({ id: input.id })
160
+ return yield* new UserNotFoundError();
161
+ // or return `yield* Effect.fail(errors.USER_NOT_FOUND())`
343
162
  }
344
-
345
- return user
346
- })
163
+ return user;
164
+ });
347
165
  ```
348
166
 
349
- ## Tagged Errors
350
-
351
- `ORPCTaggedError` lets you create Effect-native error classes that integrate seamlessly with oRPC. These errors:
352
-
353
- - Can be yielded in Effect generators (`yield* new MyError()`)
354
- - Have all ORPCError properties (code, status, data, defined)
355
- - Can be used in `.errors()` maps for type-safe error handling
356
- - Automatically convert to ORPCError when thrown
357
-
358
167
  ### Creating Tagged Errors
359
168
 
360
169
  ```ts
361
- import { ORPCTaggedError } from 'effect-orpc'
170
+ import { ORPCTaggedError } from "effect-orpc";
362
171
 
363
172
  // Basic tagged error - code defaults to 'USER_NOT_FOUND' (CONSTANT_CASE of tag)
364
- class UserNotFound extends ORPCTaggedError<UserNotFound>()('UserNotFound') {}
173
+ class UserNotFound extends ORPCTaggedError()("UserNotFound") {}
365
174
 
366
175
  // With explicit code
367
- class NotFound extends ORPCTaggedError<NotFound>()('NotFound', 'NOT_FOUND') {}
176
+ class NotFound extends ORPCTaggedError()("NotFound", "NOT_FOUND") {}
368
177
 
369
178
  // With default options (code defaults to 'VALIDATION_ERROR') (CONSTANT_CASE of tag)
370
- class ValidationError extends ORPCTaggedError<ValidationError>()(
371
- 'ValidationError',
372
- { status: 400, message: 'Validation failed' }
373
- ) {}
179
+ class ValidationError extends ORPCTaggedError()("ValidationError", {
180
+ status: 400,
181
+ message: "Validation failed",
182
+ }) {}
374
183
 
375
184
  // With explicit code and options
376
- class Forbidden extends ORPCTaggedError<Forbidden>()(
377
- 'Forbidden',
378
- 'FORBIDDEN',
379
- { status: 403, message: 'Access denied' }
380
- ) {}
381
-
382
- // With typed data
383
- class UserNotFoundWithData extends ORPCTaggedError<
384
- UserNotFoundWithData,
385
- { userId: string }
386
- >()('UserNotFoundWithData') {}
387
- ```
388
-
389
- ### Using Tagged Errors in Procedures
390
-
391
- Tagged errors can be yielded directly in Effect generators:
392
-
393
- ```ts
394
- class UserNotFound extends ORPCTaggedError<
395
- UserNotFound,
396
- { userId: string }
397
- >()('UserNotFound', { status: 404, message: 'User not found' })
398
-
399
- const getUser = effectOs
400
- .input(z.object({ id: z.string() }))
401
- .effect(function* ({ input }) {
402
- const userService = yield* UserService
403
- const user = yield* userService.findById(input.id)
404
-
405
- if (!user) {
406
- // Yield the error - it will be converted to ORPCError automatically
407
- return yield* new UserNotFound({ data: { userId: input.id } })
408
- }
409
-
410
- return user
411
- })
412
- ```
413
-
414
- ### Using Tagged Errors in Error Maps
415
-
416
- Tagged error classes can be passed directly to `.errors()`:
417
-
418
- ```ts
419
- class UserNotFound extends ORPCTaggedError<
420
- UserNotFound,
421
- { userId: string }
422
- >()('UserNotFound', 'NOT_FOUND', { status: 404, message: 'User not found' })
423
-
424
- class InvalidInput extends ORPCTaggedError<
425
- InvalidInput,
426
- { field: string }
427
- >()('InvalidInput', 'BAD_REQUEST', { status: 400 })
428
-
429
- const getUser = effectOs
430
- .errors({
431
- // Tagged error class - use the class directly
432
- // The only difference is that the code is defined by the constant version of the tag
433
- // Or when defined explicitely like in the Forbidden tagged error above
434
- UserNotFound,
435
- INVALID_INPUT: InvalidInput,
436
- // Traditional format still works, and can be colocated
437
- INTERNAL_ERROR: { status: 500, message: 'Something went wrong' },
438
- })
439
- .input(z.object({ id: z.string() }))
440
- .effect(function* ({ input, errors }) {
441
- if (!input.id) {
442
- // errors.BAD_REQUEST is the InvalidInput class
443
- return yield* new errors.INVALID_INPUT({ data: { field: 'id' } })
444
- }
445
-
446
- const userService = yield* UserService
447
- const user = yield* userService.findById(input.id)
448
-
449
- if (!user) {
450
- // errors.UserNotFound is the UserNotFound class
451
- // with the code USER_NOT_FOUND (defined at the class level)
452
- return yield* new errors.UserNotFound({ data: { userId: input.id } })
453
- }
454
-
455
- return user
456
- })
457
- ```
458
-
459
- ### Converting Tagged Errors
460
-
461
- Use `toORPCError` to convert a tagged error to a plain ORPCError:
462
-
463
- ```ts
464
- import { toORPCError, ORPCTaggedError } from 'effect-orpc'
465
- import { Effect } from 'effect'
466
-
467
- class MyError extends ORPCTaggedError<MyError>()('MyError', 'BAD_REQUEST') {}
468
-
469
- const procedure = effectOs.effect(function* () {
470
- const result = yield* someOperation.pipe(
471
- Effect.catchTag('MyError', (e) =>
472
- // Convert to plain ORPCError if needed
473
- Effect.fail(toORPCError(e))
474
- )
475
- )
476
- return result
477
- })
478
- ```
479
-
480
- ### Tagged Error Properties
481
-
482
- Tagged errors have all the properties of ORPCError plus Effect integration:
483
-
484
- ```ts
485
- const error = new UserNotFound({
486
- data: { userId: '123' },
487
- message: 'Custom message', // Override default message
488
- cause: originalError, // Attach cause for debugging
489
- })
490
-
491
- error._tag // 'UserNotFound' - for Effect's catchTag
492
- error.code // 'USER_NOT_FOUND' - ORPCError code
493
- error.status // 404 - HTTP status
494
- error.data // { userId: '123' } - typed data
495
- error.message // 'Custom message'
496
- error.defined // true - whether error is defined in error map
497
-
498
- // Convert to plain ORPCError
499
- const orpcError = error.toORPCError()
500
-
501
- // Serialize to JSON
502
- const json = error.toJSON()
503
- // { _tag: 'UserNotFound', code: 'USER_NOT_FOUND', status: 404, ... }
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") {}
504
194
  ```
505
195
 
506
- ## Generator Syntax
507
-
508
- Pass a generator function directly to `.effect()` — no need to wrap it with `Effect.fn()` or `Effect.gen()`:
509
-
510
- ```ts
511
- // Recommended: Pass generator function directly
512
- const procedureWithGen = effectOs
513
- .input(z.object({ id: z.string() }))
514
- .effect(function* ({ input }) {
515
- const service = yield* MyService
516
- return yield* service.doSomething(input.id)
517
- })
518
-
519
- // Simple procedures without yield*
520
- const simpleProcedure = effectOs
521
- .input(z.object({ name: z.string() }))
522
- .effect(function* ({ input }) {
523
- return `Hello, ${input.name}!`
524
- })
525
- ```
526
-
527
- The handler receives `{ context, input, path, procedure, signal, lastEventId, errors }` as its argument, giving you full access to the oRPC procedure context.
528
-
529
196
  ## Traceable Spans
530
197
 
531
198
  All Effect procedures are automatically traced with `Effect.withSpan`. By default, the span name is the procedure path (e.g., `users.getUser`):
532
199
 
533
200
  ```ts
534
201
  // Router structure determines span names automatically
535
- const router = os.router({
536
- users: os.router({
202
+ const router = {
203
+ users: {
537
204
  // Span name: "users.get"
538
- get: effectOs
539
- .input(z.object({ id: z.string() }))
540
- .effect(function* ({ input }) {
541
- const userService = yield* UserService
542
- return yield* userService.findById(input.id)
543
- }),
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
+ }),
544
211
  // Span name: "users.create"
545
- create: effectOs
546
- .input(z.object({ name: z.string() }))
547
- .effect(function* ({ input }) {
548
- const userService = yield* UserService
549
- return yield* userService.create(input.name)
550
- }),
551
- })
552
- })
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
+ };
553
220
  ```
554
221
 
555
222
  Use `.traced()` to override the default span name:
@@ -557,11 +224,11 @@ Use `.traced()` to override the default span name:
557
224
  ```ts
558
225
  const getUser = effectOs
559
226
  .input(z.object({ id: z.string() }))
560
- .traced('custom.span.name') // Override the default path-based name
227
+ .traced("custom.span.name") // Override the default path-based name
561
228
  .effect(function* ({ input }) {
562
- const userService = yield* UserService
563
- return yield* userService.findById(input.id)
564
- })
229
+ const userService = yield* UserService;
230
+ return yield* userService.findById(input.id);
231
+ });
565
232
  ```
566
233
 
567
234
  ### Enabling OpenTelemetry
@@ -569,22 +236,21 @@ const getUser = effectOs
569
236
  To enable tracing, include the OpenTelemetry layer in your runtime:
570
237
 
571
238
  ```ts
572
- import { NodeSdk } from '@effect/opentelemetry'
573
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
574
- import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
575
-
576
- const TracingLive = NodeSdk.layer(Effect.sync(() => ({
577
- resource: { serviceName: 'my-service' },
578
- spanProcessor: [new SimpleSpanProcessor(new OTLPTraceExporter())]
579
- })))
580
-
581
- const AppLive = Layer.mergeAll(
582
- UserServiceLive,
583
- TracingLive
584
- )
585
-
586
- const runtime = ManagedRuntime.make(AppLive)
587
- 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);
588
254
  ```
589
255
 
590
256
  ### Error Stack Traces
@@ -610,26 +276,36 @@ Returns an `EffectBuilder` instance.
610
276
 
611
277
  ```ts
612
278
  // With default builder
613
- const effectOs = makeEffectORPC(runtime)
279
+ const effectOs = makeEffectORPC(runtime);
614
280
 
615
281
  // With customized builder
616
- const effectAuthedOs = makeEffectORPC(runtime, authedBuilder)
282
+ const effectAuthedOs = makeEffectORPC(runtime, authedBuilder);
617
283
  ```
618
284
 
619
285
  ### `EffectBuilder`
620
286
 
621
287
  Wraps an oRPC Builder with Effect support. Available methods:
622
288
 
623
- | Method | Description |
624
- | ------------------ | ------------------------------------------------------------------------------- |
625
- | `.errors(map)` | Add type-safe custom errors |
626
- | `.meta(meta)` | Set procedure metadata |
627
- | `.route(route)` | Configure OpenAPI route |
628
- | `.input(schema)` | Define input validation schema |
629
- | `.output(schema)` | Define output validation schema |
630
- | `.use(middleware)` | Add middleware |
631
- | `.traced(name)` | Add a traceable span for telemetry (optional, defaults to the procedure's path) |
632
- | `.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 |
633
309
 
634
310
  ### `EffectDecoratedProcedure`
635
311
 
@@ -638,60 +314,22 @@ The result of calling `.effect()`. Extends standard oRPC `DecoratedProcedure` wi
638
314
  | Method | Description |
639
315
  | ----------------------- | --------------------------------------------- |
640
316
  | `.errors(map)` | Add more custom errors |
641
- | `.meta(meta)` | Update metadata |
642
- | `.route(route)` | Update route configuration |
317
+ | `.meta(meta)` | Update metadata (merged with existing) |
318
+ | `.route(route)` | Update route configuration (merged) |
643
319
  | `.use(middleware)` | Add middleware |
644
320
  | `.callable(options?)` | Make procedure directly invocable |
645
321
  | `.actionable(options?)` | Make procedure compatible with server actions |
646
322
 
647
- ### `ORPCTaggedError<Self, TData>()(tag, codeOrOptions?, defaultOptions?)`
323
+ ### `ORPCTaggedError(schema?)(tag, codeOrOptions?, defaultOptions?)`
648
324
 
649
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`).
650
327
 
651
- - `Self` - The class type itself (for proper typing)
652
- - `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() })`)
653
329
  - `tag` - Unique tag for discriminated unions (used by Effect's `catchTag`)
654
330
  - `codeOrOptions` - Either an ORPCErrorCode string or `{ status?, message? }` options
655
331
  - `defaultOptions` - Default `{ status?, message? }` when code is provided explicitly
656
332
 
657
- If no code is provided, it defaults to CONSTANT_CASE of the tag (e.g., `UserNotFound` → `USER_NOT_FOUND`).
658
-
659
- ```ts
660
- // Tag only - code defaults to 'MY_ERROR'
661
- class MyError extends ORPCTaggedError<MyError>()('MyError') {}
662
-
663
- // With options - code defaults to 'MY_ERROR'
664
- class MyError extends ORPCTaggedError<MyError>()(
665
- 'MyError',
666
- { status: 400, message: 'Bad request' }
667
- ) {}
668
-
669
- // With explicit code
670
- class MyError extends ORPCTaggedError<MyError>()(
671
- 'MyError',
672
- 'CUSTOM_CODE',
673
- { status: 400 }
674
- ) {}
675
-
676
- // With typed data
677
- class MyError extends ORPCTaggedError<MyError, { field: string }>()(
678
- 'MyError',
679
- 'BAD_REQUEST'
680
- ) {}
681
- ```
682
-
683
- ### `toORPCError(taggedError)`
684
-
685
- Converts an `ORPCTaggedError` instance to a plain `ORPCError`.
686
-
687
- ```ts
688
- import { toORPCError } from 'effect-orpc'
689
-
690
- const taggedError = new UserNotFound({ data: { userId: '123' } })
691
- const orpcError = toORPCError(taggedError)
692
- // => ORPCError { code: 'USER_NOT_FOUND', status: 404, data: { userId: '123' } }
693
- ```
694
-
695
333
  ## License
696
334
 
697
335
  MIT