Node.js'te Temiz Mimari (Clean Architecture): Sürdürülebilir ve Test Edilebilir Uygulamalar İçin Kılavuz

Modern yazılım geliştirme, her geçen gün daha karmaşık hale geliyor. Uygulamalar büyüdükçe, yeni özellikler eklemek, hataları ayıklamak ve mevcut kodu sürdürmek giderek zorlaşabiliyor. Bu durum, özellikle hızlı geliştirme döngüleri ve yüksek performans beklenen Node.js projelerinde büyük bir meydan okuma haline gelebilir. Benim geliştirme tecrübelerimde, iyi düşünülmüş bir mimarinin eksikliğinin, başlangıçta kazanılan hızı kısa sürede bir borca dönüştürdüğünü defalarca gözlemledim. İşte bu noktada Temiz Mimari (Clean Architecture) prensipleri, geliştiricilere bir nefes borusu sunuyor.
Bu yazıda, Node.js tabanlı uygulamalarınızda Clean Architecture'ı nasıl uygulayabileceğinizi, bu mimarinin temel prensiplerini, katmanlarını ve sağladığı avantajları derinlemesine inceleyeceğiz. Amacımız, uygulamanızı teknoloji bağımlılıklarından arındırarak daha esnek, bakımı kolay ve en önemlisi test edilebilir bir yapıya kavuşturmak.
Clean Architecture Nedir ve Neden Önemlidir?
Clean Architecture, Robert C. Martin (Uncle Bob) tarafından popülerleştirilmiş, yazılımın bağımlılıkları düzenleyerek iş mantığını çerçevelerden, veritabanlarından ve kullanıcı arayüzlerinden izole etmeyi hedefleyen bir dizi prensiptir. Temel amacı, sistemin karmaşıklığını yönetmek ve yazılımın ömrü boyunca değişimlere karşı daha dirençli olmasını sağlamaktır.
Geleneksel Mimarilerin Zorlukları
Çoğu geleneksel katmanlı mimaride, veritabanı veya web çerçevesi gibi dış bağımlılıklar iş mantığına sızar. Bu durum, teknoloji değişikliği gerektiğinde tüm kod tabanını etkileyebilecek büyük refaktörlere yol açar. Örneğin, uygulamanız Express.js ve MongoDB üzerine kuruluysa, bu teknolojileri değiştirmek, iş kurallarınızın ve hatta kullanıcı arayüzünüzün büyük bir kısmını yeniden yazmak anlamına gelebilir.
Clean Architecture'ın Temel Prensipleri: Bağımlılık Kuralı
Clean Architecture'ın kalbinde, "Bağımlılık Kuralı" yatar. Bu kurala göre, kod bağımlılıkları her zaman dıştan içe doğru olmalıdır. İç katmanlar, dış katmanlar hakkında hiçbir şey bilmemelidir. Örneğin, iş mantığı katmanı (Entities ve Use Cases), web çerçevesi veya veritabanı hakkında bilgi sahibi olmamalıdır. Bu, iç katmanların tamamen dış bileşenlerden bağımsız kalmasını sağlar.

