İçeriğe geç
Sedat Demir
Geri dön

tRPC v11: Schema ve Code Generation Olmadan Uçtan Uca Tip Güvenliği

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-query

Proje 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.json

Backend 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

tRPC Uygun Olmadığı Durumlar


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:

  1. Sıfır schema, sıfır codegen: TypeScript'in typeof operatörü ve tip çıkarımı sayesinde hiçbir ek araç gerekmez.

  2. 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.

  3. Olgun ekosistem: React Query entegrasyonu, middleware desteği, subscription'lar ve adapter çeşitliliği ile production-ready bir çözümdür.

  4. Geliştirici deneyimi: Autocompletion, inline dokümantasyon ve tip hataları sayesinde geliştirme hızı dramatik şekilde artar.

  5. 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.


Share this post on:

Sonraki Yazı
Vite 6 ile React Projesi Kurma, Yapılandırma ve Production Optimizasyonu Rehberi