Use Cases (Diperbarui: 2/6/2026)

Membangun API Type-Safe dengan tRPC dan Claude Code

Bangun API type-safe dengan Claude Code dan tRPC: Next.js, Zod, izin akses, cache invalidation, dan prompt review.

Membangun API Type-Safe dengan tRPC dan Claude Code

Mengapa tRPC dan Claude Code Cocok Digabungkan

tRPC membuat router TypeScript di server menjadi kontrak API yang juga dipakai oleh client. Front-end tidak perlu menjaga file OpenAPI terpisah atau SDK manual. Cukup import type AppRouter, lalu input, output, dan error shape ikut terbaca oleh TypeScript. Untuk tim yang sering mengubah UI dan API dalam satu repository, pola ini mengurangi risiko type yang tertinggal.

Claude Code berguna bukan hanya untuk menghasilkan boilerplate. Nilainya muncul saat ia membaca struktur repository, cara autentikasi, akses data, pola React Query, dan aturan penamaan yang sudah ada. Dengan konteks itu, Claude Code bisa membantu membuat router, procedure, validasi Zod, authorization, dan cache invalidation yang konsisten. Namun type safety tidak otomatis melindungi business rule. Mutation bisa lolos compile, tetapi tetap berbahaya jika lupa memeriksa teamId atau role pengguna.

Artikel ini memakai Next.js App Router dan contoh API kecil untuk manajemen proyek. Agar mudah dicoba, penyimpanan data memakai Map di memori. Untuk production, ganti bagian itu dengan Prisma, Drizzle, Supabase, atau data layer yang sudah ada, lalu ganti demo session dengan autentikasi nyata.

Use Case yang Cocok

tRPC paling cocok saat server dan client berada dalam satu sistem TypeScript. Jika API harus dipakai pihak ketiga untuk waktu lama, OpenAPI bisa lebih tepat. Untuk admin dashboard, internal tool, form submission, dan thin BFF, tRPC biasanya memberi keuntungan besar.

Use caseManfaat tRPCTugas untuk Claude CodeRisiko
Admin CRUDInput dan output list, create, update, delete tetap tersambungRouter, Zod schema, mutation, invalidationHanya menyembunyikan tombol tanpa server authorization
Internal toolPerubahan workflow bisa cepat dikirimProcedure dari model yang sudah adaContext terlalu besar
Form submissionEmail, panjang teks, enum divalidasi saat runtimeError message dan pencegahan double submitPercaya TypeScript tanpa Zod
Thin BFFResponse sesuai kebutuhan layarMapping dari API eksternalCache dan refetch tidak jelas
flowchart LR
  UI["React component"]
  Client["tRPC React client"]
  Router["AppRouter"]
  Procedure["protected/admin procedure"]
  Zod["Zod validation"]
  Store["DB or store"]
  Review["Claude Code review"]

  UI --> Client --> Router --> Procedure --> Zod --> Store
  Router --> Review
  Review --> Procedure

Instalasi dan Struktur File

Instal package utama terlebih dahulu. Jika project sudah memakai React Query atau Zod, samakan versi sebelum meminta Claude Code menghasilkan implementasi.

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod superjson

Struktur berikut memisahkan server code, HTTP adapter, dan React provider.

src/
  app/api/trpc/[trpc]/route.ts
  app/projects/project-list.tsx
  server/trpc.ts
  server/routers/_app.ts
  server/routers/project.ts
  trpc/client.tsx

Saat memberi instruksi ke Claude Code, jelaskan batasannya: file server tidak mengimpor client component, client component tidak mengakses store langsung, dan shared type berasal dari tRPC router, bukan DTO paralel yang dipelihara manual.

Context dan Procedure

context membawa informasi per request ke setiap procedure. Jaga tetap kecil: session, team, role, dan akses database biasanya cukup. Contoh ini membaca header hanya untuk demo lokal.

// src/server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";

