Node.js’te AsyncLocalStorage ile İstek Bazlı Context Taşımak (Request ID, Tenant, Kullanıcı)
Node.js’te AsyncLocalStorage kullanarak request-scoped context (requestId, user, tenant) yönetimi ve pratik örnekler.
Modern Node.js uygulamalarında en can sıkıcı problemlerden biri şudur: Bir HTTP isteği boyunca requestId, kullanıcı bilgisi veya tenant gibi verileri her fonksiyona parametre olarak taşımak zorunda kalırsınız. Bu hem kodu kirletir hem de hata yapma ihtimalini artırır.
Bu yazıda, Node.js’in yerleşik özelliği AsyncLocalStorage (ALS) ile istek bazlı context oluşturmayı ve bunu loglama, servis çağrıları ve hata yakalamada nasıl kullanabileceğinizi kısa ve net şekilde ele alacağız.
AsyncLocalStorage nedir?
AsyncLocalStorage, aynı asenkron çağrı zinciri içinde (Promise’ler, await’ler, callback’ler) paylaşılan bir “context” saklamanızı sağlar. Yani bir isteğin başında koyduğunuz veriyi, o isteğin içindeki herhangi bir yerde global değişken kullanmadan okuyabilirsiniz.
Not: ALS Node.js core içindedir (
node:async_hooks). Ek paket gerekmez.
1) Minimal bir context altyapısı
Önce tek bir yerde context erişimini toplayalım:
// context.js
import { AsyncLocalStorage } from 'node:async_hooks';
export const als = new AsyncLocalStorage();
export function getContext() {
return als.getStore() ?? {};
}
export function getRequestId() {
return getContext().requestId;
}
2) Express middleware ile request context başlatma
Her istek geldiğinde bir store oluşturup içine request’e özel verileri koyarız:
// app.js
import express from 'express';
import crypto from 'node:crypto';
import { als } from './context.js';
const app = express();
app.use((req, res, next) => {
const requestId = req.header('x-request-id') || crypto.randomUUID();
const tenantId = req.header('x-tenant-id') || 'public';
als.run({ requestId, tenantId }, () => {
res.setHeader('x-request-id', requestId);
next();
});
});
Buradaki kritik nokta: als.run(...) ile başlatılan context, next() sonrası tüm async akış boyunca korunur.
3) Parametre taşımadan logları zenginleştirme
Basit bir logger yazalım:
// logger.js
import { getContext } from './context.js';
export function log(message, extra = {}) {
const ctx = getContext();
const payload = {
level: 'info',
msg: message,
requestId: ctx.requestId,
tenantId: ctx.tenantId,
...extra,
};
console.log(JSON.stringify(payload));
}
Artık uygulamanın herhangi bir yerinde:
import { log } from './logger.js';
log('Sipariş işleniyor', { orderId: 123 });
…dediğinizde otomatik olarak requestId ve tenantId loglara eklenir.
4) Servis katmanında context’e erişim
Örneğin repository katmanında tenant’a göre filtrelemek isteyelim:
// orderRepository.js
import { getContext } from './context.js';
export async function listOrders(db) {
const { tenantId } = getContext();
return db.query('SELECT * FROM orders WHERE tenant_id = ?', [tenantId]);
}
Bu yaklaşım, “tenantId parametresi her yere taşınsın mı?” tartışmasını bitirir. Context, isteğin doğal parçası olur.
5) Hata yakalamada requestId ile korelasyon
Hata middleware’inde context’i kullanarak client’a anlamlı bir referans dönebilirsiniz:
app.use((err, req, res, next) => {
// context burada da okunabilir
const { requestId } = (await import('./context.js')).getContext?.() ?? {};
res.status(500).json({
error: 'Internal Server Error',
requestId,
});
});
Pratikte
context.jsimport’unu yukarıda yapıp direktgetContext()çağırın; burada kısa tutmak için örnek sadeleştirildi.
Dikkat edilmesi gerekenler
- Global state değildir: ALS, “bu isteğin akışı” için saklar. İstekler arası karışmaz.
- Bazı eski/uyumsuz kütüphaneler async zinciri bozabilir. Güncel Node sürümlerinde ve modern kütüphanelerde genelde sorunsuzdur.
- Context’i “her şeyi koyduğum çöp kutusu”na çevirmeyin. Küçük ve anlamlı alanlar:
requestId,tenantId,userIdgibi.
Sonuç
AsyncLocalStorage ile Node.js uygulamanızda temiz fonksiyon imzaları, tutarlı loglar ve istek bazlı veri erişimi elde edersiniz. Özellikle çok katmanlı servislerde “bu isteği nereden takip edeceğim?” sorusuna güçlü bir cevap olur.