Episode ini menguatkan pertahanan API tRPC: proteksi terhadap injection, overfetch, dan payload abuse, rate limiting di level router maupun adapter, serta best practice TLS, CSP, dan sanitasi data input.

Keamanan bukan fitur terakhir — ia lapisan yang menyatu dengan desain. Episode 12 membahas ancaman yang nyata menimpa API tRPC dan cara menangkisnya: injection, overfetch, dan payload abuse, lalu rate limiting, dan ditutup best practice seperti TLS, CSP, serta sanitasi input.
Karena tRPC mengekspos procedure langsung, permukaan serangannya berbeda dari REST. Memahami titik lemah ini membuat kalian bisa menutupnya sejak awal.
Ancaman pertama adalah data yang diinterpretasikan sebagai perintah — misalnya string yang masuk ke query database. Zod membatasi bentuk data, tetapi sanitasi tetap tanggung jawab resolver:
import { z } from "zod";
cari: t.procedure
.input(z.object({ q: z.string().min(1).max(100) }))
.query(({ input }) => {
// gunakan parameterized query, jangan interpolasi mentah
const escaped = input.q.replace(/[\s'"\\]/g, " ");
return users.filter((u) => u.nama.toLowerCase().includes(escaped.toLowerCase()));
}),Aturan dasarnya: validasi bentuk dengan zod (tipe, panjang, format), lalu query ke database memakai parameterized statement atau ORM. Jangan pernah menyusun query dengan string yang disisipkan mentah — ini mencegah SQL/NoSQL injection.
Overfetch: client meminta lebih banyak data dari yang dibutuhkan. Karena output procedure ditentukan resolver, hindari mengembalikan objek database penuh jika client hanya butuh beberapa field — gunakan validasi output dari episode 3 dan proyeksikan field yang diperlukan.
Underfetch adalah kebalikannya: banyak round-trip kecil. Gabungkan data terkait dalam satu procedure agar client tidak memanggil berulang kali:
userWithPosts: t.procedure
.input(z.object({ id: z.number() }))
.query(({ input }) => {
const user = users.find((u) => u.id === input.id);
const posts = posts.filter((p) => p.authorId === input.id);
return { id: user.id, nama: user.nama, postCount: posts.length };
}),Procedure userWithPosts mengembalikan hanya field yang dibutuhkan client — satu request, tidak berlebihan.
Input yang sangat besar bisa menghabiskan memori dan CPU. Batasi dengan zod (panjang, ukuran) dan batasi ukuran body di level server:
create: t.procedure
.input(
z.object({
judul: z.string().min(3).max(200),
konten: z.string().min(1).max(10_000),
tags: z.array(z.string()).max(10),
}),
)
.mutation(({ input }) => simpanPost(input)),Di Express, batasi body parser:
app.use(express.json({ limit: "1mb" }));express.json({ limit: "1mb" }) menolak request dengan body lebih dari 1 megabyte sebelum masuk pipeline tRPC.
Rate limiting membatasi frekuensi akses agar API tidak dibanjiri. Buat middleware sederhana dengan penyimpanan dalam memori:
const rateLimit = t.middleware(({ ctx, next, path }) => {
const key = `${ctx.user?.id ?? ctx.req?.ip}:${path}`;
const now = Date.now();
const hits = counters.get(key) ?? [];
const recent = hits.filter((t) => now - t < 60_000);
if (recent.length >= 30) {
throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
}
recent.push(now);
counters.set(key, recent);
return next();
});
export const limitedProcedure = t.procedure.use(rateLimit);TRPCError({ code: "TOO_MANY_REQUESTS" }) memetakan ke HTTP 429. Counter dalam memori cukup untuk satu instance, tetapi untuk produksi multi-instance gunakan penyimpanan bersama seperti Redis.
Rate limiting juga bisa dipasang di lapisan HTTP sebelum tRPC — misalnya middleware Express atau layanan edge seperti Cloudflare. Keunggulannya: limit berlaku untuk seluruh path tanpa menyentuh kode tRPC, dan berjalan sangat dekat dengan user.
Pilih sesuai kebutuhan: middleware tRPC memberi kontrol per-user dan per-procedure; adapter/edge memberi proteksi global dengan performa terbaik. Kombinasi keduanya adalah pendekatan production yang umum.
Semua traffic production harus lewat HTTPS. Di balik proxy, pastikan TLS di-terminate di edge dan header keamanan ditambahkan:
app.use((req, res, next) => {
res.setHeader("Strict-Transport-Security", "max-age=63072000");
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Referrer-Policy", "no-referrer");
next();
});Strict-Transport-Security memaksa browser selalu memakai HTTPS. Header lain mencegah jenis serangan berbasis MIME sniffing dan kebocoran referrer.
Content-Security-Policy membatasi sumber konten yang boleh dimuat halaman — menangkis XSS. Pastikan connect-src mengizinkan endpoint tRPC dan WebSocket:
Content-Security-Policy: default-src 'self'; connect-src 'self' wss://api.contoh.com;Perhatikan wss://api.contoh.com di connect-src — tanpa ini, koneksi WebSocket dari frontend ke server akan diblokir browser.
Keamanan terbaik adalah berlapis: validasi schema zod menolak input yang salah bentuk, sanitasi membersihkan data sebelum masuk penyimpanan, dan otorisasi membatasi siapa yang bisa memicu prosedur. Jangan pernah menggantungkan keamanan hanya pada satu lapisan.
Warning
Rate limit berbasis memori tidak berbagi state antar instance server. Di deployment dengan banyak instance, gunakan Redis atau layanan edge agar limit tetap konsisten di seluruh instance.
Episode 12 memperkuat pertahanan API tRPC: menangkis injection lewat validasi dan sanitasi, menghindari overfetch dan underfetch dengan proyeksi serta penggabungan data, membatasi payload abuse, dan menegakkan rate limiting di dua level plus best practice TLS dan CSP.
Inti yang harus dibawa pulang:
connect-src yang mencakup WebSocket.Di episode 13 selanjutnya kita akan membahas performansi & optimasi tRPC — mengoptimalkan batch request dan mengurangi round-trips, memakai dehydrate dan hydration di SSR untuk meminimalisasi fetch ulang, serta optimasi server seperti caching, data loader, dan lazy routers.