Blogs

Express.js ve MongoDB ile Connection Pooling Nasıl Yapılır?

Express.js ve MongoDB ile connection pooling, birden fazla veri tabanına tek bir Node.js uygulaması üzerinden dinamik bağlantı sağlamayı mümkün kılan bir mimarik yaklaşımdır. Multi-tenant SaaS uygulamalarında her müşterinin ayrı bir MongoDB veri tabanına sahip olduğu senaryolarda, her istek için yeni bağlantı açmak yerine subdomain prefix’ine göre ilgili veri tabanına yönlendirme yapılması hem performans hem de veri izolasyonu açısından kritik önem taşır. Bu yazıda Express.js, Mongoose ve MongoDB kullanarak bu yapıyı sıfırdan nasıl inşa edeceğinizi adım adım ele alıyoruz. Node.js ile API testi konusunda daha fazla bilgi edinmek isteyenler için ek kaynaklar yazı sonunda yer almaktadır.

Express Nedir?

Express.js logo

Express, Node.js içerisinde web uygulamalarının daha kolay ve hızlı bir şekilde geliştirilmesini sağlamak amacıyla yazılmış bir framework’tür.

Express ile yeni bir uygulama başlatmak için yapılması gereken çok kolay birkaç adım bulunmaktadır.

İlk önce terminal ekranında çalışılmak istenen dizine gidip npm init komutuyla yeni bir Node.js projesi başlatmanız gerekmektedir.

TERMINAL

mkdir poolapp
cd poolapp
npm init

Proje başlatma adımları tamamlandıktan sonra yapmanız gereken Node Package Manager’ı kullanarak Express çatısını projenize yüklemektedir. Npm hakkında bilgi vermek gerekirse temel olarak 3.parti yazılımları yüklemeyi sağlayan bir araçtır.

TERMINAL

npm install express --save

Bu komut express çatısını poolapp ismini verdiğimiz uygulamanın dependencies listesine ekleyecektir. İşlem başarılı olduğunda package.json dosyası içinde framework adı ve versiyon numarası gözükecektir.

JSON

"dependencies": {
     "express": "^4.17.1"
}

Mongoose Nedir?

Mongoose bir ODM modülüdür. ODM ise object data modeling olarak tanımlanmaktadır. MongoDB ile veriler karmaşık bir şekilde kaydedilebilir. Bu durum veriler üzerinde yapılacak olan işlemleri zorlaştırır. Bu işlemleri kolaylaştırmak için mongoose kullanılır. Mongoose ile birlikte belirli bir model yapısı ve bu modele ait olan herhangi bir kaydın nasıl kısıtlamalara sahip olabileceğini belirleyebiliriz. Aynı zamanda mongoose CRUD işlemlerinin daha kolay bir şekilde yapılabilmesini de sağlar.

TERMINAL

npm install mongoose --save

Yukarıda bulunan komut ile birlikte Mongoose projemize eklenebilir. Daha sonra kendinize bir schema yaratabilirsiniz. Schema sizin mongoDB üzerinde oluşturacağınız modelin içerisindeki alanların yapısını ve kısıtlamaları belirtecektir.

JAVASCRIPT

const {Schema} = require('mongoose');
module.exports = new Schema({
  name: {type: String, trim: true, required: true}
}, {collection: 'users'});

Örnek olarak yukarıda ki gibi bir user modeli oluşturulabilir ve users modelinde ki name alanı required komutu ile zorunlu alan olarak belirlenebilir. Eğer, users modeline name parametresi olmadan bir create işlemi yapılacak olursa mongoose bunu engelleyecek ve alanın zorunlu olduğu uyarısını gösterecektir.

Pool Application (PoolApp) Nedir?

PoolApp birden fazla DB ile tek uygulama üzerinden bağlantı kurabilen bir Node.js projesidir. Daha açık ifade etmek gerekirse, bu proje ile ilk önce ana bir veri tabanı bağlantısı kuracağız. Bundan sonra, ana veri tabanı içerisinde oluşturulan diğer veri tabanlarının adlarını kaydedip oluşturulan veri tabanlarına gelen requestteki prefix alanlarıyla bağlantı sağlayacağız. Yani localhost üzerinde çalıştığımızda yyy.localhost:3000/api üzerine gelen herhangi bir request poolapp-yyy veri tabanına, xxx.localhost:3000/api ise poolapp-xxx veri tabanına bağlanarak yanıt alacaktır.

Nasıl?

Bunun için ilk önce genel dizinimizde bir app.js dosyası oluşturacağız; app.js bizim ana veri tabanımızla bağlantımızı kuracağımız dosya olacaktır. Ancak, uygulamamıza start verdiğimizde app.js üzerinden başlamasını söylememiz gerekiyor. Package.json dosyası üzerinde yapılacak olan değişiklikler bunu sağlayacaktır.