type Role = "admin" | "member";
type Session = { userId: string; teamId: string; role: Role };
export type Context = { session: Session | null };

export async function createContext({
  headers,
}: {
  headers: Headers;
}): Promise<Context> {
  const roleHeader = headers.get("x-user-role");
  const role: Role =
    roleHeader === "admin"
      ? "admin"
      : roleHeader === "member"
        ? "member"
        : process.env.NODE_ENV === "production"
          ? "member"
          : "admin";

  return {
    session: {
      userId: headers.get("x-user-id") ?? "demo-user",
      teamId: headers.get("x-team-id") ?? "demo-team",
      role,
    },
  };
}

const t = initTRPC.context<Context>().create({ transformer: superjson });

export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;

const requireUser = t.middleware(({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({ code: "UNAUTHORIZED", message: "Login is required." });
  }
  return next({ ctx: { session: ctx.session } });
});

export const protectedProcedure = t.procedure.use(requireUser);

export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
  if (ctx.session.role !== "admin") {
    throw new TRPCError({ code: "FORBIDDEN", message: "Admin role is required." });
  }
  return next();
});

Memisahkan protectedProcedure dan adminProcedure membuat review lebih jelas. Mutation yang menghapus data atau mengubah billing harus terlihat langsung membutuhkan role admin.

Router dengan Validasi Zod

TypeScript membantu saat build, tetapi tidak memvalidasi JSON dari browser atau script. Zod memeriksa input pada runtime sebelum business logic berjalan.

// src/server/routers/project.ts
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";

type ProjectStatus = "todo" | "doing" | "done";
type Project = {
  id: string;
  teamId: string;
  title: string;
  ownerEmail: string;
  status: ProjectStatus;
  createdAt: string;
};

const projects = new Map<string, Project>();
const projectStatus = z.enum(["todo", "doing", "done"]);

const listProjectsInput = z
  .object({
    status: projectStatus.optional(),
    query: z.string().trim().max(60).optional(),
    limit: z.number().int().min(1).max(50).default(20),
  })
  .default({ limit: 20 });

const createProjectInput = z.object({
  title: z.string().trim().min(2).max(80),
  ownerEmail: z.string().email(),
});

export const projectRouter = createTRPCRouter({
  list: protectedProcedure.input(listProjectsInput).query(({ ctx, input }) => {
    return [...projects.values()]
      .filter((project) => project.teamId === ctx.session.teamId)
      .filter((project) => !input.status || project.status === input.status)
      .filter((project) => !input.query || project.title.includes(input.query))
      .slice(0, input.limit);
  }),

  create: adminProcedure.input(createProjectInput).mutation(({ ctx, input }) => {
    const project: Project = {
      id: crypto.randomUUID(),
      teamId: ctx.session.teamId,
      title: input.title,
      ownerEmail: input.ownerEmail,
      status: "todo",
      createdAt: new Date().toISOString(),
    };
    projects.set(project.id, project);
    return project;
  }),

  updateStatus: protectedProcedure
    .input(z.object({ id: z.string().uuid(), status: projectStatus }))
    .mutation(({ ctx, input }) => {
      const project = projects.get(input.id);
      if (!project || project.teamId !== ctx.session.teamId) {
        throw new TRPCError({ code: "NOT_FOUND" });
      }
      const nextProject = { ...project, status: input.status };
      projects.set(input.id, nextProject);
      return nextProject;
    }),
});

Pemeriksaan teamId tidak boleh dilewatkan. UUID yang valid tidak berarti pengguna berhak mengubah record tersebut. Ini adalah contoh area yang tidak bisa dijamin oleh type safety saja.

Menghubungkan App Router dan React Client

Gabungkan router utama, lalu expose melalui Route Handler.

// src/server/routers/_app.ts
import { createTRPCRouter } from "../trpc";
import { projectRouter } from "./project";

export const appRouter = createTRPCRouter({
  project: projectRouter,
});