Clean Architecture'ın Katmanları
Clean Architecture genellikle birbirini saran dört ana katmanla temsil edilir (bir soğan veya tekerlek grafiği gibi):
1. Entities (Varlıklar) - En İç Katman
- Sorumluluk: Uygulamanın en temel iş kurallarını içerir. Bunlar, uygulamanın amacını ve işletmenin genel kurallarını tanımlayan nesneler ve veri yapılarıdır. En az değişen kısımdır.
- Node.js'te: Saf JavaScript/TypeScript sınıfları veya basit obje yapıları olarak düşünülebilir. Bunlar genellikle alan modelimizi (domain model) temsil eder.
2. Use Cases (Kullanım Durumları)
- Sorumluluk: Uygulama katmanının iş kurallarını barındırır. Varlıklar üzerinde işlem yapan, uygulamanın belirli işlevselliklerini (örneğin, bir kullanıcının kaydolması, bir ürünün sipariş edilmesi) tanımlayan mantıktır.
- Node.js'te: Her kullanım durumu için ayrı bir sınıf veya fonksiyon olarak implemente edilir. Bağımlılık Enjeksiyonu (Dependency Injection) ile dış servisleri (veritabanı, dış API'ler) alır. Bu katman, Tasarım Desenleri içinde yer alan Command deseni gibi yaklaşımlarla da zenginleştirilebilir.
3. Interface Adapters (Arayüz Bağdaştırıcıları)
- Sorumluluk: İç katmanlar ile dış katmanlar arasındaki adaptasyonları sağlar. Bu katman, verilerin iç katmanlar için uygun bir formata çevrilmesinden ve dış katmanların beklentilerini karşılayacak şekilde dönüştürülmesinden sorumludur.
- Node.js'te:
- Controllers/Presenters: HTTP isteklerini alır, kullanım durumlarını tetikler ve yanıtları formatlar (Express.js kontrolörleri).
- Gateways/Repositories: Veritabanı (MongoDB, PostgreSQL) veya dış servislerle iletişim kuran kodları içerir. Kullanım durumları için soyut arayüzler (interface) tanımlar ve bu arayüzlerin somut implementasyonları bu katmanda yer alır.
- Adapters: Dışardan gelen veya dışarıya giden verileri (JSON, XML vb.) Entities ve Use Cases'in anlayacağı formata çevirir.
4. Frameworks & Drivers (Çerçeveler ve Sürücüler) - En Dış Katman
- Sorumluluk: Veritabanı, web çerçevesi, harici API'ler, UI gibi tüm dış araçlar ve teknolojiler bu katmanda yer alır.
- Node.js'te: Express.js, Mongoose, Axios gibi kütüphaneler ve veritabanı sürücüleri buraya dahildir. Bu katman, iç katmanlar tarafından 'bilinmez' ve sadece iç katmanların tanımladığı arayüzleri kullanarak onlara hizmet eder.
Bağımlılık Kuralının Node.js'te Uygulanması
Bağımlılık kuralını anlamak kritik önem taşır: Dış katmanlar, iç katmanlara bağımlı olmalıdır; iç katmanlar ise dış katmanlar hakkında hiçbir şey bilmemelidir. Bu, invert edilmiş bir bağımlılık akışı demektir. Peki bu Node.js'te nasıl sağlanır?
Node.js'te interfaceler doğrudan dil seviyesinde desteklenmese de, soyut sınıflar veya basit JavaScript objeleri/sınıfları kullanarak interface benzeri yapılar oluşturabiliriz. Örneğin, bir UserRepository interface'i tanımlayıp, bunun somut implementasyonunu (örneğin MongoUserRepository) dış katmanda (Infrastructure katmanı) oluşturabiliriz. Use Case katmanı ise sadece UserRepository interface'i ile çalışır.

