Middleware Nedir? Ne İşe Yarar?

Web ve mobil uygulamaları üzerinden yönetilen sistemlerde verinin iletimi ve alımında çeşitli ara kontrollere veya işlemlere ihtiyaç duyulabilmektedir.

AA

Güvenlik katmanlarının yanı sıra verinin kontrolü, gerekli ise yeniden ele alınması, farklı veri kaynaklarından gelen verilerin birleştirilmesi, bir veya daha fazla veri kaynağına iletilmesi, ayrıştırılması gibi pek çok tekrarlanan işlem söz konusu olabilir.

Middleware ya da diğer bir ifade ile ara yazılım, tekrar eden işlemleri üstlenen, amaca yönelik görevleri gerçekleştiren çözümlerden biridir. Ancak terimin karşılığı ilişkili olduğu konuya bağlı olarak (işletim sistemi, internet ağı vb.) değişkenlik gösterebilmektedir1. Elbette ben web uygulamaları çerçevesinde yazıyı detaylandırmaya ve örneklendirmeye çalışacağım2.

Middleware (Ara Uygulama)

Middleware, birbirleri ile iletişim halindeki uygulamalar arasında, tekrar eden görevleri gerçekleştirmek amacıyla yer alan ara yazılımlardır. Tek başlarına bir işlem gerçekleştirmezler. Örneğin, dağıtılmış uygulamalar için iletişim ve veri yönetimini sağlayabilirler. Ancak, doğrudan bir iletişim başlatmazlar ya da veri alışverişi gerçekleştiremezler3.

Özetlemek gerekirse, middleware (ara uygulama veya orta katman) aracılığı ile bir web sunucusu kullanıcının profili ile ilişkili olarak dinamik web sayfaları döndürebilir. Burada kullanıcının oturum/kimlik yönetimini ara uygulama ele alır. Klasik bir web uygulamasında kullanıcı istek gönderir ve karşılığında sunucundan bir cevap alır. Gelişmiş bir yapıda ise istek ara uygulamaya iletilir. Örneğin, kullanıcının oturumunu kontrol eder ve sunucundan gelen bilgileri kullanıcı ile ilişkili olarak değerlendirir. Kullanıcı oturum başlatmamışsa onu giriş sayfasına yönlendirir. Aksi durumda, her bir routing işleminde tekrar tekrar kontrol işlemi uygulanması gerekirdi. Bu da elbette hem maliyetleri artıracak hem de uygulamanın kontrolünü zorlaştıracaktır. Diğer bir örnek olarak, sunucudan gelen yanıta karakter setinin eklenmesi ele alınabilir.

Kullanım Amacı

Yaptığımız kısa tanıma bağlı olarak, veritabanı işlemleri, uygulama sunucuları arasındaki işlemler, ileti kontrolleri, kimlik doğrulama gibi amaçlar doğrultusunda ara yazılımlar (middleware) oluşturulabilmektedir. Uygulamaların iletişiminde SOAP, REST ve JSON gibi yapılar kullanılabilmektedir. Ara yazılım tüm bu amaçlar ve elbette gereksinimlere göre hareket eder.

Özellikleri çerçevesinde ele almak gerekirse, bir ara seviyelerde (level) konumlandırılabilir:

  • Uygulama düzeyinde ara katman yazılımı
  • Yönlendirici düzeyinde ara katman yazılımı
  • Hata işleme ara katman yazılımı
  • Yerleşik ara katman yazılımı
  • Üçüncü taraf ara katman yazılımı

Ara uygulamalar / katmanlar eğer login kontrolü ya da log gibi amaçlarla kullanılıyorlarsa çoğunlukla uygulama düzeyinde karşımıza çıkarlar ve her işlemle etkileşime geçebilirler. Yönlendirici düzeyindeki etkileşimler ise belirtilen URI‘ler temelinde devreye girerler.

Örneğin, bir web framework olan Express.js1 kendisini bir dizi ara katman fonksiyonu olarak tanımlar ve çoğu zaman bir kodun yürütülmesi, istek (request) ve yanıt (response) nesnelerinde değişiklik yapılması, istek-yanıt döngüsünün sonlandırılması, bir sonraki ara katman uygulaması fonksiyonun çağırıması gibi görevleri gerçekleştirmek amacıyla kullanılır. Elbette Express dışında connect3 gibi basit veya Netjs4 ve Moleculer5 gibi ileri işlevleri gerçekleştirebilmek amcıyla geliştirilen Node.JS modülleri de değerlendirilebilir.

Örnek Middleware İşlemi

Öncelikle bir node.js uygulamasını yapılandıralım (seneklerin hepsini Enter ile geçebilirsiniz) ve ardından express modülünü indirelim. Dilerseniz entry point için farklı bir isim (örneğin server.js) tercih edebilirsiniz.

