İçeriğe geç
TypeScript

TypeScript ile API Type Safety: End-to-End Tip Güvenliği

Tarık Tunç
TypeScript ile API Type Safety: End-to-End Tip Güvenliği

TypeScript ile API Type Safety: End-to-End Tip Güvenliği

Modern web uygulamalarında frontend ve backend arasındaki veri akışı, tip uyumsuzluklarına açık bir alandır. TypeScript ile end-to-end tip güvenliği sağlamak, API kontratlarının hem sunucu hem de istemci tarafında tutarlı kalmasını garanti eder. Bu rehberde tRPC, Zod, OpenAPI ve diğer araçlarla tam tip güvenli API geliştirme stratejilerini inceleyeceğiz.

End-to-End Type Safety Nedir?

End-to-end tip güvenliği, veritabanı şemasından kullanıcı arayüzüne kadar tüm katmanlarda tiplerin tutarlı olmasıdır:

  • Veritabanı: Prisma, Drizzle ORM ile tip oluşturma
  • Backend: API endpoint tipleri ve validasyon
  • API Kontratı: Paylaşılan tip tanımları
  • Frontend: Otomatik tip çıkarımı ile veri kullanımı

tRPC ile End-to-End Type Safety

tRPC, TypeScript projelerinde kod üzerinden tip paylaşımı sağlayan bir RPC framework'üdür:

// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.context<Context>().create();

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(authMiddleware);

// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

const UserSchema = z.object({
  id: z.string(),
  name: z.string().min(2),
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
});

const CreateUserSchema = UserSchema.omit({ id: true });

export const userRouter = router({
  list: publicProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().default(10),
    }))
    .query(async ({ input, ctx }) => {
      const users = await ctx.db.user.findMany({
        skip: (input.page - 1) * input.limit,
        take: input.limit,
      });
      return { users, total: await ctx.db.user.count() };
    }),

  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) => {
      const user = await ctx.db.user.findUnique({
        where: { id: input.id },
      });
      if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
      return user;
    }),

  create: protectedProcedure
    .input(CreateUserSchema)
    .mutation(async ({ input, ctx }) => {
      return ctx.db.user.create({ data: input });
    }),
});

tRPC Client Kullanımı (React)

// utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers';

export const trpc = createTRPCReact<AppRouter>();

// components/UserList.tsx
'use client';
import { trpc } from '@/utils/trpc';

export function UserList() {
  // Tip otomatik olarak çıkarılır: { users: User[], total: number }
  const { data, isLoading, error } = trpc.user.list.useQuery({
    page: 1,
    limit: 10,
  });

  const createUser = trpc.user.create.useMutation({
    onSuccess: () => {
      // Otomatik cache invalidation
      utils.user.list.invalidate();
    },
  });

  if (isLoading) return <p>Yükleniyor...</p>;
  if (error) return <p>Hata: {error.message}</p>;

  return (
    <ul>
      {data.users.map((user) => (
        // user.name, user.email vb. otomatik tamamlanır
        <li key={user.id}>{user.name} - {user.email}</li>
      ))}
    </ul>
  );
}
Bilgi: tRPC ile sunucu tarafında tanımlanan input/output tipleri, istemci tarafında hiçbir ek tanım yapmadan otomatik olarak kullanılabilir. Tip değişiklikleri anında her iki tarafı da etkiler.

Zod ile Runtime Validasyon

import { z } from 'zod';

// Şema tanımla ve tip çıkar
const PostSchema = z.object({
  title: z.string().min(3, 'Başlık en az 3 karakter').max(200),
  content: z.string().min(10),
  tags: z.array(z.string()).max(5),
  status: z.enum(['draft', 'published', 'archived']),
  publishedAt: z.date().optional(),
});

// Şemadan TypeScript tipi çıkar
type Post = z.infer<typeof PostSchema>;

// API route'da validasyon
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();

  const result = PostSchema.safeParse(body);
  if (!result.success) {
    return NextResponse.json(
      { errors: result.error.flatten().fieldErrors },
      { status: 400 }
    );
  }

  // result.data tipi: Post (tam tip güvenli)
  const post = await db.post.create({ data: result.data });
  return NextResponse.json(post, { status: 201 });
}

