Episode ini membahas evolusi API tRPC yang aman: mengubah schema tanpa memutus client lama, strategi versioning dan deprecation procedure, serta teknik migrasi input dan output sambil menjaga backward compatibility.

Aplikasi hidup: schema berubah, field ditambahkan, aturan bisnis diperbarui. Masalahnya, client yang sudah rilis tidak selalu ikut diperbarui. Episode 9 membahas bagaimana mengubah API tRPC tanpa memutus client — mulai dari prinsip backward compatibility, strategi versioning, hingga deprecation yang sopan.
Ini keterampilan yang membedakan API yang bisa dirawat tahunan dari API yang terus menerus "break".
Aturan emas evolusi API: tambahan selalu kompatibel, penghapusan dan perubahan tipe tidak. Menambahkan procedure atau field baru tidak akan memutus client lama karena tipe mereka tidak berubah:
user: t.router({
list: t.procedure.query(() => users),
// tambahan baru: procedure baru tidak mengganggu yang lama
search: t.procedure
.input(z.object({ q: z.string() }))
.query(({ input }) =>
users.filter((u) => u.nama.includes(input.q)),
),
}),Client lama memakai user.list dan tetap berfungsi; client baru bisa memakai user.search. Selama kita tidak menghapus atau mengubah bentuk list, tidak ada yang pecah.
Saat menambah persyaratan, jadikan field baru optional agar request lama tetap valid:
list: t.procedure
.input(
z.object({
page: z.number().optional().default(1),
filter: z.string().optional(),
}),
)
.query(({ input }) => {
// input.page selalu ada karena default; input.filter bisa undefined
return users
.filter((u) => !input.filter || u.nama.includes(input.filter))
.slice((input.page - 1) * 10, input.page * 10);
}),Client lama yang hanya mengirim { page: 1 } tetap valid. filter yang optional tidak memaksa client lama berubah. z.number().optional().default(1) memberikan nilai default sekaligus menjaga tipe tetap number.
tRPC tidak menyediakan versioning otomatis — kalian mendesainnya. Dua pendekatan umum:
Versioning per procedure: buat prosedur versi baru dengan nama jelas saat breaking change tak terhindarkan:
user: t.router({
// versi lama: tetap ada agar client lama tidak pecah
detailV1: t.procedure.input(z.object({ id: z.number() })).query(ambilV1),
// versi baru: bentuk output berbeda
detailV2: t.procedure.input(z.object({ id: z.string() })).query(ambilV2),
}),Versioning via namespace: untuk perubahan besar, bungkus versi baru dalam router tersendiri:
export const appRouter = t.router({
v1: v1Router,
v2: v2Router,
});Pilih namespace bila perubahan menyeluruh, dan suffix per procedure bila hanya satu-dua endpoint yang berubah. Tujuannya satu: client lama berjalan sambil client baru bertransisi.
Saat procedure digantikan, tandai yang lama sebagai deprecated — lewat dokumentasi atau atribut JSDoc — dan beri masa transisi:
/** @deprecated Gunakan user.byId sebagai gantinya */
byUsername: t.procedure
.input(z.object({ username: z.string() }))
.query(({ input }) => users.find((u) => u.username === input.username)),Editor akan menampilkan coretan pada user.byUsername saat dipakai, memberi sinyal ke tim untuk pindah. Aturan praktis deprecation: beri waktu paling sedikit satu siklus rilis sebelum penghapusan, dan catat jadwalnya di changelog.
Ketika bentuk output harus berubah drastis, pertahankan procedure lama dan tambahkan yang baru — jangan mengubah resolver lama:
const UserV1 = z.object({ id: z.number(), nama: z.string() });
const UserV2 = z.object({ id: z.string(), fullName: z.string(), email: z.string() });
user: t.router({
profile: t.procedure.input(z.object({ id: z.number() })).output(UserV1).query(ambilV1),
profileV2: t.procedure.input(z.object({ id: z.number() })).output(UserV2).query(ambilV2),
}),Dengan .output(UserV1) dan .output(UserV2), kedua versi divalidasi eksplisit dan tidak bisa saling mencemari. Setelah semua client pindah, profile bisa dihapus pada major release.
Urutan aman menghapus procedure lama:
@deprecated.Sepanjang proses, perhatikan bahwa tRPC adalah type-safe: jika kalian menghapus procedure dan masih ada client yang memakainya, compiler akan langsung menangkapnya — keunggulan yang tidak dimiliki REST, di mana breaking change baru terlihat saat runtime.
Warning
Hati-hati dengan zod .transform() pada input. Transform mengubah data sebelum resolver — jika aturan transformasi berubah, client yang mengirim data lama bisa menerima hasil yang berbeda. Simpan transform di dalam procedure baru saat perilakunya berubah.
Episode 9 mengajarkan evolusi API yang disiplin: menambah tanpa memutus, versioning yang terencana, deprecation yang memberi sinyal jelas, serta migrasi schema yang menjaga kompatibilitas hingga seluruh client pindah.
Inti yang harus dibawa pulang:
@deprecated dan beri masa transisi.Di episode 10 selanjutnya kita akan membahas transport, HTTP, dan WebSocket — pilihan httpBatchLink, httpLink, dan wsLink, perbedaan batching dan standard HTTP, serta konfigurasi proxy, CORS, dan request headers.