JSON

{
  "name": "poolapp",
  "version": "1.0.0",
  "description": "Pool application",
  "main": "app.js",
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "mongoose": "^5.9.17"
  },
  "devDependencies": {
    "nodemon": "^2.0.4"
  },
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "start": "nodemon app.js"
  }
}

Yukarıda ki kod bloğunda görüldüğü üzere main alanı “app.js” olarak belirlenmiş. Ancak scripts alanının altında start için “nodemon app.js” yazılmıştır.

Nodemon Nedir?

Nodemon logo

Nodemon, node js ile geliştirilen bir uygulamada anlık olarak yaptığınız değişikleri sunucuyu otomatik olarak yeniden başlatarak gösteren bir modüldür. Yani herhangi bir dosyada yapacağınız ctrl+s işleminden sonra tekrar npm start yazmanıza gerek kalmamasını sağlayarak, Node.js ile uygulama yazarken bize zaman kazandırmaktadır.

Artık app.js dosyamızı express ile yazmaya başlayabiliriz.

JAVASCRIPT

const express = require('express');
const app = express();
const server = require('http').createServer(app);
const process = require('process');
const config = require('./bin/config');
const mainModels = require('./main_models/index');
const api = require('./api/index');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');

İlk kod bloğumuz yukarıdaki gibi olacak. Burada henüz oluşturulmayan dosyalardan gelen özellikler de bulunmakta; bunları “app.js” kod bloğunu oluşturmadan önce birlikte oluşturup neye hizmet ettiğini göreceğiz.

Body Parser Nedir?

Görüldüğü üzere body-parser 3. parti modülü app.js içerisinde kullanılmış. Body-parser gönderilen herhangi bir POST isteği içerisinde ki verileri obje olarak yakalamamızı sağlayacaktır. Yine “npm install body-parser” komutuyla bu modülü yükleyebiliriz.

Main Models Nedir?

Ana veri tabanımız üzerinde oluşturacak olduğumuz veri tabanlarının isimlerini tutacağız. Bunu yapmamızın sebebi ise, eğer bir veri tabanıyla bağlantı kurulmak isteniyorsa ve bu veri tabanı ismi ana veri tabanı içerisinde ki modelde tutulan herhangi bir isimle eşleşmiyorsa bu durumu isteğe olan cevabımızda belirteceğiz. İşte bu yüzden ana veri tabanında bulunan, modellerimizi tutacağımız bir klasör oluşturacağız ve bu klasör içerisinde mongoose ile schema sınıfı kullanarak uygun modellemeyi yapacağız. Bu klasörün ismi poolapp projesinde “main_models” olarak belirlenmiştir.

main_models klasör yapısı

business.js adı verilen bir dosya oluşturarak yaratılan veri tabanlarının isimlerinin hangi alanla ve nasıl tutulması gerektiğini belirteceğiz.

JAVASCRIPT

const {Schema} = require('mongoose');
module.exports = new Schema({
  prefix: {type: String, trim: true, required: true},
}, {collection: 'business'});

Business modeli içerinde ki prefix alanının schema sınıfı ile birlikte zorunlu alan olduğunu ve tipinin String olması gerektiğini belirttik.

Bunun dışında bir de “Schema”nın bulunduğu klasörümüzde bir de index.js dosyası bulunmaktadır.

Index.js bize mongoose ile veri tabanına connection kurulduğunda, bu klasör içerinde bulunan tüm model yapılarının kurulmasını sağlayarak hizmet edecek. Bunu başarabilmek için export bloğumuza bir parametre geçeceğiz ve bu parametreyi buraya yolladığımızda modellerimiz belirlediğimiz koşullarla otomatik olarak oluşacak.

Şimdi index.js dosyasını kullanarak ana veri tabanımızı app.js üzerinden nasıl oluşturabileceğimizi görelim.

JAVASCRIPT

const path = require('path');
const basename = path.basename(__filename);
module.exports = (db) => {
  require('fs')
      .readdirSync(__dirname)
      .filter((file) => {
        return (file.indexOf('.') !== 0) &&
        (file !== basename) &&
        (file.slice(-3) === '.js');
      }).forEach((file) => {
        filename = file.split('.')[0];
        key = filename.split('_').map(
            (name) => filename.split('_')[0] !== name ?
          name.charAt(0).toUpperCase() + name.slice(1) :
          name,
        );
        db.model(key, require(__dirname + path.sep + file));
      });
  return db;
};

