Skip to content
Oxide
Esc
navigateopen⌘Jpreview
On this page

Quickstart

Install tacho, define a few procedures, serve them, and call them from a typed client.

Install

npm install tacho
pnpm add tacho
yarn add tacho
bun add tacho

Define a router

import { const tacho: <C extends Context = {}>() => Builder<C, undefined, unique symbol>tacho } from "tacho";
import { import zz } from "zod";

const 
const rpc: Builder<{
    req: Request;
}, undefined, unique symbol>
rpc
=
tacho<{
    req: Request;
}>(): Builder<{
    req: Request;
}, undefined, unique symbol>
tacho
<{ req: Requestreq: Request }>();
export const
const router: {
    ping: ProcedureDef<{
        req: Request;
    }, undefined, "pong">;
    user: {
        get: ProcedureDef<{
            req: Request;
        }, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}
router
=
const rpc: <{
    ping: ProcedureDef<{
        req: Request;
    }, undefined, "pong">;
    user: {
        get: ProcedureDef<{
            req: Request;
        }, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}>(def: {
    ping: ProcedureDef<{
        req: Request;
    }, undefined, "pong">;
    user: {
        get: ProcedureDef<{
            req: Request;
        }, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}) => {
    ping: ProcedureDef<{
        req: Request;
    }, undefined, "pong">;
    user: {
        get: ProcedureDef<{
            req: Request;
        }, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}
rpc
({
ping: ProcedureDef<{
    req: Request;
}, undefined, "pong">
ping
:
const rpc: Builder<{
    req: Request;
}, undefined, unique symbol>
rpc
.
run: <"pong">(fn: (opts: {
    input: undefined;
    ctx: {
        req: Request;
    };
}) => "pong" | Promise<"pong">) => ProcedureDef<{
    req: Request;
}, undefined, "pong">
run
(() => "pong" as type const = "pong"const),
user: {
    get: ProcedureDef<{
        req: Request;
    }, {
        id: string;
    }, {
        id: string;
        name: string;
    }>;
}
user
: {
get: ProcedureDef<{
    req: Request;
}, {
    id: string;
}, {
    id: string;
    name: string;
}>
get
:
const rpc: Builder<{
    req: Request;
}, undefined, unique symbol>
rpc
.
input<{
    id: string;
}>(schema: Schema<{
    id: string;
}>): Builder<{
    req: Request;
}, {
    id: string;
}, unique symbol>
input
(import zz.
function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>
object
({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.
run: <{
    id: string;
    name: string;
}>(fn: (opts: {
    input: {
        id: string;
    };
    ctx: {
        req: Request;
    };
}) => {
    id: string;
    name: string;
} | Promise<{
    id: string;
    name: string;
}>) => ProcedureDef<{
    req: Request;
}, {
    id: string;
}, {
    id: string;
    name: string;
}>
run
(({
input: {
    id: string;
}
input
}) => ({ id: stringid:
input: {
    id: string;
}
input
.id: stringid, name: stringname: "Ada" })),
}, }); export type
type Router = {
    ping: ProcedureDef<{
        req: Request;
    }, undefined, "pong">;
    user: {
        get: ProcedureDef<{
            req: Request;
        }, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}
Router
= typeof
const router: {
    ping: ProcedureDef<{
        req: Request;
    }, undefined, "pong">;
    user: {
        get: ProcedureDef<{
            req: Request;
        }, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}
router
;
const const pong: "pong"pong = await
const router: {
    ping: ProcedureDef<{
        req: Request;
    }, undefined, "pong">;
    user: {
        get: ProcedureDef<{
            req: Request;
        }, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}
router
.
ping: (input?: undefined, ctx?: {
    req: Request;
} | undefined) => Promise<"pong">
ping
();
await
const router: {
    ping: ProcedureDef<{
        req: Request;
    }, undefined, "pong">;
    user: {
        get: ProcedureDef<{
            req: Request;
        }, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}
router
.
user: {
    get: ProcedureDef<{
        req: Request;
    }, {
        id: string;
    }, {
        id: string;
        name: string;
    }>;
}
user
.
get: (input?: {
    id: string;
} | undefined, ctx?: {
    req: Request;
} | undefined) => Promise<{
    id: string;
    name: string;
}>
get
({ id: stringid: "1" });

Call it with no transport: await router.ping() and await router.user.get({ id: "1" }).

Serve it

import serveserve({ fetch: (request: Request) => Promise<Response>fetch: 
handle<{
    ping: ProcedureDef<{}, undefined, "pong">;
}, {}>(router: {
    ping: ProcedureDef<{}, undefined, "pong">;
}, opts?: HandleOptions<{}> | undefined): (request: Request) => Promise<Response>
handle
(
const router: {
    ping: ProcedureDef<{}, undefined, "pong">;
}
router
), port: numberport: 3000 });

handle() is (Request) => Promise<Response>. POST only. Other methods get 405.

Call it over the wire

const 
const client: RPCClient<{
    ping: ProcedureDef<{}, undefined, "pong">;
    user: {
        get: ProcedureDef<{}, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}>
client
=
createClient<{
    ping: ProcedureDef<{}, undefined, "pong">;
    user: {
        get: ProcedureDef<{}, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}>(opts: ClientOptions): RPCClient<{
    ping: ProcedureDef<{}, undefined, "pong">;
    user: {
        get: ProcedureDef<{}, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}>
createClient
<
type Router = {
    ping: ProcedureDef<{}, undefined, "pong">;
    user: {
        get: ProcedureDef<{}, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}
Router
>({ url: stringurl: "http://localhost:3000" });
await
const client: RPCClient<{
    ping: ProcedureDef<{}, undefined, "pong">;
    user: {
        get: ProcedureDef<{}, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}>
client
.ping: (input?: undefined, opts?: CallOptions | undefined) => Promise<"pong">ping();
await
const client: RPCClient<{
    ping: ProcedureDef<{}, undefined, "pong">;
    user: {
        get: ProcedureDef<{}, {
            id: string;
        }, {
            id: string;
            name: string;
        }>;
    };
}>
client
.
user: RPCClient<{
    get: ProcedureDef<{}, {
        id: string;
    }, {
        id: string;
        name: string;
    }>;
}>
user
.
get: (input: {
    id: string;
}, opts?: CallOptions | undefined) => Promise<{
    id: string;
    name: string;
}>
get
({ id: stringid: "1" });

Common workflows

Stream

const 
const ticks: AsyncGenerator<{
    i: number;
}, void, any>
ticks
= await
const client: RPCClient<{
    ticks: ProcedureDef<{}, {
        n: number;
    }, AsyncGenerator<{
        i: number;
    }, void, unknown>>;
}>
client
.
ticks: (input: {
    n: number;
}, opts?: CallOptions | undefined) => Promise<AsyncGenerator<{
    i: number;
}, void, any>>
ticks
({ n: numbern: 3 });
for await (const
const t: {
    i: number;
}
t
of
const ticks: AsyncGenerator<{
    i: number;
}, void, any>
ticks
) var console: Consoleconsole.Console.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(
const t: {
    i: number;
}
t
.i: numberi);

Protect a procedure

import { class RpcErrorRpcError, const tacho: <C extends Context = {}>() => Builder<C, undefined, unique symbol>tacho } from "tacho";

const 
const rpc: Builder<{
    req: Request;
    user?: string;
}, undefined, unique symbol>
rpc
=
tacho<{
    req: Request;
    user?: string;
}>(): Builder<{
    req: Request;
    user?: string;
}, undefined, unique symbol>
tacho
<{ req: Requestreq: Request; user?: string | undefineduser?: string }>();
const
const protect: Builder<{
    req: Request;
    user?: string;
}, undefined, unique symbol>
protect
=
const rpc: Builder<{
    req: Request;
    user?: string;
}, undefined, unique symbol>
rpc
.
function use(mw: Middleware<{
    req: Request;
    user?: string;
}>): Builder<{
    req: Request;
    user?: string;
}, undefined, unique symbol>
use
(async ({
ctx: {
    req: Request;
    user?: string;
}
ctx
,
next: (opts?: {
    ctx: Partial<{
        req: Request;
        user?: string;
    }>;
} | undefined) => Promise<unknown>
next
}) => {
const const user: string | nulluser =
ctx: {
    req: Request;
    user?: string;
}
ctx
.req: Requestreq.Request.headers: Headers
The **`headers`** read-only property of the Request interface contains the Headers object associated with the request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers)
headers
.Headers.get(name: string): string | null
The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get)
get
("x-user");
if (!const user: string | nulluser) { throw new
new RpcError({ code, message, data }: {
    code: number;
    message: string;
    data?: unknown;
}): RpcError
RpcError
({ code: numbercode: -32001, message: stringmessage: "unauthorized" });
} return
next: (opts?: {
    ctx: Partial<{
        req: Request;
        user?: string;
    }>;
} | undefined) => Promise<unknown>
next
({
ctx: Partial<{
    req: Request;
    user?: string;
}>
ctx
: { user?: string | undefineduser } });
}); export const
const router: {
    me: ProcedureDef<{
        req: Request;
        user?: string;
    }, undefined, string | undefined>;
}
router
=
const rpc: <{
    me: ProcedureDef<{
        req: Request;
        user?: string;
    }, undefined, string | undefined>;
}>(def: {
    me: ProcedureDef<{
        req: Request;
        user?: string;
    }, undefined, string | undefined>;
}) => {
    me: ProcedureDef<{
        req: Request;
        user?: string;
    }, undefined, string | undefined>;
}
rpc
({
me: ProcedureDef<{
    req: Request;
    user?: string;
}, undefined, string | undefined>
me
:
const protect: Builder<{
    req: Request;
    user?: string;
}, undefined, unique symbol>
protect
.
run: <string | undefined>(fn: (opts: {
    input: undefined;
    ctx: {
        req: Request;
        user?: string;
    };
}) => string | Promise<string | undefined> | undefined) => ProcedureDef<{
    req: Request;
    user?: string;
}, undefined, string | undefined>
run
(({
ctx: {
    req: Request;
    user?: string;
}
ctx
}) =>
ctx: {
    req: Request;
    user?: string;
}
ctx
.user?: string | undefineduser),
});

Custom path and context

handle<{
    ping: ProcedureDef<{}, undefined, "pong">;
}, {
    user: string | undefined;
}>(router: {
    ping: ProcedureDef<{}, undefined, "pong">;
}, opts?: HandleOptions<{
    user: string | undefined;
}> | undefined): (request: Request) => Promise<Response>
handle
(
const router: {
    ping: ProcedureDef<{}, undefined, "pong">;
}
router
, {
path?: string | undefinedpath: "/rpc",
createContext?: ((req: Request) => {
    user: string | undefined;
} | Promise<{
    user: string | undefined;
}>) | undefined
createContext
: (req: Requestreq) => ({
user: string | undefineduser: req: Requestreq.Request.headers: Headers
The **`headers`** read-only property of the Request interface contains the Headers object associated with the request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers)
headers
.Headers.get(name: string): string | null
The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get)
get
("x-user") ?? var undefinedundefined,
}), });

Next steps

  • Router - tacho(), .input(), .output(), .run()
  • HTTP - handle() and createClient
  • Files - File / Blob over fetch
  • WebSocket - one socket, .ready / .close()
  • SSE streaming - async function* over fetch

Was this page helpful?