export type AppRouter = typeof appRouter;
// src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/trpc";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => createContext({ headers: req.headers }),
  });

export { handler as GET, handler as POST };
// src/trpc/client.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { useState, type ReactNode } from "react";
import superjson from "superjson";
import type { AppRouter } from "@/server/routers/_app";

export const trpc = createTRPCReact<AppRouter>();

export function TRPCProvider({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      transformer: superjson,
      links: [httpBatchLink({ url: "/api/trpc" })],
    }),
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}
// src/app/projects/project-list.tsx
"use client";

import { useState } from "react";
import { trpc } from "@/trpc/client";

export function ProjectList() {
  const utils = trpc.useUtils();
  const [title, setTitle] = useState("");
  const projects = trpc.project.list.useQuery({ limit: 20 });
  const createProject = trpc.project.create.useMutation({
    onSuccess: async () => {
      setTitle("");
      await utils.project.list.invalidate();
    },
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        createProject.mutate({ title, ownerEmail: "owner@example.com" });
      }}
    >
      <input value={title} onChange={(event) => setTitle(event.target.value)} />
      <button type="submit">Add</button>
      <pre>{JSON.stringify(projects.data, null, 2)}</pre>
    </form>
  );
}

Setelah mutation berhasil, utils.project.list.invalidate() membuat daftar dimuat ulang. Tanpa langkah ini, user bisa melihat data lama walau operasi server sudah berhasil.

Prompt Review untuk Claude Code

Review implementasi Next.js App Router + tRPC ini.
Periksa:
1. publicProcedure, protectedProcedure, dan adminProcedure dipakai dengan benar.
2. Semua input eksternal memiliki Zod runtime validation.
3. teamId dan userId dicek sebelum read/write.
4. Mutation meng-invalidate query yang terdampak.
5. Router dipisah berdasarkan domain dan tidak terlalu besar.
6. Context hanya berisi dependency per request.
7. Tidak ada field rahasia yang dikembalikan ke client.

Kembalikan severity, file, masalah, solusi, dan test yang disarankan dalam tabel.

Prompt ini cocok disimpan di CLAUDE.md atau template pull request. Untuk perubahan permission, minta Claude Code memberi diff dan analisis risiko dulu, lalu review manual sebelum diterapkan.

Kesalahan Umum

Kesalahan pertama adalah context yang terlalu gemuk. Menaruh semua dependency di ctx membuat test berat. Kedua, authorization hanya di UI. Tombol yang disembunyikan tidak melindungi endpoint. Ketiga, percaya TypeScript tanpa Zod. Request nyata harus divalidasi saat runtime. Keempat, router tidak dipisah. Satu file besar menurunkan kualitas review. Kelima, cache invalidation tidak dirancang. Satu mutation bisa memengaruhi list, detail, counter, dan dashboard sekaligus.

Referensi dan Langkah Lanjut

Gunakan dokumentasi resmi tRPC untuk routers, procedures, dan React Query integration. Untuk runtime validation, baca Zod docs. Untuk endpoint Next.js, lihat Route Handlers.

Untuk materi terkait, lanjutkan ke panduan validasi Zod dengan Claude Code dan tips TypeScript dengan Claude Code. Jika tim Anda ingin meninjau admin dashboard atau internal tool yang sudah ada, konsultasi dan pelatihan Claude Code dapat membantu merapikan router, permission, prompt review, dan test.

Hasil Setelah Dicoba

Pada prototipe admin dashboard kecil, hasil terbaik muncul saat protectedProcedure dan adminProcedure dibuat sebelum CRUD. Claude Code lebih mudah menandai mutation yang harus admin-only. Menaruh Zod schema dekat procedure juga membuat perubahan form dan API input lebih mudah direview. Sebaliknya, context yang terlalu nyaman membuat test semakin berat, jadi versi akhir mempertahankan context kecil dan memindahkan business logic ke fungsi terpisah.

#Claude Code #tRPC #TypeScript #API #type safety
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.