MongoDB ilişkisel veri tabanlarında olduğu gibi daha önceden tabloların oluşturulmasına gerek duymaz. Mongoose içerisinde ki “connection” metodu ve hazırladığımız index.js dosyası ile bu işlemi kolayca yapabiliriz. Ancak tüm bu işlemleri yapabilmek için ana dizinimizde “bin” adını vereceğimiz bir klasör içerisinde konfigürasyon dosyası oluşturarak veri tabanına ait host, isim ve port gibi bilgilerimizi global olarak tanımlayacağımız bir config.js dosyası oluşturacağız.

bin/config.js dosya yapısı

JAVASCRIPT

const path = require('path');
const APP_ROOT = path.dirname(require.main.filename);
const SEP = path.sep;
module.exports = {
  db: {
    host: '127.0.0.1',
    name: 'pool_app_cloud',
    port: '27017',
    prefix: 'poolapp',
  },
  server: {
    port: 3000,
    ip: '127.0.0.1',
  },
};

Artık “app.js” dosyası üzerinde değişiklik yapabiliriz. Bu dosya üzerinde ana veri tabanı bağlantımızı sağlayacağız ve modellerimiz yeni kayıt oluşturulduğunda otomatik olarak veri tabanımıza eklenecek.

JAVASCRIPT

mongoose.createConnection(
  `mongodb://${config.db.host}:${config.db.port}/${config.db.name}`,
  {useNewUrlParser: true},
).then((connection) => {
  console.log('new connection');
  app.set('db', mainModels(connection));
  app.set('db_admin', connection.db.admin()); 
}).catch((err) => { })
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
// CORS ayarları
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header(
      'Access-Control-Allow-Headers',
      'Origin, X-Requested-With, Content-Type, Accept, Authorization',
  );
  if (req.method === 'OPTIONS') {
    res.header('Access-Control-Allow-Methods', 'PUT, POST, PATCH, DELETE, GET');
    return res.status(200).json({});
  }
  next();
});
app.use('/', api);
server.listen(config.server.port, config.server.ip);

App.js dosyamız artık config dosyamızda belirttiğimiz sunucu portu ve sunucu ip’sini dinliyor. Uygulamaya gelen herhangi bir request yukarıda yazdığımız middleware kontrollerinden geçecektir.

API Klasör Yapısı ve Route Yönetimi

Şimdi genel dizinimizde api klasörü oluşturalım ve api klasörünü main ve sub olmak üzere iki yola ayıralım.

Express.js proje klasör yapısı
api/main ve api/sub klasör yapısı

İlk önce main ve sub klasörlerimizi için index.js dosyalarımızı, kontrollerle birlikte oluşturalım. Main klasörü içerisinde ki index.js’den başlamak işimizi kolaylaştıracaktır.

JAVASCRIPT

const express = require('express');
const app = express();
const path = require('path');
const basename = path.basename(__filename);
// Özel erişim kontrolü
app.use((req, res, next) => {
  // Subdomain engelle
  if (req.hostname.split('.').length !== 2 && req.hostname !== 'localhost' ) {
    return res.json({
      type: false,
      data: 'Erişiminiz engellendi.',
    });
  }
  return next();
});
app.get('/', (req, res) => {
  return res.json({
    type: true,
    data: true,
  });
});
require('fs')
    .readdirSync(__dirname)
    .filter((file) => {
      return (
        file.indexOf('.') !== 0) &&
      (file !== basename) &&
      (file.slice(-3) === '.js'
      );
    }).forEach((file) => {
      app.use(`/${file.split('.')[0]}`, require(__dirname + path.sep + file));
    });
module.exports = app;

Sub Models ve Dinamik Veri Tabanı Bağlantısı

sub_models klasör yapısı

Users modelimiz, kullanıcı isimlerini bulunduracak ve bu alan zorunlu olacak.

JAVASCRIPT

const {Schema} = require('mongoose');
module.exports = new Schema({
  name: {type: String, trim: true, required: true}
}, {collection: 'users'});

Sub klasörü içerisinde ki index.js dosyasında ilk önce prefix olmadan gelen istekler engellenmelidir:

JAVASCRIPT

app.use((req, res, next) => {
  if (req.hostname.split('.').length !== 3 && req.hostname.split('.')[1]!=='localhost') {
    return res.json({
      type: true,
      message: 'Erişiminiz engellendi',
    });
  }
  // İstek yapılan veri tabanının var olup olmadığını kontrol et
  req.data = {
    prefix: req.hostname.split('.')[0],
    db: null,
  };
  app.get('db_admin').listDatabases((err, dbsResult) => {
    if (err) {
      return res.json({ type: false, message: err.toString() });
    }
    if (!dbsResult.databases.some((e) => e.name === `${config.db.prefix}_${req.data.prefix}`)) {
      return res.json({ type: false, data: 'Hesap Bulunamadı' });
    }
    // Veri tabanına bağlan
    mongoose.connect(
      `mongodb://${config.db.host}:${config.db.port}/${config.db.prefix}_${req.data.prefix}`,
      { useNewUrlParser: true },
    ).then((connection) => {
      req.data.db = subModels(connection);
      return next();
    }).catch((err) => {
      return res.json({ type: false, message: err.toString() });
    });
  });
});

