Eksplorasi komprehensif tentang tRPC, asal-usulnya, konsep inti, dan implementasi praktis dengan NestJS. Pelajari kapan tRPC memberikan developer experience superior dibanding REST, GraphQL, dan gRPC.

Dalam lanskap protokol API, tRPC merepresentasikan pergeseran paradigma. Sementara REST, GraphQL, dan gRPC masing-masing menyelesaikan masalah spesifik, tRPC mengajukan pertanyaan berbeda: bagaimana jika frontend dan backend Anda berbagi codebase TypeScript yang sama? Bagaimana jika Anda bisa menghilangkan dokumentasi API, mengurangi boilerplate, dan menangkap error pada compile time alih-alih runtime?
tRPC (TypeScript Remote Procedure Call) bukan hanya protokol API lainnya—ini adalah filosofi yang memanfaatkan sistem tipe TypeScript untuk menciptakan type safety end-to-end antara client dan server. Tanpa code generation, tanpa file schema, tanpa sinkronisasi tipe manual. Hanya inference TypeScript murni.
Deep dive ini mengeksplorasi arsitektur tRPC, membandingkannya dengan protokol yang sudah mapan, dan mendemonstrasikan implementasi production-grade menggunakan NestJS. Jika Anda pernah frustrasi dengan dokumentasi REST API yang drift atau maintenance schema GraphQL, tRPC menawarkan alternatif yang menarik.
tRPC muncul pada tahun 2020 dari frustrasi developer full-stack TypeScript. Diciptakan oleh Alex "KATT" Johansson, ia lahir dari observasi sederhana: ketika client dan server sama-sama menggunakan TypeScript, mengapa kita memerlukan definisi schema terpisah, code generators, atau sinkronisasi tipe manual?
Pengembangan API tradisional melibatkan beberapa pain points:
REST APIs: Anda mendefinisikan endpoints, menulis spec OpenAPI (mungkin), generate tipe TypeScript (semoga), dan berdoa agar tetap tersinkronisasi. Ketika backend berubah, frontend rusak saat runtime.
GraphQL: Anda menulis definisi schema, setup resolvers, generate tipe TypeScript dari schemas, dan memelihara dua sumber kebenaran. Lebih baik dari REST, tetapi masih pekerjaan manual.
gRPC: Anda mendefinisikan schema Protocol Buffer, generate code untuk multiple bahasa, dan berurusan dengan tooling yang kompleks. Bagus untuk microservices, overkill untuk monorepos.
tRPC menghilangkan masalah ini dengan menggunakan TypeScript sebagai kontrak. Procedures backend Anda adalah kontrak API Anda. Tipe mengalir secara otomatis dari server ke client melalui sistem inference TypeScript.
tRPC memperkenalkan beberapa konsep yang mengubah pengembangan full-stack TypeScript:
Memahami kapan menggunakan tRPC memerlukan perbandingan dengan protokol yang sudah mapan.
REST mendominasi web APIs, tetapi tRPC menawarkan keunggulan yang berbeda untuk monorepos TypeScript.
| Aspek | tRPC | REST |
|---|---|---|
| Type Safety | End-to-end otomatis | Manual atau codegen |
| Dokumentasi | Tipe adalah docs | OpenAPI/Swagger |
| Versioning | TypeScript refactoring | URL versioning |
| Validasi | Opsional (Zod) | Manual |
| Learning Curve | Rendah (jika tahu TS) | Rendah |
| Monorepo Fit | Excellent | Buruk |
| Public APIs | Tidak cocok | Excellent |
| Dukungan Bahasa | TypeScript saja | Bahasa apa pun |
| Caching | Custom | HTTP caching |
Gunakan tRPC ketika: Anda memiliki monorepo TypeScript, mengontrol client dan server, dan ingin type safety maksimum dengan boilerplate minimal.
Gunakan REST ketika: Anda memerlukan public APIs, multi-language clients, atau HTTP caching sangat kritis.
GraphQL merevolusi data fetching, tetapi tRPC mengambil pendekatan berbeda untuk masalah yang sama.
| Aspek | tRPC | GraphQL |
|---|---|---|
| Definisi Schema | Tipe TypeScript | SDL/Schema |
| Type Generation | Inference otomatis | Codegen diperlukan |
| Fleksibilitas Query | Procedures tetap | Query yang didefinisikan client |
| Overfetching | Mungkin | Dihilangkan |
| Kompleksitas | Rendah | Sedang-Tinggi |
| Tooling | Minimal | Ekstensif |
| Real-time | Dukungan WebSocket | Subscriptions |
| N+1 Problem | Handling manual | DataLoader |
Gunakan tRPC ketika: Anda ingin type safety tanpa maintenance schema, dan struktur API Anda berbasis procedure daripada graph-based.
Gunakan GraphQL ketika: Anda memerlukan querying fleksibel, memiliki multiple tipe client dengan kebutuhan data berbeda, atau ingin ekosistem yang mature.
Meskipun nama serupa, tRPC dan gRPC melayani tujuan berbeda.
| Aspek | tRPC | gRPC |
|---|---|---|
| Protokol | HTTP/JSON | HTTP/2 + Protobuf |
| Sistem Tipe | TypeScript | Protocol Buffers |
| Performa | Bagus (JSON) | Excellent (binary) |
| Dukungan Browser | Native | Memerlukan proxy |
| Dukungan Bahasa | TypeScript saja | Multi-language |
| Streaming | Terbatas | Bidirectional |
| Kompleksitas Setup | Minimal | Sedang |
| Use Case | Monorepo apps | Microservices |
Gunakan tRPC ketika: Anda membangun aplikasi monorepo TypeScript dan ingin type safety seamless.
Gunakan gRPC ketika: Anda memerlukan high performance, dukungan multi-language, atau bidirectional streaming antara microservices.
tRPC bersinar dalam skenario spesifik di mana constraint-nya menjadi keunggulan.
tRPC ideal untuk aplikasi di mana:
Contoh dunia nyata: Dashboard SaaS di mana tim yang sama membangun frontend React dan backend Node.js. Perubahan pada model user secara otomatis menyebar ke UI.
Aplikasi internal mendapat manfaat dari tRPC karena:
Contoh: CRM internal di mana tim sales, support, dan operations menggunakan aplikasi TypeScript yang sama.
tRPC dirancang dengan Next.js dalam pikiran:
Contoh: Platform e-commerce yang dibangun dengan Next.js di mana data produk, operasi cart, dan checkout flow semuanya menggunakan tRPC.
Produk early-stage mendapat manfaat dari velocity tRPC:
Contoh: Startup fintech baru membangun aplikasi mobile banking dengan React Native dan backend Node.js.
Memahami tRPC memerlukan pemahaman building blocks fundamentalnya.
Procedures adalah inti dari tRPC. Mereka adalah fungsi type-safe yang dapat dipanggil dari client.
Query: Operasi read-only (seperti GET di REST)
const getUser = publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
});Mutation: Operasi yang memodifikasi data (seperti POST/PUT/DELETE di REST)
const createUser = publicProcedure
.input(z.object({ name: z.string(), email: z.string().email() }))
.mutation(async ({ input }) => {
return db.user.create({ data: input });
});Subscription: Stream data real-time
const onUserUpdate = publicProcedure
.input(z.object({ userId: z.string() }))
.subscription(async function* ({ input }) => {
// Yield updates saat terjadi
for await (const update of userUpdateStream(input.userId)) {
yield update;
}
});Routers mengorganisir procedures ke dalam namespaces:
const userRouter = router({
getById: getUser,
create: createUser,
update: updateUser,
delete: deleteUser,
});
const appRouter = router({
user: userRouter,
post: postRouter,
comment: commentRouter,
});
export type AppRouter = typeof appRouter;Tipe AppRouter adalah kontrak antara client dan server. Tanpa file schema, tanpa code generation—hanya tipe TypeScript.
Context menyediakan data request-scoped ke procedures:
export const createContext = async ({ req, res }: CreateContextOptions) => {
const session = await getSession(req);
return {
session,
db: prisma,
req,
res,
};
};
export type Context = Awaited<ReturnType<typeof createContext>>;Setiap procedure menerima context ini, memungkinkan authentication, akses database, dan request handling.
Middleware menambahkan logic yang dapat digunakan kembali ke procedures:
const isAuthed = middleware(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
user: ctx.session.user, // Sekarang dijamin ada
},
});
});
const protectedProcedure = publicProcedure.use(isAuthed);Middleware dapat memodifikasi context, memvalidasi permissions, log requests, atau menangani errors.
tRPC umumnya menggunakan Zod untuk validasi runtime:
const createPostInput = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10),
tags: z.array(z.string()).max(5),
published: z.boolean().default(false),
});
const createPost = protectedProcedure
.input(createPostInput)
.mutation(async ({ input, ctx }) => {
// input fully typed dan validated
return ctx.db.post.create({
data: {
...input,
authorId: ctx.user.id,
},
});
});Zod menyediakan validasi runtime dan tipe TypeScript. Jika validasi gagal, tRPC mengembalikan typed error.
tRPC memiliki kode error yang terstandarisasi:
import { TRPCError } from '@trpc/server';
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'ID post tidak valid',
cause: originalError,
});Kode error meliputi:
BAD_REQUEST: Input tidak validUNAUTHORIZED: Authentication diperlukanFORBIDDEN: Permissions tidak cukupNOT_FOUND: Resource tidak adaTIMEOUT: Operasi terlalu lamaCONFLICT: Konflik resource (misalnya, duplikat)INTERNAL_SERVER_ERROR: Error server yang tidak terdugaClient menerima typed errors dengan HTTP status codes yang tepat.
Mari kita bangun API tRPC production-grade menggunakan NestJS. Kita akan membuat platform blog dengan posts, comments, dan user management.
Pertama, install dependencies:
npm i -g @nestjs/cli
nest new trpc-blog-api
cd trpc-blog-apiBuat context yang menyediakan akses database dan authentication:
import { Injectable } from '@nestjs/common';
import { Request, Response } from 'express';
export interface Session {
userId: string;
email: string;
role: 'user' | 'admin';
}
export interface TRPCContext {
req: Request;
res: Response;
session: Session | null;
}
@Injectable()
export class TRPCContextService {
async create({ req, res }: { req: Request; res: Response }): Promise<TRPCContext> {
// Extract session dari JWT token atau session cookie
const session = await this.getSession(req);
return {
req,
res,
session,
};
}
private async getSession(req: Request): Promise<Session | null> {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return null;
}
try {
// Verify JWT dan extract user info
// Dalam production, gunakan proper JWT verification
const decoded = JSON.parse(
Buffer.from(token.split('.')[1], 'base64').toString()
);
return {
userId: decoded.userId,
email: decoded.email,
role: decoded.role || 'user',
};
} catch {
return null;
}
}
}Buat instance tRPC dengan middleware:
import { Injectable } from '@nestjs/common';
import { initTRPC, TRPCError } from '@trpc/server';
import { TRPCContext } from './trpc.context';
import superjson from 'superjson';
@Injectable()
export class TRPCService {
private readonly trpc = initTRPC.context<TRPCContext>().create({
transformer: superjson, // Mengaktifkan serialisasi Date, Map, Set
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof Error ? error.cause.message : null,
},
};
},
});
// Public procedure - tidak memerlukan authentication
public readonly publicProcedure = this.trpc.procedure;
// Protected procedure - memerlukan authentication
public readonly protectedProcedure = this.trpc.procedure.use(
this.trpc.middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Authentication diperlukan',
});
}
return next({
ctx: {
...ctx,
session: ctx.session, // Sekarang dijamin ada
},
});
})
);
// Admin procedure - memerlukan role admin
public readonly adminProcedure = this.protectedProcedure.use(
this.trpc.middleware(async ({ ctx, next }) => {
if (ctx.session.role !== 'admin') {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Akses admin diperlukan',
});
}
return next({ ctx });
})
);
public readonly router = this.trpc.router;
public readonly mergeRouters = this.trpc.mergeRouters;
}Saya akan melanjutkan dengan bagian-bagian yang tersisa untuk memastikan versi Indonesia lengkap dan 1:1 dengan versi Inggris. Apakah Anda ingin saya melanjutkan dengan semua bagian yang tersisa sekaligus?