Quickstart
Install tacho, define a few procedures, serve them, and call them from a typed client.
Install
npm install tachopnpm add tachoyarn add tachobun add tachoDefine 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[]): voidThe **`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: HeadersThe **`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 | nullThe **`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: HeadersThe **`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 | nullThe **`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()andcreateClient - Files -
File/Blobover fetch - WebSocket - one socket,
.ready/.close() - SSE streaming -
async function*over fetch