Register ve Kayıt Metotları

JAVASCRIPT

app.post('/', (req, res) => {
  app.get('db').model('business').create(
    { prefix: req.body.name },
    (err) => {
      if (err) return res.json({ type: false, message: err.toString() });
      mongoose.connect(`mongodb://${config.db.host}:${config.db.port}/poolapp_${req.body.name}`)
        .then((connection) => {
          const dbResult = subModels(connection);
          dbResult.model('users').create({name: req.body.name}, (err, data) => {
            if (err) return res.json({ type: false, message: err.toString() });
            return res.json({ type: true, message: 'Şirket oluşturulmuştur' });
          });
        });
    }
  );
});

Postman ile Test

Register metodu ile yeni bir veri tabanı oluşturmak:

Postman register isteği

GET isteği ile oluşturulan veri tabanı listesinin alınması:

Postman GET isteği sonucu

Gerçekleştirdiğimiz GET isteğiyle oluşturduğumuz veri tabanı listesini görüyoruz. Görüldüğü üzere ilk kaydımızla birlikte modelimiz oluştu ve kayıt için otomatik olarak bir id atandı.

Postman users modeli sonucu

Şimdi ise örnek bir prefix kullanılarak users modelinin içine ana veri tabanıyla aynı anda kaydedilen ismi görüyoruz. Aynı örneği istediğiniz bir prefix ismiyle deneyebilirsiniz.

Bu mimariyi SAP ile entegre uygulamalarda da kullanabilirsiniz. CAP projelerinde JavaScript mi TypeScript mi tercih edilmeli? sorusu da benzer bir mimari kararı gerektirmektedir — her ikisi de okunabilirliği ve bakım kolaylığını doğrudan etkiler.

Sık Sorulan Sorular

Express.js MongoDB connection pooling neden önemlidir?

Her HTTP isteğinde yeni bir MongoDB bağlantısı açmak hem yavaş hem de kaynak yoğun bir işlemdir. Connection pooling ile mevcut bağlantılar yeniden kullanılır; bu durum özellikle yüksek trafikli uygulamalarda yanıt süresini ve sunucu yükünü önemli ölçüde azaltır. Multi-tenant uygulamalarda ise her tenant için ayrı bağlantı havuzu yönetimi veri izolasyonunu da garanti eder.

Mongoose’un createConnection ve connect metotları arasındaki fark nedir?

mongoose.connect() varsayılan bağlantıyı oluşturur ve tek bir MongoDB instance’ına bağlanır. mongoose.createConnection() ise birden fazla bağımsız bağlantı oluşturmanıza olanak tanır; her bağlantı kendi model havuzuna sahip olur. Multi-tenant mimarisinde her tenant için ayrı bağlantı yönetmek amacıyla createConnection tercih edilmelidir.

Bu mimari production ortamı için uygun mu?

Bu yöntem iyi bir temel oluşturmaktadır; ancak production’a geçmeden önce bağlantı havuzu boyutlandırması (poolSize parametresi), bağlantı zaman aşımı yönetimi, bağlantı sağlık kontrolü (heartbeat), hata durumunda yeniden bağlanma mantığı ve bellek yönetimi gibi konuların dikkate alınması önerilir.

Subdomain tabanlı tenant yönlendirmesi nasıl çalışır?

Gelen HTTP isteğinin hostname’inden subdomain prefix’i ayrıştırılır (örn. tenant1.localhost:3000 → prefix: tenant1). Bu prefix, ana veri tabanındaki business koleksiyonuyla eşleştirilir; eşleşme varsa ilgili MongoDB veri tabanına (poolapp_tenant1) dinamik olarak bağlanılır. Eşleşme yoksa istek reddedilir.

Referanslar

Mongoose Connection Docs — mongoosejs.com
Express.js Routing Guide — expressjs.com
Node.js ile API Testi Nasıl Yapılır? — MDP Group
CAP Projelerinde JavaScript mi TypeScript mi? — MDP Group


Benzer
Bloglar

Mailiniz başarıyla gönderilmiştir en kısa sürede sizinle iletişime geçilecektir.

Mesajınız ulaştırılamadı! Lütfen daha sonra tekrar deneyin.