Node.js'te Clean Architecture Uygulama Adımları ve Proje Yapısı
Bir Clean Architecture projesi genellikle aşağıdaki gibi bir dizin yapısına sahip olabilir:
src/
├── domain/
│ ├── entities/
│ │ └── User.js # Saf iş varlığı
│ └── repositories/
│ └── UserRepository.js # Kullanıcı repository interface'i
├── application/
│ ├── use-cases/
│ │ ├── CreateUser.js # Kullanıcı oluşturma kullanım durumu
│ │ └── GetUserById.js # Kullanıcıyı ID'ye göre getirme kullanım durumu
│ └── services/
│ └── AuthService.js # Uygulama düzeyinde servisler (iş kurallarının birleşimi)
├── infrastructure/
│ ├── database/
│ │ ├── MongoDBConnection.js
│ │ └── repositories/
│ │ └── MongoUserRepository.js # UserRepository interface'inin MongoDB implementasyonu
│ ├── web/
│ │ ├── routes/
│ │ │ └── userRoutes.js
│ │ └── controllers/
│ │ └── UserController.js # HTTP isteklerini Use Case'lere iletir
│ └── config/
│ └── index.js
└── app.js # Uygulama başlangıç noktası (dependency injection burada olur)
Adım Adım Uygulama (Basit Bir Kullanıcı Modülü)
1. Domain Katmanı (Entities ve Repositories Interface)
// domain/entities/User.js
class User {
constructor(id, name, email) {
if (!name || !email) {
throw new Error('Kullanıcı adı ve e-posta gereklidir.');
}
this.id = id;
this.name = name;
this.email = email;
}
}
module.exports = User;
// domain/repositories/UserRepository.js
// Bu, dışarıdaki implementasyonların uyması gereken bir interface (soyutlama).
class UserRepository {
async findById(id) {
throw new Error('Metot implemente edilmeli!');
}
async create(user) {
throw new Error('Metot implemente edilmeli!');
}
}
module.exports = UserRepository;2. Application Katmanı (Use Cases)
// application/use-cases/CreateUser.js
const User = require('../../domain/entities/User');
class CreateUser {
constructor(userRepository) {
this.userRepository = userRepository;
}
async execute(name, email) {
const newUser = new User(null, name, email); // ID veritabanı tarafından atanacak
return await this.userRepository.create(newUser);
}
}
module.exports = CreateUser;
// application/use-cases/GetUserById.js
class GetUserById {
constructor(userRepository) {
this.userRepository = userRepository;
}
async execute(id) {
return await this.userRepository.findById(id);
}
}
module.exports = GetUserById;3. Infrastructure Katmanı (Repository Implementasyonu, Controller)
// infrastructure/database/repositories/MongoUserRepository.js
const UserRepository = require('../../../domain/repositories/UserRepository');
const User = require('../../../domain/entities/User');
const { MongoClient, ObjectId } = require('mongodb');
class MongoUserRepository extends UserRepository {
constructor(mongoDb) {
super();
this.collection = mongoDb.collection('users');
}
async findById(id) {
const userData = await this.collection.findOne({ _id: new ObjectId(id) });
if (!userData) return null;
return new User(userData._id.toString(), userData.name, userData.email);
}
async create(user) {
const result = await this.collection.insertOne({ name: user.name, email: user.email });
return new User(result.insertedId.toString(), user.name, user.email);
}
}
module.exports = MongoUserRepository;
// infrastructure/web/controllers/UserController.js
class UserController {
constructor(createUserUseCase, getUserByIdUseCase) {
this.createUserUseCase = createUserUseCase;
this.getUserByIdUseCase = getUserByIdUseCase;
}
async createUser(req, res) {
try {
const { name, email } = req.body;
const user = await this.createUserUseCase.execute(name, email);
res.status(201).json(user);
} catch (error) {
console.error(error);
res.status(400).json({ error: error.message });
}
}
async getUserById(req, res) {
try {
const { id } = req.params;
const user = await this.getUserByIdUseCase.execute(id);
if (!user) {
return res.status(404).json({ error: 'Kullanıcı bulunamadı.' });
}
res.json(user);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Sunucu hatası.' });
}
}
}
module.exports = UserController;Yukarıdaki örnekte hata yönetimi ve validasyon gibi konular basitleştirilmiştir. Node.js ve Express.js'te güçlü hata yönetimi için ayrı bir strateji izlemek gerekir. Bu konuda Node.js ve Express.js'te Güçlü Hata Yönetimi yazıma göz atabilirsiniz.
4. Frameworks & Drivers Katmanı (Uygulama Başlangıcı ve Dependency Injection)
// app.js (Uygulama Başlangıç Noktası)
const express = require('express');
const { MongoClient } = require('mongodb');
const MongoUserRepository = require('./infrastructure/database/repositories/MongoUserRepository');
const CreateUser = require('./application/use-cases/CreateUser');
const GetUserById = require('./application/use-cases/GetUserById');
const UserController = require('./infrastructure/web/controllers/UserController');
async function startServer() {
const app = express();
app.use(express.json());
const mongoClient = await MongoClient.connect('mongodb://localhost:27017/cleanarchdb');
const db = mongoClient.db();
// Dependency Injection: Bağımlılıkları burada oluşturup birbirlerine iletiyoruz
const userRepository = new MongoUserRepository(db);
const createUserUseCase = new CreateUser(userRepository);
const getUserByIdUseCase = new GetUserById(userRepository);
const userController = new UserController(createUserUseCase, getUserByIdUseCase);
// Rotalar
app.post('/users', (req, res) => userController.createUser(req, res));
app.get('/users/:id', (req, res) => userController.getUserById(req, res));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Sunucu http://localhost:${PORT} adresinde çalışıyor`);
});
}
startServer().catch(console.error);Yukarıdaki `app.js` dosyasında, tüm bağımlılıklar (repository implementasyonu, kullanım durumları ve controller) uygulama başlangıcında oluşturulup birbirlerine enjekte ediliyor. Bu, iç katmanların dış katmanlar hakkında bilgi sahibi olmamasını sağlar.
Clean Architecture'ın Avantajları
- Test Edilebilirlik: En büyük avantajlardan biridir. İş mantığı (Entities ve Use Cases) dış bağımlılıklardan tamamen izole olduğu için, herhangi bir veritabanı veya framework'e ihtiyaç duymadan saf JavaScript/TypeScript ile kolayca test edilebilir. Bu, özellikle Node.js uygulamalarında güvenilir test stratejileri oluşturmak için hayati önem taşır.
- Sürdürülebilirlik ve Bakım Kolaylığı: Katmanlar arasındaki net ayrım, kodun daha anlaşılır ve yönetilebilir olmasını sağlar. Bir katmanda yapılan değişiklik, diğer katmanları minimal düzeyde etkiler.
- Teknoloji Bağımsızlığı: Veritabanını, web çerçevesini veya harici servisleri değiştirmek istediğinizde, sadece en dış katmanda (Infrastructure) ilgili implementasyonları güncellemeniz yeterlidir. İş mantığınız dokunulmadan kalır.
- Esneklik: İş gereksinimleri değiştikçe, yeni kullanım durumları eklemek veya mevcutları değiştirmek daha kolaydır.
Karşılaşılabilecek Zorluklar
- Başlangıç Karmaşıklığı: Küçük projeler için başlangıçta fazla soyutlama ve klasör yapısı karmaşık gelebilir. Ancak projeniz büyüdükçe bu yatırım kendini fazlasıyla amorti eder.
- Soyutlama Maliyeti: Her katman arasında interface'ler ve adaptörler tanımlamak ek kod yazımı gerektirir. Bu durum, yanlış uygulandığında gereksiz mühendisliğe (over-engineering) yol açabilir.
Sonuç
Clean Architecture, Node.js uygulamalarınızı daha sürdürülebilir, test edilebilir ve esnek hale getirmenin güçlü bir yoludur. İlk başta öğrenme eğrisi ve ek kod maliyeti olsa da, özellikle orta ve büyük ölçekli projelerde uzun vadede sağladığı faydalar paha biçilmezdir. Bağımlılık Kuralı'nı takip ederek, iş mantığınızı dış dünya dinamiklerinden izole edebilir ve uygulamanızın gelecekteki değişikliklere karşı dirençli olmasını sağlayabilirsiniz.
Unutmayın, mimari tercihler her zaman projenizin büyüklüğüne, ekibinizin tecrübesine ve iş gereksinimlerine göre yapılmalıdır. Ancak karmaşıklığı yönetmek ve uzun ömürlü yazılımlar inşa etmek istiyorsanız, Clean Architecture kesinlikle değerlendirmeniz gereken bir yaklaşımdır. Eğer aklınıza takılan sorular olursa veya bu konular hakkında daha derinlemesine bilgi almak isterseniz, bana ismailyagci371@gmail.com adresinden veya sosyal medya kanallarımdan ulaşabilirsiniz. Sağlıklı ve başarılı kodlamalar dilerim!
Orijinal yazı: https://ismailyagci.com/articles/nodejste-temiz-mimari-clean-architecture-surdurulebilir-ve-test-edilebilir-uygulamalar-icin-kilavuz
Yorumlar
Yorum Gönder