Prisma ile Veritabanı Tip Güvenliği

// prisma/schema.prisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
}

enum Role {
  USER
  ADMIN
  EDITOR
}

// Prisma otomatik tip oluşturur
import { PrismaClient, User, Role } from '@prisma/client';

const prisma = new PrismaClient();

// Tam tip güvenli sorgular
async function getUserWithPosts(id: string) {
  const user = await prisma.user.findUnique({
    where: { id },
    include: { posts: true },
  });
  // user tipi: (User & { posts: Post[] }) | null
  return user;
}

// Select ile kısmi tipler
async function getUserEmails() {
  const users = await prisma.user.findMany({
    select: { email: true, name: true },
  });
  // users tipi: { email: string; name: string }[]
  return users;
}

OpenAPI / Swagger ile Tip Üretimi

# OpenAPI şemasından TypeScript tipleri üret
npm install -D openapi-typescript

# Tip üretimi
npx openapi-typescript https://api.example.com/openapi.json -o ./types/api.d.ts
// Üretilen tipleri kullanma
import type { paths, components } from './types/api';
import createClient from 'openapi-fetch';

const client = createClient<paths>({
  baseUrl: 'https://api.example.com',
});

// Tam tip güvenli API çağrıları
const { data, error } = await client.GET('/users/{id}', {
  params: { path: { id: '123' } },
});
// data tipi: components['schemas']['User']

Paylaşılan Tip Paketi (Monorepo)

// packages/shared-types/src/api.ts
export interface ApiResponse<T> {
  data: T;
  meta: {
    total: number;
    page: number;
    limit: number;
  };
}

export interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string[]>;
}

// Hem frontend hem backend'de kullanılır
// apps/web/lib/api.ts
import type { ApiResponse, ApiError } from '@repo/shared-types';

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  if (!res.ok) {
    const error: ApiError = await res.json();
    throw new Error(error.message);
  }
  return res.json();
}
Uyarı: Runtime'da TypeScript tipleri mevcut değildir. API'den gelen verilerin doğruluğunu garantilemek için mutlaka Zod gibi runtime validasyon kütüphaneleri kullanın.

Tip Güvenli Fetch Wrapper

// lib/typedFetch.ts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

interface ApiEndpoints {
  'GET /users': { response: User[]; query?: { page: number } };
  'GET /users/:id': { response: User; params: { id: string } };
  'POST /users': { response: User; body: CreateUserInput };
  'PUT /users/:id': { response: User; params: { id: string }; body: UpdateUserInput };
  'DELETE /users/:id': { response: void; params: { id: string } };
}

type EndpointKey = keyof ApiEndpoints;

async function api<K extends EndpointKey>(
  endpoint: K,
  options?: Omit<ApiEndpoints[K], 'response'>
): Promise<ApiEndpoints[K]['response']> {
  // Implementation
  const [method, path] = endpoint.split(' ') as [HttpMethod, string];
  // ... fetch logic
  return {} as any;
}

// Kullanım: Tam tip güvenli
const users = await api('GET /users', { query: { page: 1 } });
const user = await api('GET /users/:id', { params: { id: '123' } });
İpucu: End-to-end tip güvenliği için en kolay yol tRPC kullanmaktır. Eğer REST API zorunluysa, OpenAPI + openapi-fetch kombinasyonu benzer güvenliği sağlar.

Sonuç

End-to-end tip güvenliği, veritabanından kullanıcı arayüzüne kadar tüm veri akışının tutarlılığını garanti eder. tRPC ile sıfır konfigürasyonla, Zod ile runtime validasyonla, Prisma ile veritabanı katmanında ve OpenAPI ile harici API'lerde tam tip güvenliği sağlayabilirsiniz. Bu araçları birlikte kullanarak runtime hatalarını minimize edin ve geliştirici deneyimini üst düzeye çıkarın.

Kaynaklar

İlgili Yazılar