Yazilim Mimarisi & Best Practices -- Kapsamli Rehber
📌 Bu Rehber Nedir?
Tüm backend rehberlerinin ustundeki "nasil duzgun yazilir" katmani. Framework'den bagimsiz yazilim muhendisligi prensipleri.
Kapsam: Mimari yaklasimlar, design patterns, SOLID, clean code, API tasarimi, auth, caching, queue, error handling, CI/CD, güvenlik, performans, testing
1) Mimari Yaklasimlar
Yazilim mimarisi, bir sistemin yüksek seviyeli yapisini tanimlar. Doğru mimari secimi projenin basarisini, olceklenebilirligini ve bakim kolayligini dogrudan etkiler.
1.1 Monolith (Yekpare Mimari)
Tüm uygulama tek bir codebase ve tek bir deployment unit olarak çalışır. Tüm moduller (auth, payment, notification, reporting) ayni process icinde yer alir.
Tipik Monolith Proje Yapisi:
my-app/
src/
controllers/
AuthController.ts
ProductController.ts
OrderController.ts
services/
AuthService.ts
ProductService.ts
OrderService.ts
repositories/
UserRepository.ts
ProductRepository.ts
OrderRepository.ts
models/
User.ts
Product.ts
Order.ts
middlewares/
authMiddleware.ts
rateLimiter.ts
routes/
index.ts
utils/
logger.ts
validator.ts
app.ts
tests/
package.json
tsconfig.jsonAvantaj / Dezavantaj Tablosu:
| Avantaj | Dezavantaj |
|---|---|
| Basit deployment (tek artifact) | Buyudukce karmasiklasir |
| Kolay debugging (tek process) | Tek bir hata tüm sistemi etkiler |
| Düşük operasyonel maliyetler | Bagimsiz olcekleme mumkun degil |
| Kolay transaction yönetimi | Büyük takimlarda merge conflict'ler artar |
| IDE'de tüm kodu gorursun | Build süresi uzar (büyük projelerde) |
| Hızlı prototipleme | Teknoloji degistirmek cok zor |
Ne Zaman Monolith?
- Küçük-orta ölçekli projeler (0-5 gelistirici)
- MVP / proof of concept
- Domain henuz net degil (sinirlari kesfediyorsun)
- Startup'in ilk versiyonu
// Monolith Express.js ornegi -- her sey tek app icinde
import express from 'express';
import { authRoutes } from './routes/auth';
import { productRoutes } from './routes/product';
import { orderRoutes } from './routes/order';
import { notificationRoutes } from './routes/notification';
const app = express();
app.use(express.json());
// Tum moduller tek uygulama icinde
app.use('/api/auth', authRoutes);
app.use('/api/products', productRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api/notifications', notificationRoutes);
app.listen(3000, () => console.log('Monolith running on :3000'));1.2 Microservices (Mikro Servis Mimarisi)
Her bir is birimini (domain) bagimsiz bir servis olarak ayirirsin. Her servis kendi veritabanina sahip olabilir, bagimsiz deploy edilir ve farkli dillerde yazılabilir.
Ne Zaman Microservices'e Gec?
Asagidaki isaretlerden en az 3-4 tanesi varsa:
- Takimin 15+ gelistirici ve birbirinin kodunu bozuyor
- Farkli moduller farkli olceklenme ihtiyaci duyuyor (ornegin: siparis servisi cok yogun, raporlama düşük)
- Deployment bir modul için tüm sistemi riske atiyor
- Farkli teknoloji ihtiyaclari var (bir modul Python ML, digeri Node.js)
- Monolit build/test süresi 30+ dakikayi gecti
- Domain sinirlar netlesti, bounded context'ler ortaya çıktı
Service Decomposition Stratejileri:
| Strateji | Açıklama | Örnek |
|---|---|---|
| Business Capability | Is yeteneklerine gore bol | Payment Service, Inventory Service |
| Subdomain (DDD) | Domain-Driven Design bounded context | Order BC, Shipping BC |
| Verb/Usecase | Is aksiyonlarina gore | CheckoutService, SearchService |
| Data Ownership | Veriye sahiplige gore | CustomerDataService, ProductCatalogService |
Bounded Context Ornegi (E-ticaret):
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Product Catalog │ │ Order Service │ │ Payment Service │
│ Service │ │ │ │ │
│ ─────────────── │ │ ─────────────── │ │ ─────────────── │
│ - product CRUD │ │ - order create │ │ - charge │
│ - categories │◄──►│ - order status │◄──►│ - refund │
│ - search/filter │ │ - cart │ │ - invoice │
│ ─────────────── │ │ ─────────────── │ │ ─────────────── │
│ PostgreSQL │ │ PostgreSQL │ │ PostgreSQL │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
┌───────────┴───────────┐
│ API Gateway │
│ (Kong / Nginx) │
└───────────────────────┘// Order Service -- bagimsiz Express app
// services/order-service/src/app.ts
import express from 'express';
import { orderRoutes } from './routes/order';
import { connectDB } from './database';
const app = express();
app.use(express.json());
app.use('/api/orders', orderRoutes);
connectDB().then(() => {
app.listen(3001, () => console.log('Order Service running on :3001'));
});
// Diger servisle iletisim: HTTP veya Message Queue
import axios from 'axios';
async function createOrder(orderData: CreateOrderDTO) {
// 1. Order'i kaydet
const order = await orderRepository.create(orderData);
// 2. Payment Service'e HTTP istegi
const payment = await axios.post('http://payment-service:3002/api/payments', {
orderId: order.id,
amount: order.totalAmount,
currency: 'TRY'
});
// 3. Notification Service'e event gonder (async -- queue uzerinden)
await messageQueue.publish('order.created', {
orderId: order.id,
userId: order.userId,
email: orderData.email
});
return order;
}1.3 Serverless (FaaS -- Function as a Service)
Her bir endpoint veya işlem, bagimsiz bir fonksiyon olarak çalışır. Altyapi yönetimi cloud provider'a birakilir. Sadece fonksiyon calisirken ucret odersin.
Yaygin Platformlar:
- AWS Lambda + API Gateway
- Vercel Functions (Next.js ile)
- Cloudflare Workers
- Google Cloud Functions
- Azure Functions
// AWS Lambda ornegi -- siparis olusturma
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
const dynamodb = new DynamoDB.DocumentClient();
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const body = JSON.parse(event.body || '{}');
const order = {
id: generateId(),
userId: body.userId,
items: body.items,
total: calculateTotal(body.items),
status: 'pending',
createdAt: new Date().toISOString()
};
await dynamodb.put({
TableName: 'Orders',
Item: order
}).promise();
return {
statusCode: 201,
body: JSON.stringify({ success: true, data: order })
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ success: false, message: 'Internal server error' })
};
}
};// Vercel Serverless Function ornegi
// api/products/[id].ts
import type { VercelRequest, VercelResponse } from '@vercel/node';
export default async function handler(req: VercelRequest, res: VercelResponse) {
const { id } = req.query;
if (req.method === 'GET') {
const product = await db.product.findUnique({ where: { id: String(id) } });
if (!product) return res.status(404).json({ message: 'Product not found' });
return res.status(200).json({ success: true, data: product });
}
return res.status(405).json({ message: 'Method not allowed' });
}Serverless Avantaj / Dezavantaj:
| Avantaj | Dezavantaj |
|---|---|
| Sifir altyapi yönetimi | Cold start sorunu (ilk istek yavas) |
| Otomatik olcekleme | Vendor lock-in riski |
| Kullanildigi kadar ode | Debugging ve monitoring zor |
| Hızlı deploy | Execution time limiti (15 dk AWS Lambda) |
| Yüksek elde edilebilirlik | Stateful islemler için uygun degil |
1.4 Modular Monolith (Moduler Yekpare)
Monolith'in basitligini korurken, iceride domain-driven modullere ayirirsin. Her modul kendi controller, service, repository katmanina sahiptir. Moduller arasi iletisim tanimli interface'ler uzerinden olur.
my-app/
src/
modules/
product/
controllers/ProductController.ts
services/ProductService.ts
repositories/ProductRepository.ts
models/Product.ts
dto/CreateProductDTO.ts
product.module.ts # modul tanimi
product.routes.ts
order/
controllers/OrderController.ts
services/OrderService.ts
repositories/OrderRepository.ts
models/Order.ts
dto/CreateOrderDTO.ts
order.module.ts
order.routes.ts
payment/
controllers/PaymentController.ts
services/PaymentService.ts
...
notification/
services/NotificationService.ts
...
shared/
middleware/
utils/
database/
app.ts// modules/product/product.module.ts
// Her modulun disindan erisim sadece tanimli interface uzerinden
export interface IProductModule {
getProductById(id: string): Promise<ProductDTO>;
getProductsByIds(ids: string[]): Promise<ProductDTO[]>;
checkStock(productId: string, quantity: number): Promise<boolean>;
}
export class ProductModule implements IProductModule {
constructor(private productService: ProductService) {}
async getProductById(id: string): Promise<ProductDTO> {
return this.productService.findById(id);
}
async getProductsByIds(ids: string[]): Promise<ProductDTO[]> {
return this.productService.findByIds(ids);
}
async checkStock(productId: string, quantity: number): Promise<boolean> {
return this.productService.hasStock(productId, quantity);
}
}
// modules/order/services/OrderService.ts
// Order modulu, Product modulune interface uzerinden erisir
class OrderService {
constructor(
private orderRepo: OrderRepository,
private productModule: IProductModule // modul arayuzu
) {}
async createOrder(dto: CreateOrderDTO) {
// Product modulune interface uzerinden eris
for (const item of dto.items) {
const hasStock = await this.productModule.checkStock(item.productId, item.quantity);
if (!hasStock) {
throw new InsufficientStockError(item.productId);
}
}
return this.orderRepo.create(dto);
}
}Modular Monolith Ne Zaman?
- Domain sinirlar belli ama microservices operasyonel yukune hazir degilsin
- Takimda 5-15 gelistirici
- Ileride microservices'e gecis ihtimali var (moduller zaten hazir)
- Monolith'in "spagetti" olmasini onlemek istiyorsun
1.5 Karar Agaci -- Hangi Mimariy Secmeliyim?
| Kriter | Monolith | Modular Monolith | Microservices | Serverless |
|---|---|---|---|---|
| Takimda kisi | 1-5 | 5-15 | 15+ | 1-10 |
| Proje yasam süresi | 0-2 yil | 2-5 yil | 5+ yil | Değişken |
| Domain karmasikligi | Düşük | Orta-Yüksek | Yüksek | Düşük-Orta |
| Olceklenme ihtiyaci | Düşük | Orta | Yüksek | Otomatik |
| DevOps olgunlugu | Düşük | Orta | Yüksek (zorunlu) | Düşük |
| MVP / prototip | En uygun | Uygun | Asiri muhendislik | Uygun |
| Bagimsiz deploy | Yok | Yok | Var | Var (fonksiyon bazli) |
| Teknoloji cesitliligi | Tek dil | Tek dil | Her servis farkli olabilir | Genellikle tek dil |
| Operasyonel maliyet | Düşük | Düşük-Orta | Yüksek | Değişken |
Önemli Kural
"Microservices" bir hedef degil, bir aractir. Cogu proje Monolith veya Modular Monolith ile baslamali. Erken mikroservis gecisi gereksiz karmasiklik ve operasyonel yuk getirir. Martin Fowler: "Don't start with microservices -- monolith first."
2) Katmanli Mimari (Layered Architecture)
Katmanli mimari, uygulama kodunu sorumluluk alanlarina gore yatay katmanlara ayirir. Her katman sadece bir alt katmanla iletisim kurar.
2.1 Katmanlar ve Akis
HTTP Request
│
▼
┌─────────────────────────────┐
│ Controller (Presentation) │ ← HTTP request/response isler
│ - Route handler │ ← Input validation yapar
│ - Request parsing │ ← Response format belirler
└──────────────┬──────────────┘
│ DTO
▼
┌─────────────────────────────┐
│ Service (Business Logic) │ ← Is kurallari burada
│ - Validation │ ← Transaction yonetimi
│ - Business rules │ ← Diger servisleri cagirabilir
│ - Orchestration │
└──────────────┬──────────────┘
│ Entity / Model
▼
┌─────────────────────────────┐
│ Repository (Data Access) │ ← Veritabani islemleri
│ - CRUD operations │ ← Query builder / ORM
│ - Query optimization │ ← Cache layer
└──────────────┬──────────────┘
│ SQL / Query
▼
┌─────────────────────────────┐
│ Database (Persistence) │ ← PostgreSQL, MongoDB, vs.
└─────────────────────────────┘2.2 Her Katmanin Sorumlulugu
| Katman | Ne Yapmali | Ne Yapmamali |
|---|---|---|
| Controller | HTTP request parse et, input validate et, response dondur | Is mantigi yazma, direkt DB sorgusu yapma |
| Service | Is kurallarini uygula, transaction yonet, servisleri koordine et | HTTP request/response ile ilgilenme, SQL yazma |
| Repository | DB CRUD islemleri, query optimize et | Is kurali uygulama, HTTP bilgisi kullanma |
| Model/Entity | Veri yapisini tanimla, basit validasyonlar | Is mantigi barindirma (anemic model tartismali) |
| DTO | Katmanlar arasi veri tasima, response seklini belirle | Is mantigi barindirma |
| Middleware | Cross-cutting concerns (auth, logging, rate limit) | Is mantigi, DB erisimi |
2.3 Gercek Dunya Ornegi: E-ticaret Product CRUD
Bir e-ticaret uygulamasinda urun yönetimi -- tüm katmanlariyla:
Model:
// models/Product.ts
export interface Product {
id: string;
name: string;
slug: string;
description: string;
price: number;
stock: number;
categoryId: string;
images: string[];
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}DTO'lar:
// dto/CreateProductDTO.ts
export interface CreateProductDTO {
name: string;
description: string;
price: number;
stock: number;
categoryId: string;
images?: string[];
}
// dto/UpdateProductDTO.ts
export interface UpdateProductDTO {
name?: string;
description?: string;
price?: number;
stock?: number;
categoryId?: string;
images?: string[];
isActive?: boolean;
}
// dto/ProductResponseDTO.ts
export interface ProductResponseDTO {
id: string;
name: string;
slug: string;
description: string;
price: number;
formattedPrice: string; // "149,99 TL"
stock: number;
inStock: boolean;
category: {
id: string;
name: string;
};
images: string[];
createdAt: string;
}
// dto/ProductListDTO.ts
export interface ProductListDTO {
items: ProductResponseDTO[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}Repository:
// repositories/ProductRepository.ts
import { Pool } from 'pg';
import { Product } from '../models/Product';
export class ProductRepository {
constructor(private db: Pool) {}
async findById(id: string): Promise<Product | null> {
const result = await this.db.query(
'SELECT * FROM products WHERE id = $1',
[id]
);
return result.rows[0] || null;
}
async findAll(options: {
page: number;
limit: number;
categoryId?: string;
search?: string;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}): Promise<{ items: Product[]; total: number }> {
const { page, limit, categoryId, search, sortBy = 'created_at', sortOrder = 'desc' } = options;
const offset = (page - 1) * limit;
const conditions: string[] = ['is_active = true'];
const params: any[] = [];
if (categoryId) {
params.push(categoryId);
conditions.push(`category_id = $${params.length}`);
}
if (search) {
params.push(`%${search}%`);
conditions.push(`(name ILIKE $${params.length} OR description ILIKE $${params.length})`);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const countResult = await this.db.query(
`SELECT COUNT(*) FROM products ${where}`,
params
);
params.push(limit, offset);
const result = await this.db.query(
`SELECT * FROM products ${where} ORDER BY ${sortBy} ${sortOrder} LIMIT $${params.length - 1} OFFSET $${params.length}`,
params
);
return {
items: result.rows,
total: parseInt(countResult.rows[0].count)
};
}
async create(data: Partial<Product>): Promise<Product> {
const result = await this.db.query(
`INSERT INTO products (id, name, slug, description, price, stock, category_id, images, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
RETURNING *`,
[data.id, data.name, data.slug, data.description, data.price, data.stock, data.categoryId, JSON.stringify(data.images), data.isActive]
);
return result.rows[0];
}
async update(id: string, data: Partial<Product>): Promise<Product | null> {
const fields: string[] = [];
const params: any[] = [];
let paramIndex = 1;
for (const [key, value] of Object.entries(data)) {
if (value !== undefined) {
fields.push(`${this.toSnakeCase(key)} = $${paramIndex}`);
params.push(key === 'images' ? JSON.stringify(value) : value);
paramIndex++;
}
}
if (fields.length === 0) return this.findById(id);
fields.push(`updated_at = NOW()`);
params.push(id);
const result = await this.db.query(
`UPDATE products SET ${fields.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
params
);
return result.rows[0] || null;
}
async delete(id: string): Promise<boolean> {
const result = await this.db.query(
'UPDATE products SET is_active = false, updated_at = NOW() WHERE id = $1',
[id]
);
return (result.rowCount ?? 0) > 0;
}
private toSnakeCase(str: string): string {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
}Service:
// services/ProductService.ts
import { v4 as uuidv4 } from 'uuid';
import { ProductRepository } from '../repositories/ProductRepository';
import { CategoryRepository } from '../repositories/CategoryRepository';
import { CreateProductDTO, UpdateProductDTO, ProductResponseDTO, ProductListDTO } from '../dto';
import { NotFoundError, ValidationError } from '../errors';
export class ProductService {
constructor(
private productRepo: ProductRepository,
private categoryRepo: CategoryRepository
) {}
async getProduct(id: string): Promise<ProductResponseDTO> {
const product = await this.productRepo.findById(id);
if (!product) {
throw new NotFoundError('Product', id);
}
const category = await this.categoryRepo.findById(product.categoryId);
return this.toResponseDTO(product, category);
}
async listProducts(query: {
page?: number;
limit?: number;
categoryId?: string;
search?: string;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}): Promise<ProductListDTO> {
const page = Math.max(1, query.page || 1);
const limit = Math.min(100, Math.max(1, query.limit || 20));
const { items, total } = await this.productRepo.findAll({
page,
limit,
categoryId: query.categoryId,
search: query.search,
sortBy: query.sortBy,
sortOrder: query.sortOrder
});
const categories = await this.categoryRepo.findByIds(
[...new Set(items.map(p => p.categoryId))]
);
const categoryMap = new Map(categories.map(c => [c.id, c]));
return {
items: items.map(p => this.toResponseDTO(p, categoryMap.get(p.categoryId))),
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit)
}
};
}
async createProduct(dto: CreateProductDTO): Promise<ProductResponseDTO> {
// Is kurali: Fiyat 0'dan buyuk olmali
if (dto.price <= 0) {
throw new ValidationError('Price must be greater than 0');
}
// Is kurali: Kategori var mi kontrol et
const category = await this.categoryRepo.findById(dto.categoryId);
if (!category) {
throw new ValidationError(`Category not found: ${dto.categoryId}`);
}
const product = await this.productRepo.create({
id: uuidv4(),
name: dto.name,
slug: this.generateSlug(dto.name),
description: dto.description,
price: dto.price,
stock: dto.stock,
categoryId: dto.categoryId,
images: dto.images || [],
isActive: true
});
return this.toResponseDTO(product, category);
}
async updateProduct(id: string, dto: UpdateProductDTO): Promise<ProductResponseDTO> {
const existing = await this.productRepo.findById(id);
if (!existing) {
throw new NotFoundError('Product', id);
}
if (dto.price !== undefined && dto.price <= 0) {
throw new ValidationError('Price must be greater than 0');
}
const updated = await this.productRepo.update(id, {
...dto,
slug: dto.name ? this.generateSlug(dto.name) : undefined
});
const category = await this.categoryRepo.findById(updated!.categoryId);
return this.toResponseDTO(updated!, category);
}
async deleteProduct(id: string): Promise<void> {
const existing = await this.productRepo.findById(id);
if (!existing) {
throw new NotFoundError('Product', id);
}
await this.productRepo.delete(id);
}
private toResponseDTO(product: any, category: any): ProductResponseDTO {
return {
id: product.id,
name: product.name,
slug: product.slug,
description: product.description,
price: product.price,
formattedPrice: `${product.price.toLocaleString('tr-TR')} TL`,
stock: product.stock,
inStock: product.stock > 0,
category: category
? { id: category.id, name: category.name }
: { id: product.categoryId, name: 'Unknown' },
images: product.images,
createdAt: product.createdAt
};
}
private generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
}Controller:
// controllers/ProductController.ts
import { Request, Response, NextFunction } from 'express';
import { ProductService } from '../services/ProductService';
import { CreateProductDTO, UpdateProductDTO } from '../dto';
export class ProductController {
constructor(private productService: ProductService) {}
// GET /api/products/:id
getProduct = async (req: Request, res: Response, next: NextFunction) => {
try {
const product = await this.productService.getProduct(req.params.id);
res.json({ success: true, data: product });
} catch (error) {
next(error);
}
};
// GET /api/products
listProducts = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.productService.listProducts({
page: parseInt(req.query.page as string) || 1,
limit: parseInt(req.query.limit as string) || 20,
categoryId: req.query.categoryId as string,
search: req.query.search as string,
sortBy: req.query.sortBy as string,
sortOrder: req.query.sortOrder as 'asc' | 'desc'
});
res.json({ success: true, ...result });
} catch (error) {
next(error);
}
};
// POST /api/products
createProduct = async (req: Request, res: Response, next: NextFunction) => {
try {
const dto: CreateProductDTO = req.body;
const product = await this.productService.createProduct(dto);
res.status(201).json({ success: true, data: product });
} catch (error) {
next(error);
}
};
// PUT /api/products/:id
updateProduct = async (req: Request, res: Response, next: NextFunction) => {
try {
const dto: UpdateProductDTO = req.body;
const product = await this.productService.updateProduct(req.params.id, dto);
res.json({ success: true, data: product });
} catch (error) {
next(error);
}
};
// DELETE /api/products/:id
deleteProduct = async (req: Request, res: Response, next: NextFunction) => {
try {
await this.productService.deleteProduct(req.params.id);
res.status(204).send();
} catch (error) {
next(error);
}
};
}Route Dosyasi:
// routes/product.routes.ts
import { Router } from 'express';
import { ProductController } from '../controllers/ProductController';
import { authMiddleware } from '../middlewares/authMiddleware';
import { adminMiddleware } from '../middlewares/adminMiddleware';
import { validate } from '../middlewares/validate';
import { createProductSchema, updateProductSchema } from '../validators/productValidator';
const router = Router();
const controller = new ProductController(productService);
router.get('/', controller.listProducts);
router.get('/:id', controller.getProduct);
router.post('/', authMiddleware, adminMiddleware, validate(createProductSchema), controller.createProduct);
router.put('/:id', authMiddleware, adminMiddleware, validate(updateProductSchema), controller.updateProduct);
router.delete('/:id', authMiddleware, adminMiddleware, controller.deleteProduct);
export { router as productRoutes };2.4 DTO Pattern -- Neden Model Direkt Dondurulmez?
// YANLIS: Model direkt dondurme
app.get('/api/users/:id', async (req, res) => {
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.json(user.rows[0]);
// Sorun: password_hash, internal_notes gibi hassas alanlar da doner!
});
// DOGRU: DTO kullanarak sadece gerekli alanlari dondur
app.get('/api/users/:id', async (req, res) => {
const user = await userService.getUser(req.params.id);
res.json({
success: true,
data: {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
createdAt: user.createdAt
// password_hash, internal_notes DONMEZ
}
});
});DTO Kullanmanin Nedenleri:
| Neden | Açıklama |
|---|---|
| Güvenlik | Hassas alanlar (password, token) disari sizamaz |
| Decoupling | DB semasi degisse API kontrati degismez |
| Performans | Sadece gerekli alanlar serialize edilir |
| Versiyon yönetimi | Farkli API versiyonlari için farkli DTO'lar |
| Dokumantasyon | DTO tipleri API'nin ne dondurdugunu net gosterir |
2.5 Dependency Flow
Bagimlilik yonu her zaman yukari katmandan asagi katmana doğru olmalidir. Asla alt katman ust katmana bagimli olmamal.
Controller ──depends on──> Service ──depends on──> Repository
│ │ │
▼ ▼ ▼
Express/HTTP Is mantigi Database/ORM
req, res, next kurallar query, CRUDYanlis Yonlu Bagimlilik:
// YANLIS: Repository'nin Service'e bagimliligi
class ProductRepository {
constructor(private productService: ProductService) {} // YANLIS!
}
// YANLIS: Service'in Request objesine bagimliligi
class ProductService {
async getProduct(req: Request) {} // YANLIS! HTTP bilgisi bilmemeli
}Doğru Yonlu Bagimlilik:
// DOGRU: Service repository'e bagimli
class ProductService {
constructor(private productRepo: ProductRepository) {} // DOGRU
}
// DOGRU: Controller service'e bagimli
class ProductController {
constructor(private productService: ProductService) {} // DOGRU
}3) Design Patterns (Tasarim Desenleri)
Design pattern'lar, yazilim gelistirmede tekrar eden sorunlara yonelik kanitlanmis cozumlerdir. Gang of Four (GoF) tarafindan Creational, Structural ve Behavioral olarak siniflandirilmistir.
3.1 Creational Patterns (Yaratimsal Desenler)
Singleton
Bir siniftan sadece bir tane instance olusturulmasini garanti eder. Global erisim noktasi sağlar.
Ne Zaman: Database connection, logger, configuration manager, cache client.
// Singleton -- Database Connection
class Database {
private static instance: Database;
private connection: any;
private constructor() {
// Private constructor -- disindan new yapilamaz
}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
async connect(connectionString: string) {
if (!this.connection) {
this.connection = await createConnection(connectionString);
}
return this.connection;
}
getConnection() {
if (!this.connection) {
throw new Error('Database not connected. Call connect() first.');
}
return this.connection;
}
}
// Kullanim
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true -- ayni instance// Modern JS/TS'de Singleton -- Module pattern ile daha basit
// database.ts
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20
});
// ES Module zaten singleton gibi davranir
// Her import ayni pool instance'ini alir
export default pool;
// Kullanim
import pool from './database';
const result = await pool.query('SELECT * FROM users');Factory
Nesne oluşturma mantigini bir fabrika sinifina/fonksiyonuna devreder. Hangi sinifin olusturulacagini parametreye gore belirler.
Ne Zaman: Bildirim gonderme (email/sms/push), odeme işleme (Stripe/PayPal/iyzico), dosya export (PDF/Excel/CSV).
// Factory -- Notification System
interface Notification {
send(to: string, message: string): Promise<void>;
}
class EmailNotification implements Notification {
async send(to: string, message: string) {
console.log(`Email to ${to}: ${message}`);
// await sendGrid.send({ to, subject: 'Notification', text: message });
}
}
class SMSNotification implements Notification {
async send(to: string, message: string) {
console.log(`SMS to ${to}: ${message}`);
// await twilio.messages.create({ to, body: message });
}
}
class PushNotification implements Notification {
async send(to: string, message: string) {
console.log(`Push to ${to}: ${message}`);
// await firebase.messaging().send({ token: to, notification: { body: message } });
}
}
// Factory
class NotificationFactory {
static create(type: 'email' | 'sms' | 'push'): Notification {
switch (type) {
case 'email': return new EmailNotification();
case 'sms': return new SMSNotification();
case 'push': return new PushNotification();
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
// Kullanim
const notification = NotificationFactory.create('email');
await notification.send('user@example.com', 'Siparisiz hazirlandi!');
// Farkli kanallardan bildirim gonder
const channels: Array<'email' | 'sms' | 'push'> = ['email', 'push'];
for (const channel of channels) {
const notifier = NotificationFactory.create(channel);
await notifier.send(user.contact[channel], 'Siparisiz kargoya verildi!');
}Builder
Karmasik nesneleri adim adim olusturur. Cok parametreli constructor yerine okunabilir bir API sağlar.
Ne Zaman: Karmasik query oluşturma, email/rapor oluşturma, konfigürasyon nesneleri.
// Builder -- Query Builder
class QueryBuilder {
private table: string = '';
private conditions: string[] = [];
private orderByClause: string = '';
private limitValue: number | null = null;
private offsetValue: number | null = null;
private selectedFields: string[] = ['*'];
private joinClauses: string[] = [];
private params: any[] = [];
from(table: string): this {
this.table = table;
return this;
}
select(...fields: string[]): this {
this.selectedFields = fields;
return this;
}
where(condition: string, value: any): this {
this.params.push(value);
this.conditions.push(`${condition} $${this.params.length}`);
return this;
}
join(table: string, on: string): this {
this.joinClauses.push(`JOIN ${table} ON ${on}`);
return this;
}
orderBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.orderByClause = `ORDER BY ${field} ${direction}`;
return this;
}
limit(value: number): this {
this.limitValue = value;
return this;
}
offset(value: number): this {
this.offsetValue = value;
return this;
}
build(): { sql: string; params: any[] } {
let sql = `SELECT ${this.selectedFields.join(', ')} FROM ${this.table}`;
if (this.joinClauses.length > 0) {
sql += ` ${this.joinClauses.join(' ')}`;
}
if (this.conditions.length > 0) {
sql += ` WHERE ${this.conditions.join(' AND ')}`;
}
if (this.orderByClause) {
sql += ` ${this.orderByClause}`;
}
if (this.limitValue !== null) {
sql += ` LIMIT ${this.limitValue}`;
}
if (this.offsetValue !== null) {
sql += ` OFFSET ${this.offsetValue}`;
}
return { sql, params: this.params };
}
}
// Kullanim -- okunabilir ve esnek
const query = new QueryBuilder()
.from('products')
.select('products.id', 'products.name', 'categories.name AS category')
.join('categories', 'categories.id = products.category_id')
.where('products.price >', 100)
.where('products.is_active =', true)
.orderBy('products.created_at', 'DESC')
.limit(20)
.offset(0)
.build();
// query.sql = "SELECT products.id, products.name, categories.name AS category
// FROM products JOIN categories ON categories.id = products.category_id
// WHERE products.price > $1 AND products.is_active = $2
// ORDER BY products.created_at DESC LIMIT 20 OFFSET 0"
// query.params = [100, true]3.2 Structural Patterns (Yapisal Desenler)
Adapter
Uyumsuz iki arayuzu birbiriyle calismaya uyumlu hale getirir. Ucuncu parti kütüphaneleri kendi interface'ine sararsin.
Ne Zaman: Ucuncu parti SDK degisikligi, farkli odeme saglayicilari, farkli email servisleri.
// Adapter -- Farkli odeme saglayicilarini tek interface altinda birlestir
// Ortak interface
interface PaymentGateway {
charge(amount: number, currency: string, source: string): Promise<PaymentResult>;
refund(transactionId: string, amount: number): Promise<RefundResult>;
}
interface PaymentResult {
transactionId: string;
status: 'success' | 'failed';
amount: number;
}
interface RefundResult {
refundId: string;
status: 'success' | 'failed';
}
// Stripe Adapter
class StripeAdapter implements PaymentGateway {
private stripe: Stripe;
constructor(apiKey: string) {
this.stripe = new Stripe(apiKey);
}
async charge(amount: number, currency: string, source: string): Promise<PaymentResult> {
const result = await this.stripe.paymentIntents.create({
amount: amount * 100, // Stripe kurusu cinsinden bekler
currency,
payment_method: source,
confirm: true
});
return {
transactionId: result.id,
status: result.status === 'succeeded' ? 'success' : 'failed',
amount: result.amount / 100
};
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
const result = await this.stripe.refunds.create({
payment_intent: transactionId,
amount: amount * 100
});
return {
refundId: result.id,
status: result.status === 'succeeded' ? 'success' : 'failed'
};
}
}
// iyzico Adapter
class IyzicoAdapter implements PaymentGateway {
private iyzico: any;
constructor(apiKey: string, secretKey: string) {
this.iyzico = new Iyzipay({ apiKey, secretKey, uri: 'https://api.iyzipay.com' });
}
async charge(amount: number, currency: string, source: string): Promise<PaymentResult> {
// iyzico farkli bir API yapisina sahip
const request = {
price: amount.toString(),
paidPrice: amount.toString(),
currency: currency === 'TRY' ? 'TRY' : currency,
paymentCard: { cardToken: source }
};
const result = await this.iyzico.payment.create(request);
return {
transactionId: result.paymentId,
status: result.status === 'success' ? 'success' : 'failed',
amount
};
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
const result = await this.iyzico.refund.create({
paymentTransactionId: transactionId,
price: amount.toString()
});
return {
refundId: result.paymentId,
status: result.status === 'success' ? 'success' : 'failed'
};
}
}
// Kullanim -- hangi provider kullanildigini bilmez
class PaymentService {
constructor(private gateway: PaymentGateway) {}
async processPayment(orderId: string, amount: number, cardToken: string) {
const result = await this.gateway.charge(amount, 'TRY', cardToken);
if (result.status === 'failed') {
throw new PaymentError('Payment failed');
}
return result;
}
}
// Stripe ile
const stripeService = new PaymentService(new StripeAdapter(STRIPE_KEY));
// iyzico ile -- kod degismeden provider degisir
const iyzicoService = new PaymentService(new IyzicoAdapter(IYZICO_KEY, IYZICO_SECRET));Decorator
Mevcut bir nesneye dinamik olarak yeni davranislar ekler. Kalitim yerine composition kullanir.
Ne Zaman: Loglama eklemek, cache eklemek, yetki kontrolu, performans olcme.
// Decorator -- Service'e loglama ve cache ekle
interface IUserService {
getUser(id: string): Promise<User>;
updateUser(id: string, data: Partial<User>): Promise<User>;
}
// Temel servis
class UserService implements IUserService {
async getUser(id: string): Promise<User> {
return await db.query('SELECT * FROM users WHERE id = $1', [id]);
}
async updateUser(id: string, data: Partial<User>): Promise<User> {
return await db.query('UPDATE users SET ... WHERE id = $1 RETURNING *', [id]);
}
}
// Logging Decorator
class LoggingUserService implements IUserService {
constructor(private wrapped: IUserService) {}
async getUser(id: string): Promise<User> {
console.log(`[UserService] getUser called with id: ${id}`);
const start = Date.now();
const result = await this.wrapped.getUser(id);
console.log(`[UserService] getUser completed in ${Date.now() - start}ms`);
return result;
}
async updateUser(id: string, data: Partial<User>): Promise<User> {
console.log(`[UserService] updateUser called with id: ${id}`);
const result = await this.wrapped.updateUser(id, data);
console.log(`[UserService] updateUser completed`);
return result;
}
}
// Cache Decorator
class CachedUserService implements IUserService {
constructor(
private wrapped: IUserService,
private cache: Map<string, { data: User; expiry: number }> = new Map()
) {}
async getUser(id: string): Promise<User> {
const cached = this.cache.get(id);
if (cached && cached.expiry > Date.now()) {
console.log(`[Cache] Hit for user ${id}`);
return cached.data;
}
console.log(`[Cache] Miss for user ${id}`);
const user = await this.wrapped.getUser(id);
this.cache.set(id, { data: user, expiry: Date.now() + 60000 }); // 1 dk cache
return user;
}
async updateUser(id: string, data: Partial<User>): Promise<User> {
const user = await this.wrapped.updateUser(id, data);
this.cache.delete(id); // Cache invalidation
return user;
}
}
// Kullanim -- decorator'lari ust uste sar
const userService = new LoggingUserService(
new CachedUserService(
new UserService()
)
);
// Cagirdiginda: Logging -> Cache kontrol -> (miss ise) DB sorgusu
const user = await userService.getUser('user-123');Facade
Karmasik bir alt sistemi basit bir arayuz arkasina gizler.
Ne Zaman: Karmasik 3. parti API'leri basitlestirme, birden fazla servisi koordine etme.
// Facade -- Siparis islemi birden fazla servisi koordine eder
class OrderFacade {
constructor(
private productService: ProductService,
private inventoryService: InventoryService,
private paymentService: PaymentService,
private shippingService: ShippingService,
private notificationService: NotificationService
) {}
// Disaridan tek bir metot cagirilir, karmasiklik icerde gizlenir
async placeOrder(userId: string, items: CartItem[], paymentInfo: PaymentInfo): Promise<Order> {
// 1. Urunleri dogrula
const products = await this.productService.validateProducts(items);
// 2. Stok kontrol
await this.inventoryService.checkAvailability(items);
// 3. Toplam tutar hesapla
const total = this.calculateTotal(products, items);
// 4. Odeme al
const payment = await this.paymentService.charge(total, paymentInfo);
// 5. Stoktan dus
await this.inventoryService.reserveStock(items);
// 6. Kargo olustur
const shipping = await this.shippingService.createShipment(userId, items);
// 7. Bildirim gonder
await this.notificationService.sendOrderConfirmation(userId, {
orderId: payment.transactionId,
total,
estimatedDelivery: shipping.estimatedDate
});
return {
id: payment.transactionId,
items: products,
total,
shippingId: shipping.id,
status: 'confirmed'
};
}
private calculateTotal(products: Product[], items: CartItem[]): number {
return items.reduce((sum, item) => {
const product = products.find(p => p.id === item.productId)!;
return sum + product.price * item.quantity;
}, 0);
}
}
// Kullanim -- tek satirda karmasik siparis islemi
const orderFacade = new OrderFacade(productService, inventoryService, paymentService, shippingService, notificationService);
const order = await orderFacade.placeOrder(userId, cartItems, paymentInfo);Proxy
Bir nesneye erisimi kontrol eder. Lazy loading, access control, logging, caching için kullanilir.
// Proxy -- API Rate Limiter
class RateLimitedApiClient {
private requestCounts: Map<string, { count: number; resetAt: number }> = new Map();
private maxRequests = 100; // dakikada max istek
private windowMs = 60000; // 1 dakika
constructor(private apiClient: HttpClient) {}
async request(endpoint: string, options: RequestOptions): Promise<Response> {
const now = Date.now();
const key = endpoint;
const record = this.requestCounts.get(key);
if (record && record.resetAt > now && record.count >= this.maxRequests) {
throw new RateLimitError(
`Rate limit exceeded for ${endpoint}. Try again in ${Math.ceil((record.resetAt - now) / 1000)}s`
);
}
if (!record || record.resetAt <= now) {
this.requestCounts.set(key, { count: 1, resetAt: now + this.windowMs });
} else {
record.count++;
}
return this.apiClient.request(endpoint, options);
}
}3.3 Behavioral Patterns (Davranissal Desenler)
Observer (Event Emitter)
Bir nesnenin durumu degistiginde bagli tüm dinleyicileri (subscriber) otomatik bilgilendirir. Event-driven mimarilerin temeli.
Ne Zaman: Kullanici kaydi sonrasi (email gonder, log yaz, analytics), siparis durumu degisikligi, real-time bildirimler.
// Observer -- Event System
type EventHandler = (...args: any[]) => void | Promise<void>;
class EventEmitter {
private listeners: Map<string, EventHandler[]> = new Map();
on(event: string, handler: EventHandler): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(handler);
}
off(event: string, handler: EventHandler): void {
const handlers = this.listeners.get(event);
if (handlers) {
this.listeners.set(event, handlers.filter(h => h !== handler));
}
}
async emit(event: string, ...args: any[]): Promise<void> {
const handlers = this.listeners.get(event) || [];
await Promise.all(handlers.map(handler => handler(...args)));
}
}
// Kullanim -- Kullanici kaydi sonrasi olaylar
const eventBus = new EventEmitter();
// Dinleyiciler (subscribers)
eventBus.on('user.registered', async (user) => {
await emailService.sendWelcomeEmail(user.email, user.name);
});
eventBus.on('user.registered', async (user) => {
await analyticsService.track('user_signup', { userId: user.id, source: user.source });
});
eventBus.on('user.registered', async (user) => {
await slackService.notify(`Yeni kullanici: ${user.name} (${user.email})`);
});
// Tetikleme (publisher)
class UserService {
constructor(private eventBus: EventEmitter) {}
async register(data: RegisterDTO) {
const user = await userRepo.create(data);
// Kayit sonrasi olaylari tetikle
await this.eventBus.emit('user.registered', user);
return user;
}
}// Node.js built-in EventEmitter ile
import { EventEmitter } from 'events';
class OrderService extends EventEmitter {
async createOrder(data: CreateOrderDTO) {
const order = await orderRepo.create(data);
this.emit('order:created', order);
return order;
}
async cancelOrder(orderId: string) {
const order = await orderRepo.updateStatus(orderId, 'cancelled');
this.emit('order:cancelled', order);
return order;
}
}
const orderService = new OrderService();
orderService.on('order:created', (order) => {
emailService.sendOrderConfirmation(order);
});
orderService.on('order:cancelled', (order) => {
paymentService.refund(order.paymentId);
inventoryService.restoreStock(order.items);
});Strategy
Algoritma ailesini tanimlar ve her birini degistirilebilir hale getirir. Runtime'da algoritmalar arasinda gecis yapılabilir.
Ne Zaman: Farkli fiyatlandirma stratejileri, farkli siralama algoritmalari, farkli dosya sıkistirma yontemleri.
// Strategy -- Fiyatlandirma stratejileri
interface PricingStrategy {
calculate(basePrice: number, context: PricingContext): number;
}
interface PricingContext {
quantity: number;
customerType: 'regular' | 'premium' | 'wholesale';
couponCode?: string;
}
class RegularPricing implements PricingStrategy {
calculate(basePrice: number, context: PricingContext): number {
return basePrice * context.quantity;
}
}
class PremiumPricing implements PricingStrategy {
calculate(basePrice: number, context: PricingContext): number {
const discount = 0.15; // %15 indirim
return basePrice * context.quantity * (1 - discount);
}
}
class WholesalePricing implements PricingStrategy {
calculate(basePrice: number, context: PricingContext): number {
let discount = 0;
if (context.quantity >= 100) discount = 0.30;
else if (context.quantity >= 50) discount = 0.20;
else if (context.quantity >= 10) discount = 0.10;
return basePrice * context.quantity * (1 - discount);
}
}
class PricingService {
private strategies: Map<string, PricingStrategy> = new Map();
constructor() {
this.strategies.set('regular', new RegularPricing());
this.strategies.set('premium', new PremiumPricing());
this.strategies.set('wholesale', new WholesalePricing());
}
calculatePrice(basePrice: number, context: PricingContext): number {
const strategy = this.strategies.get(context.customerType);
if (!strategy) {
throw new Error(`Unknown customer type: ${context.customerType}`);
}
return strategy.calculate(basePrice, context);
}
}
// Kullanim
const pricingService = new PricingService();
// Ayni urun, farkli musteri tipleri
const regularPrice = pricingService.calculatePrice(100, { quantity: 5, customerType: 'regular' });
// 500
const premiumPrice = pricingService.calculatePrice(100, { quantity: 5, customerType: 'premium' });
// 425 (%15 indirimli)
const wholesalePrice = pricingService.calculatePrice(100, { quantity: 50, customerType: 'wholesale' });
// 4000 (%20 indirimli)Command
Bir islemi nesne olarak kapsuller. Geri alma (undo), kuyruge alma, loglama için kullanilir.
// Command -- Undo/Redo destekli metin editoru
interface Command {
execute(): void;
undo(): void;
}
class InsertTextCommand implements Command {
constructor(
private document: TextDocument,
private position: number,
private text: string
) {}
execute(): void {
this.document.insert(this.position, this.text);
}
undo(): void {
this.document.delete(this.position, this.text.length);
}
}
class DeleteTextCommand implements Command {
private deletedText: string = '';
constructor(
private document: TextDocument,
private position: number,
private length: number
) {}
execute(): void {
this.deletedText = this.document.getText(this.position, this.length);
this.document.delete(this.position, this.length);
}
undo(): void {
this.document.insert(this.position, this.deletedText);
}
}
class CommandHistory {
private history: Command[] = [];
private undone: Command[] = [];
execute(command: Command): void {
command.execute();
this.history.push(command);
this.undone = []; // Yeni komut sonrasi redo gecmisi temizlenir
}
undo(): void {
const command = this.history.pop();
if (command) {
command.undo();
this.undone.push(command);
}
}
redo(): void {
const command = this.undone.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}State
Bir nesnenin davranisini ic durumuna gore degistirir. if/else zincirleri yerine her durumu ayri bir sınıf olarak modeller.
// State -- Siparis durumu yonetimi
interface OrderState {
name: string;
confirm(order: Order): void;
ship(order: Order): void;
deliver(order: Order): void;
cancel(order: Order): void;
}
class PendingState implements OrderState {
name = 'pending';
confirm(order: Order): void {
console.log('Siparis onaylandi');
order.setState(new ConfirmedState());
}
ship(order: Order): void {
throw new Error('Onaylanmamis siparis kargolanamaz');
}
deliver(order: Order): void {
throw new Error('Onaylanmamis siparis teslim edilemez');
}
cancel(order: Order): void {
console.log('Siparis iptal edildi');
order.setState(new CancelledState());
}
}
class ConfirmedState implements OrderState {
name = 'confirmed';
confirm(order: Order): void {
throw new Error('Siparis zaten onaylandi');
}
ship(order: Order): void {
console.log('Siparis kargoya verildi');
order.setState(new ShippedState());
}
deliver(order: Order): void {
throw new Error('Kargoya verilmeden teslim edilemez');
}
cancel(order: Order): void {
console.log('Onaylanmis siparis iptal edildi, iade basladi');
order.setState(new CancelledState());
}
}
class ShippedState implements OrderState {
name = 'shipped';
confirm(order: Order): void {
throw new Error('Kargodaki siparis tekrar onaylanamaz');
}
ship(order: Order): void {
throw new Error('Siparis zaten kargoda');
}
deliver(order: Order): void {
console.log('Siparis teslim edildi');
order.setState(new DeliveredState());
}
cancel(order: Order): void {
throw new Error('Kargodaki siparis iptal edilemez');
}
}
class DeliveredState implements OrderState {
name = 'delivered';
confirm(order: Order): void { throw new Error('Teslim edilmis siparis onaylanamaz'); }
ship(order: Order): void { throw new Error('Teslim edilmis siparis kargolanamaz'); }
deliver(order: Order): void { throw new Error('Siparis zaten teslim edildi'); }
cancel(order: Order): void { throw new Error('Teslim edilmis siparis iptal edilemez. Iade talebi olusturun.'); }
}
class CancelledState implements OrderState {
name = 'cancelled';
confirm(order: Order): void { throw new Error('Iptal edilmis siparis onaylanamaz'); }
ship(order: Order): void { throw new Error('Iptal edilmis siparis kargolanamaz'); }
deliver(order: Order): void { throw new Error('Iptal edilmis siparis teslim edilemez'); }
cancel(order: Order): void { throw new Error('Siparis zaten iptal edildi'); }
}
class Order {
private state: OrderState = new PendingState();
setState(state: OrderState): void {
this.state = state;
}
getState(): string {
return this.state.name;
}
confirm(): void { this.state.confirm(this); }
ship(): void { this.state.ship(this); }
deliver(): void { this.state.deliver(this); }
cancel(): void { this.state.cancel(this); }
}
// Kullanim
const order = new Order();
console.log(order.getState()); // 'pending'
order.confirm(); // 'Siparis onaylandi'
console.log(order.getState()); // 'confirmed'
order.ship(); // 'Siparis kargoya verildi'
console.log(order.getState()); // 'shipped'
// order.cancel(); // Error: Kargodaki siparis iptal edilemez
order.deliver(); // 'Siparis teslim edildi'
console.log(order.getState()); // 'delivered'3.4 Anti-Patterns (Kotu Desenler)
Kacininmasi gereken yaygin hatali yaklasimlar:
| Anti-Pattern | Açıklama | Çözüm |
|---|---|---|
| God Object | Tek bir sınıf her seyi yapar (3000+ satir, 50+ metot) | Single Responsibility -- sinifi sorumluluga gore bol |
| Spaghetti Code | Dallanma, atlama, ic ice if/else, goto benzeri akis | Fonksiyonlara ayir, early return kullan, state pattern |
| Golden Hammer | Her soruna ayni araci/teknolojiyi uygulama | Doğru araci doğru is için seç |
| Copy-Paste Programming | Kodu kopyala-yapistir ile cozmek | DRY -- ortak fonksiyon/sınıf cikar |
| Premature Optimization | Ihtiyac olmadan erken optimizasyon | "Make it work, make it right, make it fast" |
| Magic Numbers/Strings | Kodda aciklamasiz sabit degerler | Constants/enums kullan |
| Shotgun Surgery | Bir degisiklik için 20 farkli dosya değiştirme | Iliskili kodu bir arada tut (cohesion) |
| Feature Envy | Bir sınıf baska sinifin verisine cok erisir | Metodu verinin oldugu sinifa tasi |
| Dead Code | Kullanilmayan ama silinnmeyen kodlar | Sil. Version control'den geri getirebilirsin |
4) SOLID Prensipleri
SOLID, Robert C. Martin (Uncle Bob) tarafindan tanimlanan 5 nesne yonelimli tasarim prensibidir. Yazilimin bakim yapilebilir, esnek ve genisletilebilir olmasini sağlar.
4.1 S -- Single Responsibility Principle (Tek Sorumluluk)
Bir sinifin degismek için tek bir nedeni olmali.
Ihlal Ornegi:
// YANLIS: Tek sinif her seyi yapar
class UserManager {
async createUser(data: UserData) {
// Validation
if (!data.email.includes('@')) throw new Error('Invalid email');
if (data.password.length < 8) throw new Error('Password too short');
// Password hashing
const hashedPassword = await bcrypt.hash(data.password, 10);
// DB insert
const user = await db.query(
'INSERT INTO users (email, password) VALUES ($1, $2) RETURNING *',
[data.email, hashedPassword]
);
// Email gonder
await nodemailer.createTransport({...}).sendMail({
to: data.email,
subject: 'Hosgeldiniz',
html: `<h1>Merhaba ${data.name}</h1>`
});
// Log yaz
fs.appendFileSync('app.log', `User created: ${data.email}\n`);
return user;
}
}
// Bu sinifin degismesi icin 5 farkli neden var:
// validation kurali degisirse, hashing degisirse, DB semasi degisirse,
// email sablonu degisirse, log formati degisirseDoğru Ornegi:
// DOGRU: Her sinifin tek sorumlulugu var
class UserValidator {
validate(data: UserData): void {
if (!data.email.includes('@')) throw new ValidationError('Invalid email');
if (data.password.length < 8) throw new ValidationError('Password too short');
}
}
class PasswordHasher {
async hash(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
}
class UserRepository {
async create(email: string, hashedPassword: string): Promise<User> {
const result = await db.query(
'INSERT INTO users (email, password) VALUES ($1, $2) RETURNING *',
[email, hashedPassword]
);
return result.rows[0];
}
}
class WelcomeEmailSender {
async send(user: User): Promise<void> {
await this.mailer.sendMail({
to: user.email,
subject: 'Hosgeldiniz',
html: `<h1>Merhaba ${user.name}</h1>`
});
}
}
class UserService {
constructor(
private validator: UserValidator,
private hasher: PasswordHasher,
private userRepo: UserRepository,
private emailSender: WelcomeEmailSender,
private logger: Logger
) {}
async createUser(data: UserData): Promise<User> {
this.validator.validate(data);
const hashedPassword = await this.hasher.hash(data.password);
const user = await this.userRepo.create(data.email, hashedPassword);
await this.emailSender.send(user);
this.logger.info('User created', { userId: user.id });
return user;
}
}4.2 O -- Open/Closed Principle (Acik/Kapali)
Siniflar genisletmeye acik, degistirmeye kapali olmali.
Ihlal Ornegi:
// YANLIS: Yeni odeme yontemi eklemek icin mevcut kodu degistirmek lazim
class PaymentProcessor {
process(method: string, amount: number) {
if (method === 'credit_card') {
// Kredi karti islemi
return this.chargeCreditCard(amount);
} else if (method === 'paypal') {
// PayPal islemi
return this.chargePayPal(amount);
} else if (method === 'bitcoin') {
// Her yeni odeme yontemi icin buraya if eklenmeli
return this.chargeBitcoin(amount);
}
throw new Error('Unknown payment method');
}
}Doğru Ornegi:
// DOGRU: Yeni odeme yontemi eklemek icin mevcut kodu degistirmene gerek yok
interface PaymentMethod {
name: string;
charge(amount: number): Promise<PaymentResult>;
}
class CreditCardPayment implements PaymentMethod {
name = 'credit_card';
async charge(amount: number): Promise<PaymentResult> {
// Kredi karti islemi
return { success: true, transactionId: '...' };
}
}
class PayPalPayment implements PaymentMethod {
name = 'paypal';
async charge(amount: number): Promise<PaymentResult> {
// PayPal islemi
return { success: true, transactionId: '...' };
}
}
// Yeni odeme yontemi ekle -- mevcut kod degismez
class CryptoPayment implements PaymentMethod {
name = 'crypto';
async charge(amount: number): Promise<PaymentResult> {
return { success: true, transactionId: '...' };
}
}
class PaymentProcessor {
private methods: Map<string, PaymentMethod> = new Map();
register(method: PaymentMethod): void {
this.methods.set(method.name, method);
}
async process(methodName: string, amount: number): Promise<PaymentResult> {
const method = this.methods.get(methodName);
if (!method) throw new Error(`Unknown payment method: ${methodName}`);
return method.charge(amount);
}
}
// Kullanim
const processor = new PaymentProcessor();
processor.register(new CreditCardPayment());
processor.register(new PayPalPayment());
processor.register(new CryptoPayment()); // Yeni -- hicbir sey degistirmeden eklendi4.3 L -- Liskov Substitution Principle (Liskov Yer Degistirme)
Alt siniflar, ust sinifin yerine kullanilabilmeli. Davranis degistirmemeli.
Ihlal Ornegi:
// YANLIS: Square, Rectangle'in davranisini bozar
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
getArea(): number { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w: number) {
this.width = w;
this.height = w; // Kare oldugu icin ikisini de degistir
}
setHeight(h: number) {
this.width = h;
this.height = h;
}
}
// Problem: Rectangle bekleyen kod Square ile bozulur
function resizeRectangle(rect: Rectangle) {
rect.setWidth(10);
rect.setHeight(5);
console.log(rect.getArea()); // Rectangle icin 50 bekleriz
// Ama Square verirsen: setHeight(5) width'i de 5 yapar -> 25 doner!
}Doğru Ornegi:
// DOGRU: Ortak interface kullan, kalitim zorlama
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
getArea(): number { return this.width * this.height; }
}
class Square implements Shape {
constructor(private side: number) {}
getArea(): number { return this.side * this.side; }
}
// Her ikisi de Shape olarak kullanilabilir, davranis tutarli
function printArea(shape: Shape) {
console.log(`Area: ${shape.getArea()}`);
}
printArea(new Rectangle(10, 5)); // 50
printArea(new Square(5)); // 254.4 I -- Interface Segregation Principle (Arayuz Ayirma)
Istemciler kullanmadiklari metotlara bagimli olmaya zorlanmamali.
Ihlal Ornegi:
// YANLIS: Sisme interface -- her sinif kullanmadigi metotlari implement etmek zorunda
interface Worker {
work(): void;
eat(): void;
sleep(): void;
attendMeeting(): void;
writeReport(): void;
}
class Developer implements Worker {
work() { console.log('Coding...'); }
eat() { console.log('Eating...'); }
sleep() { console.log('Sleeping...'); }
attendMeeting() { console.log('In meeting...'); }
writeReport() { console.log('Writing report...'); }
}
class Robot implements Worker {
work() { console.log('Working...'); }
eat() { throw new Error('Robots do not eat!'); } // YANLIS!
sleep() { throw new Error('Robots do not sleep!'); } // YANLIS!
attendMeeting() { throw new Error('Not applicable!'); } // YANLIS!
writeReport() { throw new Error('Not applicable!'); } // YANLIS!
}Doğru Ornegi:
// DOGRU: Kucuk, amaca yonelik interface'ler
interface Workable {
work(): void;
}
interface Feedable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
interface Meetable {
attendMeeting(): void;
}
class Developer implements Workable, Feedable, Sleepable, Meetable {
work() { console.log('Coding...'); }
eat() { console.log('Eating...'); }
sleep() { console.log('Sleeping...'); }
attendMeeting() { console.log('In meeting...'); }
}
class Robot implements Workable {
work() { console.log('Working...'); }
// Sadece ilgili interface'i implement eder, gereksiz metot yok
}4.5 D -- Dependency Inversion Principle (Bagimlilik Tersine Cevirme)
Ust seviye moduller alt seviye modullere bagimli olmamali. Her ikisi de soyutlamalara (abstractions) bagimli olmali.
Ihlal Ornegi:
// YANLIS: UserService dogrudan PostgreSQL'e bagimli
import { Pool } from 'pg';
class UserService {
private db: Pool;
constructor() {
this.db = new Pool({ connectionString: '...' }); // Dogrudan bagimlilik
}
async getUser(id: string) {
return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
}
}
// Sorun: MongoDB'ye gecmek istesek tum service'i degistirmek gerekir
// Sorun: Test ederken gercek DB lazimDoğru Ornegi:
// DOGRU: Interface'e bagimli ol, concrete sinifi disaridan al
interface IUserRepository {
findById(id: string): Promise<User | null>;
create(data: CreateUserDTO): Promise<User>;
update(id: string, data: Partial<User>): Promise<User>;
delete(id: string): Promise<boolean>;
}
// PostgreSQL implementasyonu
class PostgresUserRepository implements IUserRepository {
constructor(private db: Pool) {}
async findById(id: string): Promise<User | null> {
const result = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
return result.rows[0] || null;
}
async create(data: CreateUserDTO): Promise<User> {
const result = await this.db.query(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
[data.name, data.email]
);
return result.rows[0];
}
async update(id: string, data: Partial<User>): Promise<User> { /* ... */ return {} as User; }
async delete(id: string): Promise<boolean> { /* ... */ return true; }
}
// MongoDB implementasyonu
class MongoUserRepository implements IUserRepository {
constructor(private collection: Collection) {}
async findById(id: string): Promise<User | null> {
return this.collection.findOne({ _id: new ObjectId(id) });
}
async create(data: CreateUserDTO): Promise<User> {
const result = await this.collection.insertOne(data);
return { id: result.insertedId.toString(), ...data } as User;
}
async update(id: string, data: Partial<User>): Promise<User> { /* ... */ return {} as User; }
async delete(id: string): Promise<boolean> { /* ... */ return true; }
}
// Service interface'e bagimli, concrete sinifi bilmiyor
class UserService {
constructor(private userRepo: IUserRepository) {}
async getUser(id: string): Promise<User> {
const user = await this.userRepo.findById(id);
if (!user) throw new NotFoundError('User', id);
return user;
}
}
// Production: PostgreSQL
const userService = new UserService(new PostgresUserRepository(pgPool));
// Test: Mock/Fake repository
const mockRepo: IUserRepository = {
findById: async (id) => ({ id, name: 'Test', email: 'test@test.com' } as User),
create: async (data) => ({ id: '1', ...data } as User),
update: async (id, data) => ({ id, ...data } as User),
delete: async () => true
};
const testService = new UserService(mockRepo);5) Clean Code Prensipleri
Clean code, baskalarinin (ve gelecekteki senin) kolayca okuyabilecegi, anlayabilecegi ve degistirebilecegi kodtur.
5.1 Naming Conventions (Adlandirma Kurallari)
// YANLIS adlandirma
const d = 5; // d ne?
const list = getUsers(); // hangi list?
function proc(x: any) {} // ne isliyor?
class Manager {} // neyi manage ediyor?
const flag = true; // hangi flag?
function handle() {} // neyi handle ediyor?
// DOGRU adlandirma
const maxRetryCount = 5;
const activeUsers = getActiveUsers();
function validateEmail(email: string): boolean {}
class PaymentProcessor {}
const isEmailVerified = true;
function handlePaymentFailure(error: PaymentError) {}Adlandirma Kurallari Tablosu:
| Ogge | Kural | Iyi Örnek | Kotu Örnek |
|---|---|---|---|
| Değişken | camelCase, anlamli isim | userCount, isActive | uc, flag |
| Fonksiyon | camelCase, fiil ile başla | getUser(), calculateTotal() | user(), total() |
| Sınıf | PascalCase, isim | UserService, OrderController | usrSvc, ctrl |
| Interface | PascalCase, I prefix (opsiyonel) | IUserRepository, Cacheable | iur, cache |
| Constant | UPPER_SNAKE_CASE | MAX_RETRY_COUNT, API_BASE_URL | maxRetry, url |
| Boolean | is/has/can/should ile başla | isActive, hasPermission | active, permission |
| Dosya | kebab-case veya camelCase | user-service.ts, userService.ts | US.ts, temp.ts |
| Enum | PascalCase | OrderStatus.Pending | status.p |
5.2 Fonksiyon Boyutu
Bir fonksiyon:
- Maksimum 20-30 satir olmali
- Tek bir is yapmali
- Ayni soyutlama seviyesinde işlem yapmali
// YANLIS: 80+ satirlik dev fonksiyon
async function processOrder(orderData: any) {
// Validate
if (!orderData.items) throw new Error('...');
if (!orderData.userId) throw new Error('...');
if (!orderData.paymentMethod) throw new Error('...');
// ... 20 satir daha validation
// Calculate total
let total = 0;
for (const item of orderData.items) {
const product = await db.query('...');
total += product.price * item.quantity;
if (item.coupon) {
// ... 15 satir kupon hesaplama
}
}
// Process payment
// ... 20 satir odeme islemi
// Update inventory
// ... 15 satir stok guncelleme
// Send notifications
// ... 10 satir email/sms
}
// DOGRU: Kucuk, odakli fonksiyonlar
async function processOrder(orderData: OrderInput): Promise<Order> {
validateOrder(orderData);
const items = await resolveProducts(orderData.items);
const total = calculateOrderTotal(items, orderData.couponCode);
const payment = await processPayment(total, orderData.paymentMethod);
const order = await createOrder(orderData.userId, items, total, payment.id);
await updateInventory(items);
await sendOrderConfirmation(order);
return order;
}5.3 DRY, KISS, YAGNI
DRY (Don't Repeat Yourself)
// YANLIS: Tekrar eden kod
async function getActiveUsers() {
const result = await db.query('SELECT * FROM users WHERE status = $1', ['active']);
return result.rows.map(row => ({
id: row.id,
name: row.name,
email: row.email,
avatar: row.avatar_url
}));
}
async function getPremiumUsers() {
const result = await db.query('SELECT * FROM users WHERE type = $1', ['premium']);
return result.rows.map(row => ({
id: row.id,
name: row.name,
email: row.email,
avatar: row.avatar_url // Ayni mapping tekrar
}));
}
// DOGRU: Ortak mantigi cikar
function mapUserRow(row: any): UserDTO {
return {
id: row.id,
name: row.name,
email: row.email,
avatar: row.avatar_url
};
}
async function getActiveUsers() {
const result = await db.query('SELECT * FROM users WHERE status = $1', ['active']);
return result.rows.map(mapUserRow);
}
async function getPremiumUsers() {
const result = await db.query('SELECT * FROM users WHERE type = $1', ['premium']);
return result.rows.map(mapUserRow);
}KISS (Keep It Simple, Stupid)
// YANLIS: Asiri muhendislik
class StringReverser {
private strategy: IReverseStrategy;
constructor(strategy: IReverseStrategy) {
this.strategy = strategy;
}
reverse(input: string): string {
return this.strategy.execute(input);
}
}
// DOGRU: Basit tut
function reverseString(input: string): string {
return input.split('').reverse().join('');
}YAGNI (You Ain't Gonna Need It)
// YANLIS: Ihtiyac olmayan ozellikler ekleme
class UserService {
async getUser(id: string) { /* ... */ }
async createUser(data: UserData) { /* ... */ }
async updateUser(id: string, data: UserData) { /* ... */ }
async deleteUser(id: string) { /* ... */ }
// Bunlar henuz kimse istemedi!
async exportUsersToXML() { /* ... */ }
async importUsersFromCSV() { /* ... */ }
async generateUserHeatmap() { /* ... */ }
async predictUserChurn() { /* ... */ } // Yapay zeka?!
}
// DOGRU: Sadece ihtiyac olani yaz
class UserService {
async getUser(id: string) { /* ... */ }
async createUser(data: UserData) { /* ... */ }
async updateUser(id: string, data: UserData) { /* ... */ }
async deleteUser(id: string) { /* ... */ }
// Yeni ozellik gerektiginde ekle, onceden degil
}5.4 Boy Scout Rule
"Bir yeri buldugundan daha temiz birak."
Her dosyaya dokundugununda küçük bir iyilestirme yap:
- Kullanilmayan import'u sil
- Magic number'i constant yap
- Değişken ismini duzelt
- Gereksiz yorumu sil
5.5 Comments (Yorum Satirlari)
// YANLIS: Kodu aciklayan yorumlar (kod kendini aciklamali)
// Kullaniciyi ID'ye gore bul
function getUser(id: string) { /* ... */ }
// Fiyati %15 indir
const discountedPrice = price * 0.85;
// DOGRU: "Neden" aciklayan yorumlar
// Stripe API 3 saniye timeout sonrasinda retry yapiyor,
// bu yuzden kendi retry mekanizmamizi devre disi birakiyoruz
const stripeConfig = { maxRetries: 0 };
// Turk vergi mevzuatina gore %18 KDV eklenmeli (VUK 229. madde)
const taxRate = 0.18;
// TODO: Bu gecici cozum, OAuth2 entegrasyonu tamamlaninca kaldirilacak
// Ref: JIRA-1234
const hardcodedToken = process.env.TEMP_API_TOKEN;Yorum Ne Zaman Yaz:
- Is kurali aciklamasi (neden bu hesaplama?)
- Hack/workaround aciklamasi
- TODO / FIXME
- Public API/library dokumantasyonu
- Regex pattern aciklamasi
Yorum Ne Zaman Yazma:
- Kodu Turkce/Ingilizce'ye cevirme ("kullaniciyi sil")
- Git blame ile gorulecek degisiklik notlari
- Yorum satira alinmis (commented-out) kod -- sil, git'ten geri getirebilirsin
5.6 Magic Numbers ve Constants
// YANLIS: Magic number'lar
if (password.length < 8) { /* ... */ }
if (retryCount > 3) { /* ... */ }
if (user.role === 2) { /* ... */ }
setTimeout(callback, 86400000);
// DOGRU: Constants
const MIN_PASSWORD_LENGTH = 8;
const MAX_RETRY_COUNT = 3;
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
enum UserRole {
User = 1,
Admin = 2,
SuperAdmin = 3
}
if (password.length < MIN_PASSWORD_LENGTH) { /* ... */ }
if (retryCount > MAX_RETRY_COUNT) { /* ... */ }
if (user.role === UserRole.Admin) { /* ... */ }
setTimeout(callback, TWENTY_FOUR_HOURS_MS);5.7 Early Return / Guard Clauses
// YANLIS: Derin ic ice kosullar (pyramid of doom)
function processPayment(user: User, order: Order, payment: Payment) {
if (user) {
if (user.isActive) {
if (order) {
if (order.status === 'pending') {
if (payment.amount > 0) {
if (payment.method === 'credit_card') {
// Gercek is mantigi burada, 6 seviye icerde
return chargeCard(payment);
} else {
throw new Error('Invalid payment method');
}
} else {
throw new Error('Invalid amount');
}
} else {
throw new Error('Order not pending');
}
} else {
throw new Error('Order not found');
}
} else {
throw new Error('User not active');
}
} else {
throw new Error('User not found');
}
}
// DOGRU: Guard clauses ile erken cikis
function processPayment(user: User, order: Order, payment: Payment) {
if (!user) throw new Error('User not found');
if (!user.isActive) throw new Error('User not active');
if (!order) throw new Error('Order not found');
if (order.status !== 'pending') throw new Error('Order not pending');
if (payment.amount <= 0) throw new Error('Invalid amount');
if (payment.method !== 'credit_card') throw new Error('Invalid payment method');
// Gercek is mantigi -- duz, okunabilir
return chargeCard(payment);
}5.8 Code Smells Tablosu
| Code Smell | Belirtisi | Çözüm |
|---|---|---|
| Long Method | 50+ satirlik fonksiyon | Extract Method -- küçük fonksiyonlara bol |
| Long Parameter List | 5+ parametre | Parameter Object / DTO kullan |
| Duplicate Code | Ayni mantik 2+ yerde | Ortak fonksiyon/sınıf cikar |
| Feature Envy | Sınıf baska sinifin verisini cok kullanir | Metodu verinin oldugu sinifa tasi |
| Data Clumps | Ayni değişken grubu hep birlikte gezer | Sınıf/interface oluştur |
| Primitive Obsession | Her sey string/number, ozel tip yok | Value Object oluştur (Money, Email, PhoneNumber) |
| Switch Statements | Büyük switch/if-else zincirleri | Strategy/State pattern veya polymorphism |
| Speculative Generality | Kullanilmayan soyutlamalar | Sil, YAGNI |
| Dead Code | Kullanilmayan kod bloklari | Sil |
| Comments | Cok fazla yorum, kodun kendisi anlasilmiyor | Kodu refactor et, kendini aciklayan hale getir |
| God Class | 1000+ satirlik sınıf | Sorumluluga gore bol |
| Shotgun Surgery | Bir degisiklik 10+ dosyayi etkiler | Iliskili kodu bir araya getir |
6) API Tasarimi (REST Best Practices)
6.1 RESTful URL Yapisi
# Kaynak (resource) isimleri cogul ve kucuk harf
GET /api/v1/users # Tum kullanicilari listele
GET /api/v1/users/123 # Tek kullanici getir
POST /api/v1/users # Yeni kullanici olustur
PUT /api/v1/users/123 # Kullaniciyi tamamen guncelle
PATCH /api/v1/users/123 # Kullaniciyi kismi guncelle
DELETE /api/v1/users/123 # Kullaniciyi sil
# Iliskili kaynaklar (nested resources)
GET /api/v1/users/123/orders # Kullanicinin siparisleri
GET /api/v1/users/123/orders/456 # Kullanicinin belirli siparisi
POST /api/v1/users/123/addresses # Kullaniciya adres ekle
# Aksiyon gerektiren islemler (fiil URL'de olabilir -- istisna)
POST /api/v1/orders/123/cancel # Siparisi iptal et
POST /api/v1/auth/login # Giris yap
POST /api/v1/auth/logout # Cikis yap
POST /api/v1/reports/generate # Rapor olustur
# YANLIS URL ornekleri
GET /api/v1/getUsers # Fiil kullanma (GET zaten "getir" demek)
GET /api/v1/user # Tekil degil, cogul kullan
POST /api/v1/users/create # POST zaten "olustur" demek
DELETE /api/v1/users/delete/123 # DELETE metodu yeterli
GET /api/v1/Users # Buyuk harf kullanma6.2 HTTP Methods Tablosu
| Method | Amac | Idempotent | Safe | Body | Örnek |
|---|---|---|---|---|---|
| GET | Kaynak getir | Evet | Evet | Yok | GET /api/users/123 |
| POST | Yeni kaynak oluştur | Hayir | Hayir | Var | POST /api/users |
| PUT | Kaynagi tamamen değiştir | Evet | Hayir | Var | PUT /api/users/123 |
| PATCH | Kaynagi kismi değiştir | Evet | Hayir | Var | PATCH /api/users/123 |
| DELETE | Kaynagi sil | Evet | Hayir | Opsiyonel | DELETE /api/users/123 |
| HEAD | Sadece header (body yok) | Evet | Evet | Yok | HEAD /api/users/123 |
| OPTIONS | Desteklenen metotlari sor | Evet | Evet | Yok | OPTIONS /api/users |
Idempotent: Ayni istegi 10 kez gondersen ayni sonucu alirsin. POST haric hepsi idempotent. Safe: Sunucu durumunu degistirmez. Sadece GET, HEAD, OPTIONS safe'tir.
6.3 HTTP Status Codes Tablosu
2xx -- Basarili
| Kod | Isim | Ne Zaman Kullan |
|---|---|---|
| 200 | OK | Genel basari (GET, PUT, PATCH, DELETE) |
| 201 | Created | Yeni kaynak olusturuldu (POST) |
| 204 | No Content | Basarili ama donecek veri yok (DELETE) |
3xx -- Yonlendirme
| Kod | Isim | Ne Zaman Kullan |
|---|---|---|
| 301 | Moved Permanently | Kaynak kalici olarak tasindi |
| 302 | Found | Gecici yonlendirme |
| 304 | Not Modified | Cache hala gecerli (ETag/If-None-Match) |
4xx -- Istemci Hatasi
| Kod | Isim | Ne Zaman Kullan |
|---|---|---|
| 400 | Bad Request | Gecersiz istek (validation hatasi, yanlis JSON) |
| 401 | Unauthorized | Kimlik dogrulanmadi (token yok/gecersiz) |
| 403 | Forbidden | Kimlik dogrulandi ama yetki yok |
| 404 | Not Found | Kaynak bulunamadi |
| 405 | Method Not Allowed | Bu endpoint bu HTTP method'u desteklemiyor |
| 409 | Conflict | Cakisma (ornegin: ayni email ile kayit var) |
| 413 | Payload Too Large | Istek gövdesi cok büyük |
| 415 | Unsupported Media Type | Desteklenmeyen Content-Type |
| 422 | Unprocessable Entity | Sozdizimi doğru ama anlam olarak gecersiz |
| 429 | Too Many Requests | Rate limit asildi |
5xx -- Sunucu Hatasi
| Kod | Isim | Ne Zaman Kullan |
|---|---|---|
| 500 | Internal Server Error | Beklenmeyen sunucu hatasi |
| 502 | Bad Gateway | Upstream servis cevap vermedi |
| 503 | Service Unavailable | Sunucu gecici olarak kullanilamaz (bakim, asiri yuk) |
| 504 | Gateway Timeout | Upstream servis zaman asimina ugradi |
6.4 Response Envelope
Tutarli bir response yapisi kullanimda büyük kolaylik sağlar:
// Standart response yapisi
interface ApiResponse<T> {
success: boolean;
data: T | null;
message: string | null;
errors: ValidationError[] | null;
meta: MetaData | null;
}
interface ValidationError {
field: string;
message: string;
code: string;
}
interface MetaData {
total?: number;
page?: number;
limit?: number;
totalPages?: number;
timestamp?: string;
requestId?: string;
}// Basarili liste response
{
"success": true,
"data": [
{ "id": 1, "name": "iPhone 15", "price": 49999.99 },
{ "id": 2, "name": "MacBook Pro", "price": 89999.99 }
],
"message": null,
"errors": null,
"meta": {
"total": 156,
"page": 1,
"limit": 20,
"totalPages": 8,
"timestamp": "2025-01-15T10:30:00Z",
"requestId": "req-abc123"
}
}
// Basarili tekil response
{
"success": true,
"data": {
"id": 1,
"name": "iPhone 15",
"price": 49999.99,
"stock": 42
},
"message": null,
"errors": null,
"meta": null
}
// Hata response (validation)
{
"success": false,
"data": null,
"message": "Validation failed",
"errors": [
{ "field": "email", "message": "Gecerli bir email adresi giriniz", "code": "INVALID_EMAIL" },
{ "field": "password", "message": "En az 8 karakter olmali", "code": "MIN_LENGTH" }
],
"meta": {
"timestamp": "2025-01-15T10:30:00Z",
"requestId": "req-def456"
}
}
// Hata response (genel)
{
"success": false,
"data": null,
"message": "Product not found",
"errors": null,
"meta": {
"timestamp": "2025-01-15T10:30:00Z",
"requestId": "req-ghi789"
}
}Response Helper:
// utils/response.ts
class ApiResponseHelper {
static success<T>(data: T, message?: string, meta?: MetaData): ApiResponse<T> {
return {
success: true,
data,
message: message || null,
errors: null,
meta: meta || null
};
}
static paginated<T>(data: T[], total: number, page: number, limit: number): ApiResponse<T[]> {
return {
success: true,
data,
message: null,
errors: null,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit)
}
};
}
static error(message: string, errors?: ValidationError[], statusCode?: number): ApiResponse<null> {
return {
success: false,
data: null,
message,
errors: errors || null,
meta: {
timestamp: new Date().toISOString()
}
};
}
}
// Kullanim
res.status(200).json(ApiResponseHelper.success(product));
res.status(200).json(ApiResponseHelper.paginated(products, total, page, limit));
res.status(400).json(ApiResponseHelper.error('Validation failed', validationErrors));6.5 Pagination (Sayfalama)
Offset-Based Pagination
GET /api/products?page=3&limit=20
SELECT * FROM products ORDER BY created_at DESC LIMIT 20 OFFSET 40;Cursor-Based Pagination
GET /api/products?cursor=eyJpZCI6MTAwfQ&limit=20
-- cursor = base64 encoded { "id": 100 }
SELECT * FROM products WHERE id < 100 ORDER BY id DESC LIMIT 20;Karşılaştırma Tablosu:
| Özellik | Offset-Based | Cursor-Based |
|---|---|---|
| Basitlik | Basit, anlasilir | Daha karmasik |
| Sayfa atlama | Evet (page=5 gibi) | Hayir (sadece ileri/geri) |
| Performans (büyük veri) | Yavas (OFFSET 100000 agir) | Hızlı (index kullanir) |
| Tutarlilik | Veri degisirse kayma olur | Tutarli sonuclar |
| SEO | URL'de sayfa numarasi | Uygun degil |
| Kullanım alani | Admin paneller, web siteleri | Mobil uygulamalar, feed'ler, real-time |
// Cursor-based pagination implementasyonu
interface CursorPaginationResult<T> {
items: T[];
nextCursor: string | null;
hasMore: boolean;
}
async function getProductsWithCursor(
cursor: string | null,
limit: number = 20
): Promise<CursorPaginationResult<Product>> {
let query = 'SELECT * FROM products WHERE is_active = true';
const params: any[] = [];
if (cursor) {
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());
params.push(decoded.id);
query += ` AND id < $${params.length}`;
}
params.push(limit + 1); // 1 fazla cek, hasMore belirlemek icin
query += ` ORDER BY id DESC LIMIT $${params.length}`;
const result = await db.query(query, params);
const items = result.rows.slice(0, limit);
const hasMore = result.rows.length > limit;
const nextCursor = hasMore
? Buffer.from(JSON.stringify({ id: items[items.length - 1].id })).toString('base64')
: null;
return { items, nextCursor, hasMore };
}6.6 Filtering, Sorting, Searching
# Filtreleme
GET /api/products?category=electronics&minPrice=100&maxPrice=500&inStock=true
# Siralama
GET /api/products?sortBy=price&sortOrder=asc
GET /api/products?sort=-price,+name # alternatif: - desc, + asc
# Arama
GET /api/products?search=iphone&searchFields=name,description
# Hepsi birlikte
GET /api/products?category=electronics&minPrice=100&sortBy=price&sortOrder=asc&search=samsung&page=1&limit=20// Genel amacli query builder
function buildProductQuery(filters: ProductFilters) {
const conditions: string[] = ['is_active = true'];
const params: any[] = [];
if (filters.category) {
params.push(filters.category);
conditions.push(`category_id = $${params.length}`);
}
if (filters.minPrice !== undefined) {
params.push(filters.minPrice);
conditions.push(`price >= $${params.length}`);
}
if (filters.maxPrice !== undefined) {
params.push(filters.maxPrice);
conditions.push(`price <= $${params.length}`);
}
if (filters.inStock) {
conditions.push('stock > 0');
}
if (filters.search) {
params.push(`%${filters.search}%`);
conditions.push(`(name ILIKE $${params.length} OR description ILIKE $${params.length})`);
}
// Guvenli sort -- whitelist ile SQL injection'i onle
const allowedSortFields = ['name', 'price', 'created_at', 'stock'];
const sortBy = allowedSortFields.includes(filters.sortBy || '')
? filters.sortBy
: 'created_at';
const sortOrder = filters.sortOrder === 'asc' ? 'ASC' : 'DESC';
return {
where: conditions.join(' AND '),
params,
orderBy: `${sortBy} ${sortOrder}`
};
}6.7 API Versiyonlama
| Yöntem | Örnek | Avantaj | Dezavantaj |
|---|---|---|---|
| URL Path | /api/v1/users /api/v2/users | Basit, acik, cache dostu | URL kirliligi |
| Header | Accept: application/vnd.api.v2+json | Temiz URL | Gorunurluk düşük, test zor |
| Query Param | /api/users?version=2 | Basit | Cache sorunlari, kotu pratik |
Öneri: URL path versiyonlama (/api/v1/...) en yaygin ve pratik yontemdir. Cogu büyük API (GitHub, Stripe, Twitter) bu yontemi kullanir.
// URL path versiyonlama
import { Router } from 'express';
const v1Router = Router();
const v2Router = Router();
// V1: Eski response formati
v1Router.get('/users/:id', async (req, res) => {
const user = await userService.getUser(req.params.id);
res.json({
id: user.id,
name: user.name,
email: user.email
});
});
// V2: Yeni response formati (genisletilmis)
v2Router.get('/users/:id', async (req, res) => {
const user = await userService.getUser(req.params.id);
res.json({
success: true,
data: {
id: user.id,
fullName: user.name, // alan adi degisti
email: user.email,
profile: {
avatar: user.avatar,
bio: user.bio
}
}
});
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);6.8 Rate Limiting
// Rate limiting middleware (basit in-memory)
const rateLimits = new Map<string, { count: number; resetAt: number }>();
function rateLimiter(options: { windowMs: number; max: number }) {
return (req: Request, res: Response, next: NextFunction) => {
const key = req.ip || 'unknown';
const now = Date.now();
const record = rateLimits.get(key);
if (!record || record.resetAt <= now) {
rateLimits.set(key, { count: 1, resetAt: now + options.windowMs });
setRateLimitHeaders(res, options.max, options.max - 1, now + options.windowMs);
return next();
}
if (record.count >= options.max) {
setRateLimitHeaders(res, options.max, 0, record.resetAt);
return res.status(429).json({
success: false,
message: 'Too many requests. Please try again later.',
retryAfter: Math.ceil((record.resetAt - now) / 1000)
});
}
record.count++;
setRateLimitHeaders(res, options.max, options.max - record.count, record.resetAt);
next();
};
}
function setRateLimitHeaders(res: Response, limit: number, remaining: number, resetAt: number) {
res.set({
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': Math.ceil(resetAt / 1000).toString()
});
}
// Kullanim
app.use('/api/', rateLimiter({ windowMs: 15 * 60 * 1000, max: 100 })); // 15 dk'da 100 istek
app.use('/api/auth/login', rateLimiter({ windowMs: 15 * 60 * 1000, max: 5 })); // Login: 15 dk'da 5// Production: express-rate-limit + Redis store
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
const apiLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { success: false, message: 'Too many requests' }
});
app.use('/api/', apiLimiter);6.9 GraphQL vs REST Karşılaştırma
| Özellik | REST | GraphQL |
|---|---|---|
| Veri getirme | Sabit response, over/under-fetching olabilir | Istemci istedigi alanlari secer |
| Endpoint sayisi | Kaynak basina endpoint | Tek endpoint (/graphql) |
| Versiyonlama | URL veya header ile | Genellikle gereksiz (schema evolution) |
| Cache | HTTP cache (ETag, CDN) kolay | HTTP cache zor, ozel cache gerekli |
| Dosya upload | Dogrudan destekler | Ek konfigürasyon gerekli |
| Öğrenme egrisi | Düşük | Orta-Yüksek |
| Over-fetching | Var (tüm alanlar gelir) | Yok (sadece istenen alanlar) |
| Under-fetching | Var (N+1 request) | Yok (tek request'te iliskili veri) |
| Tooling | Postman, curl, her sey | Apollo, Relay, ozel tooling |
| Real-time | WebSocket, SSE ayri | Subscription built-in |
| Dosya boyutu | Küçük | Query buyuyebilir |
| Güvenlik | Standart | Query depth limiting, complexity analysis |
| Ne zaman seç | CRUD agirlikli, public API, basitlik | Karmasik iliskili veri, mobil, dashboard |
7) Authentication & Authorization
7.1 Session vs JWT Karşılaştırma
| Özellik | Session-Based | JWT (Token-Based) |
|---|---|---|
| Durum (state) | Stateful (sunucuda saklanir) | Stateless (token icinde) |
| Olcekleme | Sticky session veya paylasilmis store gerekir | Kolayca olceklenir (state yok) |
| Guvenligi bosaltma | Sunucudan session sil | Token expire olana kadar gecerli (revocation zor) |
| Performans | Her istekte session store'a git | Token'i doğrula (CPU islemi, DB yok) |
| Cross-domain | Zor (cookie same-origin) | Kolay (Authorization header) |
| Mobil uyumluluk | Cookie yönetimi karmasik | Token gonderme basit |
| CSRF riski | Var (cookie otomatik gider) | Yok (header ile gonderilir) |
| XSS riski | Düşük (httpOnly cookie) | Yüksek (localStorage'da saklanirsa) |
| Boyut | Küçük session ID | Büyük (payload icerigi) |
| Ideal kullanım | Geleneksel web uygulamalari (SSR) | SPA, mobil, microservices, API |
7.2 JWT Yapisi
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ │ │
│ Header (Base64) │ Payload (Base64) │ Signature
│ │ │
│ { │ { │ HMACSHA256(
│ "alg": "HS256", │ "userId": "123", │ base64(header) + "." +
│ "typ": "JWT" │ "role": "admin", │ base64(payload),
│ } │ "iat": 1700000000, // issued at │ secret
│ │ "exp": 1700003600 // expires (1 saat) │ )
│ │ } │// JWT olusturma ve dogrulama
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const ACCESS_TOKEN_EXPIRY = '15m'; // 15 dakika
const REFRESH_TOKEN_EXPIRY = '7d'; // 7 gun
// Token olusturma
function generateTokens(user: User) {
const accessToken = jwt.sign(
{ userId: user.id, role: user.role, email: user.email },
JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRY }
);
const refreshToken = jwt.sign(
{ userId: user.id, tokenType: 'refresh' },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);
return { accessToken, refreshToken };
}
// Token dogrulama middleware
function authMiddleware(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ success: false, message: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
req.user = { userId: decoded.userId, role: decoded.role };
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({ success: false, message: 'Token expired' });
}
return res.status(401).json({ success: false, message: 'Invalid token' });
}
}7.3 Access + Refresh Token Akisi
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ 1. POST /auth/login │
│ { email, password } │
│────────────────────────────────────────►│
│ │ Dogrula, token olustur
│ 2. Response: │
│ { accessToken (15m), refreshToken (7d)}│
│◄────────────────────────────────────────│
│ │
│ 3. GET /api/profile │
│ Authorization: Bearer <accessToken> │
│────────────────────────────────────────►│
│ │ Token dogrula
│ 4. Response: { user data } │
│◄────────────────────────────────────────│
│ │
│ ... 15 dakika sonra ... │
│ │
│ 5. GET /api/profile │
│ Authorization: Bearer <expiredToken> │
│────────────────────────────────────────►│
│ │ Token suresi doldu
│ 6. 401 Unauthorized │
│◄────────────────────────────────────────│
│ │
│ 7. POST /auth/refresh │
│ { refreshToken } │
│────────────────────────────────────────►│
│ │ Refresh token dogrula
│ 8. Response: │ Yeni token cifti olustur
│ { newAccessToken, newRefreshToken } │
│◄────────────────────────────────────────│
│ │// Refresh token endpoint
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ success: false, message: 'Refresh token required' });
}
try {
// 1. Token'i dogrula
const decoded = jwt.verify(refreshToken, JWT_SECRET) as JwtPayload;
if (decoded.tokenType !== 'refresh') {
return res.status(401).json({ success: false, message: 'Invalid token type' });
}
// 2. Token'in blacklist'te olup olmadigini kontrol et
const isRevoked = await redisClient.get(`revoked:${refreshToken}`);
if (isRevoked) {
return res.status(401).json({ success: false, message: 'Token has been revoked' });
}
// 3. Kullaniciyi getir (hala aktif mi?)
const user = await userRepo.findById(decoded.userId);
if (!user || !user.isActive) {
return res.status(401).json({ success: false, message: 'User not found or inactive' });
}
// 4. Eski refresh token'i iptal et (token rotation)
await redisClient.set(`revoked:${refreshToken}`, '1', { EX: 7 * 24 * 60 * 60 });
// 5. Yeni token cifti olustur
const tokens = generateTokens(user);
res.json({ success: true, data: tokens });
} catch (error) {
return res.status(401).json({ success: false, message: 'Invalid refresh token' });
}
});7.4 OAuth 2.0
Authorization Code Flow (Web uygulamalari için)
┌────────┐ ┌───────────┐ ┌────────────────┐
│ User │ │ Client │ │ Auth Provider │
│ │ │ (App) │ │ (Google/GitHub) │
└───┬────┘ └─────┬─────┘ └───────┬────────┘
│ │ │
│ 1. Login butonuna tikla │
│───────────────►│ │
│ │ 2. Redirect: │
│ │ /authorize? │
│ │ client_id=xxx& │
│◄───────────────│ redirect_uri=xxx& │
│ │ scope=email+profile│
│ 3. Giris yap + izin ver │
│─────────────────────────────────────►│
│ │ │
│ 4. Redirect: callback?code=abc123 │
│◄─────────────────────────────────────│
│───────────────►│ │
│ │ 5. POST /token │
│ │ { code, secret } │
│ │────────────────────►│
│ │ │
│ │ 6. { access_token }│
│ │◄────────────────────│
│ │ │
│ │ 7. GET /userinfo │
│ │ Bearer token │
│ │────────────────────►│
│ │ │
│ │ 8. { user data } │
│ │◄────────────────────│PKCE (Proof Key for Code Exchange -- SPA / Mobil için)
Authorization Code Flow'un güvenli versiyonu. Client secret kullanilmaz, yerine code_verifier ve code_challenge kullanilir.
// PKCE Flow -- SPA ornegi
import crypto from 'crypto';
// 1. code_verifier olustur (random 43-128 karakter)
function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
// 2. code_challenge olustur (verifier'in SHA256 hash'i)
function generateCodeChallenge(verifier: string): string {
return crypto.createHash('sha256').update(verifier).digest('base64url');
}
// 3. Authorization URL
const verifier = generateCodeVerifier();
const challenge = generateCodeChallenge(verifier);
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${CLIENT_ID}` +
`&redirect_uri=${REDIRECT_URI}` +
`&response_type=code` +
`&scope=openid email profile` +
`&code_challenge=${challenge}` +
`&code_challenge_method=S256`;
// 4. Callback'te code ile token al
async function exchangeCodeForToken(code: string) {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: CLIENT_ID,
code,
code_verifier: verifier, // Secret yerine verifier gonder
grant_type: 'authorization_code',
redirect_uri: REDIRECT_URI
})
});
return response.json(); // { access_token, id_token, refresh_token }
}Client Credentials (Machine-to-Machine)
// Client Credentials -- Servisler arasi iletisim
async function getServiceToken() {
const response = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.SERVICE_CLIENT_ID,
client_secret: process.env.SERVICE_CLIENT_SECRET,
audience: 'https://api.example.com'
})
});
const data = await response.json();
return data.access_token;
}
// Diger servise istek at
const token = await getServiceToken();
const orders = await fetch('https://order-service.internal/api/orders', {
headers: { Authorization: `Bearer ${token}` }
});7.5 RBAC (Role-Based Access Control)
// Rol ve izin tanimlamalari
const PERMISSIONS = {
// Urun izinleri
'product:read': 'Urunleri goruntuleyebilir',
'product:create': 'Yeni urun olusturabilir',
'product:update': 'Urun guncelleyebilir',
'product:delete': 'Urun silebilir',
// Siparis izinleri
'order:read': 'Siparisleri goruntuleyebilir',
'order:create': 'Siparis olusturabilir',
'order:update': 'Siparis guncelleyebilir',
'order:cancel': 'Siparis iptal edebilir',
// Kullanici izinleri
'user:read': 'Kullanicilari goruntuleyebilir',
'user:create': 'Kullanici olusturabilir',
'user:update': 'Kullanici guncelleyebilir',
'user:delete': 'Kullanici silebilir',
// Rapor izinleri
'report:read': 'Raporlari goruntuleyebilir',
'report:export': 'Raporlari export edebilir'
};
// Roller ve izinleri
const ROLES = {
customer: [
'product:read',
'order:create', 'order:read'
],
seller: [
'product:read', 'product:create', 'product:update',
'order:read', 'order:update'
],
admin: [
'product:read', 'product:create', 'product:update', 'product:delete',
'order:read', 'order:create', 'order:update', 'order:cancel',
'user:read', 'user:create', 'user:update',
'report:read', 'report:export'
],
superadmin: Object.keys(PERMISSIONS) // Tum izinler
};| Rol | product:read | product:create | product:delete | order:read | order:cancel | user:delete | report:export |
|---|---|---|---|---|---|---|---|
| customer | + | - | - | + | - | - | - |
| seller | + | + | - | + | - | - | - |
| admin | + | + | + | + | + | - | + |
| superadmin | + | + | + | + | + | + | + |
// RBAC middleware
function requirePermission(...requiredPermissions: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user?.role;
if (!userRole) {
return res.status(401).json({ success: false, message: 'Unauthorized' });
}
const userPermissions = ROLES[userRole as keyof typeof ROLES] || [];
const hasAll = requiredPermissions.every(p => userPermissions.includes(p));
if (!hasAll) {
return res.status(403).json({
success: false,
message: 'Insufficient permissions',
required: requiredPermissions
});
}
next();
};
}
// Kullanim
router.get('/products', requirePermission('product:read'), listProducts);
router.post('/products', requirePermission('product:create'), createProduct);
router.delete('/products/:id', requirePermission('product:delete'), deleteProduct);
router.get('/reports', requirePermission('report:read', 'report:export'), getReports);7.6 Token Saklama
| Yöntem | XSS Riski | CSRF Riski | Erisilebilirlik | Öneri |
|---|---|---|---|---|
| localStorage | YUKSEK (JS ile okunabilir) | Yok | Kolay | Production'da kullanma |
| sessionStorage | YUKSEK (JS ile okunabilir) | Yok | Sayfa kapatinca silinir | Production'da kullanma |
| httpOnly Cookie | DUSUK (JS ile okunamaz) | VAR (CSRF token ile coz) | Otomatik gonderilir | En güvenli -- ONERILEN |
| Memory (değişken) | Orta | Yok | Sayfa yenileyince kaybeder | SPA için gecici çözüm |
// httpOnly cookie ile token saklama (en guvenli)
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await authService.login(email, password);
const { accessToken, refreshToken } = generateTokens(user);
// Access token: httpOnly cookie
res.cookie('access_token', accessToken, {
httpOnly: true, // JavaScript ile okunamaz
secure: true, // Sadece HTTPS uzerinden gonderilir
sameSite: 'strict', // CSRF korunma
maxAge: 15 * 60 * 1000, // 15 dakika
path: '/'
});
// Refresh token: httpOnly cookie
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 gun
path: '/auth/refresh' // Sadece refresh endpoint'ine gonderilir
});
res.json({
success: true,
data: { user: { id: user.id, name: user.name, email: user.email } }
});
});
// Cookie'den token oku middleware
function cookieAuthMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.cookies['access_token'];
if (!token) {
return res.status(401).json({ success: false, message: 'No token provided' });
}
try {
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ success: false, message: 'Invalid token' });
}
}8) Database Patterns
8.1 Migration Stratejisi
Migration, veritabani semasini kod olarak versiyonlama sistemidir. Her degisiklik bir migration dosyasi olarak saklanir.
// Migration dosyasi ornegi (knex.js)
// migrations/20250115_001_create_users_table.ts
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('users', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('name', 100).notNullable();
table.string('email', 255).notNullable().unique();
table.string('password_hash', 255).notNullable();
table.enum('role', ['user', 'admin', 'superadmin']).defaultTo('user');
table.boolean('is_active').defaultTo(true);
table.timestamp('email_verified_at').nullable();
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
// Indexler
table.index('email');
table.index('role');
table.index('is_active');
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists('users');
}Migration Kurallari:
- Her migration dosyasi timestamp ile isimlendirilmeli (siralama için)
- Her migration için
up(uygula) vedown(geri al) yazmali - Production'da uygulanan migration asla degistirilmemeli -- yeni migration yaz
- Migration'lar küçük ve atomik olmali (bir migration = bir degisiklik)
- Veri migration'lari (data migration) şema migration'larindan ayrilmali
8.2 Soft Delete
Kaydi veritabanindan silmek yerine "silindi" olarak isaretleme. Geri alma, audit log ve veri butunlugu için kullanilir.
// Soft delete implementasyonu
// Migration
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('products', (table) => {
table.timestamp('deleted_at').nullable();
table.index('deleted_at');
});
}
// Repository
class ProductRepository {
// Tum sorgulara deleted_at IS NULL ekle
async findAll(): Promise<Product[]> {
return db.query('SELECT * FROM products WHERE deleted_at IS NULL ORDER BY created_at DESC');
}
async findById(id: string): Promise<Product | null> {
const result = await db.query(
'SELECT * FROM products WHERE id = $1 AND deleted_at IS NULL',
[id]
);
return result.rows[0] || null;
}
// Soft delete: deleted_at set et
async softDelete(id: string): Promise<void> {
await db.query(
'UPDATE products SET deleted_at = NOW() WHERE id = $1',
[id]
);
}
// Geri yukle
async restore(id: string): Promise<void> {
await db.query(
'UPDATE products SET deleted_at = NULL WHERE id = $1',
[id]
);
}
// Gercekten sil (admin, GDPR talebi icin)
async hardDelete(id: string): Promise<void> {
await db.query('DELETE FROM products WHERE id = $1', [id]);
}
// Silinen kayitlari dahil et (admin icin)
async findAllWithDeleted(): Promise<Product[]> {
return db.query('SELECT * FROM products ORDER BY created_at DESC');
}
}8.3 Audit Log (Degisiklik Takibi)
Kim, ne zaman, neyi degistirdi -- tüm degisiklikleri izleme.
-- Audit log tablosu
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
table_name VARCHAR(100) NOT NULL,
record_id VARCHAR(100) NOT NULL,
action VARCHAR(20) NOT NULL, -- INSERT, UPDATE, DELETE
old_values JSONB,
new_values JSONB,
changed_by UUID REFERENCES users(id),
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_audit_table_record ON audit_logs(table_name, record_id);
CREATE INDEX idx_audit_changed_by ON audit_logs(changed_by);
CREATE INDEX idx_audit_created_at ON audit_logs(created_at);// Audit log service
class AuditLogService {
async log(params: {
tableName: string;
recordId: string;
action: 'INSERT' | 'UPDATE' | 'DELETE';
oldValues?: Record<string, any>;
newValues?: Record<string, any>;
changedBy: string;
ipAddress?: string;
userAgent?: string;
}): Promise<void> {
await db.query(
`INSERT INTO audit_logs (table_name, record_id, action, old_values, new_values, changed_by, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
params.tableName,
params.recordId,
params.action,
params.oldValues ? JSON.stringify(params.oldValues) : null,
params.newValues ? JSON.stringify(params.newValues) : null,
params.changedBy,
params.ipAddress,
params.userAgent
]
);
}
}
// Service'te kullanim
class ProductService {
constructor(
private productRepo: ProductRepository,
private auditLog: AuditLogService
) {}
async updateProduct(id: string, data: UpdateProductDTO, userId: string, req: Request) {
const oldProduct = await this.productRepo.findById(id);
if (!oldProduct) throw new NotFoundError('Product', id);
const updatedProduct = await this.productRepo.update(id, data);
await this.auditLog.log({
tableName: 'products',
recordId: id,
action: 'UPDATE',
oldValues: { name: oldProduct.name, price: oldProduct.price },
newValues: { name: updatedProduct.name, price: updatedProduct.price },
changedBy: userId,
ipAddress: req.ip,
userAgent: req.headers['user-agent']
});
return updatedProduct;
}
}8.4 N+1 Query Problemi
Bir listeyi cekip, her bir eleman için ayri sorgu atma problemi.
// PROBLEM: N+1 Query
// 1 sorgu: Tum siparisleri cek
const orders = await db.query('SELECT * FROM orders'); // 1 sorgu
// N sorgu: Her siparis icin kullaniciyi cek
for (const order of orders) {
const user = await db.query(
'SELECT * FROM users WHERE id = $1',
[order.user_id]
); // N sorgu (100 siparis = 100 sorgu!)
order.user = user.rows[0];
}
// Toplam: 1 + 100 = 101 sorgu!
// COZUM 1: JOIN kullan
const ordersWithUsers = await db.query(`
SELECT o.*, u.name as user_name, u.email as user_email
FROM orders o
JOIN users u ON u.id = o.user_id
ORDER BY o.created_at DESC
`); // 1 sorgu
// COZUM 2: Batch loading (IN clause)
const orders = await db.query('SELECT * FROM orders'); // 1 sorgu
const userIds = [...new Set(orders.rows.map(o => o.user_id))];
const users = await db.query(
'SELECT * FROM users WHERE id = ANY($1)',
[userIds]
); // 1 sorgu (toplam 2 sorgu!)
const userMap = new Map(users.rows.map(u => [u.id, u]));
orders.rows.forEach(order => {
order.user = userMap.get(order.user_id);
});
// COZUM 3: ORM'de eager loading (Prisma ornegi)
const orders = await prisma.order.findMany({
include: {
user: true, // JOIN yapar
items: {
include: {
product: true // Nested join
}
}
}
});8.5 Database Indexing Stratejisi
| Index Turu | Ne Zaman | Örnek |
|---|---|---|
| Primary Key | Her tablo (otomatik) | id |
| Unique | Tekil olmasi gereken alanlar | email, username |
| Standard (B-tree) | WHERE, ORDER BY, JOIN'da sik kullanilan alanlar | status, category_id |
| Composite | Birden fazla alanla sorgulama | (user_id, created_at) |
| Partial | Belirli kosuldaki satirlar | WHERE is_active = true |
| GIN | JSONB, full-text search | tags, metadata |
| Expression | Fonksiyon sonucu uzerinde | LOWER(email) |
-- Index ornekleri
-- Sik sorgulanan alanlar
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
-- Composite index (birden fazla alan)
-- Bu index su sorgulari hizlandirir: WHERE user_id = X AND created_at > Y
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
-- Partial index (sadece aktif kayitlar)
-- Silinen kayitlari indexlemeye gerek yok
CREATE INDEX idx_products_active ON products(category_id)
WHERE deleted_at IS NULL AND is_active = true;
-- GIN index (JSONB arama)
CREATE INDEX idx_products_tags ON products USING GIN(tags);
-- Expression index
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- Index kullanim analizi
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = '123' AND status = 'pending';Indexing Kurallari:
- Her foreign key için index oluştur
- WHERE, ORDER BY, GROUP BY'da sik kullanilan alanlara index ekle
- Cok fazla index yazma islemlerini yavaslatir (INSERT/UPDATE)
- Kullanilmayan index'leri bul ve sil (
pg_stat_user_indexes) - Composite index'te siralama önemli: en secici alan solda olmali
8.6 Connection Pooling
// Connection pool -- veritabani baglantilari havuzu
import { Pool } from 'pg';
const pool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
// Pool ayarlari
min: 5, // Minimum acik baglanti
max: 20, // Maximum acik baglanti
idleTimeoutMillis: 30000, // Bos baglanti 30 sn sonra kapatilir
connectionTimeoutMillis: 5000, // Baglanti kurma zaman asimi
maxUses: 7500 // Bir baglanti max 7500 sorgudan sonra yenilenir
});
// Pool olaylari
pool.on('connect', () => {
console.log('New client connected to pool');
});
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('Closing pool...');
await pool.end();
process.exit(0);
});
export default pool;9) Caching Stratejileri
9.1 Cache Seviyeleri
| Seviye | Nerede | Örnek | TTL | Invalidation |
|---|---|---|---|---|
| Application Cache | Uygulama belleginde | Node.js Map, LRU cache | Saniye-dakika | Uygulama restart'ta kaybolur |
| Database Query Cache | DB veya Redis | Sorgu sonucu cache'leme | Dakika-saat | Event-based |
| HTTP Cache | Browser / Proxy | ETag, Cache-Control | Saat-gun | Header-based |
| CDN Cache | Edge server'lar | Cloudflare, AWS CloudFront | Saat-gun | Purge API |
| OPcode Cache | PHP runtime | OPcache | Uygulama omru | Config/restart |
9.2 Cache Patterns
Cache-Aside (Lazy Loading)
En yaygin pattern. Uygulama cache'i kontrol eder, yoksa DB'den okur ve cache'e yazar.
// Cache-Aside pattern
class ProductService {
constructor(
private productRepo: ProductRepository,
private cache: RedisClient
) {}
async getProduct(id: string): Promise<Product> {
const cacheKey = `product:${id}`;
// 1. Cache'e bak
const cached = await this.cache.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. DB'den oku
const product = await this.productRepo.findById(id);
if (!product) throw new NotFoundError('Product', id);
// 3. Cache'e yaz
await this.cache.set(cacheKey, JSON.stringify(product), { EX: 3600 }); // 1 saat
return product;
}
async updateProduct(id: string, data: UpdateProductDTO): Promise<Product> {
const product = await this.productRepo.update(id, data);
// Cache'i invalidate et
await this.cache.del(`product:${id}`);
// Iliskili cache'leri de temizle
await this.cache.del('product:list:*'); // Pattern ile silme
return product;
}
}Write-Through
Veri hem cache'e hem DB'ye ayni anda yazilir.
// Write-Through pattern
class ProductService {
async updateProduct(id: string, data: UpdateProductDTO): Promise<Product> {
// 1. DB'ye yaz
const product = await this.productRepo.update(id, data);
// 2. Cache'e de ayni anda yaz
await this.cache.set(
`product:${id}`,
JSON.stringify(product),
{ EX: 3600 }
);
return product;
}
}Write-Behind (Write-Back)
Once cache'e yaz, DB'ye asenkron olarak sonra yaz. Performans kazanimi sağlar ama veri kaybi riski vardir.
// Write-Behind pattern (kavramsal)
class WriteBackCache {
private writeQueue: Map<string, { data: any; timer: NodeJS.Timeout }> = new Map();
async write(key: string, data: any): Promise<void> {
// 1. Hemen cache'e yaz
await this.cache.set(key, JSON.stringify(data));
// 2. Mevcut timer varsa iptal et (debounce)
const existing = this.writeQueue.get(key);
if (existing) clearTimeout(existing.timer);
// 3. 5 saniye sonra DB'ye yaz
const timer = setTimeout(async () => {
await this.persistToDB(key, data);
this.writeQueue.delete(key);
}, 5000);
this.writeQueue.set(key, { data, timer });
}
private async persistToDB(key: string, data: any): Promise<void> {
// DB'ye kaydet
await db.query('UPDATE ... SET ... WHERE id = $1', [data.id]);
}
}9.3 Redis Temel Kullanım
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
// String -- basit key-value
await redis.set('user:123:name', 'Fahri');
await redis.get('user:123:name'); // 'Fahri'
await redis.set('session:abc', JSON.stringify(sessionData), { EX: 3600 }); // 1 saat TTL
// Hash -- nesne alanlari
await redis.hSet('user:123', { name: 'Fahri', email: 'f@example.com', role: 'admin' });
await redis.hGet('user:123', 'name'); // 'Fahri'
await redis.hGetAll('user:123'); // { name: 'Fahri', email: '...', role: 'admin' }
// List -- siralanmis liste (kuyruk icin)
await redis.lPush('notifications:123', JSON.stringify(notification)); // basa ekle
await redis.rPop('notifications:123'); // sondan cik (FIFO)
await redis.lRange('notifications:123', 0, 9); // ilk 10 eleman
// Set -- tekil degerler
await redis.sAdd('online_users', 'user:123');
await redis.sAdd('online_users', 'user:456');
await redis.sMembers('online_users'); // ['user:123', 'user:456']
await redis.sIsMember('online_users', 'user:123'); // true
// TTL (Time To Live)
await redis.set('otp:user123', '485923', { EX: 300 }); // 5 dakika
await redis.ttl('otp:user123'); // kalan saniye9.4 Cache Invalidation
| Strateji | Açıklama | Avantaj | Dezavantaj |
|---|---|---|---|
| TTL (Time-Based) | Belirli sureden sonra otomatik silinir | Basit | Veri eski kalabilir |
| Event-Based | Veri degistiginde cache'i sil | Guncel | Ek karmasiklik |
| Version-Based | Key'e versiyon numarasi ekle | Atomik | Eski versiyon kaynaklari kalir |
// Event-based invalidation
class ProductService {
constructor(
private productRepo: ProductRepository,
private cache: RedisClient,
private eventBus: EventEmitter
) {
// Product degisikliklerini dinle
this.eventBus.on('product:updated', (productId) => this.invalidateProduct(productId));
this.eventBus.on('product:deleted', (productId) => this.invalidateProduct(productId));
}
private async invalidateProduct(productId: string): Promise<void> {
// Iliskili tum cache key'lerini sil
await Promise.all([
this.cache.del(`product:${productId}`),
this.cache.del(`product:${productId}:reviews`),
this.cache.del(`product:${productId}:recommendations`),
// List cache'lerini de temizle
this.invalidateListCaches()
]);
}
private async invalidateListCaches(): Promise<void> {
// Pattern ile tum liste cache'lerini sil
const keys = await this.cache.keys('products:list:*');
if (keys.length > 0) {
await this.cache.del(keys);
}
}
}9.5 HTTP Caching
// ETag ile cache
import crypto from 'crypto';
app.get('/api/products/:id', async (req, res) => {
const product = await productService.getProduct(req.params.id);
const productJson = JSON.stringify(product);
// ETag olustur
const etag = crypto.createHash('md5').update(productJson).digest('hex');
// Client'in gonderdigi ETag ile karsilastir
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // Not Modified -- body gonderme
}
res.set({
'ETag': etag,
'Cache-Control': 'private, max-age=60', // 1 dakika browser cache
'Vary': 'Authorization' // Kullaniciya ozel cache
});
res.json({ success: true, data: product });
});
// Statik kaynaklar icin agresif cache
app.use('/static', express.static('public', {
maxAge: '1y', // 1 yil
immutable: true, // Degismeyecek (hash'li dosyalar)
etag: true
}));
// API response'lari icin cache header'lari
app.use('/api', (req, res, next) => {
// Genel API: cache'leme
res.set('Cache-Control', 'no-store'); // Varsayilan: cache'leme
next();
});10) Message Queues & Event-Driven
10.1 Neden Queue Kullanilir?
Uzun suren islemleri kullanicinin beklemesine gerek kalmadan arka planda islemek için:
| Senaryo | Queue'suz | Queue ile |
|---|---|---|
| Kayit sonrasi email | Kullanici 3-5 sn bekler | Aninda cevap, email arka planda gider |
| Resim boyutlandirma | Upload 10 sn surer | Upload 1 sn, işlem arka planda |
| Rapor oluşturma | Request timeout olur (30 sn) | Rapor hazir olunca bildirim gider |
| PDF oluşturma | API yavaslar | API hızlı, PDF arka planda olusur |
| Toplu email gonderme | 1000 email = 30 dk bekleme | Aninda cevap, kuyruktan gonderilir |
10.2 Job/Worker Pattern
┌──────────┐ ┌───────────┐ ┌──────────┐
│ Producer │──────►│ Queue │──────►│ Consumer │
│ (API) │ │ (Redis/ │ │ (Worker) │
│ │ │ RabbitMQ)│ │ │
└──────────┘ └───────────┘ └──────────┘
Ornek:
API endpoint Redis Queue Worker process
POST /register ───► "send_welcome_ ──► EmailWorker
email" job - Email gonder
- Template render et// Producer -- Job olustur (BullMQ ornegi)
import { Queue } from 'bullmq';
const emailQueue = new Queue('email', {
connection: { host: 'localhost', port: 6379 }
});
// API'de: Job'i kuyruge ekle
app.post('/api/auth/register', async (req, res) => {
const user = await userService.register(req.body);
// Email gonderimi kuyruge ekle (async)
await emailQueue.add('welcome-email', {
userId: user.id,
email: user.email,
name: user.name
}, {
attempts: 3, // 3 kez dene
backoff: {
type: 'exponential',
delay: 2000 // 2s, 4s, 8s
},
removeOnComplete: 100, // Son 100 basarili job'i tut
removeOnFail: 500 // Son 500 basarisiz job'i tut
});
// Kullaniciya hemen cevap dondur (email arka planda gidecek)
res.status(201).json({ success: true, data: user });
});
// Consumer -- Job'i isle (ayri process)
import { Worker } from 'bullmq';
const emailWorker = new Worker('email', async (job) => {
const { userId, email, name } = job.data;
console.log(`Processing job ${job.id}: Send welcome email to ${email}`);
switch (job.name) {
case 'welcome-email':
await emailService.sendWelcome(email, name);
break;
case 'order-confirmation':
await emailService.sendOrderConfirmation(email, job.data.orderId);
break;
case 'password-reset':
await emailService.sendPasswordReset(email, job.data.resetToken);
break;
}
console.log(`Job ${job.id} completed`);
}, {
connection: { host: 'localhost', port: 6379 },
concurrency: 5 // Ayni anda 5 job isle
});
emailWorker.on('completed', (job) => {
console.log(`Job ${job.id} completed successfully`);
});
emailWorker.on('failed', (job, err) => {
console.error(`Job ${job?.id} failed: ${err.message}`);
});10.3 Queue Araçları Karşılaştırma
| Özellik | BullMQ (Redis) | RabbitMQ | AWS SQS |
|---|---|---|---|
| Altyapi | Redis gerekli | Ayri server | Managed (AWS) |
| Protokol | Redis commands | AMQP | HTTP/AWS SDK |
| Siralama garantisi | FIFO | FIFO (per queue) | FIFO (opsiyonel) |
| Retry | Built-in (backoff) | Manual ack/nack | Built-in |
| Dead letter queue | Var | Var | Var |
| Dashboard | Bull Board | RabbitMQ Management | AWS Console |
| Olcekleme | Redis cluster | Cluster + Federation | Otomatik |
| Gecikme (delay) | Var | Plugin ile | Var |
| Oncelik (priority) | Var | Var | Yok (standard) |
| Kurulum kolayligi | Kolay (Redis varsa) | Orta | Kolay (AWS) |
| Maliyet | Redis maliyeti | Server maliyeti | Kullanım basina |
| Ideal kullanım | Node.js projeleri | Karmasik routing | AWS ekosistemi |
10.4 Dead Letter Queue & Retry
// Dead Letter Queue (DLQ) -- basarisiz job'lari ayri kuyrukta topla
const mainQueue = new Queue('orders', { connection: redisConfig });
const dlq = new Queue('orders-dlq', { connection: redisConfig });
const orderWorker = new Worker('orders', async (job) => {
try {
await processOrder(job.data);
} catch (error) {
// Son denemeyse DLQ'ya gonder
if (job.attemptsMade >= (job.opts.attempts || 3) - 1) {
await dlq.add('failed-order', {
originalJobId: job.id,
data: job.data,
error: (error as Error).message,
failedAt: new Date().toISOString(),
attempts: job.attemptsMade + 1
});
}
throw error; // BullMQ retry mekanizmasini tetikle
}
}, {
connection: redisConfig,
concurrency: 10
});
// Exponential backoff -- her denemede bekleme suresi katlanir
// Deneme 1: 2 saniye bekle
// Deneme 2: 4 saniye bekle
// Deneme 3: 8 saniye bekle
// Deneme 4: 16 saniye bekle
// Deneme 5: DLQ'ya gonder
await mainQueue.add('process-order', orderData, {
attempts: 5,
backoff: {
type: 'exponential',
delay: 2000 // baslangic: 2 saniye
}
});Retry Akisi:
Job basarisiz ──► 2s bekle ──► Tekrar dene (1)
│
basarisiz
│
4s bekle ──► Tekrar dene (2)
│
basarisiz
│
8s bekle ──► Tekrar dene (3)
│
basarisiz
│
Dead Letter Queue'ya gonder
(manuel inceleme icin)11) Error Handling & Logging
11.1 Error Handling Stratejisi
Custom Exception Siniflari
// errors/AppError.ts -- Temel hata sinifi
export class AppError extends Error {
public readonly statusCode: number;
public readonly code: string;
public readonly isOperational: boolean;
constructor(
message: string,
statusCode: number,
code: string,
isOperational = true
) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = isOperational; // Beklenen hata mi, bug mi?
Object.setPrototypeOf(this, new.target.prototype);
}
}
// Ozel hata siniflari
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} not found with id: ${id}`, 404, 'RESOURCE_NOT_FOUND');
}
}
export class ValidationError extends AppError {
public readonly errors: Array<{ field: string; message: string }>;
constructor(message: string, errors: Array<{ field: string; message: string }> = []) {
super(message, 422, 'VALIDATION_ERROR');
this.errors = errors;
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401, 'UNAUTHORIZED');
}
}
export class ForbiddenError extends AppError {
constructor(message = 'Insufficient permissions') {
super(message, 403, 'FORBIDDEN');
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super(message, 409, 'CONFLICT');
}
}
export class RateLimitError extends AppError {
constructor(message = 'Too many requests') {
super(message, 429, 'RATE_LIMIT_EXCEEDED');
}
}Global Error Handler
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/AppError';
import { logger } from '../utils/logger';
export function globalErrorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
// AppError (beklenen hata)
if (err instanceof AppError) {
logger.warn({
code: err.code,
message: err.message,
statusCode: err.statusCode,
path: req.path,
method: req.method,
requestId: req.headers['x-request-id']
});
return res.status(err.statusCode).json({
success: false,
message: err.message,
code: err.code,
errors: (err as any).errors || null,
meta: {
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id']
}
});
}
// Beklenmeyen hata (bug)
logger.error({
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
body: req.body,
requestId: req.headers['x-request-id']
});
// Production'da hata detayini disari gosterme
const isProduction = process.env.NODE_ENV === 'production';
res.status(500).json({
success: false,
message: isProduction ? 'Internal server error' : err.message,
code: 'INTERNAL_ERROR',
stack: isProduction ? undefined : err.stack,
meta: {
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id']
}
});
}
// app.ts -- En sona ekle
app.use(globalErrorHandler);11.2 Error Response Standardi
// Tum hata response'lari bu formatta
interface ErrorResponse {
success: false;
message: string; // Kullaniciya gosterilecek mesaj
code: string; // Makine tarafindan okunabilir hata kodu
errors?: FieldError[]; // Validation hatalari (opsiyonel)
meta: {
timestamp: string;
requestId?: string;
path?: string;
};
}
// Ornekler:
// 400 Bad Request
{
"success": false,
"message": "Invalid request body",
"code": "BAD_REQUEST",
"meta": { "timestamp": "2025-01-15T10:30:00Z" }
}
// 422 Validation Error
{
"success": false,
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"errors": [
{ "field": "email", "message": "Gecerli bir email giriniz" },
{ "field": "password", "message": "En az 8 karakter olmali" }
],
"meta": { "timestamp": "2025-01-15T10:30:00Z" }
}
// 404 Not Found
{
"success": false,
"message": "Product not found with id: abc123",
"code": "RESOURCE_NOT_FOUND",
"meta": { "timestamp": "2025-01-15T10:30:00Z" }
}11.3 Logging Seviyeleri
| Seviye | Oncelik | Ne Zaman Kullan | Örnek |
|---|---|---|---|
| FATAL | 0 (en yüksek) | Uygulama calismaya devam edemez | DB baglantisi tamamen koptu |
| ERROR | 1 | Beklenmeyen hata, işlem basarisiz | Odeme islemi basarisiz, unhandled exception |
| WARN | 2 | Potansiyel sorun, ama işlem devam ediyor | Rate limit yaklasti, deprecation kullanimi |
| INFO | 3 | Önemli is olaylari | Kullanici kaydi, siparis oluşturma, deploy |
| DEBUG | 4 | Geliştirme amacli detay | SQL sorgusu, cache hit/miss, request/response |
| TRACE | 5 (en düşük) | En ince detay, nadiren kullanilir | Fonksiyon giris/cikis, değişken degerleri |
Production'da: INFO ve ustu (WARN, ERROR, FATAL) Development'ta: DEBUG ve ustu
11.4 Structured Logging (JSON)
// winston ile structured logging
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: {
service: 'order-service',
environment: process.env.NODE_ENV
},
transports: [
// Console (development)
new winston.transports.Console({
format: process.env.NODE_ENV === 'development'
? winston.format.combine(winston.format.colorize(), winston.format.simple())
: winston.format.json()
}),
// File (production)
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// Kullanim
logger.info('Order created', {
orderId: 'ord-123',
userId: 'usr-456',
amount: 299.99,
currency: 'TRY',
items: 3
});
// Cikti (JSON):
// {
// "level": "info",
// "message": "Order created",
// "orderId": "ord-123",
// "userId": "usr-456",
// "amount": 299.99,
// "currency": "TRY",
// "items": 3,
// "service": "order-service",
// "environment": "production",
// "timestamp": "2025-01-15T10:30:00.000Z"
// }Correlation ID (Istek Takibi)
// Her istege benzersiz ID ata -- tum log'larda izlenebilir
import { v4 as uuidv4 } from 'uuid';
import { AsyncLocalStorage } from 'async_hooks';
const asyncLocalStorage = new AsyncLocalStorage<{ requestId: string }>();
// Middleware: Her istege requestId ata
app.use((req: Request, res: Response, next: NextFunction) => {
const requestId = req.headers['x-request-id'] as string || uuidv4();
res.set('x-request-id', requestId);
asyncLocalStorage.run({ requestId }, () => {
next();
});
});
// Logger: requestId'yi otomatik ekle
function getLogger() {
const store = asyncLocalStorage.getStore();
return {
info: (message: string, meta?: object) => {
logger.info(message, { ...meta, requestId: store?.requestId });
},
error: (message: string, meta?: object) => {
logger.error(message, { ...meta, requestId: store?.requestId });
}
};
}
// Kullanim -- requestId her log satirinda otomatik bulunur
app.get('/api/orders/:id', async (req, res) => {
const log = getLogger();
log.info('Fetching order', { orderId: req.params.id });
// Log ciktisi: { message: "Fetching order", orderId: "123", requestId: "req-abc-def" }
const order = await orderService.getOrder(req.params.id);
log.info('Order fetched successfully', { orderId: order.id });
res.json({ success: true, data: order });
});11.5 Error Tracking (Sentry)
// Sentry entegrasyonu
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, // Prod'da %10 trace
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Sentry.Integrations.Express({ app })
]
});
// Sentry request handler (ilk middleware)
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
// ... routes ...
// Sentry error handler (son middleware, error handler'dan once)
app.use(Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// Sadece 500 hatalarini raporla, 4xx client hatalari degil
if (error instanceof AppError && error.statusCode < 500) return false;
return true;
}
}));
app.use(globalErrorHandler);
// Manuel hata raporlama
try {
await riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: { module: 'payment', provider: 'stripe' },
extra: { orderId: '123', amount: 299.99 },
user: { id: userId, email: userEmail }
});
throw error;
}12) CI/CD Pipeline
12.1 Pipeline Asamalari
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Lint │───►│ Test │───►│ Build │───►│ Deploy │───►│ Verify │
│ │ │ │ │ │ │ (Staging)│ │ │
│ ESLint │ │ Unit │ │ Docker │ │ │ │ Smoke │
│ Prettier│ │ Integ. │ │ Bundle │ │ CD tool │ │ Health │
│ TypeCheck│ │ E2E │ │ Compile │ │ k8s/ECS │ │ Monitor │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Fail? Fail? Fail? Staging OK? Health OK?
Stop! Stop! Stop! ──────► ──────►
Production Done!12.2 GitHub Actions Workflow Ornegi
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 1. Lint & Type Check
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
# 2. Test
test:
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run migrations
run: npm run db:migrate
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
# 3. Build
build:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
# 4. Deploy to Staging
deploy-staging:
runs-on: ubuntu-latest
needs: build
environment: staging
steps:
- name: Deploy to staging
run: |
echo "Deploying ${{ github.sha }} to staging..."
# kubectl set image deployment/app app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Smoke test
run: |
sleep 30
curl -f https://staging.example.com/health || exit 1
# 5. Deploy to Production (manual approval)
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment: production # GitHub Environment -- manual approval gerektirir
steps:
- name: Deploy to production
run: |
echo "Deploying ${{ github.sha }} to production..."
- name: Health check
run: |
sleep 30
curl -f https://api.example.com/health || exit 1
- name: Notify
run: |
echo "Deployment successful!"12.3 Branch Stratejisi
GitFlow vs Trunk-Based Development
| Özellik | GitFlow | Trunk-Based |
|---|---|---|
| Ana branch'ler | main + develop | main (tek) |
| Feature branch | feature/* (uzun omurlu) | Kisa omurlu (1-2 gun max) |
| Release | release/* branch | main'den direkt (tag ile) |
| Hotfix | hotfix/* branch | main'den direkt |
| Karmasiklik | Yüksek (cok branch) | Düşük (tek branch) |
| Merge conflict | Sik (uzun branch'ler) | Nadir (kisa branch'ler) |
| CI/CD uyumu | Orta | Yüksek |
| Takimda kisi | 5-20 (büyük takimlar) | 2-10 (küçük-orta takimlar) |
| Deploy sikligi | Haftada/ayda bir | Gunde birden fazla |
| Feature flags | Genellikle gereksiz | Gerekli olabilir |
GitFlow:
main ─────────────────────────────────────────────────
│ ▲
└──► develop ──────────────────────┤
│ ▲ ▲ │
└──► feature/x ──┘ release/1.0
└──► feature/y ──┘
Trunk-Based:
main ─────────────────────────────────────────────────
│ ▲ │ ▲ │ ▲
└─► ┘ └─► ┘ └─► ┘
short short short
lived lived lived
branch branch branch
(1-2d) (1-2d) (1-2d)12.4 Environment'lar
| Environment | Amac | Veri | Erisim | Deploy |
|---|---|---|---|---|
| Development | Geliştirme, test | Fake/seed data | Gelistiriciler | Her commit |
| Staging | Pre-production test | Production benzeri (anonimlestirilmis) | Takim + QA | develop/main merge |
| Production | Canli kullanicilar | Gercek veri | Herkes | Manuel onay |
// Environment-based konfigürasyon
// config/index.ts
interface Config {
port: number;
database: {
url: string;
ssl: boolean;
pool: { min: number; max: number };
};
redis: { url: string };
logging: { level: string };
cors: { origins: string[] };
}
const configs: Record<string, Config> = {
development: {
port: 3000,
database: {
url: 'postgresql://dev:dev@localhost:5432/myapp_dev',
ssl: false,
pool: { min: 2, max: 10 }
},
redis: { url: 'redis://localhost:6379' },
logging: { level: 'debug' },
cors: { origins: ['http://localhost:3000', 'http://localhost:5173'] }
},
staging: {
port: 3000,
database: {
url: process.env.DATABASE_URL!,
ssl: true,
pool: { min: 5, max: 20 }
},
redis: { url: process.env.REDIS_URL! },
logging: { level: 'info' },
cors: { origins: ['https://staging.example.com'] }
},
production: {
port: 3000,
database: {
url: process.env.DATABASE_URL!,
ssl: true,
pool: { min: 10, max: 50 }
},
redis: { url: process.env.REDIS_URL! },
logging: { level: 'warn' },
cors: { origins: ['https://example.com', 'https://www.example.com'] }
}
};
export const config = configs[process.env.NODE_ENV || 'development'];12.5 Deployment Stratejileri
| Strateji | Açıklama | Downtime | Rollback | Risk | Maliyet |
|---|---|---|---|---|---|
| Recreate | Eski durdur, yeni baslat | Var | Yavas | Yüksek | Düşük |
| Rolling | Pod'lari sirayla güncelle | Yok | Orta | Orta | Düşük |
| Blue-Green | Iki ortam, traffic swap | Yok | Hızlı (switch) | Düşük | Yüksek (2x kaynak) |
| Canary | %5 trafige yeni versiyon, yavas artir | Yok | Hızlı (geri cek) | Düşük | Orta |
| A/B Testing | Kullanici segmentine gore farkli versiyon | Yok | Hızlı | Düşük | Orta |
Blue-Green Deployment:
Onceki: [Load Balancer] ──────► [Blue (v1.0) AKTIF]
[Green (bos)]
Deploy: [Load Balancer] ──────► [Blue (v1.0) AKTIF]
[Green (v1.1) hazirlaniyor...]
Switch: [Load Balancer] ──────► [Green (v1.1) AKTIF]
[Blue (v1.0) yedek -- sorun olursa geri don]
Canary Deployment:
Basla: [Load Balancer] ──100%──► [v1.0 (10 pod)]
Canary: [Load Balancer] ───95%──► [v1.0 (10 pod)]
───5%───► [v1.1 (1 pod)] ← Metrikler izleniyor
Genisle: [Load Balancer] ───50%──► [v1.0 (5 pod)]
───50%──► [v1.1 (5 pod)] ← Hala iyi
Tamam: [Load Balancer] ──100%──► [v1.1 (10 pod)]13) Güvenlik Checklist (OWASP Top 10)
13.1 OWASP Top 10 (2021)
| Siralama | Zafiyet | Açıklama | Korunma Yontemi |
|---|---|---|---|
| A01 | Broken Access Control | Yetkisiz kaynaklara erisim | RBAC, her endpoint'te yetki kontrolu, IDOR korunmasi |
| A02 | Cryptographic Failures | Zayif sifreleme, hassas veri aciga cikma | HTTPS zorunlu, AES-256 sifreleme, bcrypt/argon2 |
| A03 | Injection | SQL, NoSQL, OS, LDAP injection | Parametreli sorgular, ORM kullan, input sanitize et |
| A04 | Insecure Design | Güvenlik mimari seviyede dusunulmemis | Threat modeling, güvenli tasarim prensipleri |
| A05 | Security Misconfiguration | Varsayilan ayarlar, gereksiz ozellikler acik | Sertlestirme checklist'i, gereksiz servisleri kapat |
| A06 | Vulnerable Components | Bilinen zafiyetli kutuphaneler | npm audit, Dependabot, Snyk |
| A07 | Auth Failures | Zayif kimlik doğrulama | MFA, güvenli oturum yönetimi, brute-force korunma |
| A08 | Data Integrity Failures | Guncellemelerin butunlugu dogrulanmiyor | Signed updates, CI/CD pipeline guvenceleri |
| A09 | Logging Failures | Yetersiz loglama ve izleme | Structured logging, SIEM, alert sistemi |
| A10 | SSRF | Sunucu tarafinda sahte istekler | URL whitelist, internal network erisimini kisitla |
Injection Korunma
// YANLIS: SQL Injection'a acik
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Saldirgan email: ' OR '1'='1' --
// Sonuc: SELECT * FROM users WHERE email = '' OR '1'='1' --'
// DOGRU: Parametreli sorgu
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// DOGRU: ORM kullan (Prisma)
const user = await prisma.user.findUnique({ where: { email } });
// DOGRU: ORM kullan (Knex)
const user = await knex('users').where({ email }).first();XSS Korunma
// Input sanitization
import DOMPurify from 'isomorphic-dompurify';
// Kullanici girdisini temizle
function sanitizeInput(input: string): string {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: [], // HTML tag'i kalmasin
ALLOWED_ATTR: []
});
}
// HTML iceren icerik (blog, yorum)
function sanitizeRichContent(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'target']
});
}
// Security headers
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: []
}
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: { policy: 'same-site' },
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));13.2 Production Güvenlik Checklist
Kimlik Doğrulama & Yetkilendirme:
- [ ] Sifreleri bcrypt/argon2 ile hashle (salt ile)
- [ ] JWT secret en az 256-bit, .env'den oku
- [ ] Access token süresi kisa tut (15 dk)
- [ ] Refresh token rotation uygula
- [ ] Brute-force korunmasi (hesap kilitleme veya rate limit)
- [ ] MFA (multi-factor authentication) destegi
- [ ] OAuth2 PKCE kullan (SPA/mobil için)
- [ ] Her endpoint'te yetki kontrolu yap
Veri Guvenligi:
- [ ] HTTPS zorunlu (HTTP redirect)
- [ ] Hassas veriler sifrelenmis saklanir (AES-256)
- [ ] API response'larinda hassas alan yok (password_hash, secret)
- [ ] CORS sadece izin verilen origin'ler
- [ ] SQL injection korunmasi (parametreli sorgu / ORM)
- [ ] XSS korunmasi (input sanitization + CSP header)
- [ ] CSRF korunmasi (SameSite cookie veya CSRF token)
Altyapi & Konfigürasyon:
- [ ] .env dosyasi git'e commitlenmemis (.gitignore)
- [ ] Production'da debug modu kapali
- [ ] Error mesajlarinda stack trace gosterilmiyor
- [ ] Rate limiting aktif (genel + login endpoint)
- [ ] Security headers (helmet.js) aktif
- [ ] Dependency audit (
npm audit, Snyk) duzeltilmis - [ ] Gereksiz portlar kapali
- [ ] Varsayilan sifre/kullanici adi degistirilmis
Loglama & İzleme:
- [ ] Güvenlik olaylari loglanir (login, basarisiz giris, yetki hatasi)
- [ ] Log'lar merkezi sistemde toplanir
- [ ] Alarm/alert sistemi kurulmus
- [ ] Sentry veya benzeri error tracking aktif
14) Performance & Scalability
14.1 Profiling (Darboğaz Tespiti)
// Basit profiling -- islem suresi olcumu
function measurePerformance(label: string) {
const start = process.hrtime.bigint();
return {
end: () => {
const end = process.hrtime.bigint();
const durationMs = Number(end - start) / 1_000_000;
console.log(`[PERF] ${label}: ${durationMs.toFixed(2)}ms`);
return durationMs;
}
};
}
// Kullanim
app.get('/api/products', async (req, res) => {
const perf = measurePerformance('GET /api/products');
const dbPerf = measurePerformance('DB Query');
const products = await productRepo.findAll(req.query);
dbPerf.end();
const cachePerf = measurePerformance('Cache Write');
await cache.set('products:list', JSON.stringify(products));
cachePerf.end();
perf.end();
// Cikti:
// [PERF] DB Query: 45.23ms
// [PERF] Cache Write: 2.15ms
// [PERF] GET /api/products: 48.67ms
res.json({ success: true, data: products });
});// Express middleware -- tum isteklerin suresini logla
app.use((req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const logLevel = duration > 1000 ? 'warn' : 'info'; // 1 saniyeden uzunsa warn
logger[logLevel]({
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
contentLength: res.get('content-length')
});
});
next();
});14.2 Horizontal vs Vertical Scaling
| Özellik | Vertical Scaling (Scale Up) | Horizontal Scaling (Scale Out) |
|---|---|---|
| Yaklasim | Daha guclu makine (CPU, RAM) | Daha fazla makine |
| Limit | Fiziksel donanim limiti | Teorik olarak sinirsiz |
| Maliyet | Ustel artar (2x guc = 3x fiyat) | Dogrusal artar |
| Downtime | Gerekebilir (upgrade) | Gerekli degil |
| Karmasiklik | Düşük | Yüksek (load balancing, state yönetimi) |
| State yönetimi | Basit (tek makine) | Karmasik (paylasilmis state) |
| Veritabani | Read replicas, bigger instance | Sharding, partitioning |
| Örnek | t2.micro -> t2.xlarge | 1 instance -> 10 instance |
14.3 Load Balancing Stratejileri
| Strateji | Açıklama | Ne Zaman Kullan |
|---|---|---|
| Round Robin | Istekleri sirayla dagit | Sunucular esit kapasitede |
| Weighted Round Robin | Agirlikla dagit (guclu sunucuya fazla) | Farkli kapasiteli sunucular |
| Least Connections | En az aktif baglantisi olana gonder | Uzun sureli istekler |
| IP Hash | Ayni IP hep ayni sunucuya | Session affinity (sticky session) |
| Random | Rastgele dagit | Basit, esit dağıtım |
# Nginx load balancing ornegi
upstream api_servers {
# Weighted Round Robin
server api1.internal:3000 weight=3; # 3 kat fazla trafik alir
server api2.internal:3000 weight=2;
server api3.internal:3000 weight=1;
# Health check
# Yanit vermezse 30 saniye devre disi birak
server api4.internal:3000 backup; # Yedek sunucu
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://api_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout ayarlari
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
}
}14.4 APM Araçları (Application Performance Monitoring)
| Arac | Özellik | Avantaj | Maliyet |
|---|---|---|---|
| New Relic | Full-stack APM, error tracking | Kapsamli, kolay kurulum | Ucretli (free tier var) |
| Datadog | APM, logs, metrics, tracing | Guclu dashboard, entegrasyonlar | Ucretli |
| Prometheus + Grafana | Metrics toplama + gorsellestirme | Acik kaynak, ozellestirilebilir | Ücretsiz (hosting maliyeti) |
| Elastic APM | APM, ELK stack entegrasyonu | Log + APM tek yerde | Acik kaynak + bulut opsiyonu |
// Prometheus metrics ornegi
import promClient from 'prom-client';
// Default metrikleri topla (CPU, memory, event loop)
promClient.collectDefaultMetrics({ prefix: 'myapp_' });
// Ozel metrikler
const httpRequestDuration = new promClient.Histogram({
name: 'myapp_http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5]
});
const httpRequestTotal = new promClient.Counter({
name: 'myapp_http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const activeConnections = new promClient.Gauge({
name: 'myapp_active_connections',
help: 'Number of active connections'
});
// Middleware
app.use((req, res, next) => {
activeConnections.inc();
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
const route = req.route?.path || req.path;
const labels = { method: req.method, route, status_code: res.statusCode.toString() };
end(labels);
httpRequestTotal.inc(labels);
activeConnections.dec();
});
next();
});
// Metrics endpoint (Prometheus scrape edecek)
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.send(await promClient.register.metrics());
});15) Testing Stratejisi
15.1 Test Piramidi
/ E2E Tests \ Yavas, pahali, az
/ (Cypress, \ Tum sistemi test eder
/ Playwright) \ 5-10% of tests
/───────────────────\
/ Integration Tests \ DB, API, servisler arasi
/ (Supertest, Prisma \ 20-30% of tests
/ test containers) \
/──────────────────────────\
/ Unit Tests \ Hizli, ucuz, cok
/ (Jest, Vitest, Mocha) \ 60-70% of tests
/──────────────────────────────\| Test Turu | Ne Test Eder | Hiz | Maliyet | Örnek |
|---|---|---|---|---|
| Unit | Tek fonksiyon/sınıf (izole) | Cok hızlı (ms) | Düşük | calculateTotal() |
| Integration | Birden fazla birim birlikte | Orta (saniye) | Orta | API endpoint + DB |
| E2E | Tüm sistem (kullanici gibi) | Yavas (dakika) | Yüksek | Login -> Siparis -> Odeme |
15.2 Unit Test
AAA Pattern (Arrange, Act, Assert)
// Jest / Vitest ornegi
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { PricingService } from '../services/PricingService';
describe('PricingService', () => {
let pricingService: PricingService;
beforeEach(() => {
pricingService = new PricingService();
});
describe('calculateTotal', () => {
it('should calculate total for regular customer', () => {
// Arrange (Hazirla)
const basePrice = 100;
const quantity = 5;
const customerType = 'regular';
// Act (Calistir)
const total = pricingService.calculatePrice(basePrice, {
quantity,
customerType
});
// Assert (Dogrula)
expect(total).toBe(500);
});
it('should apply 15% discount for premium customer', () => {
// Arrange
const basePrice = 100;
const quantity = 5;
// Act
const total = pricingService.calculatePrice(basePrice, {
quantity,
customerType: 'premium'
});
// Assert
expect(total).toBe(425); // 500 * 0.85
});
it('should apply volume discount for wholesale (50+ items)', () => {
// Arrange & Act
const total = pricingService.calculatePrice(100, {
quantity: 50,
customerType: 'wholesale'
});
// Assert
expect(total).toBe(4000); // 5000 * 0.80 (20% indirim)
});
it('should throw error for negative price', () => {
// Arrange & Act & Assert
expect(() => {
pricingService.calculatePrice(-100, { quantity: 1, customerType: 'regular' });
}).toThrow('Price must be positive');
});
});
});Mock, Stub, Spy Farki
| Terim | Açıklama | Örnek |
|---|---|---|
| Mock | Sahte nesne, davranisi programlanmis | jest.fn().mockResolvedValue(user) |
| Stub | Belirli bir döngü degeri olan sahte | getUser = () => ({ id: 1, name: 'Test' }) |
| Spy | Gercek fonksiyonu izler (cagirildi mi, kac kez?) | jest.spyOn(emailService, 'send') |
// Mock ornegi -- bagimliliklari taklit et
describe('UserService', () => {
it('should create user and send welcome email', async () => {
// Mock repository
const mockUserRepo = {
create: jest.fn().mockResolvedValue({
id: '1',
name: 'Test User',
email: 'test@test.com'
}),
findByEmail: jest.fn().mockResolvedValue(null) // Email kullanilmamis
};
// Mock email service
const mockEmailService = {
sendWelcome: jest.fn().mockResolvedValue(undefined)
};
const userService = new UserService(
mockUserRepo as any,
mockEmailService as any
);
// Act
const user = await userService.register({
name: 'Test User',
email: 'test@test.com',
password: 'password123'
});
// Assert
expect(user.id).toBe('1');
expect(mockUserRepo.create).toHaveBeenCalledTimes(1);
expect(mockEmailService.sendWelcome).toHaveBeenCalledWith('test@test.com', 'Test User');
});
it('should throw error if email already exists', async () => {
const mockUserRepo = {
findByEmail: jest.fn().mockResolvedValue({ id: '1', email: 'test@test.com' })
};
const userService = new UserService(mockUserRepo as any, {} as any);
await expect(
userService.register({
name: 'Test',
email: 'test@test.com',
password: 'password123'
})
).rejects.toThrow('Email already in use');
});
});// Spy ornegi -- gercek fonksiyonu izle
describe('OrderService', () => {
it('should log order creation', async () => {
const loggerSpy = jest.spyOn(logger, 'info');
await orderService.createOrder({
userId: '1',
items: [{ productId: 'p1', quantity: 2 }]
});
expect(loggerSpy).toHaveBeenCalledWith(
expect.stringContaining('Order created'),
expect.objectContaining({ userId: '1' })
);
loggerSpy.mockRestore(); // Spy'i temizle
});
});15.3 Integration Test
// Supertest ile API integration test
import request from 'supertest';
import { app } from '../app';
import { db } from '../database';
describe('Product API', () => {
// Her test oncesi veritabanini temizle
beforeEach(async () => {
await db.query('DELETE FROM products');
await db.query('DELETE FROM categories');
// Test verisi ekle
await db.query(
`INSERT INTO categories (id, name) VALUES ('cat-1', 'Electronics')`
);
});
afterAll(async () => {
await db.end();
});
describe('POST /api/products', () => {
it('should create a new product', async () => {
const response = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${adminToken}`)
.send({
name: 'iPhone 15',
description: 'Apple iPhone 15',
price: 49999.99,
stock: 100,
categoryId: 'cat-1'
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.name).toBe('iPhone 15');
expect(response.body.data.price).toBe(49999.99);
expect(response.body.data.slug).toBe('iphone-15');
// Veritabaninda kontrol
const dbProduct = await db.query(
'SELECT * FROM products WHERE name = $1',
['iPhone 15']
);
expect(dbProduct.rows).toHaveLength(1);
});
it('should return 422 for invalid price', async () => {
const response = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${adminToken}`)
.send({
name: 'Test Product',
price: -10,
stock: 5,
categoryId: 'cat-1'
})
.expect(422);
expect(response.body.success).toBe(false);
expect(response.body.message).toContain('Price');
});
it('should return 401 without auth token', async () => {
await request(app)
.post('/api/products')
.send({ name: 'Test' })
.expect(401);
});
it('should return 403 for non-admin user', async () => {
await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${userToken}`)
.send({ name: 'Test' })
.expect(403);
});
});
describe('GET /api/products', () => {
beforeEach(async () => {
// Test urunleri ekle
await db.query(
`INSERT INTO products (id, name, slug, price, stock, category_id, is_active, created_at)
VALUES
('p1', 'iPhone 15', 'iphone-15', 49999.99, 100, 'cat-1', true, NOW()),
('p2', 'MacBook Pro', 'macbook-pro', 89999.99, 50, 'cat-1', true, NOW()),
('p3', 'AirPods', 'airpods', 9999.99, 200, 'cat-1', true, NOW())`
);
});
it('should list products with pagination', async () => {
const response = await request(app)
.get('/api/products?page=1&limit=2')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.items).toHaveLength(2);
expect(response.body.meta.total).toBe(3);
expect(response.body.meta.totalPages).toBe(2);
});
it('should filter products by search', async () => {
const response = await request(app)
.get('/api/products?search=macbook')
.expect(200);
expect(response.body.items).toHaveLength(1);
expect(response.body.items[0].name).toBe('MacBook Pro');
});
it('should sort products by price ascending', async () => {
const response = await request(app)
.get('/api/products?sortBy=price&sortOrder=asc')
.expect(200);
const prices = response.body.items.map((p: any) => p.price);
expect(prices).toEqual([...prices].sort((a, b) => a - b));
});
});
});15.4 E2E Test (End-to-End)
// Playwright E2E test ornegi
import { test, expect } from '@playwright/test';
test.describe('Siparis Akisi', () => {
test.beforeEach(async ({ page }) => {
// Giris yap
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
});
test('kullanici urun satin alabilmeli', async ({ page }) => {
// 1. Urunu bul
await page.goto('/products');
await page.fill('[placeholder="Urun ara..."]', 'iPhone');
await page.click('text=iPhone 15');
// 2. Sepete ekle
await page.click('button:has-text("Sepete Ekle")');
await expect(page.locator('.cart-badge')).toHaveText('1');
// 3. Sepete git
await page.click('.cart-icon');
await expect(page.locator('.cart-item')).toHaveCount(1);
// 4. Odeme yap
await page.click('button:has-text("Odemeye Gec")');
// 5. Adres bilgisi
await page.fill('[name="address"]', 'Test Mah. Test Sk. No:1');
await page.fill('[name="city"]', 'Istanbul');
await page.click('button:has-text("Devam")');
// 6. Kart bilgisi
await page.fill('[name="cardNumber"]', '4111111111111111');
await page.fill('[name="expiry"]', '12/26');
await page.fill('[name="cvv"]', '123');
await page.click('button:has-text("Odemeyi Tamamla")');
// 7. Basari sayfasi
await expect(page.locator('h1')).toHaveText('Siparisiz Alindi!');
await expect(page.locator('.order-id')).toBeVisible();
});
});15.5 Test Coverage Hedefi
| Metrik | Minimum | Ideal | Açıklama |
|---|---|---|---|
| Line Coverage | %70 | %85+ | Satirlarin kaci calistirildi |
| Branch Coverage | %60 | %80+ | if/else dallarinin kaci test edildi |
| Function Coverage | %70 | %90+ | Fonksiyonlarin kaci cagrildi |
| Statement Coverage | %70 | %85+ | Ifadelerin kaci calistirildi |
// jest.config.js veya package.json
{
"jest": {
"coverageThreshold": {
"global": {
"branches": 70,
"functions": 80,
"lines": 80,
"statements": 80
}
},
"collectCoverageFrom": [
"src/**/*.{ts,js}",
"!src/**/*.d.ts",
"!src/**/*.test.{ts,js}",
"!src/**/index.{ts,js}",
"!src/migrations/**"
]
}
}Önemli Not
%100 coverage hedefleme. %100 coverage her seyin doğru test edildigini garanti etmez. Anlamli testler yazmak, coverage rakami yukseltmekten daha onemlidir. "Getters/setters test etme" gibi anlamsiz testler coverage'i siser ama deger katmaz.
15.6 CI'da Test
# GitHub Actions'da test
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test -- --coverage --ci
- run: npm run test:e2e
# Coverage raporunu comment olarak PR'a ekle
- name: Coverage Report
uses: davelosert/vitest-coverage-report-action@v2
if: always()16) Hızlı Referans
16.1 Pattern Secim Tablosu
| Sorun | Önerilen Pattern |
|---|---|
| Tek instance gerekli (DB, logger) | Singleton veya Module pattern |
| Nesne oluşturma karmasik | Factory |
| Cok parametreli nesne | Builder |
| 3. parti kutuphaney sarmak | Adapter |
| Mevcut nesneye özellik eklemek | Decorator |
| Karmasik alt sistemi basitlestirmek | Facade |
| Erisim kontrolu, lazy loading | Proxy |
| Olay bildirimi, event sistemi | Observer |
| Degistirilebilir algoritma | Strategy |
| Undo/redo, kuyruge alma | Command |
| Durum bazli davranis | State |
| Is/veri boru hatti | Pipeline / Chain of Responsibility |
16.2 HTTP Status Code Referans
2xx Basari:
200 OK -- Basarili istek
201 Created -- Kaynak olusturuldu
204 No Content -- Basarili ama icerik yok
3xx Yonlendirme:
301 Moved Permanently -- Kalici tasinma
304 Not Modified -- Cache gecerli
4xx Client Hatasi:
400 Bad Request -- Hatali istek
401 Unauthorized -- Kimlik dogrulanamadi
403 Forbidden -- Yetki yok
404 Not Found -- Bulunamadi
405 Method Not Allowed -- Metot desteklenmiyor
409 Conflict -- Cakisma
422 Unprocessable -- Anlam olarak gecersiz
429 Too Many Requests -- Rate limit
5xx Server Hatasi:
500 Internal Error -- Sunucu hatasi
502 Bad Gateway -- Upstream hata
503 Service Unavailable-- Gecici kullanilamaz
504 Gateway Timeout -- Upstream zaman asimi16.3 API Tasarim Checklist
- [ ] URL'ler cogul, küçük harf, kebab-case (
/api/v1/order-items) - [ ] HTTP metotlari doğru kullaniliyor (GET okuma, POST oluşturma...)
- [ ] Tutarli response envelope (
{ success, data, message, errors, meta }) - [ ] Pagination implement edilmis (offset veya cursor)
- [ ] Filtering, sorting, searching destegi
- [ ] Input validation (request body, query params)
- [ ] Anlamli HTTP status code'lari (201 Created, 404 Not Found...)
- [ ] Rate limiting aktif
- [ ] CORS doğru ayarlanmis
- [ ] API versiyonlama (
/api/v1/...) - [ ] Authentication gerekli endpoint'ler korunmus
- [ ] Error response'lar tutarli format
- [ ] Swagger/OpenAPI dokumantasyonu
- [ ] Request/response loglama
16.4 Production Deployment Checklist
Deploy Oncesi:
- [ ] Tüm testler geciyor (unit, integration, E2E)
- [ ] Lint hataları yok
- [ ] Type check basarili
- [ ] Migration'lar hazir
- [ ] Environment degiskenleri ayarlandi
- [ ] Rollback plani var
- [ ] Takim bilgilendirildi
Deploy Sirasinda:
- [ ] Staging'de test edildi
- [ ] Blue-green veya canary deployment
- [ ] Migration'lar uygulandi
- [ ] Health check endpoint calisiyor
Deploy Sonrasi:
- [ ] Health check basarili
- [ ] Smoke test (kritik akislar) basarili
- [ ] Error rate normal
- [ ] Response time normal
- [ ] Log'larda beklenmedik hata yok
- [ ] Monitoring dashboard kontrol edildi
- [ ] Kullanicilara duyuru (gerekiyorsa)
Ilgili Rehberler
Backend
- Backend Genel Bakış
- API Development
- Laravel Rehberi
- ASP.NET Core Guide
- Node.js Rehberi
- Python Rehberi
- AI & LLM