TRPC TYPESCRIPT API-DEVELOPMENT FULLSTACK REACT NEXTJS DEVELOPER-EXPERIENCE TYPE-SAFETY WEB-DEVELOPMENT BACKEND FRONTEND API

tRPC: Membangun API Type-Safe End-to-End dengan TypeScript

⏱️ 15 menit baca
👨‍💻

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:

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:

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:

  1. Type-Safety End-to-End: Ini adalah bintang utamanya. tRPC memanfaatkan kekuatan type inference TypeScript untuk secara otomatis “membawa” tipe dari backend ke frontend.
  2. Minimal Boilerplate: Tidak ada schema definition yang terpisah, tidak ada code generation yang kompleks. Anda hanya menulis fungsi di backend, dan tRPC mengurus sisanya.
  3. Developer Experience (DX) Terbaik: Dapatkan autocompletion, validasi tipe, dan refactoring yang mudah di frontend seolah-olah Anda memanggil fungsi lokal.
  4. 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:

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:

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:

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:

6. Kapan Menggunakan tRPC? (dan Kapan Tidak)

Seperti alat lainnya, tRPC memiliki kasus penggunaan terbaiknya:

Ideal untuk:

Kurang ideal untuk:

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