Episode ini membangun API tRPC pertama secara utuh: struktur project yang rapi, router dengan query, mutation, dan subscription, serta penerapan zod untuk validasi input dan output modern dengan schema yang aman terhadap tipe.

Cukup teori — sekarang kalian akan membangun API tRPC pertama. Episode 3 memandu langkah demi langkah: menyusun struktur project yang rapi dan bisa berkembang, mendefinisikan router dengan tiga jenis procedure, serta memakai zod untuk memvalidasi input dan output dengan cara modern.
Semua contoh di episode ini berjalan tanpa server HTTP — kita akan memakai caller untuk mengeksekusi procedure langsung. Di episode 4, API yang sama akan dilayani lewat server nyata dan dikonsumsi client.
Project tRPC biasanya dipecah berdasarkan domain. Struktur yang direkomendasikan untuk project yang akan terus tumbuh:
src/
├── server/
│ ├── trpc.ts
│ └── routers/
│ ├── index.ts
│ ├── user.ts
│ └── post.tsFile trpc.ts menjadi fondasi: di sinilah initTRPC dibuat dan di-export. Router-router domain memakai fondasi ini, lalu digabung di routers/index.ts.
import { initTRPC } from "@trpc/server";
export const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;Dengan ekspor ini, setiap file router cukup mengimpor router dan publicProcedure dari trpc.ts — satu sumber kebenaran untuk seluruh API.
Query adalah procedure untuk membaca data, setara dengan GET pada REST. Contoh router user:
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
const daftarUser = [
{ id: 1, nama: "Arman", role: "admin" },
{ id: 2, nama: "Budi", role: "user" },
];
export const userRouter = router({
list: publicProcedure.query(() => daftarUser),
byId: publicProcedure
.input(z.object({ id: z.number() }))
.query(({ input }) =>
daftarUser.find((u) => u.id === input.id),
),
});publicProcedure.query(() => ...) tanpa .input() berarti procedure tidak menerima argumen. Untuk byId, .input(z.object({ id: z.number() })) memastikan client mengirim id bertipe number.
Mutation dipakai untuk operasi yang mengubah state, setara dengan POST, PUT, atau DELETE:
export const userRouter = router({
create: publicProcedure
.input(z.object({ nama: z.string().min(1) }))
.mutation(({ input }) => {
const user = { id: daftarUser.length + 1, ...input, role: "user" };
daftarUser.push(user);
return user;
}),
});Perhatikan bahwa output mutation adalah nilai yang dikembalikan resolver — tipe-nya langsung diturunkan ke client.
Subscription memakai observable untuk mengalirkan data real-time, cocok untuk notifikasi atau feed:
import { observable } from "@trpc/server/observable";
clock: publicProcedure.subscription(() =>
observable<{ waktu: string }>((emit) => {
const timer = setInterval(() => {
emit.next({ waktu: new Date().toISOString() });
}, 1000);
return () => clearInterval(timer);
}),
),Semua router domain digabung dalam satu root router:
import { userRouter } from "./user";
export const appRouter = router({
user: userRouter,
});
export type AppRouter = typeof appRouter;Selain input, zod juga bisa memvalidasi output. Ini penting agar data yang bocor ke client sesuai kontrak, misalnya menyembunyikan field sensitif:
import { z } from "zod";
const UserPublic = z.object({
id: z.number(),
nama: z.string(),
});
export const userRouter = router({
profile: publicProcedure
.input(z.object({ id: z.number() }))
.output(UserPublic)
.query(({ input }) => {
const user = daftarUser.find((u) => u.id === input.id);
if (!user) throw new Error("User tidak ditemukan");
return { id: user.id, nama: user.nama, role: user.role };
}),
});Resolver di atas mengembalikan objek dengan field role, tetapi karena .output(UserPublic) menetapkan kontrak hanya id dan nama, tRPC akan memvalidasi dan menolak response yang tidak sesuai schema. Error handling yang layak akan kita bahas di episode 5.
Tanpa server HTTP, kita bisa langsung menguji seluruh router lewat caller:
import { createCallerFactory } from "@trpc/server";
import { appRouter } from "./routers";
const createCaller = createCallerFactory(appRouter);
const caller = createCaller({});
const user = await caller.user.byId({ id: 1 });
const created = await caller.user.create({ nama: "Citra" });
console.log(user, created);Panggilan caller.user.byId({ id: 1 }) mengeksekusi procedure lengkap dengan validasi input dan output, tanpa melibatkan network. Jika kalian mengirim { id: "satu" }, zod akan menolaknya sebelum resolver berjalan.
Episode 3 mengajarkan kalian membangun API tRPC pertama: struktur project yang terorganisir, router dengan query, mutation, dan subscription, validasi input dan output dengan zod, serta pengujian cepat lewat caller.
Inti yang harus dibawa pulang:
trpc.ts dari router domain agar project rapi..input() memvalidasi argumen; .output() memvalidasi response.AppRouter.Di episode 4 selanjutnya kalian akan menjalankan server tRPC dan mengkonsumsi API dari client — menyiapkan server dengan Express atau Next.js App Router, membuat client tRPC di frontend, dan melihat autocompletion type-safe pada request pertama.