Build type-safe, error-safe functions in TypeScript with full inference, typed errors, and zero async overhead...
Zagora creates type-safe, error-safe functions that never throw. It provides full TypeScript inference across inputs, outputs, errors, and context. Unlike RPC frameworks, Zagora produces pure functions - no network layer, just supercharged TypeScript functions.
Use Zagora to build:
Avoid Zagora for:
Zagora achieves 100% test coverage, ensuring every aspect of the library is rigorously tested for reliability and correctness. Complementing this, it includes dedicated type tests that utilize expectType to verify TypeScript types at compile time. Together, these provide robust guarantees that both the compile-time and runtime systems match, delivering confidence of another level.
Zagora is lightweight with zero dependencies and bloat, built entirely on StandardSchema for universal validation. This means you can use Zod, Valibot, ArkType, or any compliant validator. No lock-in, just the tools you already know and love.
Every function returns a predictable { ok, data, error } result -- exceptions are eliminated completely. Your process never crashes from unhandled errors, similar to Effect.ts or neverthrow. This gives you total control and deterministic error handling across your entire codebase.
Define error schemas upfront and get strongly-typed error helpers inside your handlers. Each error kind is validated at runtime and fully typed at compile-time. You'll never see try/catch blocks or guess error shapes again.
Complete TypeScript inference across inputs, outputs, errors, context, defaults, and optionals. Even JavaScript consumers get full autocomplete and IntelliSense support. The type system has been battle-tested with dedicated type-level tests.
Define multiple function arguments using schema tuples with per-argument validation and defaults. Call your functions naturally like procedure('Alice', 25) instead of procedure({ name: 'Alice', age: 25 }). This creates a familiar API that feels like native TypeScript functions.
Zagora supports compile-time reporting for each argument through TypeScript in IDEs and CLIs, catching potential errors before runtime. This diagnostic capability operates at every level, from schema validation to handler invocation, to context, to environment variables. Developers receive immediate, precise feedback on argument mismatches, improving code reliability and productivity.
Zagora dynamically infers whether procedures are sync or async based on handler and schema behavior. Sync handlers return Result, async handlers return Promise<Result> -- no forced async everywhere. This is impossible with oRPC/tRPC where everything is always async.
Add memoization to any procedure with a simple cache adapter. Cache keys include input, schemas, and handler body for intelligent invalidation. Works with both sync and async cache implementations seamlessly.
Zagora produces regular TypeScript functions -- no special clients, routers, or network glue required. Export your procedures directly and call them like any other function. Perfect for building type-safe libraries, SDKs, and internal tooling.
Validate environment variables with the same schema system used for inputs and outputs. Get type-safe access to process.env or import.meta.env inside handlers. Coercion, defaults, and optionals work exactly as expected.
Unlike neverthrow or similar libraries, you directly access result.data or result.error. No .unwrap(), .map(), or monadic chains needed. The discriminated union type guides you naturally with TypeScript's narrowing.
import { z } from 'zod';
import { zagora } from 'zagora';
const greeting = zagora()
.input(z.tuple([z.string(), z.number().default(18), z.string().optional()]))
.handler((_, name, age, country) => {
// name: string
// age: number <-- because there is a default value in schema!
// country: string | undefined <-- because it's marked as optional in schema!
return `${name} is ${age}, from ${country || 'unknown'}`
})
.callable();
// NOTE: examples below omit the { ok, data, error } wrapper for brevity.
greeting('John', 30);
// => John is 30
// @ts-expect-error -- reported at compile-time AND runtime, invalid second argument
greeting('John', 'foo');
// @ts-expect-error -- reported at compile-time AND runtime, missing required argument
greeting();
// NOTE: fine, because second and third arguments are optional (default or optional)
greeting('Barry') // => Barry is 18, from unknown
greeting('Barry', 25) // => Barry is 25, from unknown
greeting('Barry', 33, 'USA') // => Barry is 33, from USA
const result = greeting('Alice');
if (result.ok) {
console.log(result.data); // "Alice is 18, from unknown"
} else {
console.error(result.error.kind);
console.error(result.error);
// ^ { kind: 'UNKNOWN_ERROR', message, cause }
// or
// ^ { kind: 'VALIDATION_ERROR', message, issues: Schema.Issue[] }
}
Zagora works with any StandardSchema-compliant validator like Zod, Valibot, or ArkType.
bun add zagora
Choose a StandardSchema-compliant validation library:
bun add zod
# or
bun add valibot
# or
bun add arktype
Create a simple procedure to verify everything works:
import { z } from 'zod';
import { zagora } from 'zagora';
const greet = zagora()
.input(z.string())
.handler((_, name) => `Hello, ${name}!`)
.callable();
const result = greet('World');
console.log(result);
// { ok: true, data: 'Hello, World!', error: undefined }
Zagora is written in TypeScript and provides full type inference out of the box. No additional configuration is required, but ensure your tsconfig.json has strict mode enabled for the best experience:
{
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler"
}
}
NOTE: Auto‑callable skips .callable() but also removes per‑call (at callsite) configuration of context, env, and cache. This means you would need to use the methods at definition time (eg. where you write zagora()), like zagora().env(schema).context(schema).cache(cacheAdapter).
.callable() step for a smaller & cleaner API (use only when you don't need per-call config for context, env, and cache)