tRPC: Membangun API Type-Safe End-to-End dengan TypeScript
1. Pendahuluan
Pernahkah Anda merasa frustrasi saat mengembangkan aplikasi fullstack dengan TypeScript? Di satu sisi, Anda menikmati keamanan dan kejelasan tipe di backend maupun frontend. Namun, di sisi lain, seringkali ada “jurang” yang memisahkan keduanya, terutama saat berinteraksi dengan API. Anda harus mendefinisikan tipe data untuk request dan response di backend, lalu mendefinisikannya lagi (atau meng-generate-nya) di frontend. Perubahan kecil di backend bisa berarti Anda harus mengupdate tipe di frontend secara manual, yang rawan kesalahan dan memakan waktu. 😫
Masalah ini umum terjadi:
- Boilerplate Tipe: Mendefinisikan ulang tipe untuk payload API di kedua sisi.
- Mismatched Types: Tipe di frontend tidak sinkron dengan backend, menyebabkan runtime error yang sulit dideteksi.
- Kurangnya Developer Experience (DX): Tidak ada autocompletion atau validasi tipe yang mulus saat memanggil API dari frontend.
Di sinilah tRPC (Type-safe Remote Procedure Call) hadir sebagai solusi elegan. tRPC adalah framework yang memungkinkan Anda membangun API yang sepenuhnya type-safe dari backend hingga frontend, tanpa perlu code generation atau schema definition terpisah. Bayangkan menulis logika backend Anda, dan secara otomatis, frontend Anda mendapatkan autocompletion dan validasi tipe yang akurat untuk setiap panggilan API. ✨ Ini bukan sihir, ini tRPC!
Artikel ini akan membawa Anda menyelami tRPC, mengapa ia begitu menarik, dan bagaimana Anda bisa mulai membangun API yang lebih menyenangkan dan bebas bug dengan TypeScript.
2. Apa itu tRPC dan Mengapa Kita Membutuhkannya?
Sebelum masuk ke tRPC, mari kita sedikit kilas balik tentang bagaimana kita biasanya membangun API:
- REST API: Populer, berbasis HTTP, menggunakan resource dan verb (GET, POST, PUT, DELETE). Membutuhkan definisi endpoint dan payload yang seringkali tidak memiliki tipe yang kuat secara native.
- GraphQL: Memberi klien kemampuan untuk meminta data spesifik yang mereka butuhkan. Membutuhkan schema definition (SDL) dan code generation untuk mendapatkan type-safety di klien.
- gRPC: Menggunakan protocol buffers dan HTTP/2 untuk komunikasi berkinerja tinggi. Juga memerlukan definisi schema terpisah dan code generation.
Semua pendekatan di atas memiliki kelebihannya masing-masing. Namun, untuk proyek fullstack yang didominasi TypeScript, terutama di lingkungan monorepo atau proyek di mana backend dan frontend dikembangkan oleh tim yang sama, tRPC menawarkan pengalaman yang jauh lebih mulus.
🎯 Fokus Utama tRPC:
- Type-Safety End-to-End: Ini adalah bintang utamanya. tRPC memanfaatkan kekuatan type inference TypeScript untuk secara otomatis “membawa” tipe dari backend ke frontend.
- Minimal Boilerplate: Tidak ada schema definition yang terpisah, tidak ada code generation yang kompleks. Anda hanya menulis fungsi di backend, dan tRPC mengurus sisanya.
- Developer Experience (DX) Terbaik: Dapatkan autocompletion, validasi tipe, dan refactoring yang mudah di frontend seolah-olah Anda memanggil fungsi lokal.
- Fleksibilitas: tRPC dapat diintegrasikan dengan framework backend Node.js apa pun (Express, Fastify, Koa, dll.) dan sangat cocok dengan Next.js API Routes. Di frontend, ia berpasangan sempurna dengan React Query (TanStack Query).
Bagaimana tRPC mencapai ini? Ini adalah kombinasi dari beberapa hal:
- TypeScript Inference: Ketika Anda mendefinisikan router tRPC di backend, TypeScript dapat menginferensi semua tipe input dan output.
- Monorepo Magic (atau Shared Types): Dalam setup monorepo, atau jika Anda membagikan tipe dari backend ke frontend, frontend tRPC client dapat “melihat” tipe dari backend router Anda dan memberikan type-safety yang lengkap.
Ini berarti, jika Anda mengubah payload di backend, TypeScript akan langsung memberi tahu Anda di frontend jika ada sesuatu yang tidak cocok, bahkan sebelum Anda menjalankan kode! 💡
3. Memulai dengan tRPC: Setup Sederhana
Mari kita lihat bagaimana setup tRPC bekerja. Kita akan menggunakan contoh Next.js karena integrasinya sangat mulus, tetapi konsepnya bisa diterapkan di backend Node.js lainnya.
📌 Prasyarat:
- Proyek Next.js (atau React + Node.js backend terpisah).
- Pengetahuan dasar TypeScript.
3.1 Setup Backend (Next.js API Routes)
Pertama, kita akan menginstal dependensi yang diperlukan:
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
# atau
yarn add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
Sekarang, mari kita buat struktur file untuk tRPC backend kita.
src/server/trpc.ts: Ini adalah inti dari tRPC backend kita. Di sini kita akan menginisialisasi tRPC dan mendefinisikan context dan middleware dasar.
// src/server/trpc.ts
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';
import superjson from 'superjson'; // Untuk serialisasi data yang lebih baik
/**
* Konteks yang akan tersedia di setiap prosedur tRPC Anda.
* Di sini Anda bisa menambahkan koneksi database, sesi pengguna, dll.
* Untuk contoh ini, kita akan membuatnya sederhana.
*/
export const createContext = () => ({}); // Kosong untuk contoh sederhana
const t = initTRPC.context<typeof createContext>().create({
transformer: superjson, // Menggunakan superjson untuk menangani Date, Map, Set, dll.
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
/**
* Export reusable router dan procedure helpers.
* Ini memungkinkan Anda untuk membuat router dan prosedur tRPC yang type-safe.
*/
export const router = t.router;
export const publicProcedure = t.procedure;
src/server/routers/_app.ts: Ini adalah root router Anda, tempat semua sub-router (misalnya, router untuk pengguna, produk, auth) akan digabungkan.
// src/server/routers/_app.ts
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
// Contoh router untuk pengguna
const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
// Di sini Anda akan mengambil data pengguna dari database
return { id: input.id, name: `User ${input.id}`, email: `user${input.id}@example.com` };
}),
create: publicProcedure
.input(z.object({ name: z.string().min(3), email: z.string().email() }))
.mutation(({ input }) => {
// Di sini Anda akan menyimpan pengguna baru ke database
console.log('Creating user:', input);
return { id: `user-${Date.now()}`, ...input };
}),
});
// Root router
export const appRouter = router({
user: userRouter, // Gabungkan user router
// Anda bisa menambahkan router lain di sini, misal: product: productRouter
});
// Export tipe dari appRouter untuk digunakan di frontend
export type AppRouter = typeof appRouter;
src/pages/api/trpc/[trpc].ts: Ini adalah endpoint API Next.js yang akan menangani semua request tRPC.
// src/pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';
import { createContext } from '../../../server/trpc';
// Export API handler
export default createNextApiHandler({
router: appRouter,
createContext,
onError({ error, type, path, input, ctx, req }) {
console.error('tRPC Error:', error);
if (error.code === 'INTERNAL_SERVER_ERROR') {
// Kirim error ke layanan pelaporan error Anda
// Sentry.captureException(error);
}
},
});
✅ Dengan ini, backend tRPC Anda sudah siap!
3.2 Setup Frontend (Next.js)
Sekarang, kita akan menghubungkan frontend kita ke backend tRPC yang baru kita buat.
src/utils/trpc.ts: Ini adalah client tRPC yang akan digunakan di frontend.
// src/utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers/_app'; // Import tipe dari backend
import superjson from 'superjson';
export const trpc = createTRPCReact<AppRouter>();
export function getBaseUrl() {
if (typeof window !== 'undefined') return ''; // Browser request
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // Vercel
return `http://localhost:${process.env.PORT ?? 3000}`; // Development
}
export const trpcClient = trpc.createClient({
transformer: superjson,
links: [
trpc.httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
});
src/pages/_app.tsx: Kita perlu membungkus aplikasi kita dengan QueryClientProvider dari React Query dan trpc.Provider dari tRPC.
// src/pages/_app.tsx
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { trpcClient, trpc } from '../utils/trpc';
function MyApp({ Component, pageProps }: any) {
const [queryClient] = useState(() => new QueryClient());
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</trpc.Provider>
);
}
export default MyApp;
🎉 Sekarang frontend Anda siap untuk berkomunikasi dengan backend tRPC dengan type-safety penuh!
4. Membangun Prosedur (Queries & Mutations) yang Type-Safe
Mari kita lihat bagaimana kita menggunakan client tRPC di frontend untuk memanggil query dan mutation yang telah kita definisikan.
Misalnya, di komponen React Anda (src/pages/index.tsx):
// src/pages/index.tsx
import React, { useState } from 'react';
import { trpc } from '../utils/trpc';
export default function Home() {
// 1. Mengambil data pengguna dengan Query
const { data: user, isLoading: isUserLoading, error: userError } = trpc.user.getById.useQuery({ id: '123' });
// 2. Membuat pengguna baru dengan Mutation
const createUserMutation = trpc.user.create.useMutation();
const [newName, setNewName] = useState('');
const [newEmail, setNewEmail] = useState('');
const handleCreateUser = async () => {
try {
// ✅ Perhatikan autocompletion dan type-safety untuk input!
const newUser = await createUserMutation.mutateAsync({
name: newName,
email: newEmail,
});
alert(`User created: ${newUser.name} (${newUser.id})`);
setNewName('');
setNewEmail('');
} catch (error: any) {
console.error('Failed to create user:', error);
alert(`Error creating user: ${error.message || 'Unknown error'}`);
}
};
if (isUserLoading) return <p>Loading user...</p>;
if (userError) return <p>Error loading user: {userError.message}</p>;
return (
<div>
<h1>tRPC Demo</h1>
<h2>Current User</h2>
{user ? (
<div>
<p>ID: {user.id}</p>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
) : (
<p>No user found.</p>
)}
<h2>Create New User</h2>
<div>
<input
type="text"
placeholder="Name"
value={newName}
onChange={(e) => setNewName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
/>
<button onClick={handleCreateUser}>Create User</button>
</div>
{createUserMutation.isLoading && <p>Creating user...</p>}
{createUserMutation.isError && (
<p style={{ color: 'red' }}>Error: {createUserMutation.error?.message}</p>
)}
{createUserMutation.isSuccess && (
<p style={{ color: 'green' }}>User created successfully!</p>
)}
</div>
);
}
Coba perhatikan beberapa hal penting di sini:
trpc.user.getById.useQuery({ id: '123' }): Ini adalah panggilan query. Perhatikan bagaimana kita bisa mengaksesuserrouter, lalu proceduregetById. TypeScript akan secara otomatis menyarankanidsebagai input yang diperlukan dan menginferensi tipe dari responseuser.trpc.user.create.useMutation(): Ini adalah panggilan mutation. Saat Anda memanggilmutateAsync, TypeScript akan memandu Anda untuk menyediakan object dengannamedanemailyang sesuai dengan skema Zod yang Anda definisikan di backend.- Autocompletion & Type Checking: Jika Anda mencoba memberikan input yang salah (misalnya,
id: 123alih-alihid: '123') atau mencoba mengakses properti yang tidak ada di objectuser(misalnya,user.addressjika tidak ada), TypeScript akan langsung memberi tahu Anda! 🚀
Ini adalah inti dari tRPC: pengalaman pengembangan yang mulus dan bebas bug karena type-safety yang konsisten di seluruh stack.
5. Fitur Unggulan tRPC untuk Developer Produktif
Selain type-safety end-to-end, tRPC juga membawa beberapa fitur dan keunggulan lain yang meningkatkan produktivitas developer:
-
⚡ Automatic Type Inference: Ini adalah super power tRPC. Anda tidak perlu lagi menulis atau meng-generate file tipe untuk API Anda. Cukup definisikan router tRPC di backend, dan frontend Anda akan secara ajaib mendapatkan semua tipe yang diperlukan. Ini mengurangi boilerplate dan menjaga konsistensi tipe secara otomatis.
-
✅ Zod for Input Validation: tRPC terintegrasi erat dengan Zod, sebuah pustaka validasi skema yang type-safe. Anda mendefinisikan skema validasi untuk input procedure Anda menggunakan Zod, dan tRPC akan secara otomatis memvalidasi request yang masuk. Jika validasi gagal, tRPC akan mengembalikan error yang jelas ke frontend, lengkap dengan detail validasi. Ini memastikan data yang masuk ke logika bisnis Anda selalu valid.
// Contoh validasi dengan Zod di backend const postRouter = router({ create: publicProcedure .input(z.object({ title: z.string().min(5, 'Judul minimal 5 karakter'), content: z.string().optional(), tags: z.array(z.string()).max(5, 'Maksimal 5 tag'), })) .mutation(({ input }) => { /* ... */ }), });Di frontend, error validasi ini dapat diakses melalui object
errordari React Query, memungkinkan Anda menampilkan pesan error yang spesifik kepada pengguna. -
🤝 Integrasi Mulus dengan React Query (TanStack Query): tRPC tidak memiliki mekanisme caching atau state management data sendiri. Sebaliknya, ia berpasangan dengan pustaka yang sudah mapan seperti React Query. Ini adalah keputusan desain yang cerdas karena Anda mendapatkan semua fitur canggih React Query secara gratis:
- Caching otomatis
- Refetching di background
- Loading dan error states yang mudah diatur
- Optimistic updates
- Pagination dan infinite scroll
- Devtools yang hebat Ini berarti Anda tidak perlu mempelajari pustaka data fetching baru; Anda hanya perlu mengintegrasikan tRPC dengan apa yang sudah Anda kenal dan sukai.
-
📦 Minimal Bundle Size: Karena tRPC memanfaatkan type inference dan tidak memerlukan code generation yang berat, ukuran bundle yang dikirim ke browser sangat minimal. Hanya kode client tRPC itu sendiri dan tipe yang diperlukan yang dikirim, bukan seluruh backend router Anda. Ini berkontribusi pada waktu loading yang lebih cepat untuk aplikasi Anda.
-
🚨 Penanganan Error yang Terstruktur: tRPC menyediakan cara yang konsisten untuk menangani error dari backend. Error yang dilemparkan di backend (termasuk error validasi Zod) akan di-serialize dan dikirim ke frontend dalam format yang terstruktur, membuatnya lebih mudah untuk di-debug dan ditampilkan kepada pengguna.
6. Kapan Menggunakan tRPC? (dan Kapan Tidak)
Seperti alat lainnya, tRPC memiliki kasus penggunaan terbaiknya:
✅ Ideal untuk:
- Proyek Fullstack TypeScript: Ini adalah sweet spot tRPC. Jika Anda membangun aplikasi di mana frontend dan backend sama-sama menggunakan TypeScript, tRPC akan sangat meningkatkan DX Anda.
- Monorepo: Dalam monorepo di mana backend dan frontend berbagi basis kode, tRPC bersinar karena type inference dapat bekerja secara langsung tanpa perlu proses build atau sync tipe yang rumit.
- Aplikasi Internal atau Aplikasi Pribadi: Untuk aplikasi yang tidak perlu mengekspos API publik yang terdokumentasi dengan baik (seperti OpenAPI/Swagger), tRPC adalah pilihan yang fantastis karena fokusnya pada DX developer internal.
- Prototyping Cepat: Kemampuan untuk dengan cepat membangun API yang berfungsi dengan type-safety penuh membuat tRPC sangat cocok untuk prototyping.
- Tim Kecil hingga Menengah: Di mana komunikasi antar tim frontend dan backend (jika terpisah) sangat erat, tRPC dapat mengurangi gesekan integrasi.
❌ Kurang ideal untuk:
- Public API: tRPC tidak memiliki standar schema definition yang universal seperti OpenAPI (untuk REST) atau SDL (untuk GraphQL). Ini membuatnya tidak cocok jika Anda perlu mengekspos API ke klien eksternal yang tidak menggunakan tRPC atau TypeScript.
- Integrasi Multi-Bahasa: Jika backend Anda ditulis dalam bahasa selain TypeScript (misalnya, Python, Go, Java) atau jika Anda memiliki klien mobile native yang tidak menggunakan TypeScript, tRPC tidak akan memberikan manfaat type-safety end-to-end yang sama.
- Proyek yang Membutuhkan Fleksibilitas Klien yang Ekstrem: Untuk kasus di mana Anda membutuhkan klien untuk secara dinamis membuat query yang sangat kompleks (seperti di beberapa kasus GraphQL), tRPC mungkin terasa lebih terbatas karena sifatnya yang berbasis procedure.
Singkatnya, jika Anda seorang developer JavaScript/TypeScript yang membangun aplikasi fullstack dan mencari cara untuk menghilangkan rasa sakit dari integrasi API sambil memaksimalkan type-safety dan DX, maka tRPC patut Anda coba!
Kesimpulan
tRPC adalah game-changer bagi developer TypeScript fullstack. Dengan menghilangkan kebutuhan akan schema generation dan boilerplate tipe, tRPC memungkinkan Anda untuk fokus pada logika bisnis, bukan pada masalah sinkronisasi tipe. Pengalaman developer yang mulus, autocompletion yang akurat, dan validasi input yang kuat mengubah proses pengembangan API dari tugas yang membosankan menjadi pengalaman yang menyenangkan dan produktif.
Jika Anda lelah dengan runtime error akibat type mismatch antara frontend dan backend, atau hanya ingin mempercepat alur kerja pengembangan Anda, berikan kesempatan pada tRPC. Anda mungkin akan terkejut betapa menyenangkannya membangun API dengan type-safety end-to-end. Selamat mencoba! 🚀
🔗 Baca Juga
- Memilih Strategi CSS-in-JS yang Tepat: Dari Runtime hingga Compile-Time
- Menguasai Prettier dan ESLint: Otomatisasi Kualitas Kode untuk Developer JavaScript/TypeScript
- React Server Components (RSC): Revolusi Rendering di Aplikasi React Modern
- Optimasi Data Fetching di Frontend: Menggali Lebih Dalam React Query (TanStack Query) dan SWR