Modern yazılım geliştirme, özellikle mikroservisler ve dağıtık mimariler çağında, karmaşıklığı yönetmek ve hata oranını en aza indirmek giderek daha kritik bir hal alıyor. Bir tarafta hızla değişen kullanıcı talepleri, diğer tarafta büyüyen ve birbirinden farklı teknolojilerle inşa edilmiş birçok servis… Bu senaryoda frontend ve backend ekipleri arasında net bir iletişim ve anlaşma olmadan ilerlemek, adeta gözleri kapalı bir mayın tarlasında yürümek gibidir. Peki ya bu süreçte en çok karşılaşılan sorunlardan biri olan “kontrat uyumsuzluklarını” nasıl ortadan kaldırabiliriz? Monorepo ve BFF (Backend-for-Frontend) mimarilerinin sunduğu avantajları kullanarak, geliştirme süreçlerinizi nasıl daha güvenli, hızlı ve işbirlikçi hale getirebiliriz? İşte bu rehber, monorepo ve BFF mimarilerinde TypeScript tiplerini etkili bir şekilde paylaşarak geliştirme sürecinizi nasıl hızlandıracağınızı ve hata oranını düşüreceğinizi keşfetmenizi sağlayacak.
Tip güvenliği, günümüz yazılım dünyasında üzerinde en çok durulan konulardan biridir. Özellikle JavaScript ekosisteminde, çalışma zamanı hatalarının büyük bir kısmı tip uyumsuzluklarından kaynaklanır. TypeScript, bu problemi derleme anında yakalayarak bize önemli bir güvence sunar. Ancak, projeler büyüdükçe ve farklı servisler arasında veri alışverişi arttıkça, bu tipleri yönetmek ve tüm ekosistemde tutarlılığı sağlamak başlı başına bir meydan okumaya dönüşür. Örneğin, bir frontend uygulaması bir API’den kullanıcı verisi beklerken, backend bu veriyi farklı bir formatta gönderebilir. Bu tür senaryolar, hem geliştirme süresini uzatır hem de potansiyel hatalara zemin hazırlar. Monorepo yapıları, birden fazla projenin tek bir depoda barındırılmasıyla bu tip yönetimi için ideal bir ortam sunarken, BFF ise frontend’e özel API katmanı oluşturarak veri dönüşümlerini daha kontrollü hale getirir. Bu iki güçlü mimariyi TypeScript ile birleştirdiğimizde, tam anlamıyla tip güvenli ve verimli bir geliştirme ortamı inşa edebiliriz.
Bu makale boyunca, öncelikle monorepo ve BFF gibi temel kavramları ele alacak, ardından paylaşılan tiplerin olmaması durumunda karşılaşılan gerçek dünya sorunlarına odaklanacağız. Uygulamalı örnekler ve adım adım yönergelerle, TypeScript tiplerini monorepo içinde nasıl oluşturup paylaşacağımızı gösterecek, BFF mimarilerinde API kontratlarını nasıl güvence altına alabileceğimizi inceleyeceğiz. Son olarak, daha deneyimli geliştiriciler için ileri düzey teknikler ve ipuçlarıyla, tip paylaşımı stratejinizi bir sonraki seviyeye taşıyacak bilgiler sunacağız. Amacımız, tüm ekibinizin ortak bir dil ve tutarlı bir veri yapısı üzerinde çalışmasını sağlayarak, yazılım projelerinizdeki güvenliği, hızı ve işbirliğini maksimize etmektir. Hazırsanız, bu heyecan verici yolculuğa başlayalım ve TypeScript tiplerini paylaşmanın inceliklerini keşfedelim.
Temel Kavramlar: Monorepo, BFF ve TypeScript Tipleri Nedir?
Herhangi bir teknik konuya dalmadan önce, temel taşları sağlam bir şekilde oturtmak önemlidir. Bu bölümde, “Monorepo”, “BFF (Backend-for-Frontend)” ve “TypeScript Tipleri” kavramlarını detaylı bir şekilde ele alacağız. Bu kavramlar, makalemizin ana eksenini oluşturduğu için, onları tam olarak anlamak, ileriye dönük stratejileri kavramak adına büyük önem taşımaktadır.
Monorepo Nedir? Neden Kullanılır?
Monorepo, “tek bir depo” anlamına gelir ve birçok bağımsız projenin (uygulama, kütüphane, servis vb.) aynı sürüm kontrol deposunda (örneğin Git deposu) barındırıldığı bir yazılım geliştirme stratejisidir. Bu yaklaşımın aksine, “polyrepo” ise her projenin kendi ayrı deposuna sahip olduğu geleneksel bir yaklaşımdır. Monorepo kullanmanın birçok önemli avantajı bulunmaktadır. Öncelikle, kod tekrarını büyük ölçüde azaltır. Paylaşılan kütüphaneler veya tipler, tek bir merkezden yönetilerek tüm projelere kolayca entegre edilebilir. Bu, özellikle büyük ekiplerde ve birden fazla uygulamanın olduğu senaryolarda geliştirme hızını artırır ve bakım maliyetlerini düşürür. Ayrıca, bağımlılık yönetimini basitleştirir; tüm projeler aynı depoda olduğu için, bağımlılık versiyonlarını senkronize etmek ve güncel tutmak daha kolaydır. Büyük çaplı refactoring operasyonları da monorepo’larda daha güvenli bir şekilde yapılabilir, çünkü bir değişiklik yapıldığında etkilenen tüm projeler tek bir yerde görülebilir ve test edilebilir. Google, Facebook, Microsoft gibi teknoloji devlerinin de monorepo kullandığını düşünürsek, bu yaklaşımın ölçeklenebilirlik ve verimlilik açısından sağladığı faydalar daha iyi anlaşılacaktır.
Ancak monorepo’nun bazı zorlukları da vardır. Deponun zamanla çok büyümesi, klonlama ve indirme sürelerini uzatabilir. Build ve test süreçlerinin yönetimi, özel araçlar (Lerna, Nx, Yarn Workspaces gibi) gerektirebilir. Dolayısıyla, monorepo’ya geçiş kararı verilirken projenin ölçeği, ekibin büyüklüğü ve yönetim araçlarının kullanımı gibi faktörler dikkatlice değerlendirilmelidir.
BFF (Backend-for-Frontend) Nedir? Amacı Nedir?
BFF, “Backend-for-Frontend” teriminin kısaltmasıdır ve mikroservis mimarisinde sıkça karşılaşılan bir tasarım kalıbıdır. Temelinde, her frontend uygulamasının (örneğin, web, mobil iOS, mobil Android) kendi optimize edilmiş ve özelleştirilmiş backend’ine sahip olması fikri yatar. Geleneksel yaklaşımlarda, birden fazla frontend uygulamasının tek bir genel API’yi kullanması, çeşitli sorunlara yol açabilir. Örneğin, mobil uygulama için farklı bir veri formatı veya daha az bilgi gerekebilirken, web uygulaması daha zengin ve detaylı veri setlerine ihtiyaç duyabilir. Bu durumda, tek bir API her iki frontend’in de ihtiyaçlarını tam olarak karşılayamaz ve frontend’ler ya gereksiz veri çeker ya da veri dönüşümlerini kendi tarafında yapmak zorunda kalır.
BFF, bu sorunu çözerek her frontend’in spesifik gereksinimlerine göre veri toplama, dönüştürme ve optimize etme yeteneği sunar. Bir BFF servisi genellikle şu işlevleri yerine getirir:
- Birden fazla mikroservisten veri toplar ve tek bir API yanıtında birleştirir.
- Frontend’in ihtiyaçlarına göre veri formatını ve yapısını dönüştürür.
- Frontend’e özel kimlik doğrulama, yetkilendirme ve güvenlik kontrolleri sağlar.
- Performans optimizasyonları (önbellekleme, toplu işlem) yapar.
BFF, frontend geliştiricilerine daha fazla kontrol ve esneklik sağlarken, ana backend servislerinin daha genel ve iş alanına odaklanmasını mümkün kılar. Bu da hem frontend hem de backend ekiplerinin daha hızlı ve bağımsız çalışmasına olanak tanır. Özellikle karmaşık kullanıcı arayüzleri ve farklı platformlara hizmet veren uygulamalar için BFF mimarisi oldukça değerli bir çözüm sunar.
TypeScript Tipleri: Neden Vazgeçilmez?
TypeScript, Microsoft tarafından geliştirilen ve JavaScript’e opsiyonel statik tip sistemi ekleyen açık kaynaklı bir programlama dilidir. Aslında JavaScript’in bir “üst kümesidir” (superset), yani geçerli tüm JavaScript kodları aynı zamanda geçerli TypeScript kodudur. TypeScript’in en büyük vaadi, geliştirme sırasında (derleme anında) tip hatalarını yakalamaktır. Bu, çalışma zamanında ortaya çıkabilecek beklenmedik hataları önemli ölçüde azaltır ve uygulamanın genel güvenilirliğini artırır.
TypeScript ile değişkenler, fonksiyon parametreleri ve dönüş değerleri, nesne yapıları (interface’ler, type alias’lar) gibi tüm kod öğelerine tipler tanımlayabilirsiniz. Örneğin, bir fonksiyonun sadece sayı beklediğini veya bir nesnenin belirli alanlara sahip olması gerektiğini belirtmek, hem kodun okunabilirliğini artırır hem de geliştiricilerin hatalı veri girişlerini yapmasını engeller. IDE’ler (örneğin VS Code), TypeScript sayesinde geliştiricilere zengin otomatik tamamlama, anında hata bildirimleri ve kod gezinme gibi özellikler sunarak geliştirici deneyimini bir üst seviyeye taşır. Özellikle büyük ve karmaşık projelerde, farklı ekiplerin birbiriyle entegre olması gereken durumlarda, TypeScript tipleri adeta ortak bir dil görevi görerek entegrasyon süreçlerini basitleştirir ve hata payını minimuma indirir. Bu nedenle, TypeScript tipleri, modern ve ölçeklenebilir yazılım geliştirme için vazgeçilmez bir araç haline gelmiştir.
Vaka Analizi: Paylaşılan Tipler Olmadan Karşılaşılan Sorunlar
Geliştirme dünyasında her şey yolundaymış gibi görünürken, aniden patlak veren bir üretim hatası, genellikle derinlerde yatan bir uyumsuzluğun işaretidir. Bu tür durumların başında da genellikle “tip uyumsuzlukları” gelir. Paylaşılan tiplerin kullanılmadığı veya yetersiz kullanıldığı senaryolar, birçok projenin kabusu haline gelmiştir. Şimdi, bu durumu somutlaştırmak adına bir vaka analizi yapalım ve paylaşılan tipler olmadan karşılaşılan gerçek dünya sorunlarını inceleyelim.
Senaryo: E-ticaret Platformunda Veri Uyumsuzluğu
Büyük bir e-ticaret platformu üzerinde çalıştığımızı hayal edelim. Bu platformda aşağıdaki ana bileşenler mevcut:
- Frontend (React/Next.js): Kullanıcıların ürünleri görebileceği, sepete ekleyebileceği ve sipariş verebileceği web arayüzü.
- BFF (Node.js/Express): Frontend’e özel API katmanı. Ana backend servislerinden veri toplar, işler ve frontend’in ihtiyaç duyduğu formatta sunar.
- Ana Backend (Java/Spring Boot): Ürün yönetimi, kullanıcı hesabı, sipariş işleme gibi temel iş mantığını barındıran mikroservisler.
Bu platformda, “Ürün” (Product) bilgileri en merkezi veri yapılarından biridir. Frontend, BFF ve Ana Backend, farklı zamanlarda ve farklı ekipler tarafından geliştirilmiştir. Başlangıçta, her ekip kendi “Ürün” tanımını oluşturmuştur:
- Ana Backend: Veritabanı modeli olarak
ProductEntityadında bir sınıf kullanır. - BFF: Ana Backend’den gelen veriyi alıp frontend’e göndermeden önce kendi iç modellemesi olan
ProductDtoveyaProductInterfacekullanır. - Frontend: API’den beklediği
Producttipini kendi tarafındainterface Productolarak tanımlar.
Bu ayrık tip tanımları, kısa vadede sorun çıkarmayabilir. Ancak projenin yaşam döngüsü ilerledikçe, kaçınılmaz olarak sorunlar ortaya çıkar. Şöyle düşünün: Bir pizza sipariş ediyorsunuz ve telefonla sipariş alan kişi, aşçıya siparişi yanlış iletiyor. Aşçı da bildiği gibi yapıyor. Sonuç: Yanlış pizza! Tipler de bu “sipariş kontratını” belirliyor.
Karşılaşılan Sorunlar: Bir Zincirleme Reaksiyon
1. Alan Değişikliği ve Kırılan Kod:
* Ana Backend ekibi, ProductEntity içindeki price alanının tipini decimal‘dan string‘e değiştirmeye karar verir (örneğin, para birimi sembolü ile birlikte göstermek için).
* Bu değişiklik, Ana Backend’de yapıldıktan sonra, BFF’in bu yeni tip ile uyumlu hale gelmesi gerekir. Ancak, BFF ekibi bu değişiklikten anında haberdar olmayabilir veya gözden kaçırabilir.
* BFF, hala eski decimal tipini beklerken, yeni string tipinde veri almaya başlar. Bu durum, BFF’te beklenmedik çalışma zamanı hatalarına (TypeError veya veri dönüşüm hatalarına) yol açar.
* BFF’teki hata giderilmeden, Frontend’e yanlış tipte veri ulaşır. Frontend, price alanını bir sayı olarak beklerken bir string alır. Bu da UI’da hatalı gösterimlere, matematiksel işlemlerin başarısız olmasına veya uygulamanın çökmesine neden olur.
// Ana Backend (Önceden)
interface ProductEntity {
id: string;
name: string;
price: number; // Önceki durum
}
// Ana Backend (Sonra)
interface ProductEntity {
id: string;
name: string;
price: string; // Yeni durum: "12.99 USD" gibi
}
// BFF'te (Güncellenmemişse)
interface FrontendProduct {
id: string;
name: string;
price: number; // Hala number bekliyor!
}
// Frontend'de (Güncellenmemişse)
interface ProductCardProps {
product: {
name: string;
price: number; // Hala number bekliyor!
};
}
2. Yeni Alan Ekleme ve Eksik Veri:
* Pazarlama ekibi, ürünlere "stok durumu" (stockStatus: 'InStock' | 'OutOfStock') eklenmesini ister.
* Ana Backend bu alanı ProductEntity'e ekler.
* BFF, bu yeni alanı ana backend'den çeker, ancak kendi ProductDto'suna eklemeyi unutur veya gerekli görmez. Dolayısıyla, bu alan frontend'e iletilmez.
* Frontend ekibi, yeni özelliği geliştirmeye başlar ve kendi interface Product tanımına stockStatus alanını ekler. Ancak API'den bu alan gelmediği için UI'da "undefined" veya "null" değerleriyle karşılaşır. Bu durum, yanlış kullanıcı deneyimine veya eksik özelliklerin yayına çıkmasına neden olur.
3. Geliştirici Deneyiminin Düşmesi:
* Her ekip, diğer ekiplerin API kontratlarını veya veri yapılarını dokümantasyonlardan veya Slack mesajlarından takip etmek zorunda kalır. Bu, zaman kaybına ve yanlış anlaşılmalara yol açar.
* IDE'ler, tip bilgisi olmadığı için otomatik tamamlama veya anlık hata bildirimleri sunamaz. Geliştiriciler, sürekli olarak API dokümantasyonlarına başvurmak veya çalışma zamanı hatalarıyla karşılaşana kadar beklemek zorunda kalır.
4. Bakım Zorlukları ve Test Yükü:
* Herhangi bir tip değişikliğinde, tüm ilgili projelerdeki tiplerin manuel olarak güncellenmesi gerekir. Bu, büyük bir bakım yükü oluşturur ve insan hatasına açıktır.
* Bu uyumsuzlukları yakalamak için entegrasyon testlerinin kapsamının çok geniş olması gerekir. Hatta bazen, üretim ortamında ortaya çıkan senaryoları test ortamında yakalamak mümkün olmayabilir.
Bu vaka analizi, paylaşılan tiplerin önemini net bir şekilde ortaya koymaktadır. Bir sonraki bölümde, bu sorunları ortadan kaldırmak için TypeScript tiplerini monorepo içinde nasıl etkili bir şekilde paylaşacağımızı adım adım inceleyeceğiz.
TypeScript Tiplerini Monorepo'da Nasıl Paylaşırız? Uygulamalı Yaklaşımlar
Paylaşılan tiplerin olmamasının yarattığı sorunları anladığımıza göre, şimdi bu sorunları nasıl çözeceğimize odaklanalım. Monorepo yapısı, TypeScript tiplerini birden fazla proje arasında tutarlı bir şekilde paylaşmak için mükemmel bir ortam sunar. Bu bölümde, TypeScript tiplerini monorepo içinde paylaşmak için kullanabileceğiniz başlıca yaklaşımları, adım adım örneklerle ve kod bloklarıyla açıklayacağız.
Yaklaşım 1: Ayrı Bir types Paketi Oluşturma (Önerilen)
Bu, büyük ve orta ölçekli monorepolar için en yaygın ve önerilen yaklaşımdır. Monorepo'nuzun içinde, sadece paylaşılan tip tanımlarını içeren ayrı bir "paket" (package) oluşturursunuz. Diğer tüm projeleriniz (frontend uygulamaları, BFF servisleri vb.) bu tip paketini bir bağımlılık olarak kullanır.
Adım 1: Monorepo Yapısı ve packages Klasörü
Monorepo'nuz genellikle bir packages veya apps klasörüne sahip olacaktır. Bu klasörün altında her proje kendi alt klasöründe yer alır. Örneğin:
/monorepo-root
├── packages/
│ ├── app-frontend/ // React uygulaması
│ ├── api-bff/ // Node.js BFF servisi
│ └── @monorepo/types/ // Paylaşılan tipler paketi
├── package.json // Monorepo root package.json
└── tsconfig.base.json // Ortak TypeScript ayarları
Adım 2: @monorepo/types Paketini Oluşturma
Öncelikle, packages/@monorepo/types klasörünü oluşturun. İçine basit bir package.json ve tsconfig.json dosyası ekleyin. package.json dosyasında, paketin adını (genellikle @scope/name formatında), versiyonunu ve tip dosyalarının nerede bulunduğunu belirtmeniz önemlidir.
// packages/@monorepo/types/package.json
{
"name": "@monorepo/types",
"version": "1.0.0",
"description": "Monorepo genelinde paylaşılan TypeScript tip tanımları.",
"main": "dist/index.js",
"types": "dist/index.d.ts", // Tip tanımlarının yeri
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"files": [
"dist" // Yayınlarken sadece dist klasörünü dahil et
],
"publishConfig": {
"access": "public"
}
}
Ardından, bu paketin kendi TypeScript yapılandırmasını tanımlayın. Bu tsconfig.json, tsc komutunun nasıl çalışacağını belirler.
// packages/@monorepo/types/tsconfig.json
{
"extends": "../../tsconfig.base.json", // Monorepo'nun kök tsconfig'inden devralma
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true, // d.ts dosyaları oluştur
"emitDeclarationOnly": true, // Sadece d.ts dosyalarını oluştur, JS değil
"noEmit": false, // Üretimi etkinleştir
"module": "esnext", // Modern modül sistemi
"target": "esnext" // Modern ES versiyonu
},
"include": [
"src/**/*"
]
}
emitDeclarationOnly: true seçeneği, tip paketinizde sadece .d.ts (declaration) dosyalarının oluşturulmasını sağlar. Bu sayede gereksiz JavaScript çıktı dosyaları oluşmaz ve paket boyutu küçülür. noEmit: false ise üretimi tetiklemek için gereklidir.
Adım 3: Ortak Tip Tanımlarını Yazma
packages/@monorepo/types/src/index.ts dosyası içinde paylaşılan tiplerinizi tanımlayın:
// packages/@monorepo/types/src/index.ts
export interface User {
id: string;
firstName: string;
lastName: string;
email: string;
isActive: boolean;
role: 'admin' | 'user' | 'guest';
}
export interface Product {
id: string;
name: string;
description: string;
price: number; // Önceki vaka analizimizdeki sorun burada çözülmeli: number veya string
currency: string;
stockCount: number;
imageUrl: string;
}
export interface ApiResponse {
success: boolean;
data?: T;
message?: string;
statusCode: number;
}
Adım 4: Tip Paketini Oluşturma (Build Etme)
@monorepo/types paketi içinde npm run build veya yarn build komutunu çalıştırarak tip dosyalarını (dist/index.d.ts) oluşturun. Monorepo araçları (Yarn Workspaces, Lerna, Nx) genellikle bu adımı otomatikleştirir veya kolaylaştırır.
Adım 5: Diğer Projelerde Tip Paketini Kullanma
Şimdi app-frontend veya api-bff gibi diğer projelerinizde bu tip paketini bir bağımlılık olarak ekleyebilirsiniz. Monorepo araçları genellikle "workspace" bağımlılıklarını destekler, bu da paketi yayınlamadan direkt olarak monorepo içinden kullanmanızı sağlar.
# app-frontend projesinin kök dizininde
cd packages/app-frontend
yarn add @monorepo/types
# veya Lerna/Nx kullanıyorsanız
lerna add @monorepo/types --scope=app-frontend
Bağımlılık eklendikten sonra, projenizdeki herhangi bir TypeScript dosyasında bu tipleri içe aktarabilirsiniz:
// packages/app-frontend/src/components/UserProfile.tsx
import React from 'react';
import { User } from '@monorepo/types'; // Paylaşılan User tipini içe aktar
interface UserProfileProps {
user: User;
}
const UserProfile: React.FC = ({ user }) => {
return (
{user.firstName} {user.lastName}
Email: {user.email}
Role: {user.role}
Status: {user.isActive ? 'Active' : 'Inactive'}
);
};
export default UserProfile;
Yaklaşım 2: tsconfig.json paths Takma Adları ile Direkt Referans Verme (Küçük Monorepolar İçin)
Daha küçük monorepolar için, ayrı bir tip paketi oluşturmak yerine, monorepo kök tsconfig.json dosyasındaki paths ayarlarını kullanarak doğrudan tip dosyalarına referans verebilirsiniz. Bu yöntem, daha az yapılandırma gerektirir ancak tipleri ayrı bir paket gibi yönetme esnekliğini sunmaz.
Adım 1: Ortak Tipler Klasörü Oluşturma
Monorepo'nun kökünde veya shared gibi bir klasör altında tiplerinizi saklayın:
/monorepo-root
├── packages/
│ ├── app-frontend/
│ └── api-bff/
├── shared/
│ └── types/
│ └── index.ts // Ortak tip tanımları
├── package.json
└── tsconfig.json // Monorepo'nun ana tsconfig'i
Adım 2: tsconfig.json paths Ayarlarını Yapılandırma
Monorepo'nun ana tsconfig.json dosyasına baseUrl ve paths ayarlarını ekleyin:
// monorepo-root/tsconfig.json
{
"compilerOptions": {
"baseUrl": ".", // Kök dizini temel al
"paths": {
"@shared/types": ["./shared/types/index.ts"], // Takma ad ile yolu eşle
"@shared/types/*": ["./shared/types/*"]
},
// Diğer ortak ayarlar
"target": "esnext",
"module": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"noEmit": true
},
"exclude": ["node_modules"],
"include": [
"shared/**/*.ts", // Paylaşılan tipleri de dahil et
"packages/**/*.ts",
"packages/**/*.tsx"
]
}
Adım 3: Tipleri Kullanma
Artık diğer projelerinizde tipleri doğrudan takma adla içe aktarabilirsiniz:
// packages/api-bff/src/routes/users.ts
import { User, ApiResponse } from '@shared/types'; // Takma ad ile içe aktar
import express from 'express';
const router = express.Router();
router.get('/users/:id', (req, res: express.Response>) => {
const userId = req.params.id;
// Veritabanından kullanıcıyı çekme mantığı
const user: User = {
id: userId,
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
isActive: true,
role: 'user'
};
return res.status(200).json({ success: true, data: user, statusCode: 200 });
});
export default router;
Her iki yaklaşım da monorepo içinde TypeScript tiplerini paylaşmak için geçerlidir, ancak ayrı bir paket yaklaşımı, özellikle büyük ve karmaşık monorepolar için daha ölçeklenebilir ve yönetilebilir bir çözüm sunar. Bir sonraki bölümde, bu yaklaşımları BFF mimarisi bağlamında nasıl kullanabileceğimizi inceleyeceğiz.
BFF Mimarilerinde Tip Paylaşımı: API Kontratlarının Güvence Altına Alınması
Backend-for-Frontend (BFF) mimarisi, farklı frontend uygulamalarına özelleştirilmiş API katmanları sunarak geliştirme sürecini kolaylaştırma ve performansı artırma potansiyeline sahiptir. Ancak bu katman, aynı zamanda frontend ile ana backend servisleri arasında bir aracı görevi gördüğü için, tip tutarlılığı konusunda ek bir sorumluluk da yükler. Bu bölümde, BFF mimarilerinde tip paylaşımının neden bu kadar kritik olduğunu ve API kontratlarını nasıl güvence altına alabileceğimizi detaylıca ele alacağız.
BFF'in Rolü ve Tip Güvenliği Gereksinimi
BFF, temelde frontend'in "isteğini tercüme eden" ve "cevabını hazırlayan" bir aracıdır. Genellikle şu görevleri üstlenir:
- Frontend'den gelen tek bir isteği, ana backend'deki birden fazla mikroservise dağıtmak.
- Farklı mikroservislerden gelen verileri toplamak ve frontend'in ihtiyaç duyduğu tek bir yapıya dönüştürmek (aggregation ve transformation).
- Frontend'e özel kimlik doğrulama, yetkilendirme ve güvenlik katmanları eklemek.
- Gereksiz veri alışverişini önlemek için veriyi filtrelemek ve optimize etmek.
Bu aracı rolü, özellikle veri dönüşümü aşamasında tip güvenliğini hayati kılar. Düşünün ki frontend, belirli bir JSON yapısı bekliyor. BFF, ana backend'den gelen veriyi bu yapıya dönüştürürken bir hata yaparsa veya backend'deki bir değişiklik BFF tarafından yanlış yorumlanırsa, frontend'de beklenmedik hatalar ortaya çıkar. Bu da geliştiricilerin "API doğru çalışıyor ama frontend'de neden veri gelmiyor?" gibi sorularla boğuşmasına neden olur.
BFF ve Frontend Arasında Tip Paylaşımı
BFF mimarisinde tip paylaşımının en kritik noktası, BFF ile doğrudan iletişim kuran frontend arasındaki kontratın güvence altına alınmasıdır. Monorepo ortamında bu, önceki bölümde ele aldığımız "ayrı bir @monorepo/types paketi oluşturma" yaklaşımıyla en etkili şekilde sağlanabilir.
Nasıl Yapılır?
- Paylaşılan API Tiplerini Tanımlama:
@monorepo/typespaketi içinde, hem frontend'in BFF'e gönderdiği request body'leri hem de BFF'ten aldığı response body'lerini tanımlayın. Bu tipler, API kontratınızın resmî tanımı olacaktır. - Frontend ve BFF'te Kullanım: Hem frontend uygulamanız (örneğin React), hem de BFF servisiniz (örneğin Node.js/Express) bu paylaşılan tipleri kullanarak API çağrılarını ve yanıtlarını doğrular.
Örnek Senaryo: Ürün Detayları API'si
E-ticaret örneğimize geri dönelim. Frontend, belirli bir ürünün detaylarını göstermek için BFF'e bir istek gönderiyor. BFF ise bu isteği işleyip ana backend'den ürünü çekiyor ve frontend'e dönüştürülmüş bir şekilde sunuyor.
1. Paylaşılan Tipler (@monorepo/types):
// packages/@monorepo/types/src/product.ts
export interface ProductDetailsResponse {
id: string;
name: string;
description: string;
price: number;
currency: string;
availableStock: number; // Farklı bir isimle gönderilebilir
images: string[];
reviewsCount: number;
averageRating: number;
}
export interface AddToCartRequest {
productId: string;
quantity: number;
}
// packages/@monorepo/types/src/common.ts
export interface ApiResponse {
success: boolean;
data?: T;
message?: string;
statusCode: number;
}
2. BFF Servisi (api-bff):
BFF servisi, ana backend'den gelen ProductEntity (varsayılan olarak) tipindeki veriyi ProductDetailsResponse tipine dönüştürür ve frontend'e sunar.
// packages/api-bff/src/routes/products.ts
import express, { Request, Response } from 'express';
import { ProductDetailsResponse, AddToCartRequest, ApiResponse } from '@monorepo/types';
import axios from 'axios'; // Ana backend ile iletişim için
const router = express.Router();
// Varsayalım ana backend'den gelen Product tipi
interface BackendProductEntity {
id: string;
title: string;
summary: string;
cost: number;
currencyCode: string;
quantity: number;
mediaUrls: string[];
userReviews: { rating: number }[];
}
router.get('/products/:id', async (req: Request, res: Response>) => {
try {
const productId = req.params.id;
// Ana backend'den ürün detaylarını çek
const backendResponse = await axios.get(http://main-backend/api/products/${productId});
const backendProduct = backendResponse.data;
// Backend'den gelen veriyi frontend'in beklediği tipe dönüştür (BFF'in ana görevi)
const productDetails: ProductDetailsResponse = {
id: backendProduct.id,
name: backendProduct.title,
description: backendProduct.summary,
price: backendProduct.cost,
currency: backendProduct.currencyCode,
availableStock: backendProduct.quantity,
images: backendProduct.mediaUrls,
reviewsCount: backendProduct.userReviews.length,
averageRating: backendProduct.userReviews.reduce((sum, r) => sum + r.rating, 0) / backendProduct.userReviews.length,
};
return res.status(200).json({ success: true, data: productDetails, statusCode: 200 });
} catch (error) {
console.error('Error fetching product details:', error);
return res.status(500).json({ success: false, message: 'Ürün detayları alınamadı.', statusCode: 500 });
}
});
router.post('/cart/add', (req: Request<{}, {}, AddToCartRequest>, res: Response>) => {
const { productId, quantity } = req.body;
// Sepete ekleme mantığı
console.log(Ürün ID: ${productId}, Miktar: ${quantity} sepete eklendi.);
return res.status(200).json({ success: true, message: 'Ürün sepete eklendi.', statusCode: 200 });
});
export default router;
3. Frontend Uygulaması (app-frontend):
Frontend, BFF'ten gelen yanıtı ProductDetailsResponse olarak bekler ve AddToCartRequest tipinde istek gönderir.
// packages/app-frontend/src/pages/ProductDetail.tsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { ProductDetailsResponse, AddToCartRequest, ApiResponse } from '@monorepo/types'; // Paylaşılan tipleri kullan
interface ProductDetailPageProps {
productId: string;
}
const ProductDetailPage: React.FC = ({ productId }) => {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProduct = async () => {
try {
const response = await axios.get>(/api/products/${productId});
if (response.data.success && response.data.data) {
setProduct(response.data.data);
} else {
setError(response.data.message || 'Ürün bilgisi alınamadı.');
}
} catch (err) {
setError('Sunucu hatası.');
} finally {
setLoading(false);
}
};
fetchProduct();
}, [productId]);
const handleAddToCart = async () => {
if (!product) return;
const requestBody: AddToCartRequest = {
productId: product.id,
quantity: 1,
};
try {
const response = await axios.post>('/api/cart/add', requestBody);
if (response.data.success) {
alert('Ürün sepete eklendi!');
} else {
alert(response.data.message || 'Sepete eklenirken bir hata oluştu.');
}
} catch (err) {
alert('Sepete eklenirken sunucu hatası.');
}
};
if (loading) return Yükleniyor...;
if (error) return Hata: {error};
if (!product) return Ürün bulunamadı.;
return (
{product.name}
{product.description}
Fiyat: {product.price} {product.currency}
Stok: {product.availableStock}
);
};
export default ProductDetailPage;
Bu örnekte görüldüğü gibi, ProductDetailsResponse ve AddToCartRequest tiplerini api-bff ve app-frontend arasında paylaşarak, hem geliştirme aşamasında IDE desteği sağlıyor hem de çalışma zamanı tip uyumsuzluklarının önüne geçiyoruz. BFF, ana backend'den aldığı veriyi frontend'in beklediği formatta dönüştürürken bile, bu dönüşümün tip güvenliğini sağlamış oluyoruz. Bu, ekipler arası iletişimi güçlendirir, entegrasyon hatalarını azaltır ve genel geliştirme hızını önemli ölçüde artırır.
Partial, Pick veya Omit gibi TypeScript utility tiplerini kullanarak daha esnek ve güvenli tip dönüşümleri yapabilirsiniz.
Bu yaklaşım, API kontratlarınızın canlı, sürüm kontrollü ve her zaman güncel kalmasını sağlar. Bir sonraki bölümde, tip paylaşımı stratejinizi daha da optimize etmek için ileri düzey tekniklere ve ipuçlarına değineceğiz.
İleri Düzey Teknikler ve İpuçları: Daha Verimli Bir Akış İçin Neler Yapmalı?
TypeScript tiplerini monorepo ve BFF mimarilerinde paylaşmanın temellerini öğrendik. Ancak büyük ve karmaşık projelerde, bu süreci daha da verimli hale getirmek ve olası darboğazları aşmak için ileri düzey tekniklere başvurmak gerekebilir. Bu bölümde, tip yönetimini bir sonraki seviyeye taşıyacak ipuçlarını ve yaklaşımları inceleyeceğiz.
Otomatik Tip Üretimi (Type Generation): Manuel Hataları Sıfırlayın
Manuel olarak tip tanımları yazmak, özellikle dış sistemlerle (GraphQL API, REST API, veritabanı şemaları) entegre olurken zaman alıcı ve hataya açık bir süreçtir. Bu nedenle, otomatik tip üretimi (code generation) modern geliştirme süreçlerinin vazgeçilmez bir parçası haline gelmiştir. Bu sayede, API kontratınız değiştiğinde tip dosyalarınızı el ile güncellemek yerine, tek bir komutla otomatik olarak yenileyebilirsiniz.
1. OpenAPI/Swagger Şemalarından Tip Üretimi:
REST API'leri için OpenAPI (eski adıyla Swagger) en yaygın standarttır. Backend ekibiniz API'lerini OpenAPI şemasıyla tanımlıyorsa, bu şemadan TypeScript tiplerini otomatik olarak generate edebilirsiniz.
* Nasıl Yapılır? openapi-typescript gibi kütüphaneler bu iş için idealdir.
# openapi-typescript kurulumu
npm install -D openapi-typescript
# OpenAPI şemasından tipleri üretme
npx openapi-typescript ./path/to/your/openapi-schema.yaml --output ./src/generated/api.d.ts
* Bu komut, belirtilen YAML veya JSON şemasından .d.ts uzantılı bir dosya oluşturur. Oluşturulan bu dosyayı packages/@monorepo/types paketinize dahil ederek veya doğrudan BFF ve frontend projelerinizde kullanarak, API kontratınızın her zaman güncel kalmasını sağlarsınız.
2. GraphQL Şemalarından Tip Üretimi:
Eğer projenizde GraphQL kullanılıyorsa, graphql-codegen gibi araçlar, GraphQL şemasından ve sorgularından (queries/mutations) TypeScript tiplerini otomatik olarak üretebilir. Bu, özellikle büyük GraphQL API'lerinde sorgu ve yanıt tiplerini yönetmek için çok güçlü bir yaklaşımdır.
* Nasıl Yapılır? Bir codegen.yml yapılandırma dosyası oluşturarak GraphQL şemanızın nerede olduğunu ve hangi tiplerin üretileceğini belirtirsiniz.
# graphql-codegen kurulumu
npm install -D @graphql-codegen/cli @graphql-codegen/typescript
# codegen.yml örneği
overwrite: true
schema: "http://localhost:4000/graphql" # GraphQL API'nizin adresi
documents: "src/**/*.graphql" # GraphQL sorgularınızın olduğu dosyalar
generates:
./src/generated/graphql.ts: # Çıktı dosyasının yolu
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo" # React projeleri için
config:
skipTypename: true
* Ardından, graphql-codegen komutunu çalıştırarak tipleri oluşturursunuz.
Tip Versiyonlama Stratejileri: Değişim Yönetimi
Büyük projelerde API'ler zamanla gelişir ve değişir. Bu durum, farklı versiyonlarda çalışan frontend veya backend servisleriniz olduğunda tip uyumsuzluklarına yol açabilir. Tip versiyonlama, bu karmaşıklığı yönetmek için önemlidir.
- Semantik Versiyonlama (Semantic Versioning): Paylaşılan tip paketiniz için (örneğin
@monorepo/types) semantik versiyonlama (MAJOR.MINOR.PATCH) kullanın. API'deki kırıcı değişiklikler (breaking changes) için MAJOR versiyonu artırın, geriye dönük uyumlu yeni özellikler için MINOR, hata düzeltmeleri için PATCH kullanın. - Ayrı Klasörler: Eğer API'nizin birden fazla aktif versiyonu varsa (
/v1/users,/v2/usersgibi), tip tanımlarınızı da buna göre klasörleyebilirsiniz:packages/@monorepo/types/src/v1,packages/@monorepo/types/src/v2. Bu, farklı versiyonların aynı anda kullanılması gerektiğinde netlik sağlar.
Linting ve Static Analiz: Güvenliği Artırın
ESLint ve TypeScript'in güçlü entegrasyonu sayesinde, tip bağımlı linting kuralları uygulayabilirsiniz. Bu, tip güvenliği en üst seviyede tutmak için harika bir yoldur.
@typescript-eslint/eslint-plugin: Bu eklenti, TypeScript kodu için özel linting kuralları sunar. Örneğin,no-unsafe-assignment,no-explicit-anygibi kurallar, tiplerin yanlış kullanılmasını veyaanytipinin gereksiz yere kullanılmasını engeller.// .eslintrc.js module.exports = { parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], rules: { '@typescript-eslint/no-explicit-any': 'error', // 'any' kullanımını yasaklar '@typescript-eslint/no-unsafe-assignment': 'warn', // Güvenli olmayan atamaları uyarır }, };strictModu:tsconfig.jsondosyanızdastrict: trueayarını mutlaka etkinleştirin. Bu, null/undefined kontrolleri, kesin tip kontrolleri gibi birçok önemli güvenlik özelliğini otomatik olarak açar.
"Type First" Yaklaşımı: Tasarımı Önden Yapın
Bu yaklaşım, kod yazmaya başlamadan önce API kontratlarını (yani tipleri) tasarlamayı ve üzerinde anlaşmayı içerir. Özellikle BFF gibi bir ara katmanınız varsa, bu yaklaşım çok değerlidir.
- Önce
packages/@monorepo/typespaketi içinde API istek ve yanıt tiplerini oluşturun. - Frontend ve BFF ekipleri bu tipler üzerinde anlaşır.
- Ardından, hem frontend bu tiplere göre UI bileşenlerini geliştirir hem de BFF bu tipleri kullanarak API implementasyonunu yapar.
Bu, geliştirme sürecindeki belirsizliği azaltır ve her iki tarafın da net bir kontrat üzerinden çalışmasını sağlar.
Generic Tipler ve Utility Tipler: Esneklik ve Tekrar Kullanım
TypeScript'in sunduğu generic tipler () ve utility tipler (Partial, Pick, Omit, Exclude, Record vb.) tip tanımlarınızı daha esnek, tekrar kullanılabilir ve okunabilir hale getirir.
Örneğin, bir ApiResponse tipini generic olarak tanımlamak, farklı veri tipleriyle kullanılabilmesini sağlar:
// packages/@monorepo/types/src/common.ts
export interface ApiResponse {
success: boolean;
data?: T;
message?: string;
statusCode: number;
}
// Kullanım
const userResponse: ApiResponse = { success: true, data: { /* user object */ }, statusCode: 200 };
const productListResponse: ApiResponse = { success: true, data: [/* product array */], statusCode: 200 };
Bu ileri düzey teknikler, tip paylaşımı stratejinizi daha sağlam, otomatik ve sürdürülebilir hale getirecektir. Özellikle büyük ölçekli ve uzun soluklu projelerde, bu yaklaşımlar geliştirici verimliliğini ve yazılım kalitesini önemli ölçüde artıracaktır.
Sonuç: Güvenli ve Hızlı Geliştirmenin Anahtarı
Bu makale boyunca, monorepo ve BFF (Backend-for-Frontend) mimarilerinde TypeScript tiplerini paylaşmanın neden modern yazılım geliştirme için vazgeçilmez olduğunu detaylıca inceledik. Paylaşılan tiplerin olmaması durumunda ortaya çıkan çalışma zamanı hatalarından, geliştirici deneyimi düşüşlerine kadar birçok soruna değindik ve bu sorunların önüne geçmek için somut yaklaşımlar sunduk. Gördük ki, ister ayrı bir tip paketi oluşturarak ister tsconfig.json paths kullanarak olsun, tip güvenliğini tüm ekosistemde sağlamak mümkündür.
Özellikle BFF katmanında, frontend ve ana backend arasındaki kritik aracı rolü nedeniyle tip paylaşımı çok daha büyük bir önem taşımaktadır. API kontratlarının TypeScript tipleriyle güvence altına alınması, hem frontend hem de backend ekiplerinin daha bağımsız ve güvenle çalışmasına olanak tanır. Otomatik tip üretimi, versiyonlama stratejileri ve gelişmiş linting kuralları gibi ileri düzey teknikler ise bu süreci daha da optimize ederek manuel hataları sıfıra indirmeyi ve geliştirme akışını hızlandırmayı hedefler.
Özetle, TypeScript tiplerini monorepo ve BFF yapılarında paylaşmak, sadece derleme anı hatalarını engellemekle kalmaz, aynı zamanda ekipler arası iletişimi güçlendirir, entegrasyon süreçlerini basitleştirir, kod kalitesini artırır ve en önemlisi, genel geliştirme hızınızı ve güvenilirliğinizi yükseltir. Yazılım projelerinizde daha az hata, daha hızlı teslimat ve daha mutlu geliştiriciler istiyorsanız, tip paylaşımını geliştirme stratejinizin merkezine almalısınız. Bu rehberdeki bilgi ve örnekleri kullanarak, siz de projelerinizde bu güçlü yaklaşımları uygulamaya başlayabilir, güvenli ve hızlı geliştirmenin kapılarını aralayabilirsiniz.
Sıkça Sorulan Sorular (SSS)
1. Paylaşılan tiplerin boyutunun büyümesi performans sorunlarına yol açar mı?
Hayır, genellikle yol açmaz. Paylaşılan tip paketleri, genellikle sadece .d.ts (declaration) dosyalarını içerir. Bu dosyalar, derleme sırasında TypeScript tarafından kullanılır ancak çalışma zamanında JavaScript koduna dahil edilmezler. Dolayısıyla, uygulama boyutunu veya çalışma zamanı performansını etkilemezler. Derleme süresinde ise, ekstra tip kontrolü nedeniyle minimal bir artış olabilir, ancak bu genellikle sağladığı güvenlik ve geliştirici verimliliği faydalarının yanında ihmal edilebilir düzeydedir.
2. Her zaman ayrı bir tip paketi (@monorepo/types) oluşturmalı mıyım?
Büyük ve karmaşık monorepolar için ayrı bir tip paketi oluşturmak kesinlikle önerilir. Bu, tip tanımlarını daha düzenli bir şekilde organize etmenizi, bağımsız olarak versiyonlamanızı ve diğer projelere net bir bağımlılık olarak eklemenizi sağlar. Ancak daha küçük, az sayıda projenin bulunduğu monorepolar için tsconfig.json içindeki paths takma adları da yeterli olabilir. Seçiminiz, projenizin ölçeğine ve uzun vadedeki bakım beklentilerinize bağlı olmalıdır.
3. Frontend ve Backend aynı tipleri mi kullanmalı? Yoksa BFF'te dönüşüm olmalı mı?
İdeal senaryo, frontend'in doğrudan kullanacağı API kontrat tiplerinin paylaşılan tipler içinde tanımlanmasıdır. BFF'in rolü, ana backend'den gelen veriyi (genellikle daha genel veya farklı formatta) frontend'in beklediği bu paylaşılan tiplere uygun hale getirmektir. Dolayısıyla, ana backend'in kendi iç tipleri olabilir, ancak BFF, frontend ile arasındaki kontratı temsil eden paylaşılan tiplere göre dönüşüm yapmalıdır. Bu, hem tip güvenliğini sağlar hem de BFF'in esnekliğini korur.
4. TypeScript tipleri güvenliği ne kadar artırır? Tüm hataları engeller mi?
TypeScript tipleri, çalışma zamanı hatalarının büyük bir kısmını (özellikle tip uyumsuzluklarından kaynaklananları) derleme anında yakalayarak uygulama güvenliğini önemli ölçüde artırır. Bu, geliştiricilerin kodlarını daha güvenle yazmasını ve potansiyel hataları erken aşamada tespit etmesini sağlar. Ancak, TypeScript iş mantığı hatalarını, asenkronizmle ilgili sorunları veya dış entegrasyonlardaki beklenmedik durumları otomatik olarak engellemez. Tip güvenliği, sağlam bir test stratejisi (birim testleri, entegrasyon testleri) ve kapsamlı hata yönetimi ile birlikte kullanıldığında tam potansiyeline ulaşır.