Skip to content

Instantly share code, notes, and snippets.

@AlexanderHott
Created November 9, 2024 17:19
Show Gist options
  • Save AlexanderHott/f0621a24289269bcd2cab8ba217e5c1d to your computer and use it in GitHub Desktop.
Save AlexanderHott/f0621a24289269bcd2cab8ba217e5c1d to your computer and use it in GitHub Desktop.
A tiny trpc.io client from https://trpc.io/blog/tinyrpc-client
import { z } from "zod";
import type {
AnyProcedure,
inferProcedureInput,
inferProcedureOutput,
AnyQueryProcedure,
AnyMutationProcedure,
ProcedureRouterRecord,
AnyRouter,
} from "@trpc/server";
import { initTRPC, TRPCError } from "@trpc/server";
import type { TRPCResponse } from "@trpc/server/rpc";
type Post = { id: string; title: string };
const posts: Post[] = [];
const t = initTRPC.create({});
const router = t.router;
const publicProcedure = t.procedure;
function uuid() {
return "uuid";
}
const appRouter = router({
post: router({
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
const post = posts.find((p) => p.id === input.id);
if (!post) throw new TRPCError({ code: "NOT_FOUND" });
return post;
}),
byTitle: publicProcedure
.input(z.object({ title: z.string() }))
.query(({ input }) => {
const post = posts.find((p) => p.title === input.title);
if (!post) throw new TRPCError({ code: "NOT_FOUND" });
return post;
}),
create: publicProcedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => {
const post = { id: uuid(), ...input };
posts.push(post);
return post;
}),
}),
});
// ===========
// Tiny Client
// ===========
type Resolver<TProcedure extends AnyProcedure> = (
input: inferProcedureInput<TProcedure>,
) => Promise<inferProcedureOutput<TProcedure>>;
type DecorateProcedure<TProcedure> = TProcedure extends AnyQueryProcedure
? {
query: Resolver<TProcedure>;
}
: TProcedure extends AnyMutationProcedure
? {
mutate: Resolver<TProcedure>;
}
: never;
type AppRouter = typeof appRouter;
type PostById = Resolver<AppRouter["post"]["byId"]>;
type DecoratedProcedureRecord<TProcedures extends ProcedureRouterRecord> = {
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<TProcedures[TKey]["_def"]["record"]>
: TProcedures[TKey] extends AnyProcedure
? DecorateProcedure<TProcedures[TKey]>
: never;
};
type ProxyCallbackOptions = {
path: readonly string[];
args: readonly unknown[];
};
type ProxyCallback = (opts: ProxyCallbackOptions) => unknown;
function createRecursiveProxy(
callback: ProxyCallback,
path: readonly string[],
) {
const proxy: unknown = new Proxy(() => {}, {
get(_obj, key) {
if (typeof key !== "string") return undefined;
return createRecursiveProxy(callback, [...path, key]);
},
apply(_1, _2, args) {
return callback({ path, args });
},
});
return proxy;
}
export const createTinyTRPCClient = <TRouter extends AnyRouter>(
baseUrl: string,
) =>
createRecursiveProxy(async (opts) => {
const path = [...opts.path];
const method = path.pop()! as "query" | "mutate";
const dotPath = path.join(".");
let uri = `${baseUrl}/${dotPath}`;
let [input] = opts.args;
const stringifiedInput = input !== undefined && JSON.stringify(input);
let body: string | undefined = undefined;
if (stringifiedInput !== false) {
if (method === "query") {
uri += `?input=${encodeURIComponent(stringifiedInput)}`;
} else {
body = stringifiedInput;
}
}
const json: TRPCResponse = await fetch(uri, {
method: method === "query" ? "GET" : "POST",
headers: { "Content-Type": "application/json" },
body,
}).then((res) => res.json());
if ("error" in json) {
throw new Error(`Error: ${json.error.message}`);
}
return json.result.data;
}, []) as DecoratedProcedureRecord<TRouter["_def"]["record"]>;
const client = createTinyTRPCClient<AppRouter>("https://api.example.com");
const post1 = await client.post.byId.query({ id: "123" });
const post2 = await client.post.byTitle.query({ title: "Hello world" });
const newPost = await client.post.create.mutate({ title: "Foo" });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment