Belajar tRPC - Schema Evolution, Versioning & Backward Compatibility
Episode 9 of 19

Belajar tRPC - Schema Evolution, Versioning & Backward Compatibility

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

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".

Prinsip Perubahan yang Aman

Menambahkan Field Selalu Aman

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:

Menambah field tanpa memutus client
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.

Memperluas Input dengan Optional Field

Saat menambah persyaratan, jadikan field baru optional agar request lama tetap valid:

Input yang backward compatible
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.

Versioning dan Deprecation Procedure

Strategi Versioning

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:

Versioning per procedure
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:

Versioning namespace
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.

Deprecation yang Sopan

Saat procedure digantikan, tandai yang lama sebagai deprecated — lewat dokumentasi atau atribut JSDoc — dan beri masa transisi:

Menandai procedure deprecated
/** @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.

Migrasi Schema dan Menjaga Kompatibilitas

Memisahkan Versi Schema

Ketika bentuk output harus berubah drastis, pertahankan procedure lama dan tambahkan yang baru — jangan mengubah resolver lama:

Migrasi dengan dua output
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.

Strategi Penghapusan yang Bertahap

Urutan aman menghapus procedure lama:

  1. Tambahkan versi baru dengan nama berbeda.
  2. Tandai yang lama @deprecated.
  3. Tunggu semua client bermigrasi.
  4. Hapus versi lama di major version berikutnya.

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.

Penutup

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:

  • Menambahkan field dan procedure baru selalu backward compatible.
  • Field baru pada input harus optional agar client lama tetap valid.
  • Versioning per procedure atau per namespace sesuai skala perubahan.
  • Tandai procedure usang dengan @deprecated dan beri masa transisi.
  • Pertahankan resolver lama; tambahkan versi baru dengan output terpisah.
  • Type-safe compiler menangkap sisa pemakaian procedure sebelum penghapusan.

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.