Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
vs oRPC / tRPC - Zagora
Skip to content

vs oRPC / tRPC

Comparison with RPC frameworks

Zagora and RPC frameworks like oRPC and tRPC have different design goals. Here's an honest comparison.

Philosophy: Zagora focuses on building low-level, composable functions for libraries, and optionally backends or APIs. oRPC and tRPC are designed primarily for API endpoints with network communication. Choose Zagora when you need type-safe functions without the network overhead.

Feature Comparison

FeatureZagoraoRPCtRPC
Sync procedure supportYes (dynamic inference)No (always async)No (always async)
Tuple/multiple argumentsYes (z.tuple([...]))No (single object)No (single object)
Network layer requiredNoOptional (server-side callable)Optional (server-side callable)
Router conceptOptional (DIY)RequiredRequired
Middleware systemNo (external/DIY)YesYes
Typed errorsYes (schema-validated)Yes (compile-time)Partial
Error payload validationYes (runtime)NoNo
StandardSchema supportYesYesPartial
OpenAPI generationDIY (see guide)Built-inPlugin
Bundle sizeMinimalModerateModerate
Primary use caseLibraries & functionsAPIs & backendsAPIs & backends

Key Differences

1. Sync vs Async

Zagora: Infers sync/async from handler - useful for performance-critical paths

// Sync procedure - no await needed
const add = zagora()
  .input(z.tuple([z.number(), z.number()]))
  .handler((_, a, b) => a + b)
  .callable();
 
const result = add(1, 2);  // Immediate result, not a Promise

oRPC/tRPC: Always async, even for trivial operations

// Must always await, even for sync logic
const result = await add.mutate({ a: 1, b: 2 });

2. Function Signature

Zagora: Natural function signatures with tuple arguments - ideal for SDKs and libraries

// Call like a normal function
getUser('alice', 25);
sendEmail('to@example.com', 'Subject', 'Body');
 
// Great for third-party consumers and SDK building

oRPC/tRPC: Single object input - works well for APIs, less natural for libraries

// Always object input
getUser({ name: 'alice', age: 25 });
sendEmail({ to: 'to@example.com', subject: 'Subject', body: 'Body' });

3. Router Requirements

Zagora: Routers are optional - export procedures directly or build your own patterns

// Export directly - it's just a function
export const createUser = zagora()
  .input(schema)
  .handler(handler)
  .callable();
 
// Consumer imports and calls
import { createUser } from 'your-library';
createUser({ name: 'Alice' });

oRPC/tRPC: Routers are required, even for server-side usage

// Router structure required
const appRouter = router({
  createUser: publicProcedure
    .input(schema)
    .mutation(handler)
});
 
// Even server-side calls go through the router

4. Error Handling

Zagora: Schema-validated typed errors with runtime validation

const proc = zagora()
  .errors({
    NOT_FOUND: z.object({ id: z.string() }),  // Schema validated at runtime!
    RATE_LIMIT: z.object({ retryAfter: z.number() }),
  })
  .handler(({ errors }) => {
    throw errors.NOT_FOUND({ id: '123' });
  });
 
// Invalid error payload = VALIDATION_ERROR
throw errors.NOT_FOUND({ wrongField: 'x' });  // Caught at runtime!

oRPC: Typed errors (compile-time), but HTTP-oriented with some quirks

// oRPC has typed errors, but they're HTTP-focused
throw new ORPCError('NOT_FOUND', { message: 'User not found' });
// Error shape is not schema-validated at runtime

tRPC: Error classes without schema validation

throw new TRPCError({
  code: 'NOT_FOUND',
  message: 'User not found',
  // No schema validation of error shape
});

5. Server-Side Usage

Both oRPC and tRPC support "server-side clients" for calling procedures without network overhead. However, they still require the router infrastructure.

Zagora: No special setup needed - procedures are just functions

// Direct call in server handlers
app.post('/users', async (c) => {
  const result = await createUser(await c.req.json());
  return result.ok ? c.json(result.data) : c.json(result.error, 400);
});

oRPC/tRPC: Server-side clients require router context

// Still need router + context setup
const caller = appRouter.createCaller(context);
const result = await caller.createUser(input);

6. OpenAPI / API Generation

oRPC: Built-in OpenAPI spec generation - excellent for public APIs

tRPC: OpenAPI available via plugin (trpc-openapi)

Zagora: DIY approach using StandardSchema - see Generating OpenAPI for patterns using toJSONSchema helpers

When to Use Each

Use Zagora When

  • Building libraries or SDKs with type-safe APIs
  • You want natural function signatures (fn(a, b) not fn({ a, b }))
  • You need sync procedures for performance-critical paths
  • You want error payloads validated at runtime, not just compile-time
  • You prefer minimal dependencies and DIY patterns
  • You're building internal tooling or utility functions

See Building Routers for HTTP integration, middleware patterns, and authenticated procedures.

Use oRPC When

  • Building client-server APIs with HTTP/WebSocket
  • You need OpenAPI spec generation out of the box
  • You want built-in router and middleware system
  • You're building a public API that needs documentation
  • Everything is already async anyway

Use tRPC When

  • Building full-stack TypeScript apps (Next.js, etc.)
  • You want tight framework integration
  • You need batching and deduplication
  • Your team is already familiar with tRPC patterns

Migration Path

From tRPC to Zagora (for library code)

Before (tRPC-style):
export const userRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return db.users.find(input.id);
    }),
});
After (Zagora):
export const getUser = zagora()
  .input(z.string())  // Simpler - just the ID
  .handler(async (_, id) => {
    return db.users.find(id);
  })
  .callable();
 
// Usage - natural function call
const result = await getUser('123');

Philosophy

Zagora's philosophy: Build low-level, composable functions for libraries. No assumptions about network, routing, or application architecture. Just type-safe functions with predictable results. When you need routers or HTTP integration, build exactly what you need.

oRPC's philosophy: Provide a complete, type-safe API solution with OpenAPI generation, middleware, and server-side calling. Best for teams building documented APIs.

tRPC's philosophy: End-to-end type safety for full-stack TypeScript apps. Best for teams using Next.js or similar frameworks where client and server share types.

Choose the tool that matches your use case. Zagora excels at library building and internal tooling; oRPC/tRPC excel at API development.