npm init -y
npm install express --save

Bu işlemi localhost üzerinden gerçekleştiriyorsanız bir sorun yok. Ancak, DigitalOcean veya Heroku gibi bir servis kullanacaksanız kullanacağınız IP için izin oluşturmalısınız. Ben örnek içerisinde 3000 no’lu portu kullanacağım.

ufw allow 3000

Evet, artık entry point olarak belirttiğimiz dosyayı (öntanımlı index.js) açabilir ve örnek işlemlerimize geçebiliriz.

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
        res.send('Express is working...');
});

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});

En yalın haliyle web server’ımızı şu şekilde işleme alalım.

node server.js

127.0.0.1 ya da bir sunucu üzerinden artık ilgili portu belirterek içeriğimize ulaşabiliriz. Görüldüğü üzere şu anda bir istekte bulunuyor ve bu isteye karşı sunucudan Express is working... cevabını alıyoruz. O halde bu işlem arasına uygulama düzeyinde bir log katmanı ekleyelim.

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

const mw = {
        logger : (req, res, next) => {
                console.log('Time:', Date.now())
                next();
        }
}

app.use(mw.logger);

app.get('/', (req, res) => {
        res.send('Express is working...');
});

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});

mw adında logger fonksiyonunu tanımlayan bir değişkenimiz var. Uygulama düzeyinde oluşturduğumuz katmanın tüm app tanımlarının üzerinde olması gerekiyor. Dolayısıyla app.use(mw.logger); öncelikli olarak çalışacak şekilde uygulamamızı düzenliyoruz. Bu önceliklendirmenin sebebi logger içerisindeki next fonksiyonunun bizi sonraki aşamaya yönlendirecek olması. Yani, örnekteki akış üzerinden ilerleyecek olursak, ilk olarak app.use işleme alınıyor, console.log('Time:', Date.now()) ile zaman etiketi konsolumuza iletiliyor ve next bir sonraki işlem olarak app.get‘e geçilmesini sağlıyor. Şayet, next fonksiyonunu kaldırırsak işlemlerin yürütülmediğini görebilirsiniz ya da app.use satırını app.get satırı sonrasına eklerseniz zaman etiketi konsola düşmeyecektir.

Şimdi, örneğimize bir de route düzeyinde bir katman ekleyelim.

const express = require('express');

const app = express();
const PORT = process.env.PORT || 3000;

const mw = {
        requireAuthentication : (req, res, next) => {
                console.log('Request URL: ' + req.originalUrl);
                next();
        },
        logger : (req, res, next) => {
                console.log('Time:', Date.now())
                next();
        }
}

app.use(mw.logger);

app.get('/', (req, res) => {
        res.send('It is homepage!');
});

app.get('/user', mw.requireAuthentication, (req, res) => {
        res.send('It is userpage!');
});

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});

Evet, <ip>:3000 bize It is homepage! karşılığını ve konsolda zaman etiketini verirken, <ip>:3000/user bize It is userpage! karşılığının yanı sıra konsolda hem zaman etiketini hem de istek URL’ini verecektir. Çünkü, app.use(mw.logger) tüm isteklerde devreye girerken app.get('/user'...) içerisinde cevap iletilmeden önce mw.requireAuthentication devreye girecek ve Request URL bilgisini vererek next ile devam edecektir.

Son olarak, yukarıdaki örneğimizi toparlayaım ve ilgili ara katmanı bir modül aracılığıyla çağıralım. Bunun için bir bir js dosyası daha oluşturalım. Ben middleware.js ismini tercih ettim. index.js içerisinde oluşturduğumuz const mw satırını doğrudan bu dosya içerisine taşıyalım ve sonuna ilgili kodun modül olarak kullanılacağını belirtelim.

const mw = {
        requireAuthentication : (req, res, next) => {
                console.log('Request URL: ' + req.originalUrl);
                next();
        },
        logger : (req, res, next) => {
                console.log('Time:', Date.now())
                next();
        }
}

module.exports = mw;

Şimdi tekrar index.js dosyamıza dönelim ve const mw = require('./middleware.js') ile modülümüzü çağıralım.

const express = require('express');
const mw = require('./middleware.js');

const app = express();
const PORT = process.env.PORT || 3000;

app.use(mw.logger);

app.get('/', (req, res) => {
        res.send('It is homepage!');
});

app.get('/user', mw.requireAuthentication, (req, res) => {
        res.send('It is user page!');
});

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});

Elbette yapılabilecek işlemlerin ve bu işlemler çerevesinde sunulan fonksiyonlar oldukça çeşitli. Express özelinde daha fazla detay almak isterseniz Express.js API6 dokümanını inceleyebilirsiniz.

İleri Okumalar