tRPC v11: Schema ve Code Generation Olmadan Uçtan Uca Tip Güvenliği
Modern full-stack geliştirmede en büyük sorunlardan biri, backend ile frontend arasındaki tip uyumsuzluklarıdır. REST API kullanırken backend'de bir alanın adını değiştirdiğinizde, frontend ancak runtime'da — genellikle production'da — patlar. GraphQL bu soruna kısmi bir çözüm getirir, ancak schema tanımları, code generation araçları ve karmaşık bir toolchain gerektirir.
tRPC, bu denklemin tamamını alt üst ediyor: Hiçbir schema dosyası yazmadan, hiçbir code generation adımı çalıştırmadan, TypeScript'in kendi tip sistemini kullanarak uçtan uca (end-to-end) tip güvenliği sağlıyor. Ve v11 ile bu deneyim daha da olgunlaştı.
tRPC Nedir ve Neden Önemlidir?
tRPC (TypeScript Remote Procedure Call), TypeScript monorepo'larında backend ve frontend arasında doğrudan tip paylaşımı yapmanıza olanak tanıyan bir kütüphanedir. Temel felsefesi şudur:
"Eğer hem backend hem frontend TypeScript ile yazılıyorsa, neden arada bir çeviri katmanına ihtiyaç duyalım?"
Geleneksel Yaklaşımların Sorunları
| Yaklaşım | Schema Gerekli mi? | Code Generation? | Tip Güvenliği |
|---|---|---|---|
| REST API | Hayır (genelde) | Hayır | ❌ Yok |
| REST + OpenAPI | Evet (YAML/JSON) | Evet | ⚠️ Kısmi |
| GraphQL | Evet (.graphql) | Evet (codegen) | ✅ Var |
| tRPC | Hayır | Hayır | ✅ Tam |
tRPC bu tabloyu tek bir satırda özetler: Sıfır ceremony, tam tip güvenliği.
tRPC v11'deki Yenilikler
tRPC v11, önceki sürümlere göre önemli iyileştirmeler ve yeni özellikler getiriyor:
1. Yeni @trpc/server Mimarisi
v11'de server paketi tamamen yeniden yapılandırıldı. Artık daha modüler ve tree-shakeable bir yapıya sahip.
2. Geliştirilmiş createTRPCClient
Yeni client API'si daha temiz ve daha esnek. Vanilla client, React Query entegrasyonu ve diğer framework adaptörleri arasında tutarlı bir deneyim sunuyor.
3. Daha İyi Middleware Desteği
Middleware'ler artık daha güçlü tip çıkarımları yapabiliyor. Context'e eklenen her bilgi otomatik olarak sonraki middleware ve procedure'lara tip olarak yansıyor.
4. Streaming ve Server-Sent Events (SSE)
v11, httpSubscriptionLink ile SSE desteğini birinci sınıf bir özellik olarak sunuyor. Subscription'lar artık WebSocket gerektirmeden çalışabiliyor.
5. FormData ve Non-JSON İçerik Desteği
v11 ile dosya yükleme gibi senaryolar için FormData desteği geliştirildi.
Kurulum ve Proje Yapısı
Bir tRPC v11 projesi kurmak için tipik bir monorepo yapısı tercih edilir. Aşağıda adım adım kurulumu inceleyelim.
Paketlerin Yüklenmesi
# Server tarafı
npm install @trpc/server@next zod
# Client tarafı (React + React Query ile)
npm install @trpc/client@next @trpc/react-query@next @tanstack/react-queryProje Dizin Yapısı
my-app/
├── server/
│ ├── trpc.ts # tRPC instance
│ ├── routers/
│ │ ├── user.ts # User router
│ │ ├── post.ts # Post router
│ │ └── _app.ts # Root router
│ └── index.ts # Server entry
├── client/
│ ├── trpc.ts # tRPC client hooks
│ └── App.tsx
└── package.jsonBackend Kurulumu: Router ve Procedure'lar
tRPC Instance Oluşturma
İlk adım, tRPC instance'ını ve temel yapıtaşlarını oluşturmaktır:
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
// Context tipi — her request için oluşturulan bağlam
interface Context {
user: { id: string; role: string } | null;
db: PrismaClient;
}
const t = initTRPC.context<Context>().create();
// Temel yapıtaşlarını export et
export const router = t.router;
export const publicProcedure = t.procedure;
// Kimlik doğrulaması gerektiren procedure
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Giriş yapmanız gerekiyor',
});
}
return next({
ctx: {
// ctx.user artık null olamaz — TypeScript bunu bilir!
user: ctx.user,
},
});
});Dikkat edin: protectedProcedure kullandığınızda, TypeScript otomatik olarak ctx.user'ın null olmadığını bilir. Hiçbir ek tanım yapmadık, hiçbir schema oluşturmadık. Tip sistemi middleware zincirinden otomatik olarak türetilir.
Router Tanımlama
// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
export const userRouter = router({
// Kullanıcı bilgisi getir
getById: publicProcedure
.input(z.object({
id: z.string().uuid(),
}))
.query(async ({ input, ctx }) => {
const user = await ctx.db.user.findUnique({
where: { id: input.id },
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
});
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Kullanıcı bulunamadı',
});
}
return user;
// Return tipi otomatik çıkarılır:
// { id: string; name: string; email: string; createdAt: Date }
}),
// Profil güncelle (kimlik doğrulaması gerekli)
updateProfile: protectedProcedure
.input(z.object({
name: z.string().min(2).max(50),
bio: z.string().max(500).optional(),
}))
.mutation(async ({ input, ctx }) => {
// ctx.user burada kesinlikle var — TypeScript garanti ediyor
const updated = await ctx.db.user.update({
where: { id: ctx.user.id },
data: {
name: input.name,
bio: input.bio,
},
});
return { success: true, user: updated };
}),
// Kullanıcı listesi (sayfalama ile)
list: publicProcedure
.input(z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(20),
search: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
const { page, limit, search } = input;
const skip = (page - 1) * limit;
const [users, total] = await Promise.all([
ctx.db.user.findMany({
where: search ? { name: { contains: search } } : undefined,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
ctx.db.user.count({
where: search ? { name: { contains: search } } : undefined,
}),
]);
return {
users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}),
});Root Router
// server/routers/_app.ts
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
export const appRouter = router({
user: userRouter,
post: postRouter,
});
// Bu tip, client tarafında kullanılacak — işte sihir burada!
export type AppRouter = typeof appRouter;AppRouter tipi — bu tek satır, tüm uçtan uca tip güvenliğinin anahtarıdır. Backend'deki tüm router'lar, procedure'lar, input şemaları ve return tipleri bu tek tip üzerinden frontend'e aktarılır. Hiçbir code generation yok, hiçbir build adımı yok.
Server'ı Ayağa Kaldırma
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './routers/_app';
const server = createHTTPServer({
router: appRouter,
createContext: async ({ req }) => {
// Token'dan kullanıcıyı çıkar
const user = await getUserFromToken(req.headers.authorization);
return { user, db: prisma };
},
});
server.listen(3000);
console.log('tRPC server listening on port 3000');Frontend Kurulumu: React ile tRPC
tRPC Client Hooks
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/routers/_app';
// Sadece TİP import ediyoruz — runtime'da hiçbir server kodu bundle'a girmez
export const trpc = createTRPCReact<AppRouter>();
export const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000',
headers: () => {
const token = localStorage.getItem('token');
return token ? { authorization: `Bearer ${token}` } : {};
},
}),
],
});Provider Kurulumu
// client/App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { trpc, trpcClient } from './trpc';
import { useState } from 'react';
export function App() {
const [queryClient] = useState(() => new QueryClient());
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<UserProfile userId="some-uuid" />
</QueryClientProvider>
</trpc.Provider>
);
}Bileşenlerde Kullanım
İşte tRPC'nin büyüsünün gerçekten parladığı yer:
// client/components/UserProfile.tsx
import { trpc } from '../trpc';
function UserProfile({ userId }: { userId: string }) {
// ✅ user.getById'nin input ve return tipleri otomatik olarak biliniyor
const { data: user, isLoading, error } = trpc.user.getById.useQuery({
id: userId,
});
// ✅ updateProfile mutation'ının input tipi otomatik
const updateMutation = trpc.user.updateProfile.useMutation({
onSuccess: (data) => {
// data.success ve data.user tipleri otomatik çıkarılır
console.log('Profil güncellendi:', data.user.name);
},
});
if (isLoading) return <div>Yükleniyor...</div>;
if (error) return <div>Hata: {error.message}</div>;
// ✅ user.name, user.email, user.createdAt — hepsi tipli
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<p>Kayıt: {user.createdAt.toLocaleDateString()}</p>
<button
onClick={() => {
updateMutation.mutate({
name: 'Yeni İsim',
bio: 'Yeni biyografi',
// ❌ TypeScript hatası: 'age' property'si input'ta yok!
// age: 25,
});
}}
>
Güncelle
</button>
</div>
);
}Sayfalama ile Liste Bileşeni
// client/components/UserList.tsx
import { useState } from 'react';
import { trpc } from '../trpc';
function UserList() {
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const { data, isLoading } = trpc.user.list.useQuery({
page,
limit: 20,
search: search || undefined,
});
if (isLoading) return <div>Yükleniyor...</div>;
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Kullanıcı ara..."
/>
{/* data.users ve data.pagination tipleri otomatik */}
<ul>
{data?.users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<div>
Sayfa {data?.pagination.page} / {data?.pagination.totalPages}
<button
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
>
Önceki
</button>
<button
disabled={page >= (data?.pagination.totalPages ?? 1)}
onClick={() => setPage((p) => p + 1)}
>
Sonraki
</button>
</div>
</div>
);
}Gerçek Zamanlı Veri: Subscription'lar
tRPC v11 ile SSE tabanlı subscription'lar kurmak oldukça kolaydır:
// server/routers/post.ts
import { observable } from '@trpc/server/observable';
export const postRouter = router({
onNewPost: publicProcedure
.subscription(() => {
return observable<{ id: string; title: string }>((emit) => {
const handler = (post: { id: string; title: string }) => {
emit.next(post);
};
eventEmitter.on('new-post', handler);
return () => eventEmitter.off('new-post', handler);
});
}),
});// Client tarafında
function LiveFeed() {
trpc.post.onNewPost.useSubscription(undefined, {
onData: (post) => {
// post.id ve post.title tipleri otomatik
console.log('Yeni post:', post.title);
},
});
return <div>Canlı akış dinleniyor...</div>;
}tRPC vs Alternatifler: Ne Zaman tRPC Tercih Edilmeli?
tRPC İdeal Olduğu Durumlar
- Monorepo projeler: Backend ve frontend aynı repo'da
- TypeScript-only ekipler: Hem server hem client TS kullanıyor
- Hızlı iterasyon: Startup'lar, MVP'ler, dahili araçlar
- BFF (Backend for Frontend) katmanı: Tek bir frontend'e hizmet veren API
tRPC Uygun Olmadığı Durumlar
- Çoklu client dilleri: iOS (Swift), Android (Kotlin) gibi farklı dillerde client'lar varsa
- Public API: Üçüncü parti geliştiricilerin kullanacağı açık API'ler
- Microservice'ler arası iletişim: Farklı dillerde yazılmış servisler arasında
Performans İpuçları
Batch Link ile İstekleri Birleştirme
tRPC'nin httpBatchLink'i, aynı render döngüsünde yapılan birden fazla isteği otomatik olarak tek bir HTTP isteğinde birleştirir:
// Bu iki query aslında tek bir HTTP request'e dönüşür
const user = trpc.user.getById.useQuery({ id: '1' });
const posts = trpc.post.list.useQuery({ userId: '1' });Prefetching
// Sayfa geçişlerinde veriyi önceden yükle
const utils = trpc.useUtils();
function UserCard({ userId }: { userId: string }) {
return (
<div
onMouseEnter={() => {
// Mouse üzerine gelince veriyi prefetch et
utils.user.getById.prefetch({ id: userId });
}}
>
<Link to={`/users/${userId}`}>Profili Gör</Link>
</div>
);
}Sonuç
tRPC v11, TypeScript ekosisteminde backend-frontend iletişimini kökten değiştiren bir araçtır. İşte temel çıkarımlar:
Sıfır schema, sıfır codegen: TypeScript'in
typeofoperatörü ve tip çıkarımı sayesinde hiçbir ek araç gerekmez.Compile-time güvenlik: Backend'de bir alan adını değiştirdiğinizde, frontend'deki tüm kullanım noktaları anında kırmızıya döner — production'da değil, IDE'nizde.
Olgun ekosistem: React Query entegrasyonu, middleware desteği, subscription'lar ve adapter çeşitliliği ile production-ready bir çözümdür.
Geliştirici deneyimi: Autocompletion, inline dokümantasyon ve tip hataları sayesinde geliştirme hızı dramatik şekilde artar.
v11 iyileştirmeleri: Daha modüler yapı, gelişmiş SSE desteği ve daha temiz API tasarımı ile tRPC olgunluk seviyesini bir üst kademeye taşıdı.
Eğer full-stack TypeScript projesi geliştiriyorsanız ve henüz tRPC denemediyseniz, v11 başlamak için mükemmel bir zamanlama. Schema yazmak ve codegen çalıştırmakla harcadığınız zamanı, gerçek özellik geliştirmeye ayırabilirsiniz.
Tip güvenliği artık bir lüks değil — tRPC ile bir standart.