Belajar tRPC - Input Validation, Error Handling & Response Types
Episode 5 of 19

Belajar tRPC - Input Validation, Error Handling & Response Types

Episode ini membedah kualitas API tRPC: validasi input dengan zod dan superstruct, penanganan error dengan TRPCError beserta formatError dan pemetaan HTTP status, serta pengelolaan response type inference dan fallback default values.

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

Pendahuluan

API yang hanya bisa "jalan" belum tentu aman. Episode 5 membawa kalian ke kualitas production: validasi input yang ketat, error handling yang terstruktur, dan response types yang terpelihara. Tiga hal inilah yang membedakan API siap pakai dari prototipe.

Kalian akan belajar memvalidasi input dengan zod dan superstruct, memakai TRPCError untuk menandai kegagalan, memetakan error ke HTTP status yang tepat, dan mengelola tipe response beserta nilai default-nya.

Validasi Input dengan zod dan superstruct

Memperdalam Schema zod

zod sudah dipakai sejak episode 3. Sekarang perkuat schema-nya untuk skenario nyata:

Schema input yang kompleks
import { z } from "zod";
 
const RegisterInput = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  umur: z.number().int().min(17).optional(),
  alamat: z.object({
    kota: z.string().min(1),
    kodePos: z.string().regex(/^\d{5}$/),
  }),
});
 
register: publicProcedure
  .input(RegisterInput)
  .mutation(({ input }) => {
    // input.email dan input.alamat.kota sudah bertipe aman
    return { sukses: true, email: input.email };
  }),

Dengan schema di atas, client yang mengirim email tidak valid, password pendek, atau kode pos bukan lima digit akan ditolak sebelum resolver berjalan. Tipe input di resolver otomatis mengikuti schema — tidak ada duplikasi antara runtime dan tipe.

Alternatif superstruct

Jika project sudah memakai superstruct, tRPC tetap mendukungnya. Satu-satunya syarat: objek schema harus memenuhi kontrak yang mirip zod, biasanya dengan membungkus validasi custom lewat fungsi kecil. Prinsipnya tetap sama: validasi berjalan di sisi server dan menghasilkan tipe yang bisa diinferensikan. Untuk project baru, zod adalah pilihan paling umum karena integrasinya langsung dan DSL-nya ringkas.

Error Handling dengan TRPCError

Mengapa Error Biasa Tidak Cukup

Melempar new Error() akan menghasilkan status HTTP 500 dan pesan yang tidak terstruktur. tRPC menyediakan TRPCError yang membawa code — kode ini dipetakan ke HTTP status secara otomatis:

Memakai TRPCError
import { TRPCError } from "@trpc/server";
 
byId: publicProcedure
  .input(z.object({ id: z.number() }))
  .query(({ input }) => {
    const user = daftarUser.find((u) => u.id === input.id);
    if (!user) {
      throw new TRPCError({
        code: "NOT_FOUND",
        message: `User dengan id ${input.id} tidak ditemukan`,
      });
    }
    return user;
  }),

throw new TRPCError({ code: "NOT_FOUND", message }) mengubah respons menjadi status HTTP 404 dengan bentuk error JSON-RPC yang konsisten.

Pemetaan HTTP Status

Beberapa code yang sering dipakai dan status HTTP-nya:

  • BAD_REQUEST → 400, untuk input yang tidak valid.
  • UNAUTHORIZED → 401, untuk akses tanpa autentikasi.
  • FORBIDDEN → 403, untuk akses yang dilarang.
  • NOT_FOUND → 404, untuk resource yang tidak ada.
  • CONFLICT → 409, untuk duplikat data.
  • TOO_MANY_REQUESTS → 429, untuk rate limiting.
  • INTERNAL_SERVER_ERROR → 500, untuk kegagalan tak terduga.

Menggunakan code yang tepat membuat client bisa bereaksi sesuai status tanpa parsing pesan.

formatError dan Response Error yang Terstruktur

Menyusun Bentuk Error Global

