Node.js and Express.js
Phase 1: Node.js Core, Architecture & Asynchronous Programming
Question 1: Architecturally, how does Node.js work under the hood? (V8 Engine & libuv)
Short Answer:
Node.js is a runtime environment that allows JavaScript to run on the server. Architecturally, it combines Google’s V8 Engine (which compiles JS into machine code) with libuv (a C library that implements the Node.js Event Loop and Thread Pool), allowing Node to handle asynchronous, non-blocking I/O operations efficiently on a single thread.
Detailed Answer:
Understanding the architecture is crucial for senior developers to debug performance bottlenecks.
- V8 Engine: The brain that reads your JavaScript, optimizes it, and compiles it down to raw machine code.
- libuv: The muscle. When you perform an asynchronous task (like a database query or reading a file), Node doesn’t wait. It offloads that task to
libuv. - Thread Pool:
libuvmaintains a pool of worker threads (default is 4) in C++. For heavy OS-level tasks (like File System operations or Crypto hashing),libuvassigns the work to this background thread pool, leaving the main JavaScript thread completely free to handle thousands of other incoming HTTP requests.
প্রশ্ন ১: আর্কিটেকচারাল দিক থেকে Node.js কীভাবে কাজ করে? (V8 Engine এবং libuv)
সংক্ষিপ্ত উত্তর:
Node.js হলো একটি রানটাইম এনভায়রনমেন্ট যা সার্ভারে জাভাস্ক্রিপ্ট রান করতে দেয়। আর্কিটেকচারের দিক থেকে এটি গুগলের V8 Engine (যা JS কে মেশিন কোডে রূপান্তর করে) এবং libuv (একটি C লাইব্রেরি যা ইভেন্ট লুপ এবং থ্রেড পুল তৈরি করে)-এর সমন্বয়ে গঠিত। এর ফলেই Node.js সিঙ্গেল থ্রেডে নন-ব্লকিং I/O অপারেশন খুব দক্ষতার সাথে হ্যান্ডল করতে পারে।
বিস্তারিত উত্তর:
পারফরম্যান্স ইস্যুগুলো বোঝার জন্য এর আর্কিটেকচার বোঝা একজন সিনিয়র ডেভেলপারের জন্য অত্যন্ত জরুরি।
১. V8 ইঞ্জিন: এটি ব্রেইন হিসেবে কাজ করে, যা আপনার জাভাস্ক্রিপ্ট কোড পড়ে এবং অপ্টিমাইজ করে সরাসরি মেশিন কোডে রূপান্তর করে।
২. libuv: এটি পেশি (muscle) হিসেবে কাজ করে। যখন আপনি কোনো অ্যাসিনক্রোনাস কাজ করেন (যেমন- ডেটাবেস কল বা ফাইল রিড), Node.js বসে থাকে না। সে কাজটি libuv এর কাছে পাঠিয়ে দেয়।
৩. থ্রেড পুল (Thread Pool): libuv C++ এর তৈরি একটি ওয়ার্কার থ্রেড পুল মেইনটেইন করে (ডিফল্ট সাইজ ৪)। ফাইল সিস্টেম বা ক্রিপ্টোগ্রাফির মতো ভারী কাজের জন্য libuv ব্যাকগ্রাউন্ড থ্রেড ব্যবহার করে। ফলে মূল জাভাস্ক্রিপ্ট থ্রেডটি হাজার হাজার ইউজার রিকোয়েস্ট হ্যান্ডল করার জন্য একদম ফ্রি থাকে।
Question 2: Explain the Event Loop and its different phases in Node.js.
Short Answer:
The Event Loop is the core mechanism that allows Node.js to perform non-blocking I/O operations despite being single-threaded. It constantly monitors the execution stack and callback queues, offloading operations to the system kernel and executing callbacks in a specific sequence of phases (Timers, Pending, Poll, Check, Close).
Detailed Answer:
When developers say “Node is asynchronous,” they are talking about the Event Loop. It executes in specific phases:
- Timers: Executes callbacks scheduled by
setTimeout()andsetInterval(). - Pending Callbacks: Executes I/O callbacks deferred to the next loop iteration (like TCP errors).
- Poll: The most critical phase. It retrieves new I/O events (like incoming HTTP requests or file read completions) and executes their callbacks. If the poll queue is empty, it will wait here for new I/O events unless a
setImmediatescript is queued. - Check: Executes callbacks explicitly scheduled by
setImmediate(). - Close Callbacks: Executes close events (e.g.,
socket.on('close', ...)).
প্রশ্ন ২: Node.js-এ ইভেন্ট লুপ (Event Loop) এবং এর বিভিন্ন ফেজগুলো (phases) বুঝিয়ে বলুন।
সংক্ষিপ্ত উত্তর:
সিঙ্গেল-থ্রেডেড হওয়া সত্ত্বেও Node.js যে মেকানিজমের সাহায্যে নন-ব্লকিং I/O অপারেশনগুলো সম্পন্ন করে, তাকেই ইভেন্ট লুপ বলে। এটি সবসময় কলব্যাক কিউ (queue) এবং এক্সিকিউশন স্ট্যাক মনিটর করে এবং একটি নির্দিষ্ট ক্রমানুসারে (Timers, Pending, Poll, Check, Close) কাজগুলো সম্পন্ন করে।
বিস্তারিত উত্তর:
ডেভেলপাররা যখন বলে “Node.js অ্যাসিনক্রোনাস”, তারা মূলত এই ইভেন্ট লুপের কথাই বলে। এর নির্দিষ্ট কিছু ফেজ আছে:
১. Timers: এই ফেজে setTimeout() এবং setInterval() এর কলব্যাকগুলো রান হয়।
২. Pending Callbacks: সিস্টেম লেভেলের কোনো এরর (যেমন TCP এরর) বা আটকে থাকা I/O কলব্যাকগুলো এখানে রান হয়।
৩. Poll: এটি সবচেয়ে গুরুত্বপূর্ণ ফেজ। এটি নতুন I/O ইভেন্ট (যেমন- ইউজারের HTTP রিকোয়েস্ট বা ফাইল রিড শেষ হওয়া) রিসিভ করে এবং তার কলব্যাক রান করে।
৪. Check: setImmediate() দিয়ে শিডিউল করা কলব্যাকগুলো এই ফেজে এক্সিকিউট হয়।
৫. Close Callbacks: সকেট বা কানেকশন ক্লোজ করার ইভেন্টগুলো এখানে রান হয়।
Question 3: What is the difference between process.nextTick() and setImmediate()?
Short Answer:
Both function to delay the execution of a callback, but their timing is fundamentally different. process.nextTick() places its callback in a microtask queue that is evaluated immediately after the current operation finishes, before the event loop moves to its next phase. setImmediate() schedules the callback to run specifically in the Check phase of the upcoming event loop cycle.
Detailed Answer:
This is a classic senior-level gotcha.
process.nextTick(): It is incredibly aggressive. If you recursively callprocess.nextTick(), you will trap Node.js in an infinite microtask loop, preventing the Event Loop from ever reaching the "Poll" phase. Your server will freeze and stop accepting new HTTP requests. It is best used for emitting events or handling immediate state cleanups before the stack unwinds.setImmediate(): This is much safer for deferring heavy CPU tasks without blocking the server. It politely waits its turn, allowing the "Poll" phase to process incoming I/O events first before executing the immediate callback in the "Check" phase.
JavaScript
setTimeout(() => console.log('1. setTimeout'), 0);
setImmediate(() => console.log('2. setImmediate'));
process.nextTick(() => console.log('3. process.nextTick'));// Output order:
// 3. process.nextTick
// 1. setTimeout
// 2. setImmediate (or 1 and 2 might swap depending on the event loop initialization delay)প্রশ্ন ৩: process.nextTick() এবং setImmediate() এর মধ্যে পার্থক্য কী?
সংক্ষিপ্ত উত্তর:
উভয় ফাংশনই কলব্যাক রান করতে কিছুটা দেরি করায়, কিন্তু তাদের টাইমিং সম্পূর্ণ আলাদা। process.nextTick() তার কলব্যাকটিকে মাইক্রোটাস্ক কিউ-তে পাঠায়, যা বর্তমান কাজ শেষ হওয়ামাত্রই—ইভেন্ট লুপের পরবর্তী ফেজে যাওয়ার আগেই—রান হয়ে যায়। অন্যদিকে setImmediate() তার কলব্যাকটিকে ইভেন্ট লুপের Check ফেজে শিডিউল করে, যা লুপ ঘুরে আসার পর রান হয়।
বিস্তারিত উত্তর:
সিনিয়র লেভেলের ইন্টারভিউতে এটি একটি খুব কমন প্রশ্ন।
process.nextTick(): এটি খুবই এগ্রেসিভ। আপনি যদি লুপের ভেতর বারবার এটি কল করেন, তবে ইভেন্ট লুপ আটকে যাবে এবং সার্ভার নতুন কোনো রিকোয়েস্ট নিতে পারবে না (একে I/O Starvation বলে)। খুব দ্রুত কোনো স্টেট ক্লিনআপ করার জন্য এটি ব্যবহৃত হয়।setImmediate(): সার্ভার ব্লক না করে কোনো ভারী কাজ পিছিয়ে দেওয়ার জন্য এটি অনেক নিরাপদ। এটি ভদ্রভাবে নিজের সিরিয়ালের জন্য অপেক্ষা করে, ফলে "Poll" ফেজটি ইউজারদের রিকোয়েস্ট রিসিভ করার পর্যাপ্ত সময় পায়।
Question 4: Does Node.js support Multithreading? Explain Worker Threads.
Short Answer:
By default, Node.js executes JavaScript on a single main thread. However, it absolutely supports multithreading for CPU-intensive operations via the worker_threads module. Unlike external child processes, Worker Threads share memory using SharedArrayBuffer, making parallel processing highly efficient without the heavy overhead of spinning up entirely new Node instances.
Detailed Answer:
A common misconception is that because Node is single-threaded, it can’t handle heavy computations.
- I/O Tasks: For network calls or DB queries, the single thread +
libuv(Event Loop) is actually better and more memory-efficient than traditional multi-threaded languages (like Java). - CPU Tasks: If you try to process a massive image or generate 10,000 PDF files in the main thread, the Event Loop gets blocked. No other user can log in.
- The Architecture: We use
new Worker('./worker.js'). This spawns a separate V8 isolate with its own Event Loop. The main thread delegates the heavy image processing to the worker. When finished, the worker sends the result back via message passing (postMessage), ensuring the main thread remains lightning fast.
প্রশ্ন ৪: Node.js কি মাল্টিথ্রেডিং (Multithreading) সাপোর্ট করে? Worker Threads বুঝিয়ে বলুন।
সংক্ষিপ্ত উত্তর:
ডিফল্টভাবে Node.js সিঙ্গেল থ্রেডে জাভাস্ক্রিপ্ট রান করে। তবে এটি worker_threads মডিউলের মাধ্যমে CPU-ইন্টেনসিভ (ভারী) কাজের জন্য পুরোপুরি মাল্টিথ্রেডিং সাপোর্ট করে। আলাদা চাইল্ড প্রসেস তৈরির বদলে, ওয়ার্কার থ্রেডগুলো SharedArrayBuffer ব্যবহার করে মেমরি শেয়ার করতে পারে, ফলে সম্পূর্ণ নতুন Node ইনস্ট্যান্স তৈরি করার ওভারহেড ছাড়াই খুব দ্রুত প্যারালাল প্রসেসিং করা যায়।
বিস্তারিত উত্তর:
অনেকের ভুল ধারণা থাকে যে সিঙ্গেল-থ্রেডেড হওয়ায় Node.js ভারী কাজ করতে পারে না।
- I/O কাজ: নেটওয়ার্ক বা ডেটাবেস কলের জন্য সিঙ্গেল থ্রেড এবং ইভেন্ট লুপ ট্র্যাডিশনাল মাল্টিথ্রেডেড ভাষার (যেমন Java) চেয়ে অনেক বেশি মেমরি-সাশ্রয়ী এবং ফাস্ট।
- CPU কাজ: কিন্তু আপনি যদি মেইন থ্রেডে বিশাল কোনো ইমেজ প্রসেস করেন বা ১০,০০০ পিডিএফ তৈরি করেন, তবে ইভেন্ট লুপ ব্লক হয়ে যাবে এবং অন্য কোনো ইউজার সাইটে ঢুকতে পারবে না।
- আর্কিটেকচার: এক্ষেত্রে আমরা
new Worker()ব্যবহার করি। এটি নিজস্ব V8 ইঞ্জিন এবং ইভেন্ট লুপসহ একটি নতুন থ্রেড তৈরি করে। মেইন থ্রেড ভারী কাজটি ওয়ার্কারের কাছে পাঠিয়ে দেয়। কাজ শেষ হলে ওয়ার্কারpostMessageদিয়ে রেজাল্ট ফেরত পাঠায়, ফলে মেইন থ্রেড সবসময় ফাস্ট থাকে।
Question 5: What are Streams in Node.js? Why are they crucial for building scalable applications?
Short Answer:
Streams are collections of data that are processed piece-by-piece (in chunks) rather than reading the entire payload into the server’s RAM all at once. Architecturally, streams are essential for scalability because they keep the memory footprint extremely low, allowing a Node server to process multi-gigabyte files or real-time data pipelines (like Netflix video delivery) with only a few megabytes of memory.
Detailed Answer:
There are four types of Streams: Readable, Writable, Duplex (both read/write, like TCP sockets), and Transform (modifies data as it passes through, like compression/zlib).
- The Problem: Using
fs.readFileSync('10GB_video.mp4')tries to load the entire 10GB file into V8's memory limit (usually ~1.5GB). The server instantly crashes with anOut of Memory(OOM) error. - The Stream Solution: By creating a readable stream (
fs.createReadStream), you read a 64KB chunk, process it, pipe it to a writable stream (resobject in Express), and immediately garbage collect the chunk. The server memory usage remains flat regardless of the file size.
JavaScript
const fs = require('fs');// Scalable streaming approach
app.get('/video', (req, res) => {
const readStream = fs.createReadStream('./movie.mp4');
readStream.pipe(res); // Pipes the chunks directly to the HTTP response
});প্রশ্ন ৫: Node.js-এ স্ট্রীম (Streams) কী? স্কেলেবল অ্যাপ্লিকেশন তৈরির জন্য এগুলো কেন এত গুরুত্বপূর্ণ?
সংক্ষিপ্ত উত্তর:
স্ট্রীম হলো ডেটা প্রসেস করার এমন একটি পদ্ধতি যেখানে পুরো ডেটা একসাথে সার্ভারের RAM-এ লোড না করে ছোট ছোট অংশে (chunks) ভাগ করে প্রসেস করা হয়। আর্কিটেকচারালি এটি স্কেলেবিলিটির জন্য অপরিহার্য, কারণ এটি মেমরি খরচ একদম কমিয়ে দেয়। এর ফলে কয়েক মেগাবাইট মেমরি ব্যবহার করেই একটি সার্ভার গিগাবাইট সাইজের ফাইল বা রিয়েল-টাইম ভিডিও (যেমন- নেটফ্লিক্স) প্রসেস করতে পারে।
বিস্তারিত উত্তর:
স্ট্রীম মূলত ৪ প্রকার: Readable, Writable, Duplex (রিড/রাইট দুটোই, যেমন TCP সকেট), এবং Transform (যাওয়ার পথেই ডেটা পরিবর্তন বা কমপ্রেস করা)।
- সমস্যা: আপনি যদি
fs.readFileSync('10GB_video.mp4')ব্যবহার করেন, তবে সার্ভার পুরো ১০ জিবি ফাইল V8-এর মেমরিতে লোড করার চেষ্টা করবে। ফলে সার্ভার সাথে সাথেOut of Memoryএরর দিয়ে ক্র্যাশ করবে। - স্ট্রীমের সমাধান:
fs.createReadStreamব্যবহার করলে ফাইলটি ৬৪ কেবি (64KB) চাঙ্কে ভাগ হয়ে যায়। সার্ভার একটু ডেটা রিড করে, এক্সপ্রেসেres-এর মাধ্যমে ইউজারের কাছে পাঠায় (pipe), এবং মেমরি ক্লিয়ার করে দেয়। ফাইলের সাইজ যত বড়ই হোক না কেন, সার্ভারের মেমরি সবসময় কম থাকে।
Question 6: Explain Buffers in Node.js. Why does Node need them?
Short Answer:
A Buffer is a globally available class in Node.js used to allocate a fixed-size chunk of raw memory outside the V8 JavaScript heap. Node needs them because JavaScript strings were traditionally built for Unicode characters and lacked the mechanism to read, write, and manipulate raw binary data streams (like TCP streams, image files, or compressed buffers).
Detailed Answer:
When streams process data in chunks (as discussed in Q5), those chunks are temporarily stored in a Buffer before they are processed or sent out.
- Memory Allocation: Because V8’s memory management and garbage collection are not optimized for massive, raw binary processing, Buffers are actually allocated in C++.
- Immutability: Once a Buffer is created (e.g.,
Buffer.alloc(1024)), its size cannot be changed. It is a strictly typed array of integers ranging from 0 to 255 (representing 8-bit bytes). - Use Cases: You interact with buffers directly when parsing multipart form data (like file uploads), decoding Base64 strings, or implementing custom network protocols.
প্রশ্ন ৬: Node.js-এ বাফার (Buffers) কী? Node এর জন্য এটি কেন দরকার?
সংক্ষিপ্ত উত্তর:
বাফার হলো Node.js-এর একটি গ্লোবাল ক্লাস যা V8 ইঞ্জিন বা জাভাস্ক্রিপ্ট হিপের (heap) বাইরে সরাসরি একটি ফিক্সড-সাইজের কাঁচা মেমরি (raw memory) বরাদ্দ করে। জাভাস্ক্রিপ্ট মূলত স্ট্রিং বা টেক্সট নিয়ে কাজ করার জন্য তৈরি হয়েছিল, এতে সরাসরি বাইনারি ডেটা (যেমন- TCP ডেটা, ইমেজ বা জিপ ফাইল) নিয়ে কাজ করার ক্ষমতা ছিল না। এই শূন্যস্থান পূরণ করতেই বাফারের প্রয়োজন হয়।
বিস্তারিত উত্তর:
যখন স্ট্রীম ডেটাকে ছোট ছোট চাঙ্কে (chunks) ভাগ করে প্রসেস করে, তখন সেই চাঙ্কগুলো প্রসেস হওয়ার আগে সাময়িকভাবে একটি বাফারে এসে জমা হয়।
১. মেমরি অ্যালোকেশন: V8-এর মেমরি ম্যানেজমেন্ট কাঁচা বাইনারি প্রসেসিংয়ের জন্য খুব একটা ভালো না হওয়ায়, বাফার মূলত C++ দ্বারা তৈরি এবং ম্যানেজ হয়।
২. পরিবর্তনযোগ্যতা (Immutability): একবার একটি বাফার তৈরি হলে (যেমন- Buffer.alloc(1024)), তার সাইজ আর পরিবর্তন করা যায় না। এটি মূলত ০ থেকে ২৫৫ পর্যন্ত সংখ্যার একটি অ্যারে (যা ৮-বিট বাইট প্রকাশ করে)।
৩. ব্যবহার: ফাইল আপলোড হ্যান্ডল করা, Base64 স্ট্রিং ডিকোড করা বা কাস্টম নেটওয়ার্ক প্রোটোকল লেখার সময় ডেভেলপারদের সরাসরি বাফার নিয়ে কাজ করতে হয়।
Question 7: What is the EventEmitter in Node.js and how does it implement the Publish/Subscribe pattern?
Short Answer:
The EventEmitter class (from the events module) is the foundational building block of Node's asynchronous, event-driven architecture. It allows objects to emit named custom events (Publish) and allows other parts of the system to attach functions to listen for and react to those events (Subscribe).
Detailed Answer:
Almost all core Node.js modules (like http, fs, stream) inherit from EventEmitter.
- Loose Coupling: Architecturally, it heavily decouples your code. A user registration service can simply emit a
user_registeredevent with the user data. It doesn't need to know anything about the Email Service or the Analytics Service. Those services simply subscribe to the event and trigger their respective logic. - Memory Leaks: A senior-level consideration is managing event listeners. If you attach too many listeners (default max is 10) or forget to use
emitter.removeListener()when a component unmounts or finishes, you will create a severe memory leak.
JavaScript
const EventEmitter = require('events');
const myEmitter = new EventEmitter();// Subscriber (Listener)
myEmitter.on('orderPlaced', (orderId) => {
console.log(`Processing payment for order: ${orderId}`);
});// Publisher (Emitter)
myEmitter.emit('orderPlaced', 99102);
প্রশ্ন ৭: Node.js-এ EventEmitter কী এবং এটি কীভাবে Publish/Subscribe প্যাটার্ন ইমপ্লিমেন্ট করে?
সংক্ষিপ্ত উত্তর:
events মডিউল থেকে আসা EventEmitter ক্লাসটি মূলত Node.js এর অ্যাসিনক্রোনাস ও ইভেন্ট-ড্রিভেন আর্কিটেকচারের মূল ভিত্তি। এর মাধ্যমে সিস্টেমের এক অংশের কোনো অবজেক্ট নির্দিষ্ট নামের ইভেন্ট তৈরি বা এমিট (Publish) করতে পারে, এবং অন্য অংশ সেই ইভেন্ট লিসেন করে বা শুনে নির্দিষ্ট ফাংশন রান করাতে পারে (Subscribe)।
বিস্তারিত উত্তর:
Node.js এর প্রায় সব কোর মডিউল (যেমন http, fs, stream) মূলত এই EventEmitter থেকেই তৈরি (inherit করা)।
১. লুজ কাপলিং (Loose Coupling): আর্কিটেকচারের দিক থেকে এটি কোডকে খুব সুন্দরভাবে আলাদা করে। ইউজার রেজিস্ট্রেশন সার্ভিস শুধু একটি user_registered ইভেন্ট এমিট করেই খালাস। ইমেইল পাঠানো বা অ্যানালিটিক্সের কাজ তার জানার দরকার নেই। ওই সার্ভিসগুলো শুধু ইভেন্টটিতে সাবস্ক্রাইব করে রাখবে এবং নিজে থেকেই কাজ শুরু করবে।
২. মেমরি লিক: সিনিয়র ডেভেলপার হিসেবে ইভেন্ট লিসেনার ম্যানেজ করা জরুরি। আপনি যদি একটি ইভেন্টে অনেক বেশি লিসেনার যুক্ত করেন (ডিফল্ট লিমিট ১০) অথবা কাজ শেষে লিসেনার রিমুভ করতে ভুলে যান, তবে অ্যাপ্লিকেশনে মারাত্মক মেমরি লিক তৈরি হবে।
Question 8: CommonJS vs ES Modules in Node.js. What are the core differences and how does Node resolve them?
Short Answer:
CommonJS (CJS) is the legacy Node.js module system that uses require() and module.exports, which loads modules synchronously at runtime. ES Modules (ESM) is the modern ECMAScript standard that uses import and export, which loads modules asynchronously and parses them statically before execution, allowing for advanced optimizations like Tree Shaking.
Detailed Answer:
Understanding module resolution is vital when migrating legacy systems or writing isomorphic libraries.
- CommonJS: Since
require()is synchronous, you can conditionally load a file inside anifblock. It calculates the dependency graph sequentially during execution. - ES Modules: Because
importstatements must be at the top level, Node parses the entire dependency graph before running any code. This static analysis is what allows bundlers (like Webpack/Rollup) to remove unused code (Tree Shaking). - Interoperability: To use ESM natively in Node, you must add
"type": "module"in yourpackage.jsonor use the.mjsextension. You can import CJS inside ESM, but you cannotrequire()an ES Module natively.
প্রশ্ন ৮: Node.js-এ CommonJS বনাম ES Modules এর মূল পার্থক্য কী এবং Node এগুলো কীভাবে কাজ করে?
সংক্ষিপ্ত উত্তর:
CommonJS (CJS) হলো Node.js-এর পুরনো মডিউল সিস্টেম যা require() এবং module.exports ব্যবহার করে এবং এটি রানটাইমে সিনক্রোনাসভাবে মডিউল লোড করে। অন্যদিকে, ES Modules (ESM) হলো আধুনিক জাভাস্ক্রিপ্ট স্ট্যান্ডার্ড যা import এবং export ব্যবহার করে। এটি অ্যাসিনক্রোনাসভাবে মডিউল লোড করে এবং কোড রান করার আগেই স্ট্যাটিক্যালি স্ক্যান করে, যা কোড অপ্টিমাইজেশন (যেমন ট্রি শেকিং) সহজ করে।
বিস্তারিত উত্তর:
পুরনো সিস্টেম মাইগ্রেট করার সময় এই মডিউল সিস্টেম বোঝা অত্যন্ত জরুরি।
- CommonJS: যেহেতু
require()সিনক্রোনাস, তাই আপনি চাইলেifকন্ডিশনের ভেতরে ডাইনামিকভাবে কোনো ফাইল লোড করতে পারেন। কোড রান করার সময়ই এটি মডিউল লোড করে। - ES Modules:
importস্টেটমেন্ট সবসময় ফাইলের একেবারে ওপরে লিখতে হয়। এর ফলে Node কোনো কোড রান করার আগেই পুরো ফাইলের নির্ভরতা বা ডিপেন্ডেন্সি ম্যাপ তৈরি করে ফেলে। এই স্ট্যাটিক স্ক্যানিংয়ের কারণেই অব্যবহৃত কোড (unused code) মুছে ফেলা যায়। - ব্যবহার: Node-এ ESM ব্যবহার করতে চাইলে
package.json-এ"type": "module"যুক্ত করতে হবে অথবা ফাইলের নামের শেষে.mjsব্যবহার করতে হবে।
Question 9: How does the Cluster module work, and how does it relate to scaling Node.js applications?
Short Answer:
Because a single Node.js instance runs on a single thread, it can only utilize one core of a CPU, severely underutilizing modern multi-core servers. Architecturally, the Cluster module solves this by forking the main process into multiple child processes (Workers) that share the same server port and distribute incoming HTTP traffic efficiently using an OS-level round-robin algorithm.
Detailed Answer:
If you deploy a standard Express app on an AWS EC2 instance with 8 cores, 7 of those cores will remain completely idle.
- The Master-Worker Pattern: You write code to check
cluster.isMaster. The Master process doesn't handle HTTP requests; its only job is to fork child instances (typicallyos.cpus().lengthamount) and manage them. - IPC Communication: Workers communicate with the master via Inter-Process Communication (IPC). If a worker crashes due to an unhandled exception, the master detects the
exitevent and instantly forks a new worker to replace it, ensuring zero downtime. - Modern Equivalents: While you can write cluster logic manually, enterprise environments typically use process managers like PM2 (
pm2 start app.js -i max) or containerize the application using Docker/Kubernetes to achieve the exact same horizontal scaling architecture natively.
প্রশ্ন ৯: Cluster মডিউল কীভাবে কাজ করে এবং Node.js অ্যাপ্লিকেশন স্কেল (Scale) করার ক্ষেত্রে এর ভূমিকা কী?
সংক্ষিপ্ত উত্তর:
যেহেতু একটি Node.js ইনস্ট্যান্স সিঙ্গেল থ্রেডে চলে, তাই এটি প্রসেসরের মাত্র একটি কোর ব্যবহার করতে পারে, যা আধুনিক মাল্টি-কোর সার্ভারের জন্য একটি বড় অপচয়। আর্কিটেকচারের দিক থেকে Cluster মডিউল এর সমাধান দেয়। এটি মূল প্রসেসটিকে (Master) ফর্ক করে একাধিক চাইল্ড প্রসেস (Workers) তৈরি করে, যারা একই সার্ভার পোর্ট শেয়ার করে এবং রাউন্ড-রবিন (round-robin) অ্যালগরিদম ব্যবহার করে ইউজার ট্রাফিক নিজেদের মধ্যে ভাগ করে নেয়।
বিস্তারিত উত্তর:
আপনি যদি ৮টি কোরের একটি AWS সার্ভারে সাধারণ Express অ্যাপ ডিপ্লয় করেন, তবে ৭টি কোরই অলস বসে থাকবে।
১. মাস্টার-ওয়ার্কার প্যাটার্ন: আমরা cluster.isMaster চেক করে কোড লিখি। মাস্টার প্রসেস কোনো ইউজারের রিকোয়েস্ট হ্যান্ডল করে না; তার একমাত্র কাজ হলো চাইল্ড প্রসেস (সাধারণত CPU-র কোরের সমান) তৈরি করা এবং তাদের মনিটর করা।
২. IPC কমিউনিকেশন: ওয়ার্কাররা মাস্টারের সাথে IPC (Inter-Process Communication) এর মাধ্যমে যোগাযোগ করে। কোনো কারণে যদি কোনো ওয়ার্কার ক্র্যাশ করে, তবে মাস্টার সাথে সাথে ইভেন্টটি ধরে ফেলে এবং নতুন একটি ওয়ার্কার তৈরি করে দেয়, ফলে সাইট কখনো ডাউন হয় না।
৩. আধুনিক বিকল্প: ম্যানুয়ালি ক্লাস্টার কোড লেখার চেয়ে বড় কোম্পানিগুলো সাধারণত PM2 (pm2 start app.js -i max) ব্যবহার করে, অথবা Docker/Kubernetes ব্যবহার করে একই স্কেলিং আর্কিটেকচার তৈরি করে।
Question 10: Memory leaks in Node.js: How do they happen and how do you debug them as a senior developer?
Short Answer:
A memory leak in Node.js occurs when objects are no longer needed by the application but are still referenced in memory (e.g., in global variables, closures, or uncleared event listeners), preventing V8’s Garbage Collector from freeing up that RAM. As a senior developer, I debug this using Node’s built-in --inspect flag, generating Heap Snapshots, and analyzing them in Chrome DevTools to identify memory anomalies.
Detailed Answer:
Memory leaks will quietly degrade performance until the server throws a fatal OOM (Out of Memory) crash.
- Common Causes:
- Creating global variables that continuously grow (e.g., an in-memory caching array without a size limit).
- Forgetting to use
removeListenerorclearInterval. - Stale closures holding onto large objects.
- Debugging Strategy:
- I monitor memory spikes using APM tools (like Datadog).
- I reproduce the issue locally and start Node with
node --inspect index.js. - I connect Chrome DevTools (
chrome://inspect) and take a Heap Snapshot. - I simulate heavy load (using tools like Artillery or Autocannon) and take a second Heap Snapshot.
- By comparing the two snapshots, I can see exactly which objects (Strings, Arrays) have drastically increased in size and track down the exact file causing the leak.
প্রশ্ন ১০: Node.js-এ মেমরি লিক (Memory Leaks) কীভাবে হয় এবং একজন সিনিয়র ডেভেলপার হিসেবে আপনি এগুলো কীভাবে ডিবাগ করেন?
সংক্ষিপ্ত উত্তর:
Node.js-এ মেমরি লিক তখন ঘটে যখন কোনো অবজেক্টের কাজ শেষ হয়ে গেলেও সেটি মেমরিতে রেফারেন্স হিসেবে রয়ে যায় (যেমন- গ্লোবাল ভেরিয়েবল, ক্লোজার বা লিসেনারে)। এর ফলে V8-এর গার্বেজ কালেক্টর (Garbage Collector) মেমরি ক্লিয়ার করতে পারে না এবং RAM ফুল হয়ে যায়। সিনিয়র ডেভেলপার হিসেবে আমি Node-এর --inspect ফ্ল্যাগ ব্যবহার করে হিপ স্ন্যাপশট (Heap Snapshots) নিই এবং Chrome DevTools-এ তা অ্যানালাইজ করে সমস্যার মূল কারণ খুঁজে বের করি।
বিস্তারিত উত্তর:
মেমরি লিক খুব নীরবে অ্যাপের পারফরম্যান্স কমিয়ে দেয় এবং একপর্যায়ে সার্ভার OOM (Out of Memory) এরর দিয়ে ক্র্যাশ করে।
- সাধারণ কারণগুলো:
- এমন গ্লোবাল ভেরিয়েবল তৈরি করা যা প্রতিনিয়ত বড় হতে থাকে (যেমন লিমিট ছাড়া ক্যাশিং অ্যারে)।
- কাজ শেষে
clearIntervalবা ইভেন্ট লিসেনার রিমুভ করতে ভুলে যাওয়া। - ক্লোজারের (closure) ভেতরে ভারী অবজেক্ট আটকে থাকা।
- ডিবাগিং কৌশল:
- ১. আমি প্রথমে Datadog বা New Relic দিয়ে সার্ভারের মেমরি স্পাইক মনিটর করি।
- ২. লোকাল মেশিনে
node --inspect index.jsদিয়ে কোড রান করি। - ৩. Chrome DevTools (
chrome://inspect) কানেক্ট করে প্রথম Heap Snapshot নিই। - ৪. Artillery এর মতো টুল দিয়ে সার্ভারে প্রচুর ফেক রিকোয়েস্ট পাঠাই এবং দ্বিতীয় স্ন্যাপশট নিই।
- ৫. দুটি স্ন্যাপশট তুলনা করলেই খুব সহজে দেখা যায় কোন ডেটা বা অ্যারে মেমরি ধরে রেখেছে, যা থেকে ঠিক কোন ফাইলে সমস্যা তা বের করা যায়।
আগের পর্বে কোড এক্সাম্পল কম থাকার জন্য দুঃখিত! এবার থেকে প্রতিটি প্রশ্নের উত্তরে ইংরেজি এবং বাংলা উভয় অংশেই উপযুক্ত কোড এক্সাম্পল (নিজস্ব ভাষার কমেন্টসহ) যুক্ত করে দিচ্ছি।
এখানে Node.js এবং Express.js এর Phase 2 (Q11 থেকে Q20) দেওয়া হলো, যেখানে মূলত Express.js আর্কিটেকচার, মিডলওয়্যার, সিকিউরিটি এবং অ্যাডভান্সড এরর হ্যান্ডলিং নিয়ে আলোচনা করা হয়েছে।
Phase 2: Express.js Architecture, Middleware & Security
Question 11: Architecturally, how does the Middleware pattern work in Express.js under the hood?
Short Answer:
Express.js is essentially a routing and middleware web framework. Architecturally, it implements the Chain of Responsibility design pattern. A middleware is simply a function that has access to the request (req), response (res), and the next function. It executes code, modifies the request/response objects, and then explicitly calls next() to pass control to the next middleware in the stack.
Detailed Answer:
When a request hits an Express server, it goes through a pipeline of middleware functions in the exact order they were defined.
- The Flow: If a middleware does not end the request-response cycle (e.g., by calling
res.send()), it must callnext(). If it forgets to callnext(), the request will hang indefinitely, and the client will experience a timeout. - Decoupling: As a senior developer, I use middleware to deeply decouple logic. Instead of writing authentication, logging, and validation all inside the final route controller, I separate them into modular middleware functions.
JavaScript
// English Code Example: Middleware Flow
const express = require('express');
const app = express();// 1. Logger Middleware
const logger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // Pass control to the next function
};// 2. Auth Middleware
const requireAuth = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: "Unauthorized" }); // Ends the cycle
}
req.user = { id: 1, role: 'admin' }; // Modifying the request object
next();
};// 3. Final Controller
app.get('/dashboard', logger, requireAuth, (req, res) => {
res.json({ message: "Welcome to dashboard", user: req.user });
});
প্রশ্ন ১১: আর্কিটেকচারের দিক থেকে Express.js-এ মিডলওয়্যার (Middleware) প্যাটার্ন কীভাবে কাজ করে?
সংক্ষিপ্ত উত্তর:
Express.js মূলত একটি রাউটিং এবং মিডলওয়্যার ফ্রেমওয়ার্ক। আর্কিটেকচারালি, এটি Chain of Responsibility ডিজাইন প্যাটার্ন মেনে চলে। মিডলওয়্যার হলো এমন একটি ফাংশন যার কাছে req (রিকোয়েস্ট), res (রেসপন্স) এবং next ফাংশনের অ্যাক্সেস থাকে। এটি কিছু কাজ করে, ডেটা পরিবর্তন করে এবং পরবর্তী মিডলওয়্যারের কাছে কন্ট্রোল ট্রান্সফার করার জন্য next() কল করে।
বিস্তারিত উত্তর:
যখন Express সার্ভারে কোনো রিকোয়েস্ট আসে, তখন সেটি ডিফাইন করা সিরিয়াল অনুযায়ী মিডলওয়্যারের পাইপলাইনের মধ্য দিয়ে যায়।
১. ফ্লো (Flow): কোনো মিডলওয়্যার যদি রেসপন্স সাইকেল শেষ না করে (যেমন res.send() না করে), তবে তাকে অবশ্যই next() কল করতে হবে। next() কল করতে ভুলে গেলে রিকোয়েস্টটি আটকে থাকবে এবং ক্লায়েন্ট টাইমআউট (timeout) এরর পাবে।
২. ডিকাপলিং (Decoupling): সিনিয়র ডেভেলপার হিসেবে আমি লজিক আলাদা করার জন্য মিডলওয়্যার ব্যবহার করি। মূল কন্ট্রোলারের ভেতর অথেনটিকেশন, লগিং এবং ভ্যালিডেশনের কোড না লিখে, আমি সেগুলোকে ছোট ছোট স্বাধীন মিডলওয়্যারে ভাগ করে নিই।
JavaScript
// বাংলা কোড এক্সাম্পল: মিডলওয়্যার ফ্লো
const express = require('express');
const app = express();// ১. লগার (Logger) মিডলওয়্যার
const logger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // পরবর্তী ফাংশনে কন্ট্রোল পাঠিয়ে দিচ্ছে
};// ২. অথেনটিকেশন মিডলওয়্যার
const requireAuth = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: "অনুমতি নেই" }); // সাইকেল এখানেই শেষ
}
req.user = { id: 1, role: 'admin' }; // রিকোয়েস্ট অবজেক্টে ডেটা যুক্ত করা হচ্ছে
next();
};// ৩. ফাইনাল কন্ট্রোলার
app.get('/dashboard', logger, requireAuth, (req, res) => {
res.json({ message: "ড্যাশবোর্ডে স্বাগতম", user: req.user });
});
Question 12: What is the fundamental difference between app.use() and app.get()/app.post()?
Short Answer:
app.use() binds middleware to an application and matches routes based on a prefix. It triggers for any HTTP method (GET, POST, PUT, etc.) that starts with the specified path. In contrast, app.get() or app.post() are strict route handlers; they require an exact match for both the specific HTTP method and the complete URL path.
Detailed Answer:
Understanding how Express matches paths is critical for structuring large APIs.
app.use('/api', authMiddleware): This will executeauthMiddlewarefor/api/users,/api/products,/api/orders, and it applies to GET, POST, DELETE, etc. It is the primary tool for mounting global middleware (likeexpress.json()) or connecting modular routers (app.use('/users', userRouter)).app.get('/api/users', getController): This will only trigger for a GET request exactly at/api/users.
JavaScript
// English Code Example
// 1. Matches ANY method (GET, POST) starting with '/api'
app.use('/api', (req, res, next) => {
console.log("Global API Middleware triggered");
next();
});// 2. Matches EXACTLY a GET request to '/api/users'
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});প্রশ্ন ১২: Express.js-এ app.use() এবং app.get()/app.post() এর মধ্যে মূল পার্থক্য কী?
সংক্ষিপ্ত উত্তর:
app.use() মূলত অ্যাপ্লিকেশনে মিডলওয়্যার যুক্ত করে এবং এটি URL-এর প্রিফিক্স (prefix) বা শুরুর অংশ মিলিয়ে কাজ করে। নির্দিষ্ট করা পাথ দিয়ে শুরু হওয়া যেকোনো HTTP মেথডের (GET, POST, PUT ইত্যাদি) জন্যই এটি রান হয়। অন্যদিকে app.get() বা app.post() হলো স্ট্রিক্ট রাউট হ্যান্ডলার; এগুলো কাজ করার জন্য নির্দিষ্ট HTTP মেথড এবং পুরো URL পাথের হুবহু মিল (exact match) থাকতে হয়।
বিস্তারিত উত্তর:
বড় API তৈরি করার সময় Express কীভাবে পাথ মেলায় তা বোঝা খুব জরুরি।
app.use('/api', authMiddleware): এটি/api/users,/api/products,/api/ordersসবগুলোর জন্যই কাজ করবে এবং GET, POST সব রিকোয়েস্টেই রান হবে। গ্লোবাল মিডলওয়্যার (যেমনexpress.json()) বসাতে বা আলাদা রাউটার কানেক্ট করতে এটি ব্যবহৃত হয়।app.get('/api/users', getController): এটি শুধুমাত্র/api/usersপাথে আসা GET রিকোয়েস্টের জন্যই কাজ করবে, অন্য কিছুর জন্য নয়।
JavaScript
// বাংলা কোড এক্সাম্পল
// ১. '/api' দিয়ে শুরু হওয়া যেকোনো মেথডের (GET, POST) জন্য এটি কাজ করবে
app.use('/api', (req, res, next) => {
console.log("গ্লোবাল API মিডলওয়্যার ট্রিগার হয়েছে");
next();
});// ২. হুবহু '/api/users' পাথে আসা GET রিকোয়েস্টের জন্যই শুধু এটি কাজ করবে
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});Question 13: Express route handlers are synchronous by default. How do you handle Asynchronous Errors (Promises/Async-Await) without crashing the server?
Short Answer:
Prior to Express 5, if an asynchronous route handler (async (req, res)) threw an error or a Promise rejected, Express wouldn't catch it, leading to an Unhandled Promise Rejection crash. To fix this architecturally, you must either wrap every controller in a try/catch block and pass the error to next(err), or use a wrapper utility/package like express-async-errors to automatically forward async errors to the global error handler.
Detailed Answer:
Writing try/catch in hundreds of controllers violates the DRY (Don't Repeat Yourself) principle and clutters business logic.
- The Problem: Express 4’s internal error catcher doesn’t await your controllers. A rejected promise simply vanishes into the Node runtime.
- The Professional Solution: I build a Higher-Order Function (HOF) called
catchAsyncthat wraps the controller. It catches any rejected promises and automatically pipes them intonext(). (Note: Express 5 natively supports async error handling, making this wrapper unnecessary).
JavaScript
// English Code Example: The catchAsync Wrapper
// 1. The Wrapper Utility
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next); // Forwards any async error to global handler
};
};// 2. Clean Controller (No try-catch needed!)
const getUser = catchAsync(async (req, res, next) => {
const user = await Database.findById(req.params.id);
if (!user) throw new Error("User not found"); // Automatically goes to next()
res.json(user);
});app.get('/users/:id', getUser);
প্রশ্ন ১৩: Express রাউট হ্যান্ডলারগুলো ডিফল্টভাবে সিনক্রোনাস। সার্ভার ক্র্যাশ না করে আপনি অ্যাসিনক্রোনাস এরর (Promises/Async-Await) কীভাবে হ্যান্ডল করেন?
সংক্ষিপ্ত উত্তর:
Express 5 এর আগে, কোনো অ্যাসিনক্রোনাস রাউট হ্যান্ডলার (async (req, res)) যদি এরর থ্রো করত বা প্রমিস রিজেক্ট হতো, Express তা ধরতে পারত না। এর ফলে 'Unhandled Promise Rejection' এরর দিয়ে সার্ভার ক্র্যাশ করত। এর আর্কিটেকচারাল সমাধান হলো—হয় প্রতিটি কন্ট্রোলারকে try/catch দিয়ে মুড়িয়ে এররটিকে next(err)-এ পাস করা, অথবা express-async-errors-এর মতো কোনো র্যাপার (wrapper) ফাংশন ব্যবহার করে স্বয়ংক্রিয়ভাবে গ্লোবাল এরর হ্যান্ডলারে এরর পাঠিয়ে দেওয়া।
বিস্তারিত উত্তর:
শত শত কন্ট্রোলারের ভেতরে বারবার try/catch লেখা কোডকে নোংরা করে এবং DRY (Don't Repeat Yourself) প্রিন্সিপাল ভাঙে।
- সমস্যা: Express 4 এর নিজস্ব এরর ক্যাচার আপনার অ্যাসিনক্রোনাস ফাংশনের জন্য অপেক্ষা করে না।
- প্রফেশনাল সমাধান: আমি
catchAsyncনামের একটি হায়ার-অর্ডার ফাংশন (HOF) তৈরি করি যা মূল কন্ট্রোলারকে র্যাপ (wrap) করে রাখে। এটি প্রমিস রিজেক্ট হলে তা ধরে সোজাnext()এ পাঠিয়ে দেয়। (নোট: Express 5 এ নিজস্ব অ্যাসিনক্রোনাস এরর হ্যান্ডলিং আছে, তখন আর এই র্যাপার লাগে না)।
JavaScript
// বাংলা কোড এক্সাম্পল: catchAsync র্যাপার
// ১. র্যাপার ইউটিলিটি
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next); // যেকোনো অ্যাসিনক্রোনাস এরর গ্লোবাল হ্যান্ডলারে পাঠায়
};
};// ২. ক্লিন কন্ট্রোলার (কোনো try-catch এর প্রয়োজন নেই!)
const getUser = catchAsync(async (req, res, next) => {
const user = await Database.findById(req.params.id);
if (!user) throw new Error("ইউজার পাওয়া যায়নি"); // এটি নিজে থেকেই next() এ চলে যাবে
res.json(user);
});app.get('/users/:id', getUser);
Question 14: How do you architect a Centralized Error Handling mechanism in an Express application?
Short Answer:
Centralized error handling ensures that all API errors return a consistent, predictable JSON structure to the frontend. Architecturally, this is achieved by defining a specific Error-Handling Middleware function that takes exactly four parameters: (err, req, res, next). This middleware must be placed at the very end of the app.use() stack, catching all errors forwarded by next(err) from previous routes.
Detailed Answer:
In an enterprise app, you don’t send raw stack traces or random string messages to the client.
- Custom Error Class: I first create a generic
AppErrorclass that extends the built-in NodeErrorclass, attaching properties likestatusCodeandisOperational. - The Middleware: The 4-parameter middleware sits at the bottom of
app.js. It intercepts the error, formats it (e.g., hiding stack traces in production environments), and sends a clean JSON response.
JavaScript
// English Code Example: Centralized Error Handler
// 1. Custom Error Class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
}
}// 2. Global Error Handling Middleware (must be at the very bottom of app.js)
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
// Clean response structure
res.status(err.statusCode).json({
status: err.status || 'error',
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }) // Only show stack in dev
});
});প্রশ্ন ১৪: Express অ্যাপ্লিকেশনে একটি সেন্ট্রালাইজড এরর হ্যান্ডলিং (Centralized Error Handling) মেকানিজম কীভাবে আর্কিটেক্ট করবেন?
সংক্ষিপ্ত উত্তর:
সেন্ট্রালাইজড এরর হ্যান্ডলিং নিশ্চিত করে যে অ্যাপ্লিকেশনের সব এরর একটি নির্দিষ্ট এবং গোছানো JSON ফরম্যাটে ফ্রন্টএন্ডে যাবে। আর্কিটেকচারালি, এটি তৈরি করা হয় একটি বিশেষ এরর-হ্যান্ডলিং মিডলওয়্যার দিয়ে, যা হুবহু চারটি প্যারামিটার রিসিভ করে: (err, req, res, next)। আগের কোনো রাউট থেকে next(err) এর মাধ্যমে আসা এররগুলো ধরার জন্য এই ফাংশনটিকে app.use() স্ট্যাকের একদম শেষে বসাতে হয়।
বিস্তারিত উত্তর:
বড় স্কেলের অ্যাপে আপনি ক্লায়েন্টকে র-স্ট্যাক ট্রেস (raw stack trace) বা এলোমেলো মেসেজ পাঠাতে পারেন না।
১. কাস্টম এরর ক্লাস: আমি প্রথমে Node এর নিজস্ব Error ক্লাসকে এক্সটেন্ড করে AppError নামের একটি ক্লাস বানাই, যেখানে statusCode এর মতো প্রপার্টি যুক্ত করা থাকে।
২. মিডলওয়্যার: ৪-প্যারামিটারের এই মিডলওয়্যারটি app.js এর একদম নিচে থাকে। এটি এরর রিসিভ করে, প্রোডাকশন এনভায়রনমেন্টে স্ট্যাক ট্রেস লুকিয়ে ফেলে এবং একটি ক্লিন JSON রেসপন্স পাঠায়।
JavaScript
// বাংলা কোড এক্সাম্পল: সেন্ট্রালাইজড এরর হ্যান্ডলার
// ১. কাস্টম এরর ক্লাস
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
}
}// ২. গ্লোবাল এরর হ্যান্ডলিং মিডলওয়্যার (অবশ্যই app.js এর একদম নিচে থাকতে হবে)
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
// গোছানো রেসপন্স স্ট্রাকচার
res.status(err.statusCode).json({
status: err.status || 'error',
message: err.message,
// ডেভেলপমেন্ট মোডে থাকলে স্ট্যাক ট্রেস দেখাবে
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});Question 15: How do you handle large file uploads in Express.js without exhausting server RAM (OOM errors)?
Short Answer:
Express does not handle multipart/form-data natively. To handle large file uploads securely, I use the multer middleware. Crucially, to prevent memory exhaustion (OOM), I configure Multer with diskStorage (writing chunks directly to the temporary disk space) rather than memoryStorage (which holds the entire file in RAM). For cloud architectures, I use multer-s3 to stream the file directly to AWS S3 without saving it on the Node server at all.
Detailed Answer:
Junior developers often use multer({ storage: multer.memoryStorage() }). If 10 users upload 50MB videos simultaneously, Node's RAM jumps by 500MB instantly, which crashes small containers.
- The Architecture: The safest way is to use Node streams. I configure Multer to stream the incoming chunks either to the local file system (DiskStorage) or directly to a Cloud Object Storage via pipes. This keeps the Node.js memory footprint consistently flat regardless of the file size.
JavaScript
// English Code Example: Safe File Uploading
const express = require('express');
const multer = require('multer');// Configure Disk Storage to stream file directly to OS disk, bypassing RAM
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '/tmp/uploads'); // Save to temp directory
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});// Enforce strict file size limits to prevent DDoS via massive files
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
});const app = express();
app.post('/profile', upload.single('avatar'), (req, res) => {
res.json({ message: "File uploaded safely via stream to disk." });
});
প্রশ্ন ১৫: Express.js-এ সার্ভারের RAM ফুল (OOM Error) না করে বড় ফাইল আপলোড কীভাবে হ্যান্ডল করবেন?
Get Yeasaleh’s stories in your inbox
Join Medium for free to get updates from this writer.
সংক্ষিপ্ত উত্তর:
Express নিজে থেকে multipart/form-data হ্যান্ডল করতে পারে না। বড় ফাইল নিরাপদে আপলোড করার জন্য আমি multer মিডলওয়্যার ব্যবহার করি। মেমরি ক্র্যাশ (OOM) ঠেকাতে আমি Multer-এ memoryStorage (যা পুরো ফাইল RAM-এ ধরে রাখে) ব্যবহার না করে diskStorage (যা ফাইল সরাসরি ডিস্কে লেখে) ব্যবহার করি। ক্লাউড আর্কিটেকচারের ক্ষেত্রে আমি multer-s3 ব্যবহার করে ফাইলটি Node সার্ভারে সেভ না করে সরাসরি AWS S3-তে স্ট্রীম (stream) করে দিই।
বিস্তারিত উত্তর:
নতুন ডেভেলপাররা প্রায়ই multer.memoryStorage() ব্যবহার করে। যদি ১০ জন ইউজার একসাথে ৫০ এমবির ভিডিও আপলোড করে, তবে Node এর RAM সাথে সাথে ৫০০ এমবি বেড়ে যাবে এবং ছোট সার্ভার ক্র্যাশ করবে।
- আর্কিটেকচার: সবচেয়ে নিরাপদ উপায় হলো Node স্ট্রীম ব্যবহার করা। আমি Multer কে এমনভাবে কনফিগার করি যাতে ডেটা আসার সাথে সাথে তা সরাসরি লোকাল ডিস্কে অথবা ক্লাউড স্টোরেজে পাইপ (pipe) হয়ে যায়। এতে ফাইলের সাইজ যত বড়ই হোক না কেন, Node.js-এর মেমরি বাড়ে না।
JavaScript
// বাংলা কোড এক্সাম্পল: নিরাপদ ফাইল আপলোড
const express = require('express');
const multer = require('multer');// RAM বাইপাস করে ফাইল সরাসরি ডিস্কে স্ট্রীম করার জন্য Disk Storage কনফিগারেশন
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '/tmp/uploads'); // টেম্পোরারি ফোল্ডারে সেভ করা
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});// বিশাল ফাইল দিয়ে সার্ভার ডাউন করা (DDoS) আটকাতে ফাইল সাইজ লিমিট দেওয়া
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 } // ১০ এমবি লিমিট
});const app = express();
app.post('/profile', upload.single('avatar'), (req, res) => {
res.json({ message: "ফাইলটি নিরাপদে ডিস্কে স্ট্রীম করা হয়েছে।" });
});
Question 16: What essential Security Middlewares do you implement in an enterprise Express application?
Short Answer:
An enterprise Express app requires strict defense-in-depth at the routing layer. I always implement Helmet to secure HTTP headers (preventing XSS and clickjacking), CORS to restrict which domains can access the API, and express-rate-limit to mitigate brute-force attacks and DDoS by restricting the number of requests a single IP can make within a specific timeframe.
Detailed Answer:
Express by default is highly unopinionated and insecure.
- Helmet: It hides the
X-Powered-By: Expressheader (preventing attackers from identifying your stack) and sets strict Content Security Policies (CSP). - CORS: Cross-Origin Resource Sharing is essential. Using a wildcard
*in production is a critical vulnerability. I explicitly whitelist specific frontend domains. - Rate Limiting: Protects endpoints like
/loginor/otpfrom brute-force bot attacks.
JavaScript
// English Code Example: Security Configuration
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');const app = express();// 1. Secure HTTP Headers
app.use(helmet());// 2. Strict CORS policy
app.use(cors({
origin: 'https://mytrustedfrontend.com',
methods: ['GET', 'POST']
}));// 3. Global Rate Limiting
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes window
max: 100, // Limit each IP to 100 requests per window
message: "Too many requests from this IP, please try again later."
});
app.use('/api/', apiLimiter);
প্রশ্ন ১৬: এন্টারপ্রাইজ Express অ্যাপ্লিকেশনে আপনি কোন গুরুত্বপূর্ণ সিকিউরিটি মিডলওয়্যারগুলো (Security Middlewares) ব্যবহার করেন?
সংক্ষিপ্ত উত্তর:
এন্টারপ্রাইজ Express অ্যাপের রাউটিং লেভেলে কঠোর সিকিউরিটির প্রয়োজন হয়। আমি সবসময় Helmet ব্যবহার করি যা HTTP হেডার সুরক্ষিত করে (XSS বা ক্লিকজ্যাকিং থেকে বাঁচায়), CORS ব্যবহার করি যা নির্দিষ্ট করে দেয় কোন ডোমেইনগুলো API ব্যবহার করতে পারবে, এবং express-rate-limit ব্যবহার করি যা নির্দিষ্ট সময়ের মধ্যে একটি IP থেকে আসা রিকোয়েস্টের সংখ্যা সীমিত করে বট বা DDoS অ্যাটাক প্রতিহত করে।
বিস্তারিত উত্তর:
ডিফল্টভাবে Express খুব সাধারণ এবং সিকিউরিটি-মুক্ত একটি ফ্রেমওয়ার্ক।
১. Helmet: এটি X-Powered-By: Express হেডার লুকিয়ে ফেলে (যাতে হ্যাকাররা আপনার টেকনোলজি বুঝতে না পারে) এবং কঠোর কনটেন্ট সিকিউরিটি পলিসি (CSP) সেট করে।
২. CORS: প্রোডাকশনে * (সবাই অ্যাক্সেস পাবে) ব্যবহার করা মারাত্মক ভুল। আমি নির্দিষ্ট ফ্রন্টএন্ড ডোমেইনগুলো হোয়াইটলিস্ট করে দিই।
৩. Rate Limiting: /login বা /otp এর মতো রাউটগুলোকে ব্রুট-ফোর্স (brute-force) অ্যাটাক থেকে বাঁচাতে এটি অপরিহার্য।
JavaScript
// বাংলা কোড এক্সাম্পল: সিকিউরিটি কনফিগারেশন
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');const app = express();// ১. HTTP হেডার সুরক্ষিত করা
app.use(helmet());// ২. কঠোর CORS পলিসি
app.use(cors({
origin: 'https://mytrustedfrontend.com', // শুধুমাত্র এই ডোমেইন থেকে রিকোয়েস্ট আসবে
methods: ['GET', 'POST']
}));// ৩. গ্লোবাল রেট লিমিটিং
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // ১৫ মিনিটের উইন্ডো
max: 100, // প্রতিটি IP কে ১০০টি রিকোয়েস্টের লিমিট দেওয়া হলো
message: "এই IP থেকে অনেক বেশি রিকোয়েস্ট এসেছে, পরে আবার চেষ্টা করুন।"
});
app.use('/api/', apiLimiter);
Question 17: How do you structure a scalable Express.js application? Explain the Controller-Service-Repository pattern.
Short Answer:
I structure enterprise Express apps using the Controller-Service-Repository (3-Tier) architecture. This prevents “Fat Controllers” by strictly separating concerns. Controllers only handle HTTP objects (req/res), Services contain the core business logic and validations, and Repositories (or DAOs) are exclusively responsible for executing direct database queries.
Detailed Answer:
In monolithic Node.js applications, putting everything inside app.get() makes the code impossible to test and maintain.
- Controller Layer: The entry point. It parses
req.body, calls the Service, and sends backres.json(). It knows nothing about SQL or MongoDB. - Service Layer: The brain. It receives raw data, calculates business rules (e.g., checking if a user has enough balance), and calls the Repository. This logic can be easily unit-tested without an active HTTP server.
- Repository Layer: The database wrapper. It runs Mongoose or Prisma queries. If we switch from MongoDB to PostgreSQL tomorrow, we only rewrite this layer; the Controllers and Services remain untouched.
JavaScript
// English Code Example: 3-Tier Architecture
// 1. REPOSITORY (Only Database Logic)
const UserRepo = {
findByEmail: async (email) => await db.users.findOne({ email }),
create: async (data) => await db.users.insert(data)
};// 2. SERVICE (Only Business Logic)
const UserService = {
registerUser: async (email, password) => {
const existingUser = await UserRepo.findByEmail(email);
if (existingUser) throw new Error("Email already exists");
// Hash password, apply business rules, etc.
return await UserRepo.create({ email, password });
}
};// 3. CONTROLLER (Only HTTP Logic)
const registerController = async (req, res, next) => {
try {
const newUser = await UserService.registerUser(req.body.email, req.body.password);
res.status(201).json(newUser);
} catch (error) {
next(error);
}
};
প্রশ্ন ১৭: আপনি একটি স্কেলেবল Express.js অ্যাপ্লিকেশন কীভাবে স্ট্রাকচার করেন? Controller-Service-Repository প্যাটার্ন বুঝিয়ে বলুন।
সংক্ষিপ্ত উত্তর:
আমি এন্টারপ্রাইজ Express অ্যাপগুলোকে Controller-Service-Repository (৩-টায়ার) আর্কিটেকচার ব্যবহার করে সাজাই। এটি কোডের কাজগুলোকে আলাদা করে “Fat Controllers” তৈরি হতে বাধা দেয়। Controllers শুধু HTTP অবজেক্ট (req/res) হ্যান্ডল করে, Services মূল বিজনেস লজিক এবং ভ্যালিডেশন করে, এবং Repositories (বা DAO) শুধুমাত্র ডেটাবেসে কুয়েরি চালানোর দায়িত্ব পালন করে।
বিস্তারিত উত্তর:
সব কোড app.get() এর ভেতরে লিখে রাখলে সেই কোড টেস্ট বা মেইনটেইন করা অসম্ভব হয়ে যায়।
১. Controller Layer: এটি রিকোয়েস্টের এন্ট্রি পয়েন্ট। এটি req.body রিসিভ করে, Service কে কল করে এবং res.json() পাঠায়। ডেটাবেসের ব্যাপারে এর কোনো ধারণা থাকে না।
২. Service Layer: এটি অ্যাপের ব্রেইন। এটি ডেটা নিয়ে বিজনেস রুল চেক করে (যেমন ইউজারের ব্যালেন্স আছে কিনা) এবং Repository কে কল করে। এই লেয়ারটিকে HTTP সার্ভার ছাড়াই খুব সহজে ইউনিট টেস্ট করা যায়।
৩. Repository Layer: এটি ডেটাবেস র্যাপার। এটি Mongoose বা Prisma কুয়েরি রান করে। কাল যদি আমরা MongoDB থেকে PostgreSQL-এ মাইগ্রেট করি, তবে শুধু এই লেয়ারটি পরিবর্তন করলেই হবে, বাকিগুলো ঠিক থাকবে।
JavaScript
// বাংলা কোড এক্সাম্পল: ৩-টায়ার আর্কিটেকচার
// ১. REPOSITORY (শুধুমাত্র ডেটাবেস লজিক)
const UserRepo = {
findByEmail: async (email) => await db.users.findOne({ email }),
create: async (data) => await db.users.insert(data)
};// ২. SERVICE (শুধুমাত্র বিজনেস লজিক)
const UserService = {
registerUser: async (email, password) => {
const existingUser = await UserRepo.findByEmail(email);
if (existingUser) throw new Error("ইমেইলটি আগে থেকেই আছে");
// পাসওয়ার্ড হ্যাশ করা বা অন্যান্য রুলস অ্যাপ্লাই করা
return await UserRepo.create({ email, password });
}
};// ৩. CONTROLLER (শুধুমাত্র HTTP লজিক)
const registerController = async (req, res, next) => {
try {
// রিকোয়েস্টের ডেটা সরাসরি সার্ভিসে পাস করা হচ্ছে
const newUser = await UserService.registerUser(req.body.email, req.body.password);
res.status(201).json(newUser);
} catch (error) {
next(error); // গ্লোবাল এরর হ্যান্ডলারে পাঠানো হলো
}
};
Question 18: What is the difference between exec, spawn, and fork in the Node.js child_process module?
Short Answer:
These methods create child processes but handle data fundamentally differently. exec buffers the entire command output in memory and returns it at once, making it suitable only for small outputs. spawn executes a command and streams the data asynchronously chunk-by-chunk, making it mandatory for massive data payloads. fork is a special variation of spawn designed exclusively to spin up new Node.js instances with a built-in IPC (Inter-Process Communication) channel for passing messages.
Detailed Answer:
Choosing the wrong child process method will crash a production Node server.
exec(The Buffer): Runs a shell command (likels -la) and holds the result in RAM. If the output exceeds the default 1MB buffer limit, Node throws aMaxBufferErrorand crashes the process.spawn(The Stream): Bypasses the shell and returns a readable stream. If you are processing a 10GB video using FFmpeg via Node, you must usespawn. It handles infinite amounts of data efficiently.fork(The Node Cloner): Used specifically for multithreading in Node (beforeworker_threadsexisted). It creates a fresh V8 engine and provides a.send()method to communicate back and forth with the parent process.
JavaScript
// English Code Example: Child Process Usage
const { exec, spawn, fork } = require('child_process');// 1. exec: Good for small shell commands
exec('cat package.json', (error, stdout, stderr) => {
console.log(`Buffered Output: ${stdout}`);
});// 2. spawn: Mandatory for heavy/infinite data
const findProcess = spawn('find', ['/']); // Searches entire hard drive
findProcess.stdout.on('data', (chunk) => {
// Streams data in small chunks without crashing RAM
console.log(`Stream Chunk: ${chunk}`);
});// 3. fork: Running a heavy Node.js script in the background
const backgroundNode = fork('./heavy-computation.js');
backgroundNode.send({ action: 'START' }); // IPC communication
প্রশ্ন ১৮: Node.js এর child_process মডিউলে exec, spawn এবং fork এর মধ্যে পার্থক্য কী?
সংক্ষিপ্ত উত্তর:
এই তিনটি মেথডই চাইল্ড প্রসেস তৈরি করে, তবে ডেটা হ্যান্ডল করার ধরন আলাদা। exec পুরো আউটপুটটি মেমরিতে বাফার (buffer) করে একসাথে রিটার্ন করে, যা শুধু ছোট ডেটার জন্য উপযুক্ত। spawn কমান্ড রান করে ডেটাকে স্ট্রীম হিসেবে চাঙ্ক-বাই-চাঙ্ক পাঠায়, যা বিশাল ডেটার জন্য ব্যবহার করা বাধ্যতামূলক। আর fork হলো spawn এর একটি বিশেষ রূপ, যা শুধুমাত্র নতুন Node.js ইনস্ট্যান্স তৈরি করতে ব্যবহৃত হয় এবং এতে মেসেজ আদান-প্রদানের জন্য নিজস্ব IPC (Inter-Process Communication) চ্যানেল থাকে।
বিস্তারিত উত্তর:
ভুল চাইল্ড প্রসেস ব্যবহার করলে প্রোডাকশন সার্ভার ক্র্যাশ করতে পারে।
১. exec (বাফার): এটি শেল কমান্ড (যেমন ls -la) রান করে রেজাল্ট RAM-এ ধরে রাখে। আউটপুট যদি ডিফল্ট ১ এমবি (1MB) লিমিট পার করে, তবে Node MaxBufferError দিয়ে ক্র্যাশ করবে।
২. spawn (স্ট্রীম): এটি শেল বাইপাস করে একটি রিডেবল স্ট্রীম রিটার্ন করে। আপনি যদি FFmpeg দিয়ে ১০ জিবি ভিডিও প্রসেস করেন, তবে আপনাকে অবশ্যই spawn ব্যবহার করতে হবে। এটি মেমরি ক্র্যাশ ছাড়াই আনলিমিটেড ডেটা হ্যান্ডল করতে পারে।
৩. fork (Node ক্লোনার): এটি মূলত Node-এ মাল্টিথ্রেডিংয়ের জন্য ব্যবহৃত হয় (আগে ব্যবহৃত হতো)। এটি নতুন একটি V8 ইঞ্জিন চালু করে এবং প্যারেন্ট প্রসেসের সাথে কথা বলার জন্য .send() মেথড তৈরি করে দেয়।
JavaScript
// বাংলা কোড এক্সাম্পল: Child Process এর ব্যবহার
const { exec, spawn, fork } = require('child_process');// ১. exec: ছোট শেল কমান্ডের জন্য ভালো
exec('cat package.json', (error, stdout, stderr) => {
console.log(`বাফার্ড আউটপুট: ${stdout}`);
});// ২. spawn: বিশাল ডেটার জন্য বাধ্যতামূলক
const findProcess = spawn('find', ['/']); // পুরো হার্ডড্রাইভ সার্চ করছে
findProcess.stdout.on('data', (chunk) => {
// RAM ক্র্যাশ না করে ছোট ছোট চাঙ্কে ডেটা স্ট্রীম করছে
console.log(`স্ট্রীম চাঙ্ক: ${chunk}`);
});// ৩. fork: ব্যাকগ্রাউন্ডে ভারী Node.js স্ক্রিপ্ট চালানোর জন্য
const backgroundNode = fork('./heavy-computation.js');
backgroundNode.send({ action: 'START' }); // IPC এর মাধ্যমে কমান্ড পাঠানো
Question 19: What is the role of package-lock.json, and what is the difference between npm install and npm ci in CI/CD pipelines?
Short Answer:
The package-lock.json file guarantees deterministic and reproducible builds across different environments by locking down the exact, precise versions of every dependency and sub-dependency in your project's dependency tree. In a CI/CD pipeline (like GitHub Actions), you must use npm ci (Clean Install) instead of npm install, because npm ci strictly reads from the lock file, bypasses resolution checks, and installs the exact locked versions significantly faster without modifying the lock file itself.
Detailed Answer:
- The Problem with
npm install: Yourpackage.jsonmight define"express": "^4.17.1". The caret (^) means "install the latest minor version." If you runnpm installlocally today, you get4.17.1. If you run it on the CI server a month later, you might get4.18.0. If a bug exists in4.18.0, your build breaks in production even though it works on your machine. - The Lock File Solution:
package-lock.jsonhardcodes that version to exactly4.17.1. npm ci(Clean Install): It completely deletes thenode_modulesfolder, strictly follows the exact versions inpackage-lock.json, and installs them. It is 2x-3x faster and guarantees your CI/CD server builds the exact same code you tested locally.
প্রশ্ন ১৯: package-lock.json ফাইলের ভূমিকা কী এবং CI/CD পাইপলাইনে npm install ও npm ci এর মধ্যে পার্থক্য কী?
সংক্ষিপ্ত উত্তর:
package-lock.json ফাইলটি আপনার প্রজেক্টের প্রতিটি ডিপেন্ডেন্সি এবং তাদের ভেতরের সাব-ডিপেন্ডেন্সির একদম নির্দিষ্ট ভার্সন লক করে রাখে, যার ফলে যেকোনো এনভায়রনমেন্টে হুবহু একই কোড বিল্ড হওয়া নিশ্চিত হয়। CI/CD পাইপলাইনে (যেমন GitHub Actions) npm install এর বদলে অবশ্যই npm ci (Clean Install) ব্যবহার করতে হয়। কারণ npm ci সরাসরি লক ফাইল থেকে দ্রুত প্যাকেজ ইনস্টল করে এবং লক ফাইলে কোনো পরিবর্তন করে না।
বিস্তারিত উত্তর:
npm installএর সমস্যা: আপনারpackage.jsonএ হয়তো লেখা আছে"express": "^4.17.1"। ক্যারেট (^) মানে হলো "লেটেস্ট মাইনর ভার্সন ইনস্টল করো"। আজকে লোকাল পিসিতে রান করলে আপনি পাবেন4.17.1। এক মাস পর CI সার্ভারে রান করলে সে হয়তো4.18.0ডাউনলোড করবে। ওই নতুন ভার্সনে বাগ থাকলে প্রোডাকশন সার্ভার ক্র্যাশ করবে, যদিও আপনার পিসিতে কোড ঠিকঠাক কাজ করছিল।- লক ফাইলের সমাধান:
package-lock.jsonভার্সনটিকে হুবহু4.17.1তেই লক করে দেয়। npm ci: এটি শুরুতেnode_modulesফোল্ডার ডিলিট করে দেয় এবং লক ফাইলে থাকা নির্দিষ্ট ভার্সনগুলোই ইনস্টল করে। এটি সাধারণ ইনস্টলের চেয়ে ২-৩ গুণ ফাস্ট এবং নিশ্চিত করে যে লোকাল পিসির হুবহু একই কোড প্রোডাকশনে বিল্ড হচ্ছে।
Question 20: How do you implement stateless Authentication (JWT) in an Express API using middleware?
Short Answer:
Stateless authentication means the server does not store active sessions in its memory or database. I implement this using JSON Web Tokens (JWT). The client receives an encrypted token upon login and sends it back in the Authorization: Bearer <token> header for subsequent requests. Architecturally, I create a dedicated Express middleware that extracts the token, verifies its cryptographical signature using jsonwebtoken, and securely attaches the decoded user payload to the req.user object for the final route controller to consume.
Detailed Answer:
JWTs are ideal for microservices and scalable Node architectures because the server doesn’t need to perform a database lookup just to verify who the user is.
- The Flow: The client sends the token. The Express middleware intercepts it. If the token is missing, expired, or tampered with, the middleware instantly returns a
401 Unauthorizedor403 Forbiddenresponse, preventing the request from ever reaching the controller or database.
JavaScript
// English Code Example: JWT Auth Middleware
const jwt = require('jsonwebtoken');const protectRoute = (req, res, next) => {
// 1. Extract Token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: "No token provided" });
} const token = authHeader.split(' ')[1]; try {
// 2. Verify the cryptographical signature
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 3. Attach user payload to request and proceed
req.user = decoded;
next();
} catch (err) {
// Automatically catches expired or modified tokens
return res.status(403).json({ error: "Invalid or expired token" });
}
};// Usage
app.get('/api/profile', protectRoute, (req, res) => {
res.json({ message: "Secure Data", userId: req.user.id });
});
প্রশ্ন ২০: Express API-তে মিডলওয়্যার ব্যবহার করে স্টেটলেস অথেনটিকেশন (JWT) কীভাবে ইমপ্লিমেন্ট করবেন?
সংক্ষিপ্ত উত্তর:
স্টেটলেস অথেনটিকেশন মানে হলো সার্ভার তার মেমরি বা ডেটাবেসে লগইন সেশন সেভ করে রাখে না। এটি করার জন্য আমি JSON Web Tokens (JWT) ব্যবহার করি। লগইনের পর ক্লায়েন্ট একটি এনক্রিপ্টেড টোকেন পায় এবং পরের রিকোয়েস্টগুলোতে Authorization: Bearer <token> হেডারের মাধ্যমে সেটি সার্ভারে পাঠায়। আর্কিটেকচারালি, আমি একটি ডেডিকেটেড Express মিডলওয়্যার বানাই যা টোকেনটি রিসিভ করে, সিগনেচার ভেরিফাই করে এবং ডিকোড করা ইউজারের ডেটা req.user অবজেক্টে সেট করে মূল কন্ট্রোলারে পাঠিয়ে দেয়।
বিস্তারিত উত্তর:
মাইক্রোসার্ভিস বা স্কেলেবল Node আর্কিটেকচারে JWT সবচেয়ে ভালো কাজ করে, কারণ ইউজারের পরিচয় নিশ্চিত করার জন্য সার্ভারকে বারবার ডেটাবেসে কুয়েরি করতে হয় না।
- ফ্লো: ক্লায়েন্ট টোকেন পাঠালে মিডলওয়্যার তা ধরে ফেলে। টোকেন না থাকলে, এক্সপায়ার হয়ে গেলে বা কেউ টোকেন টেম্পার (tamper) করলে, মিডলওয়্যার সাথে সাথে
401 Unauthorizedবা403 Forbiddenরেসপন্স পাঠিয়ে দেয়। রিকোয়েস্ট মূল কন্ট্রোলার পর্যন্ত পৌঁছাতেই পারে না।
JavaScript
// বাংলা কোড এক্সাম্পল: JWT অথেনটিকেশন মিডলওয়্যার
const jwt = require('jsonwebtoken');const protectRoute = (req, res, next) => {
// ১. হেডার থেকে টোকেন বের করা
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: "কোনো টোকেন দেওয়া হয়নি" });
} const token = authHeader.split(' ')[1]; try {
// ২. সিগনেচার ভেরিফাই করা
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// ৩. ইউজারের ডেটা রিকোয়েস্ট অবজেক্টে যুক্ত করে দেওয়া
req.user = decoded;
next();
} catch (err) {
// এক্সপায়ার হওয়া বা টেম্পার করা টোকেনগুলো এখানে ধরা পড়বে
return res.status(403).json({ error: "ইনভ্যালিড বা এক্সপায়ার টোকেন" });
}
};// ব্যবহার
app.get('/api/profile', protectRoute, (req, res) => {
res.json({ message: "সিকিউর ডেটা", userId: req.user.id });
});
(এই হলো Phase 2। আপনি চাইলে আমি Phase 3 (শেষ ১০টি প্রশ্ন) প্রস্তুত করে দিতে পারি, যেখানে ডেটাবেস অপ্টিমাইজেশন, ডিজাইন প্যাটার্ন এবং আরও অ্যাডভান্সড টপিক থাকবে!)
Here is the final Phase 3 (Q21 to Q30) for Node.js and Express.js. This phase covers the most critical enterprise-level topics: Database architecture, Caching, Real-time WebSockets, Message Queues, Security tokens, and Production deployment.
As a senior developer, these are the concepts that differentiate a basic CRUD developer from a system architect.
Phase 3: Advanced Architecture, Databases, Real-time & Production Deployment
Question 21: How do you implement a “Graceful Shutdown” in a Node.js Express application, and why is it critical for production?
Short Answer:
A Graceful Shutdown ensures that when a server receives a kill signal (like SIGTERM from Docker/Kubernetes), it stops accepting new requests but finishes processing existing ongoing requests, securely closes database connections, and then exits. Architecturally, failing to do this results in abruptly dropped user requests and potentially corrupted database transactions.
Detailed Answer:
Junior developers often just let pm2 restart or Kubernetes kill the Node process instantly. This is dangerous.
- Intercepting Signals: We listen for
SIGINT(Ctrl+C) orSIGTERM(System kill). - Closing the Server: We call
server.close(). This stops the Express server from accepting any new HTTP connections. - Closing Databases: Once the HTTP server is closed, we safely disconnect from MongoDB, PostgreSQL, or Redis.
- Exiting: Finally, we call
process.exit(0)to tell the OS the shutdown was successful.
JavaScript
// English Code Example: Graceful Shutdown
const server = app.listen(3000);process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
// 1. Stop accepting new requests
server.close(() => {
console.log('HTTP server closed');
// 2. Close database connections securely
mongoose.connection.close(false, () => {
console.log('MongoDb connection closed');
// 3. Exit process cleanly
process.exit(0);
});
});
});প্রশ্ন ২১: Node.js Express অ্যাপ্লিকেশনে আপনি “Graceful Shutdown” কীভাবে ইমপ্লিমেন্ট করেন এবং প্রোডাকশনে এটি কেন এত গুরুত্বপূর্ণ?
সংক্ষিপ্ত উত্তর:
Graceful Shutdown নিশ্চিত করে যে সার্ভার যখন বন্ধ হওয়ার সিগন্যাল (যেমন Docker/Kubernetes থেকে SIGTERM) পায়, তখন সে নতুন রিকোয়েস্ট নেওয়া বন্ধ করে দেয়, কিন্তু চলমান রিকোয়েস্টগুলোর কাজ শেষ করে, ডেটাবেস কানেকশন নিরাপদে ক্লোজ করে এবং তারপর সার্ভার বন্ধ হয়। এটি না করলে চলমান ইউজার রিকোয়েস্টগুলো হুট করে কেটে যায় এবং ডেটাবেস ট্রানজ্যাকশন করাপ্ট (corrupt) হতে পারে।
বিস্তারিত উত্তর:
১. সিগন্যাল ধরা: আমরা SIGINT বা SIGTERM সিগন্যাল লিসেন করি।
২. সার্ভার ক্লোজ করা: server.close() কল করলে Express নতুন কোনো রিকোয়েস্ট রিসিভ করা বন্ধ করে দেয়।
৩. ডেটাবেস ক্লোজ করা: HTTP সার্ভার ক্লোজ হওয়ার পর আমরা ডেটাবেস (MongoDB/Postgres) থেকে ডিসকানেক্ট করি।
৪. এক্সিট (Exit): সবশেষে process.exit(0) কল করে সিস্টেমকে জানাই যে সার্ভার সফলভাবে বন্ধ হয়েছে।
JavaScript
// বাংলা কোড এক্সাম্পল: Graceful Shutdown
const server = app.listen(3000);process.on('SIGTERM', () => {
console.log('সার্ভার বন্ধ করার সিগন্যাল পাওয়া গেছে');
// ১. নতুন রিকোয়েস্ট নেওয়া বন্ধ করা
server.close(() => {
console.log('HTTP সার্ভার ক্লোজ হয়েছে');
// ২. ডেটাবেস কানেকশন নিরাপদে বন্ধ করা
mongoose.connection.close(false, () => {
console.log('MongoDB কানেকশন ক্লোজ হয়েছে');
// ৩. প্রসেস ক্লিনভাবে এক্সিট করা
process.exit(0);
});
});
});Question 22: Access Tokens vs. Refresh Tokens: How do you architect a secure JWT authentication system in Node.js?
Short Answer:
I architect JWT authentication using a dual-token system. The Access Token is short-lived (e.g., 15 minutes) and sent in the Authorization header by the client. The Refresh Token is long-lived (e.g., 7 days) and is stored exclusively in a secure, httpOnly cookie. This prevents Cross-Site Scripting (XSS) attacks from stealing the long-lived token while maintaining seamless user sessions.
Detailed Answer:
Storing a JWT in localStorage is a massive security vulnerability. If an attacker injects a malicious script (XSS), they can read localStorage and steal the token forever.
- The Flow: When the short-lived Access Token expires, the client makes a request to the
/api/refreshendpoint. Because the Refresh Token is in anhttpOnlycookie, the browser sends it automatically. The Express server verifies the Refresh Token against the database, generates a new Access Token, and sends it back to the client. If a user is compromised, we simply delete their Refresh Token from the database, forcing a logout.
প্রশ্ন ২২: Access Tokens বনাম Refresh Tokens: Node.js-এ আপনি একটি সুরক্ষিত JWT অথেনটিকেশন সিস্টেম কীভাবে আর্কিটেক্ট করেন?
সংক্ষিপ্ত উত্তর:
আমি ডুয়াল-টোকেন (dual-token) সিস্টেম ব্যবহার করি। Access Token এর মেয়াদ থাকে খুব কম (যেমন ১৫ মিনিট), যা ক্লায়েন্ট Authorization হেডারে পাঠায়। আর Refresh Token এর মেয়াদ থাকে বেশি (যেমন ৭ দিন), যা শুধুমাত্র একটি সুরক্ষিত httpOnly কুকিতে সেভ করা থাকে। এর ফলে হ্যাকাররা XSS অ্যাটাক করে দীর্ঘমেয়াদী টোকেনটি চুরি করতে পারে না এবং ইউজারের সেশনও নিরবচ্ছিন্ন থাকে।
বিস্তারিত উত্তর:
localStorage-এ টোকেন সেভ করা মারাত্মক অনিরাপদ। হ্যাকাররা ম্যালিশিয়াস স্ক্রিপ্ট চালিয়ে খুব সহজেই সেটি চুরি করতে পারে।
- ফ্লো: যখন ১৫ মিনিটের Access Token এক্সপায়ার হয়, তখন ক্লায়েন্ট
/api/refreshরাউটে একটি রিকোয়েস্ট পাঠায়। যেহেতু Refresh TokenhttpOnlyকুকিতে থাকে, তাই ব্রাউজার নিজে থেকেই সেটি সার্ভারে পাঠিয়ে দেয়। সার্ভার কুকি চেক করে নতুন একটি Access Token ক্লায়েন্টকে দেয়। কোনো ইউজারের অ্যাকাউন্ট হ্যাক হলে আমরা শুধু ডেটাবেস থেকে তার Refresh Token ডিলিট করে দিই, ফলে সে স্বয়ংক্রিয়ভাবে লগআউট হয়ে যায়।
Question 23: How do you implement Redis Caching in an Express application to optimize database performance?
Short Answer:
I implement Redis using the Cache-Aside pattern. When a user requests data (e.g., a product catalog), the Express controller first checks Redis. If the data exists (Cache Hit), it returns it instantly in milliseconds. If it doesn’t exist (Cache Miss), the controller queries the primary database, stores the result in Redis with an Expiration Time (TTL), and then returns it to the user.
Detailed Answer:
Querying a PostgreSQL or MongoDB database for heavy, frequently accessed data (like homepage products) kills scalability. Redis stores data in RAM, making it incredibly fast.
- Invalidation Strategy: The hardest part of caching is invalidation (keeping data fresh). If an admin updates a product price in the CMS, the Express
PUT /product/:idroute must explicitly delete the old cache key (redis.del('products_list')) so the next user fetches the fresh data from the DB.
JavaScript
// English Code Example: Redis Cache-Aside Pattern
app.get('/api/products', async (req, res) => {
// 1. Check Redis Cache
const cachedProducts = await redis.get('all_products');
if (cachedProducts) {
return res.json(JSON.parse(cachedProducts)); // Extremely fast response
}// 2. Cache Miss: Query Database
const products = await Database.getProducts(); // 3. Store in Redis with TTL (e.g., 3600 seconds / 1 hour)
await redis.setex('all_products', 3600, JSON.stringify(products)); res.json(products);
});
প্রশ্ন ২৩: ডেটাবেস পারফরম্যান্স অপ্টিমাইজ করার জন্য আপনি Express অ্যাপে Redis Caching কীভাবে ইমপ্লিমেন্ট করেন?
সংক্ষিপ্ত উত্তর:
আমি Cache-Aside প্যাটার্ন ব্যবহার করে Redis ইমপ্লিমেন্ট করি। ইউজার ডেটা (যেমন প্রোডাক্ট লিস্ট) রিকোয়েস্ট করলে, Express কন্ট্রোলার প্রথমে Redis চেক করে। ডেটা থাকলে (Cache Hit) তা মিলি-সেকেন্ডে রিটার্ন করে। ডেটা না থাকলে (Cache Miss) কন্ট্রোলার মূল ডেটাবেস থেকে ডেটা আনে, একটি নির্দিষ্ট এক্সপায়ার টাইম (TTL) সহ সেটি Redis-এ সেভ করে এবং তারপর ইউজারকে ডেটা পাঠায়।
বিস্তারিত উত্তর:
বারবার রিকোয়েস্ট আসা ভারী ডেটার জন্য ডেটাবেসে কুয়েরি করলে সার্ভার স্লো হয়ে যায়। Redis ডেটাকে RAM-এ সেভ রাখে বলে এটি অত্যন্ত ফাস্ট।
- ইনভ্যালিডেশন স্ট্র্যাটেজি (Invalidation): ক্যাশিংয়ের সবচেয়ে কঠিন কাজ হলো ডেটা ফ্রেশ রাখা। অ্যাডমিন যদি কোনো প্রোডাক্টের দাম আপডেট করে, তবে
PUT /product/:idরাউট থেকে আমাদের অবশ্যই পুরনো ক্যাশ ডিলিট (redis.del('products_list')) করে দিতে হবে, যাতে পরের ইউজাররা আপডেট ডেটা পায়।
JavaScript
// বাংলা কোড এক্সাম্পল: Redis Cache-Aside প্যাটার্ন
app.get('/api/products', async (req, res) => {
// ১. প্রথমে Redis ক্যাশ চেক করা
const cachedProducts = await redis.get('all_products');
if (cachedProducts) {
return res.json(JSON.parse(cachedProducts)); // ক্যাশ থেকে ফাস্ট রেসপন্স
}// ২. ক্যাশে না থাকলে ডেটাবেস থেকে ডেটা আনা
const products = await Database.getProducts(); // ৩. Redis-এ ডেটা সেভ করা (TTL: ৩৬০০ সেকেন্ড বা ১ ঘণ্টার জন্য)
await redis.setex('all_products', 3600, JSON.stringify(products)); res.json(products);
});
Question 24: How do you handle heavy background tasks (like sending 10,000 emails or video processing) in Node.js without blocking the Event Loop?
Short Answer:
I never execute long-running tasks inside an Express HTTP controller because it blocks the Event Loop, causing API timeouts for other users. Architecturally, I use a Message Queue (like RabbitMQ or BullMQ with Redis). The Express controller simply pushes a job payload to the queue and instantly returns a 202 Accepted response. A separate Node.js worker process consumes the queue and performs the heavy processing in the background.
Detailed Answer:
This is the standard enterprise architecture for decoupling web servers from worker servers.
- The Web Server: Handles API requests quickly. If a user uploads a video, the server uploads it to S3, pushes a job like
{ videoId: 123, action: 'compress' }to BullMQ, and tells the user "Video is processing". - The Worker Server: A completely separate Node.js instance listening to BullMQ. It picks up the job, runs FFmpeg, and updates the database upon completion. If the worker crashes, the queue automatically retries the job.
প্রশ্ন ২৪: Node.js-এর ইভেন্ট লুপ ব্লক না করে ভারী ব্যাকগ্রাউন্ড টাস্ক (যেমন ১০,০০০ ইমেইল পাঠানো বা ভিডিও প্রসেসিং) কীভাবে হ্যান্ডল করবেন?
সংক্ষিপ্ত উত্তর:
আমি কখনোই Express কন্ট্রোলারের ভেতরে দীর্ঘ মেয়াদী কাজ রান করি না, কারণ এটি ইভেন্ট লুপ ব্লক করে দেয় এবং অন্য ইউজারদের API টাইমআউট হয়ে যায়। আর্কিটেকচারালি, আমি Message Queue (যেমন RabbitMQ বা BullMQ) ব্যবহার করি। Express কন্ট্রোলার শুধু কাজটির ডেটা কিউ-তে (queue) পুশ করে সাথে সাথে ইউজারকে 202 Accepted রেসপন্স পাঠিয়ে দেয়। অন্য একটি আলাদা Node.js ওয়ার্কার প্রসেস ব্যাকগ্রাউন্ডে সেই কাজগুলো সম্পন্ন করে।
বিস্তারিত উত্তর:
ওয়েব সার্ভার এবং ওয়ার্কার সার্ভার আলাদা করার এটিই স্ট্যান্ডার্ড এন্টারপ্রাইজ আর্কিটেকচার।
- ওয়েব সার্ভার: এটি শুধু ফাস্ট API হ্যান্ডল করে। কেউ ভিডিও আপলোড করলে সার্ভার সেটি S3 তে রেখে BullMQ-তে একটি মেসেজ পাঠায় (যেমন
{ videoId: 123 }) এবং ইউজারকে বলে দেয় "ভিডিও প্রসেস হচ্ছে"। - ওয়ার্কার সার্ভার: এটি সম্পূর্ণ আলাদা একটি Node সার্ভার। সে কিউ থেকে মেসেজ নেয়, ভিডিও প্রসেস করে এবং ডেটাবেস আপডেট করে। ওয়ার্কার ক্র্যাশ করলেও কিউ (queue) কাজটি হারায় না, পুনরায় চেষ্টা (retry) করে।
Question 25: Explain the architecture of WebSockets (Socket.io) in Node.js. How does it differ from standard HTTP?
Short Answer:
HTTP is a stateless, unidirectional protocol where the client must explicitly request data from the server. WebSockets provide a stateful, persistent, full-duplex (bidirectional) TCP connection between the client and server. Once the initial HTTP handshake is upgraded to a WebSocket connection, the Node.js server can push real-time data (like chat messages or live stock updates) directly to the client at any time without the client constantly polling the API.
Detailed Answer:
In the past, developers used “Short Polling” (making an HTTP request every 3 seconds to check for new messages), which destroyed server performance.
- Socket.io Architecture: Socket.io is a library that wraps raw WebSockets. It provides built-in fallback mechanisms (e.g., falling back to HTTP long-polling if corporate firewalls block WebSockets), automatic reconnections, and the concept of “Rooms” and “Namespaces” to broadcast messages selectively to specific groups of users (e.g., users in a specific chat group).
প্রশ্ন ২৫: Node.js-এ WebSockets (Socket.io) এর আর্কিটেকচার বুঝিয়ে বলুন। সাধারণ HTTP থেকে এটি কীভাবে আলাদা?
সংক্ষিপ্ত উত্তর:
HTTP হলো একটি স্টেটলেস (stateless) এবং একমুখী প্রোটোকল, যেখানে ডেটা পাওয়ার জন্য ক্লায়েন্টকে বারবার সার্ভারে রিকোয়েস্ট পাঠাতে হয়। অন্যদিকে, WebSockets ক্লায়েন্ট এবং সার্ভারের মধ্যে একটি স্থায়ী ও দ্বিমুখী (bidirectional) TCP কানেকশন তৈরি করে। একবার কানেকশন তৈরি হয়ে গেলে, ক্লায়েন্ট রিকোয়েস্ট না করলেও Node.js সার্ভার যেকোনো সময় সরাসরি রিয়েল-টাইম ডেটা (যেমন- লাইভ চ্যাট বা নোটিফিকেশন) ক্লায়েন্টের কাছে পুশ করতে পারে।
বিস্তারিত উত্তর:
আগেকার দিনে ডেভেলপাররা “শর্ট পোলিং” ব্যবহার করত (অর্থাৎ নতুন মেসেজ আছে কিনা তা চেক করতে প্রতি ৩ সেকেন্ড পরপর HTTP রিকোয়েস্ট পাঠাত), যা সার্ভারের পারফরম্যান্স নষ্ট করে দিত।
- Socket.io আর্কিটেকচার: Socket.io র-ওয়েবসকেটের (raw WebSockets) ওপর তৈরি একটি লাইব্রেরি। ফায়ারওয়ালের কারণে ওয়েবসকেট ব্লক থাকলে এটি নিজে থেকেই HTTP লং-পোলিংয়ে শিফট করে। তাছাড়া এর “Rooms” ফিচারের মাধ্যমে সার্ভার নির্দিষ্ট কোনো ইউজার গ্রুপে (যেমন একটি নির্দিষ্ট চ্যাট রুম) মেসেজ ব্রডকাস্ট করতে পারে।
Question 26: What is Database Connection Pooling in Node.js, and why is it mandatory for scalability?
Short Answer:
Connection Pooling is a technique where a Node.js application maintains a cache of active, reusable database connections (a “pool”) rather than opening and closing a brand new TCP connection for every single incoming HTTP request. Architecturally, it is mandatory because establishing a new database connection (handshake, authentication) is highly CPU and time-intensive; pooling eliminates this overhead and prevents database exhaustion.
Detailed Answer:
If an Express API handles 500 requests per second, executing db.connect() 500 times per second will instantly crash the database server.
- How it works: When the Express app boots up, the database driver (like
pgfor Postgres) opens 10-20 connections and keeps them alive in the background. When a controller needs to query the DB, it borrows an open connection from the pool, runs the query, and instantly returns the connection to the pool for the next request to use.
প্রশ্ন ২৬: Node.js-এ ডেটাবেস কানেকশন পোলিং (Connection Pooling) কী এবং স্কেলেবিলিটির জন্য এটি কেন বাধ্যতামূলক?
সংক্ষিপ্ত উত্তর:
কানেকশন পোলিং হলো এমন একটি মেকানিজম যেখানে Node.js অ্যাপ্লিকেশন প্রতিবার নতুন রিকোয়েস্ট আসার পর নতুন করে ডেটাবেস কানেকশন তৈরি না করে, আগে থেকেই তৈরি করে রাখা বেশ কিছু অ্যাকটিভ কানেকশনের একটি “পুল (pool)” বা সংগ্রহ ব্যবহার করে। ডেটাবেসের সাথে নতুন কানেকশন তৈরি করা অত্যন্ত সময়সাপেক্ষ এবং প্রসেসরের ওপর চাপ ফেলে; পোলিং এই বারবার কানেকশন তৈরির ঝামেলা দূর করে এবং সার্ভার ক্র্যাশ হওয়া থেকে বাঁচায়।
বিস্তারিত উত্তর:
আপনার Express API-তে যদি প্রতি সেকেন্ডে ৫০০ রিকোয়েস্ট আসে, তবে সেকেন্ডে ৫০০ বার db.connect() রান করলে ডেটাবেস সার্ভার সাথে সাথে ডাউন হয়ে যাবে।
- যেভাবে কাজ করে: Express অ্যাপ চালু হওয়ার সময় ডেটাবেস ড্রাইভার ১০-২০টি কানেকশন তৈরি করে ব্যাকগ্রাউন্ডে চালু রাখে। যখন কোনো কন্ট্রোলারের ডেটাবেস কুয়েরি করার দরকার হয়, সে পুল থেকে একটি অ্যাকটিভ কানেকশন ধার নেয়, কাজ শেষ করে সেটি আবার পুলে ফেরত দিয়ে দেয়, যাতে অন্য রিকোয়েস্ট সেটি ব্যবহার করতে পারে।
Question 27: Prisma vs Mongoose: As an architect, how do you choose between an SQL ORM and a NoSQL ODM for a new Node.js project?
Short Answer:
The choice dictates the entire data architecture. I choose Prisma (SQL) when the application requires strict data integrity, complex relational transactions (e.g., Fintech, E-commerce, Inventory), and heavily benefits from auto-generated TypeScript type-safety. I choose Mongoose (MongoDB) when the data structure is highly dynamic, schema-less, rapidly changing, or when dealing with massive document-based data (e.g., IoT logs, Content Management Systems).
Detailed Answer:
- Prisma (ORM): It reads your
schema.prismafile and generates a fully type-safe query client. If you rename a database column, TypeScript will throw compilation errors everywhere you used the old name. It prevents runtime bugs. - Mongoose (ODM): While Mongoose adds schemas to MongoDB, it remains fundamentally flexible. It’s excellent for rapid prototyping or applications where an “Item” might have completely different attributes depending on its category.
প্রশ্ন ২৭: Prisma বনাম Mongoose: একজন আর্কিটেক্ট হিসেবে নতুন প্রজেক্টের জন্য SQL ORM এবং NoSQL ODM এর মধ্যে আপনি কীভাবে সিদ্ধান্ত নেন?
সংক্ষিপ্ত উত্তর:
এই সিদ্ধান্তটি পুরো অ্যাপের আর্কিটেকচার নির্ধারণ করে। আমি Prisma (SQL) বেছে নিই যখন অ্যাপ্লিকেশনে ডেটার সিকিউরিটি ও সম্পর্ক খুব জরুরি হয় (যেমন- ফিনটেক, ই-কমার্স বা ইনভেন্টরি) এবং টাইপস্ক্রিপ্টের (TypeScript) কঠোর টাইপ-সেফটির প্রয়োজন হয়। আর আমি Mongoose (MongoDB) বেছে নিই যখন ডেটার স্ট্রাকচার প্রতিনিয়ত পরিবর্তনশীল হয়, স্কিমা-লেস ফ্লেক্সিবিলিটি লাগে, বা বিশাল সাইজের ডকুমেন্ট ডেটা (যেমন IoT লগ বা CMS) নিয়ে কাজ করতে হয়।
বিস্তারিত উত্তর:
- Prisma (ORM): এটি
schema.prismaফাইল থেকে নিজে নিজেই সম্পূর্ণ টাইপ-সেফ কোড তৈরি করে। আপনি ডেটাবেসের কলামের নাম বদলালে টাইপস্ক্রিপ্ট সাথে সাথে কোডে এরর ধরে ফেলবে, ফলে প্রোডাকশনে বাগ যায় না। - Mongoose (ODM): Mongoose মঙ্গোডিবিতে স্কিমা যুক্ত করলেও এটি মূলত ফ্লেক্সিবল। যেখানে ডেটার আকার নির্দিষ্ট নয় (যেমন একেক ক্যাটাগরির প্রোডাক্টের স্পেসিফিকেশন একেক রকম), সেখানে এটি দারুণ কাজ করে।
Question 28: How and why do you implement API Versioning in an Express.js application?
Short Answer:
API Versioning (e.g., /api/v1/users vs /api/v2/users) is critical to ensure backward compatibility. If you introduce breaking changes to your database or response structure, mobile apps that haven't been updated by users will crash. Architecturally, I implement versioning using URL routing (mounting separate Express routers for v1 and v2) to allow legacy clients to function uninterrupted while new clients consume the updated endpoints.
Detailed Answer:
You can version via URL path, Query Params (?v=2), or Custom Headers (Accept-Version: v2).
- URL Routing Strategy: This is the most explicit and caching-friendly method. I create separate folder structures for controllers (
controllers/v1/andcontrollers/v2/). In the mainapp.js, I mount them usingapp.use('/api/v1', routerV1)andapp.use('/api/v2', routerV2).
প্রশ্ন ২৮: Express.js অ্যাপ্লিকেশনে আপনি API Versioning কীভাবে এবং কেন ইমপ্লিমেন্ট করেন?
সংক্ষিপ্ত উত্তর:
পুরনো ক্লায়েন্টদের সাপোর্ট (backward compatibility) দেওয়ার জন্য API Versioning (যেমন /api/v1/users বনাম /api/v2/users) অত্যন্ত জরুরি। আপনি যদি API এর রেসপন্স স্ট্রাকচারে বড় কোনো পরিবর্তন আনেন, তবে যে ইউজাররা মোবাইল অ্যাপ আপডেট করেনি তাদের অ্যাপ ক্র্যাশ করবে। আর্কিটেকচারালি, আমি URL রাউটিংয়ের মাধ্যমে v1 এবং v2 এর জন্য আলাদা Express রাউটার ব্যবহার করি, যাতে পুরনো ইউজারদের কোনো সমস্যা না হয় এবং নতুন ক্লায়েন্টরা আপডেট API ব্যবহার করতে পারে।
বিস্তারিত উত্তর:
ভার্সনিং সাধারণত URL পাথ, কুয়েরি প্যারামিটার বা হেডারের মাধ্যমে করা যায়।
- URL রাউটিং স্ট্র্যাটেজি: এটি সবচেয়ে পরিষ্কার পদ্ধতি। আমি
v1এবংv2এর জন্য আলাদা কন্ট্রোলার ফোল্ডার বানাই এবংapp.js-এapp.use('/api/v1', routerV1)ওapp.use('/api/v2', routerV2)ব্যবহার করে রাউটগুলো যুক্ত করি।
Question 29: How do you architect Automated Testing (Integration Tests) for an Express API using Jest and Supertest?
Short Answer:
Integration testing validates that the Express routing, middlewares, and database controllers work together correctly. Architecturally, I use Jest as the test runner and Supertest to simulate HTTP requests against the Express app without actually binding it to a network port. This allows the CI/CD pipeline to execute hundreds of API tests rapidly in memory before deploying to production.
Detailed Answer:
I separate app.js (which exports the Express instance) from server.js (which calls app.listen()).
- Supertest Flow: In the test file, I pass the un-started
appinstance to Supertest. Supertest creates an ephemeral in-memory server, makes a fake HTTP GET/POST request, and asserts the response status codes and JSON payloads. I also connect to an in-memory database (like MongoDB Memory Server) to ensure tests don't pollute the real development database.
JavaScript
// English Code Example: Testing with Supertest
const request = require('supertest');
const app = require('./app'); // Import app, NOT serverdescribe('POST /api/users', () => {
it('should create a new user and return 201', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'test@test.com', password: 'password123' });
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('id');
});
});প্রশ্ন ২৯: Jest এবং Supertest ব্যবহার করে Express API এর জন্য Automated Testing (Integration Tests) কীভাবে আর্কিটেক্ট করবেন?
সংক্ষিপ্ত উত্তর:
ইন্টিগ্রেশন টেস্টিং নিশ্চিত করে যে Express রাউটিং, মিডলওয়্যার এবং ডেটাবেস একসাথে ঠিকঠাক কাজ করছে। আর্কিটেকচারালি, আমি টেস্ট রান করার জন্য Jest এবং কোনো নেটওয়ার্ক পোর্টে সার্ভার চালু না করেই ফেক HTTP রিকোয়েস্ট পাঠানোর জন্য Supertest ব্যবহার করি। এর ফলে প্রোডাকশনে কোড পুশ করার আগেই CI/CD পাইপলাইন মেমরিতে খুব দ্রুত শত শত API টেস্ট করে নিতে পারে।
বিস্তারিত উত্তর:
এর জন্য আমি app.js (যেখানে Express অ্যাপ থাকে) এবং server.js (যেখানে app.listen() থাকে) আলাদা ফাইলে রাখি।
- Supertest ফ্লো: টেস্ট ফাইলে আমি স্টার্ট না করা
appটি Supertest কে দিই। সে মেমরিতে একটি ফেক রিকোয়েস্ট পাঠায় এবং স্ট্যাটাস কোড বা JSON রেসপন্স ঠিক আছে কিনা তা চেক করে (assert)।
JavaScript
// বাংলা কোড এক্সাম্পল: Supertest দিয়ে টেস্টিং
const request = require('supertest');
const app = require('./app'); // শুধু app ইমপোর্ট করা, server নয়describe('POST /api/users', () => {
it('নতুন ইউজার তৈরি করে 201 রিটার্ন করবে', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'test@test.com', password: 'password123' });
expect(res.statusCode).toEqual(201); // 201 স্ট্যাটাস কোড চেক
expect(res.body).toHaveProperty('id'); // রেসপন্সে id আছে কিনা চেক
});
});Question 30: What is PM2, and why is it considered the industry standard for deploying Node.js applications on Virtual Machines?
Short Answer:
PM2 is an advanced, production-grade Process Manager for Node.js. Architecturally, it provides three critical enterprise features: Auto-Restart (if the Node app crashes, PM2 instantly restarts it), Cluster Mode (it utilizes all CPU cores by spawning multiple instances of the app under a built-in load balancer), and Zero-Downtime Reloads (allowing you to deploy new code without dropping active user connections).
Detailed Answer:
While Docker/Kubernetes are standard for containerized cloud environments, PM2 is heavily used on raw EC2 instances, Droplets, or VPS servers.
- Cluster Mode (
pm2 start app.js -i max): It automatically implements the Node.jsclustermodule under the hood. If your server has 8 cores, PM2 boots 8 instances of your app and balances traffic seamlessly. - Zero Downtime: When you run
pm2 reload app, it doesn't shut down the server immediately. It spins up the new code on a new worker, waits for it to be ready, routes traffic to it, and gently gracefully shuts down the old worker.
প্রশ্ন ৩০: PM2 কী এবং সাধারণ সার্ভারে (VM) Node.js অ্যাপ ডিপ্লয় করার জন্য এটিকে কেন ইন্ডাস্ট্রি স্ট্যান্ডার্ড মনে করা হয়?
সংক্ষিপ্ত উত্তর:
PM2 হলো Node.js এর জন্য একটি প্রোডাকশন-গ্রেড প্রসেস ম্যানেজার (Process Manager)। আর্কিটেকচারালি এটি ৩টি প্রধান সুবিধা দেয়: Auto-Restart (অ্যাপ ক্র্যাশ করলে সাথে সাথে রিস্টার্ট করে), Cluster Mode (বিল্ট-ইন লোড ব্যালেন্সার দিয়ে সার্ভারের সব CPU কোর ব্যবহার করে অ্যাপের একাধিক কপি রান করে), এবং Zero-Downtime Reloads (ইউজার কানেকশন না কেটেই নতুন কোড ডিপ্লয় বা আপডেট করতে সাহায্য করে)।
বিস্তারিত উত্তর:
কন্টেইনার বা ক্লাউডের জন্য Docker/Kubernetes স্ট্যান্ডার্ড হলেও, সাধারণ EC2 বা VPS সার্ভারে PM2 সবচেয়ে বেশি ব্যবহৃত হয়।
- ক্লাস্টার মোড (
pm2 start app.js -i max): এটি নিজে থেকেই Node.js-এরclusterমডিউল ইমপ্লিমেন্ট করে। সার্ভারে ৮টি কোর থাকলে, PM2 অ্যাপের ৮টি ইনস্ট্যান্স চালু করে ট্রাফিক ব্যালেন্স করে। - জিরো ডাউনটাইম:
pm2 reload appকমান্ড দিলে এটি পুরনো সার্ভার হুট করে বন্ধ করে দেয় না। এটি আগে নতুন কোডের ওয়ার্কার চালু করে, সেটিকে রেডি করে ট্রাফিক শিফট করে, তারপর পুরনো ওয়ার্কারটি বন্ধ করে দেয়। ফলে ইউজার টেরই পায় না যে সাইট আপডেট হয়েছে।




