vs oRPC / tRPC
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
| Feature | Zagora | oRPC | tRPC |
|---|---|---|---|
| Sync procedure support | Yes (dynamic inference) | No (always async) | No (always async) |
| Tuple/multiple arguments | Yes (z.tuple([...])) | No (single object) | No (single object) |
| Network layer required | No | Optional (server-side callable) | Optional (server-side callable) |
| Router concept | Optional (DIY) | Required | Required |
| Middleware system | No (external/DIY) | Yes | Yes |
| Typed errors | Yes (schema-validated) | Yes (compile-time) | Partial |
| Error payload validation | Yes (runtime) | No | No |
| StandardSchema support | Yes | Yes | Partial |
| OpenAPI generation | DIY (see guide) | Built-in | Plugin |
| Bundle size | Minimal | Moderate | Moderate |
| Primary use case | Libraries & functions | APIs & backends | APIs & 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 PromiseoRPC/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 buildingoRPC/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 router4. 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 runtimetRPC: 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)notfn({ 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);
}),
});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.