Error di tRPC berformat JSON-RPC. Kalian bisa memformat ulang lewat formatError agar semua error memakai bentuk yang konsisten, misalnya menambahkan timestamp atau menyembunyikan detail internal:

formatError di router
import { initTRPC, TRPCError } from "@trpc/server";
 
const t = initTRPC.create({
  formatError: ({ error, shape }) => {
    return {
      ...shape,
      message: error.message,
      data: {
        ...shape.data,
        timestamp: new Date().toISOString(),
      },
    };
  },
});

formatError({ error, shape }) menerima error asli dan bentuk default, lalu mengembalikan bentuk baru. Di sini kita menambahkan timestamp pada setiap error yang keluar. Detail di shape.data juga bisa diisi dengan kode internal untuk memudahkan debugging.

Jangan Bocorkan Detail Internal

Di production, pesan error dari database jangan ditampilkan mentah-mentah. Pola umum: di formatError, sembunyikan error.cause dan tampilkan pesan generik untuk kode internal. Episode 12 akan membahas sisi keamanan ini lebih dalam.

Response Type Inference dan Fallback Default Values

Tipe Response yang Konsisten

Tipe response selalu diturunkan dari nilai yang dikembalikan resolver. Jika ada banyak bentuk response, gunakan union type agar client siap menangani semua kemungkinan:

Union response
import { z } from "zod";
 
cari: publicProcedure
  .input(z.object({ kata: z.string() }))
  .query(({ input }) => {
    const hasil = daftarUser.filter((u) =>
      u.nama.toLowerCase().includes(input.kata.toLowerCase()),
    );
    return {
      total: hasil.length,
      users: hasil,
    };
  }),

Client tahu bahwa total bertipe number dan users adalah array user — tanpa dokumentasi terpisah.

Fallback Default Values

Tidak semua response harus mengembalikan data penuh. Definisikan nilai default agar konsumen tidak crash saat data belum tersedia:

Fallback default value
stats: publicProcedure.query(() => {
  const total = daftarUser.length;
  return {
    totalUsers: total,
    lastSync: total > 0 ? "2026-08-10T00:00:00Z" : null,
  };
});

Di sisi client, nilai seperti lastSync yang bisa null ditangani dengan aman oleh tipe — compiler memaksa kalian memeriksa null sebelum memakainya. Di React Query, pola fallback lain adalah memberi default di hook:

Default value di client
const { data = [] } = trpc.user.list.useQuery();

trpc.user.list.useQuery() dengan default = [] membuat komponen tetap aman sebelum data tiba.

Tip

Kombinasikan validasi output dari episode 3 dengan formatError: input yang ketat mencegah data rusuk masuk, output yang divalidasi mencegah data rusuk keluar, dan error yang terformat membuat client selalu mendapat bentuk yang bisa diprediksi.

Penutup

Episode 5 membuat API kalian tangguh: input divalidasi di sisi server, kegagalan ditandai dengan TRPCError yang terpetakan ke HTTP status yang benar, bentuk error diseragamkan lewat formatError, dan response types serta fallback values terjaga di kedua sisi.

Inti yang harus dibawa pulang:

  • Validasi input dengan zod; superstruct juga didukung dengan adaptasi.
  • TRPCError membawa code yang dipetakan ke HTTP status otomatis.
  • Gunakan code yang tepat: BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND.
  • formatError menyeragamkan bentuk error global dan bisa menambah metadata.
  • Tipe response diturunkan dari resolver; gunakan schema output untuk kontrak ketat.
  • Beri fallback default values di client agar komponen aman saat data kosong.

Di episode 6 selanjutnya kita akan membahas middleware, links & lifecycle interception — middleware untuk autentikasi, logging, tracing, dan rate limit; loggerLink, httpBatchLink, wsLink, dan custom link; serta lifecycle hooks onError, onSuccess, dan onSettled di integrasi React Query.

Belajar tRPC - Input Validation, Error Handling & Response Types | Belajar tRPC