Zagora
A minimalist & robust way to create type-safe and error-safe never throwing functions & libraries in TypeScript. No batteries, no routers, just functions.
Install / Use
/learn @tunnckoCore/ZagoraREADME
Zagora
<!-- COV_BADGE:START -->
<!-- COV_BADGE:END -->
Elevate your TypeScript workflow with Zagora: a sleek, bulletproof toolkit for forging type-safe, error-proof functions and libraries that never throw. Powered by StandardSchema-compliant validators like Zod, Valibot, and Arktype, it delivers rock-solid input/output validation and richly typed errors. No routers, no network baggage — just pure, exportable functions ready to supercharge your code. The ultimate streamlined alternative to oRPC and tRPC, stripping away the network layer for unmatched type-safety, simplicity and robustness.
Highlights
- 🪶 Minimal: Lightweight and focused, built on StandardSchema for seamless validation.
- 🛡️ Error-Safe: Eliminates exceptions - always
{ ok, data, error }for predictable, crash-free execution. - 🦢 Graceful: Functions never throw or disrupt your process, akin to
effect.tsandneverthrow. - 📝 Typed Errors: Define error schemas for strongly-typed error helpers, enhancing handler reliability.
- 🧹 Clean Error Model: Three distinct error types - unknown, validation, and user-defined—for clarity.
- 🔒 Type-Safe: Full type inference across inputs, outputs, errors, context, optionals, and defaults.
- ✋ Ergonomic: Pure functions with auto-filled defaults, optional args, and detailed diagnostics.
- 🏠 Familiar: Echoes remote-RPC patterns from oRPC and tRPC, but focused on libraries, not apps.
- ⚖️ Unopinionated: Zero assumptions - no routers, middlewares, or network concepts.
- 🎁 No Unwrapping: Direct access to results, unlike
neverthrow- no extra steps required. - 🎁 EnvVars Handling: Handling and validation of environment variables.
- 🤖 Agents Ready: Rules for LLMs with subtle nuances and where to be careful. Read/get here
This library is product of 3+ months of dedication and passion, after 10 years in Open Source.<br> It's the best library I've ever done (i have 300+).<br> It's the best TypeScript library i've ever wrote (i love it).<br> It's the most complex TypeScript I've ever wrote.<br> It's the most TypeScript I've ever learned.<br> I went all-in on TypeScript just this year - the experience is unparalleled.<br>
Table of Contents
<!--### Highlights - **Minimal:** Tiny surface, powered by StandardSchema (Zod, Valibot, Arktype) - **Error-safety:** Nothing ever throws, you always get `{ ok, data, error }` - **Gracefulness:** Functions will never throw or crash your process, similar to `effect.ts` and `neverthrow` - **Typed-Errors:** Defined error schemas give you error helpers later used from inside handlers - **Clean Separation:** There is only 3 kinds of errors - unknown/internal, validation, and user defined - **Type-safety:** Full type inference everywhere, including for optionals and defaults - **Ergonomics:** Just pure functions, default filling, optional trailing args, per-argument diagnostics - **Familiar:** Familiar to remote-RPC frameworks like oRPC and tRPC - **Unopinionated:** No assumptions, no opinions, no routers, no middlewares, no network glue - **No unwrap:** Unlike `neverthrow` and other similar libaries, you don't need to unwrap anything-->Install
This is ESM-only package with built-in types.
bun install zagora@next
Usage
import { z } from 'zod';
import { zagora } from 'zagora';
const za = zagora();
const getUser = za
.input(z.tuple([
z.string(),
z.number().default(18),
]))
.output(z.object({ name: z.string(), age: z.number(), email: z.string() }))
.handler(async (_, name, age) => {
// name: string;
// age: number; -- even if not passed!
return { name, age, email: `${name.toLowerCase()}@example.com` };
})
.callable();
const result = await getUser('Charlie');
if (result.ok) {
console.log(result.data);
// ^ { name: 'Charlie', age: 18, email: 'charlie@example.com' }
} else {
console.error(result.error);
// ^ { kind: 'UNKNOWN_ERROR', message, cause }
// or
// ^ { kind: 'VALIDATION_ERROR', message, issues: Schema.Issue[] }
}
// primitive input
const helloUppercased = za
.input(z.string())
.handler((_, str) => str.toUpperCase())
.callable();
const res = helloUppercased('Hello world');
if (res.ok) {
console.log(res);
// ^ { ok: true, data: 'HELLO WORLD', error: undefined }
}
// array input
const uppercase = zagora({ autoCallable: true, disableOptions: true })
.input(z.array(z.string()))
.handler((arrayOfStrings) => {
// NOTE: `x` is typed as string too!
return arrayOfStrings.map((x) => x.toUpperCase());
})
const upRes = uppercase(['foo', 'bar', 'qux']);
if (upRes.ok) {
console.log(upRes);
// ^ { ok: true, data: ['FOO', 'BAR', 'QUX' ] }
}
You'll also have access to all the types, utils, and error-related stuff through package exports.
import {
isValidationError,
isInternalError,
isDefinedError,
isZagoraError,
} from 'zagora/errors';
import * as ZagoraTypes from 'zagora/types';
import * as zagoraUtils from 'zagora/utils';
Why zagora?
Motivation
While orpc is great and you can use it for direct function calls (and not network requests with createRouterClient), and for example for building "type-safe SDK"s, it does have a few opinions that may get in the way. I use it extensively in my projects, but zagora is smaller and even more focused approach - i always wanted "just functions" where you define input, outputs, and you get error-safe, typed function back not a wrapper around it.
Both tRPC and oRPC are promoted as "backend", or specifically for when you're building "apps". Recently, all major frameworks also introduced similar concepts, like "server actions" and so on. All that is cool, but zagora is focused on building just functions, a low-level library for building other libraries - I have a lot of them, so i need a simple way for building type-safe and error-safe functions, where i don't necessarily need network layer and i don't need "routers" concept, and etc.
They are built around the network, Zagora is built around functions with excellent ergonomics, developer experience, and no assumptions. It produces just functions, regular TypeScript functions, I cannot stress that enough.
Why Zagora over oRPC/tRPC/neverthrow/effect.ts?
- Zagora is focused on producing "just functions", not networks, routers, or groups.
- oRPC and tRPC does not "support" creating synchornous functions, they are always async
- in contrast, Zagora does not use
async/awaitanywhere in the codebase, butinstanceof Promisechecks - the return type of Zagora procedures is dynamically inferred based on many factors
- return type is NOT a union like
ZagoraResult | Promise<ZagoraResult>which gives amazing DX
- in contrast, Zagora does not use
- oRPC/tRPC cannot create procedures that look like regular functions, they always accept a single object
- that's important if you want to create a basic function with multiple input arguments
- of course, with oRPC/tRPC you can just pass them as object, but that's not always wanted effect for end-users of libraries
- Zagora allows you to use schema tuples (
z.tuple([z.string(), z.number().default(10)])to define multiple arguments - Zagora is lower-level, focused on building libraries, but can be used to build groups and routers.
- groups/routers could be just
const router = { users: { get: getUserProcedure } }and everything remains type-safe - for more complex stuff, or if type performance is reached, we can explore further
- groups/routers could be just
- Zagora does support injecting typed/runtime "context" to procedures, if/when needed.
- The whole error system across Zagora is build around typed error objects, never Errors.
- meaning, even if your handler fail with syntax error - you'll get ZagoraResult with error in it
- it also gives absolute guarantees for never crashing the process, total predictability and type-safety
- error determinism - if there's ANY error at ANY level - you'll get
result.error
- With Zagora, unlike
neverthrow, you don't need any kind of "unwrapping" nor need to jump into too much functional programming - you always have either the "ok result" or the "error result". - With Zagora, unlike
Effect.ts, you don't need to learn a whole other mindset or kind of a language- please, just check out
ReScript Langbefore
- please, just check out
