Lecture 16 - [Backend 3] Understand Express Middleware

Lecture 16 - [Backend 3] Understand Express Middleware

Introduction

গত ক্লাসে আমরা একটা ব্যাকএন্ডের স্টেপের একটা পাইপলাইন বানিয়েছিলাম। যা ছিল এরকমঃ

REQUEST -> MIDDLEWARE[logger, body parser, file parser, user ip, block ip, authentication, authorization, validation] -> CONTROLLER (Business Logic) -> MIDDLEWARE[error handler] -> RESPONSE

প্রথমে আমরা রিকোয়েস্ট পাঠাচ্ছি। এরপর অনেক মিডলওয়্যার আমরা পার করছি। এরপর কন্ট্রোলার। এরপর আবার এরর হ্যান্ডলিং এর জন্য আরেকটা মিডলওয়্যার থাকলেও থাকতে পারে। সবশেষে রেসপন্স ব্যাক করা। এর মধ্যে আমরা রিকোয়েস্ট এবং রেসপন্সের কনসেপ্ট মোটামুটি ভালভাবে বুঝেছি। কিন্তু আমাদের এখনও সমস্যা আছে মিডলওয়্যার এবং কন্ট্রোলারে। যারা Express Js Crash Course In Bangla এবং Express 101 দেখেছেন তারা কিছুটা হয়তো বুঝেছেন। যারা এগুলো কমপ্লিট করেছেন তারা ধরতে গেলে ব্যাকএন্ডের ৭৫% শিখে ফেলেছেন। আর যদি ২৫% শিখে ফেলেন তাহলে দুনিয়াতে যতো অ্যাপ্লিকেশন আছে, তার ৯০% ব্যাকএন্ড আপনারা বানাতে পারবেন।

আমরা এক্সপ্রেস, লারাভেল, জ্যাংগো, স্প্রিংবুট, ফ্লাস্ক, নেস্ট, রুবি অন রেইলস যেটা দিয়েই ব্যাকএন্ডের কাজ করি না কেন সব জায়গায় আমাদের এই পাইপলাইনটাই ব্যবহার করতে হবে। সুতরাং আমরা যদি এই পাইপলাইনটা ভালভাবে শিখে ফেলতে পারি তাহলে দুনিয়ার যেকোনো ব্যাকএন্ড ফেমওয়ার্ক দিয়ে কাজ করতে পারবো। কারণ আমাদের কনসেপ্ট জানা থাকবে। বাকিটা ডকুমেন্টেশন দেখে শিখে ফেলতে পারবো। এক্সপ্রেস যদি শিখতে পারি তাহলে অন্য ফ্রেমওয়ার্কে কাজ আরো সহজ। সেখানে কিছু কাজ করে রাখা হয়েছে। যেমন নেস্টে রিকোয়েস্ট, রেসপন্স অটোমেটিক্যালি করা আছে। সেখানে শুধু কাজ করতে হবে কন্ট্রোলার নিয়ে। এক্সপ্রেসে সব আমরা নিজে থেকে করবো। যদি আমাদের সব শেখা থাকে তাহলে অন্য যেকোনো ফেমওয়ার্কে আমরা সহজেই সুইচ করতে পারবো।

ডাটাবেজের সাথে অ্যাপ্লিকেশনের কোনো সম্পর্ক নেই। আমরা যদি চাই আমাদের ভবিষ্যতে ব্যবহারের জন্য আমরা আমাদের ডাটাগুলোকে সংরক্ষণ করবো, সেক্ষেত্রে আমরা আমাদের অ্যাপ্লিকেশনে ডাটাবেজ কানেক্ট করবো। নাহয় আমাদের ডাটাবেজের প্রয়োজনই নেই। দুনিয়াতে এমন অনেক অ্যাপ্লিকেশন আছে যেখানে ডাটাবেজের প্রয়োজনই হয় না।

আজ আমাদের আলোচ্য বিষয়গুলো নিচে দেয়া হলো।

  • Middleware
  • Project Structure
  • Project

এই প্লেলিস্টে টেমপ্লেট ইঞ্জিন হিসেবে ejs ব্যবহার করা হয়েছিল। কিন্তু বর্তমানে প্রায় সব অ্যাপ্লিকেশন সিংগেল পেইজ হওয়ার কারণে আমাদের দরকার হয় রিয়্যাক্ট, ভিউ, অ্যাঙ্গুলার এর মতো ফ্রেমওয়ার্কগুলো। এখন আর ejs, handle bar, pug ইত্যাদি টেমপ্লেট ইঞ্জিনের দরকার হয় না। আমরা nodejs নিয়ে কাজ করছি মানে API বানাচ্ছি। তাই আমাদের মূল ফোকাস থাকবে API এর দিকে। আর ফ্রন্টএন্ডের জন্য তো আমাদের রিয়্যাক্ট, ভিউ এর মতো ফ্রেমওয়ার্ক আছেই।

এখন আসি প্রথমে আমাদের মিডলওয়্যার জিনিসটা কি সেটা নিয়ে।

  • What is Middleware?

এটা একটা ফাংশন। কি ধরণের ফাংশন? এটা একটা কন্ট্রোলার ফাংশন। আমরা গত ক্লাসে একটা ফাংশন লিখেছিলাম।

app.get('/books', (req, res) => {
    if (req.query.show === 'all') {
        return res.json(books);
    }

    if (req.query.price === '500') {
        const result = books.filter((book) => book.price <= 500);
        return res.json(result);
    }

    if (req.query.price === '1000') {
        const result = books.filter((book) => book.price <= 1000);
        return res.json(result);
    }

    return res.json(books);
});

এই ফাংশন আর মিডলওয়্যার ফাংশনের মধ্যে বেসিক কোনো পার্থক্য নেই। মিডলওয়্যার চাইলে যেকোনো জায়গা থেকে রেসপন্স পাঠিয়ে দিতে পারে।

Req -> M1 -> M2 -> M3 -> Res

এখানে M1 এর যে ক্ষমতা, M2 এরও একই ক্ষমতা, M3 এরও একই ক্ষমতা। আবার রেসপন্সেরও একই ক্ষমতা। যদি সবার ক্ষমতা এক হয় তাহলে এখানে ভিন্ন ভিন্ন মিডলওয়্যার কেন? ভিন্ন ভিন্ন হওয়ার কারণ তাদের রেসপন্সিবিলিটি। আমরা চাইলে এই তিনটি মিডলওয়্যার তৈরি না করে রেসপন্স থেকে এই কাজগুলো করে ফেলতে পারতাম। আমরা চাইলে প্রথম মিডলওয়্যার M1 দিয়েই রেসপন্স তৈরি করে ফেলতে পারতাম। তাহলে আমরা মিডলওয়্যার কেন বানাচ্ছি? মিডলওয়্যারের কনসেপ্ট এসেছে DRY (Don't repeat yourself) principle থেকে। আমাদের অনেক কাজ করতে হবে। প্রতিটা রিকোয়েস্টের জন্যই কাজগুলো সেইম। এখন তাহলে কি আমরা প্রতিটা রিকোয়েস্টের জন্য এই কোডগুলো বারবার লিখবো? নাকি একবার কোথাও লিখে সেটাকে বারবার যেখানে লাগবে সেখানে ব্যবহার করবো? অবশ্যই আমরা কোড রিইউজ করবো। এই রিইউজ কোডকে একটা ফাংশনের মধ্যে রাখা হয়। এই ফাংশনটাকেই বলে মিডলওয়্যার। আমি লিখবো একবার। কিন্তু চাইলে আমি সব রিকোয়েস্টে এটা ব্যবহার করবো, চাইলে কিছু কিছু রাউটে ব্যবহার করবো বাকিগুলোতে করবো না। বলতে গেলে কন্ট্রোল আমার হাতে। মিডলওয়্যার তাই এক ধরণের কন্ট্রোলার ফাংশন।

একটা মিডলওয়্যার বানাতে গেলে এর একটা সিগ্নেচার আছে। সেটা একটু আমরা আগে দেখি। এরপর বিশ্লেষণ করবো।

// We will never call it, express will automatically invoke for us.
// This is middleware
function xyz(req, res, next) {
    next();
}

// This is controller
function xyz(req, res, next) {
    res.send();
}

আমরা একটা ফাংশন নিলাম। এই ফাংশনটা আমরা কল করবো না। শুধু রেফারেন্স আকারে পাস করবো। আমাদের জন্য এই ফাংশনটাকে কল করবে এক্সপ্রেস। এখন এক্সপ্রেস এই ফাংশনটা কল করার সময় তিনটা প্যারামিটার দিবে। req, res, next. রিকোয়েস্ট আর রেসপন্স অবজেক্টের মধ্যে যতো মেথড আছে সব এক্সপ্রেস এখানের মধ্যে কল করতে পারে। next ফাংশন আমরা কল করে দিবো। যদি আমরা তা কল না করি তাহলে মিডলওয়্যার সমস্ত কাজ শেষে থেমে বসে থাকবে। অন্য কোথাও যেতে পারবে না। ধরেন উপরের পাইপলাইনে রিকোয়েস্ট পেলাম। এরপর সে দেখলো M1 আছে এরপর। সে তার সমস্ত কিছু M1 কে দিয়ে দিলো। এখন M1 রিকোয়েস্ট অবজেক্টকে প্রসেস করে দেখবে কি আছে এরপর। যদি next() লেখা দেখে তাহলে সে পরের মিডলওয়্যারের কাছে সব হ্যান্ডওভার করে দিবে। এখানে যদি আমরা next ফাংশন কল না করি তাহলে মিডলওয়্যার প্রসেসিং শেষে ওখানেই থেমে যাবে। কারণ আমরা বলে দিইনি তাকে কি করতে হবে এরপর। এখন এখানে একটা জিনিস বুঝার ব্যাপার আছে। আমরা বলছি মিডলওয়্যার একটা কন্ট্রোলার ফাংশন। কন্ট্রোলার ফাংশন কি? কন্ট্রোলার ফাংশন হলো সে সবশেষে রেসপন্স ব্যাক করে। সে আর কারো কাছে যাবে না। আর মিডলওয়্যার হলো যে সব শেষে next() ফাংশন কল করে অন্য কারো কাছে মডিফাইড ডাটা হ্যান্ডওভার করবে।

আমরা গত ক্লাসে server.js এ app.use(express.json()) লিখেছিলাম। express.json() এক ধরণের মিডলওয়্যার। মজার ব্যাপার হলো এক্সপ্রেসে যেভাবে রাউট লেখা হয় সেগুলোও এক ধরণের মিডলওয়্যার। সেগুলো আমরা পরে আলোচনা করবো।

এবার একটু আমরা কোডে যায়। আমরা server.js নামে একটা ফাইল তৈরি করবো। এরপর যেভাবে এক্সপ্রেস ইমপোর্ট করে অ্যাপ বানাতে হয় বানাবো।

const express = require('express');

const app = express();

এবার আমরা লিসেন করবো।

app.listen(8000, () => {
    console.log('Application running on port 8000');
});

listen ফাংশন সাধারণত ফাইলের শেষে লেখা হয়। এটা একটা কনভেনশন। তাই আমরাও এটাকে সবার শেষে রাখবো। আর অন্যান্য কাজ এটার উপর করবো।

এখন আমাদের কাজ কিভাবে মিডলওয়্যার বানাবো, কিভাবে রাউটার বানাবো সেগুলো নিয়ে। রাউটার বানানো অলরেডি আমরা শিখে ফেলেছি। আমরা প্রথমে রাউটার বানিয়ে ফেলি।

const express = require('express');

const app = express();

app.get('/hello', (req, res, next) => {
    res.json({ message: 'Hello' });
});

app.get('/', (req, res, next) => {
    res.json({ message: 'Sweet Home' });
});

app.listen(8000, () => {
    console.log('Application running on port 8000');
});

এবার যদি আমরা সার্ভার রান করে ব্রাউজারে গিয়ে '/' এ হিট করি তাহলে {message: 'Sweet Home'} দেখাবে। আর যদি '/hello' তে হিট করি তাহলে { message: 'Hello' } দেখাবে।

এখন প্রতিবার আমি হিট করলে ডাটা পাচ্ছি। কিন্তু আমি আমার কনসোলে সেটা বুঝতে পারছি না। আমি চাইছি প্রতিবার হিট করলে আমার কনসোলে তা লগ করবে। এটার জন্য আমরা আমাদের যে দুইটা রাউট আছে সেখানে আমরা console.log(${req.url} - ${req.method} - ${new Date().toISOString()}); এটা লিখে রাখতে পারি। req.url বলতে আমাদের রাউট, req.method মানে আমাদের রিকোয়েস্ট মেথড সেটা, এবং কোন সময় আমরা হিট করেছি সেটা আমরা আমাদের কনসোলে দেখতে চাই। এখন যদি আমরা ব্রাউজার '/' হিট করি তাহলে কনসোলে / - GET - 2022-06-26T07:43:17.067Z এরকম আউটপুট আসবে। আর যদি '/hello' হিট করি তাহলে /hello - GET - 2022-06-26T07:43:22.746Z এরকম আউটপুট আসবে।

এখন আমার তো ১০০০টা কন্ট্রোলার থাকতে পারে। আমি কি সবগুলোর জন্য এই লাইন লিখতেই থাকবো? মোটেই না। এখানেই আসবে আমাদের মিডলওয়্যার বানানোর উদ্দেশ্য। আমরা একটা মিডলওয়্যার বানিয়ে রাখবো। এরপর সব রাউট আমরা সেটা ইউজ করতে পারি। চলুন তাহলে আমরা একটা মিডলওয়্যার বানিয়ে ফেলি।

const express = require('express');

const app = express();

const simpleLogger = (req, res, next) => {
    console.log(`${req.url} - ${req.method} - ${new Date().toISOString()}`);
    next();
};

app.get('/hello', (req, res, next) => {
    res.json({ message: 'Hello' });
});

app.get('/', (req, res, next) => {
    res.json({ message: 'Sweet Home' });
});

app.listen(8000, () => {
    console.log('Application running on port 8000');
});

এবার আমরা এটাকে ব্যবহার করবো কিভাবে? দুইভাবে ব্যবহার করা যায়। গ্লোবালি এবং লোকালি। গ্লোবালি বলতে আমার যতো রিকোয়েস্ট আছে সবগুলোর জন্য আমি এটা ব্যবহার করবো, সেক্ষেত্রে আমরা গ্লোবালি ব্যবহার করবো। আর আমার সব রিকোয়েস্টে এটা লাগবে না, নির্দিষ্ট কিছু রিকোয়েস্টে লাগবে, সেক্ষেত্রে আমরা লোকালি ব্যবহার করবো। এখন ধরি আমরা শুধু আমার '/' রাউটের ক্ষেত্রে মিডলওয়্যার ব্যবহার করবো। তাহলে আমাদের লোকালি ব্যবহার করতে হবে।

const express = require('express');

const app = express();

const simpleLogger = (req, res, next) => {
    console.log(`${req.url} - ${req.method} - ${new Date().toISOString()}`);
    next();
};

app.get('/hello', (req, res, next) => {
    res.json({ message: 'Hello' });
});

app.get('/', simpleLogger, (req, res, next) => {
    res.json({ message: 'Sweet Home' });
});

app.listen(8000, () => {
    console.log('Application running on port 8000');
});

এখানে আমরা মিডলওয়্যারটা আমাদের কন্ট্রোলার ফাংশনের আগে বসাবো। আমরা কল করবো না, আমাদের হইয়ে এক্সপ্রেস কল করবে, আমরা শুধু আমাদের ফাংশনের রেফারেন্সটা পাঠিয়ে দিবো। আমরা চাইলে যত ইচ্ছা মিডলওয়্যার পাশাপাশি বসিয়ে যেতে পারি। কোনো সমস্যা নেই।

এবার আমরা চাইছি আমরা সব রাউটের জন্য এই মিডলওয়্যারটা ব্যবহার করবো। তাহলে কি সব রাউটের ভিতর আমার লিখতে হবে? কোনো প্রয়োজন নেই। আমরা গ্লোবালি সেটা একবারেই করে দিতে পারি জাস্ট রাউটের আগে app.use(simpleLogger); লিখে।

const express = require('express');

const app = express();

const simpleLogger = (req, res, next) => {
    console.log(`${req.url} - ${req.method} - ${new Date().toISOString()}`);
    next();
};

app.use(simpleLogger);

app.get('/hello', (req, res, next) => {
    res.json({ message: 'Hello' });
});

app.get('/', (req, res, next) => {
    res.json({ message: 'Sweet Home' });
});

app.listen(8000, () => {
    console.log('Application running on port 8000');
});

এবার সব রাউটের জন্যই এটা কাজ করবে। আপনারা চেক করে দেখতে পারবেন।

যদি আমরা দুইটা মিডলওয়্যার গ্লোবালি ব্যবহার করতে চাই তাহলেও পারবো। তখন মিডলওয়্যারের একটা অ্যারে আকারে রাখতে হবে। যেমনঃ

const express = require('express');

const app = express();

app.use(express.static(__dirname + '/public'));

const simpleLogger = (req, res, next) => {
    console.log(`${req.url} - ${req.method} - ${new Date().toISOString()}`);
    next();
};

const secondMiddleWare = (res, req, next) => {
    console.log('I am second middleware');
    next();
};

app.use([simpleLogger, secondMiddleWare]);

app.get('/hello', (req, res, next) => {
    res.json({ message: 'Hello' });
});

app.get('/', (req, res, next) => {
    res.json({ message: 'Sweet Home' });
});

app.listen(8000, () => {
    console.log('Application running on port 8000');
});

এখন যদি আমরা হিট করি আমরা কনসোলে দুইটা মিডলওয়্যারের আউটপুটই দেখতে পারবো।

এবার আমরা মিডলওয়্যারের ইউজ কেইসগুলো বুঝার চেষ্টা করি। তার জন্য আমরা এক্সপ্রেস মিডলওয়্যারের ডকুমেন্টেশন এ যেতে হবে। এখানে কিছু built-in মিডলওয়্যার আছে, এগুলো নিয়ে একটু আলোচনা করা যাক।

  • body-parser: এটা মূলত ক্লায়েন্ট থেকে সার্ভারে ডাটা পাঠাতে ব্যবহার করা হয়। সেটা হতে পারে ইমেজ, ফাইল বা একটা সিংগেল আর্গুমেন্ট।
  • compression: আমাদের রেসপন্সের সাইজ যখন অনেক বেশি হয়ে যাবে তখন টাইম অনেক বেশি লাগবে। আর এই রেসপন্সকে কমপ্রেস করে ছোট করার জন্য আমরা এই মিডলওয়্যার ব্যবহার করে থাকি।
  • connect-rid: আমরা যখন মাইক্রোসার্ভিস নিয়ে কাজ করি তখন একটা রিকোয়েস্ট মাল্টিপল সার্ভার ঘুরে বেড়ায়। যেমন আপনি চাইছেন আপনার একটা ইমেজ লাগবে। ক্লায়েন্ট থেকে অ্যাপ্লিকেশন রিকোয়েস্ট করলো যে আমার একটা ইমেজ লাগবে। এখন সেই ইমেজটা পাওয়ার জন্য প্রথমে আপনার অথোরাইজেশন সার্ভারে রিকোয়েস্টটা যাবে, অথেনটিকেশন চেক করবে, আপনার পারমিশন আছে কিনা চেক করবে, এরপর খুঁজে বের করবে এই ইমেজটা কোথায় রাখা আছে, তারপর সেখান থেকে যাবে কোনো স্টোরেজ সার্ভারে, সেখান থেকে রাউটটাকে কল করে ইমেজটাকে খুঁজে নিয়ে আসবে। তার মানে মাল্টিপল সার্ভারে কমিউনিকেশন হবে। কিন্তু মাল্টিপল সার্ভারে কমিউনিকেশন করলেও এটা মেইনলি একটা রিকোয়েস্টের জন্য কাজ করবে। কারণ আপনি ক্লায়েন্ট থেকে একটা রিকোয়েস্টই জেনারেট করেছেন। তো এই ক্ষেত্রে রিকোয়েস্ট আইডি অনেক কাজ দেয়। একটা রিকোয়েস্ট আইডি দিয়ে আমরা ভেরিফাই করতে পারি কোন রিকোয়েস্টের জন্য আমরা বাকি রিকোয়েস্টগুলো জেনারেট করেছি। ফিল্টার করার কাজে রিকোয়েস্ট আইডি লাগে। আর এই রিকোয়েস্ট আইডি জেনারেট করার জন্য আমরা এই মিডলওয়্যারটা ব্যবহার করি।
  • cookie-parser: যদি আমাদের কোনো রিকোয়েস্টের হেডারের মধ্যে কুকি পাওয়া যায়, সেটা স্ট্রিং আকারে থাকে। এই মিডলওয়্যার দিয়ে আমরা কুকি পার্স করে অবজেক্ট আকারে req.cookies এর মধ্যে রাখতে পারি।
  • cookie-session: যখন আমরা কুকি নিয়ে কাজ করি তখন আমাদের সেশন নিয়েও কাজ করতে হয়। সেই সেশন নিয়ে কাজ করার জন্যই আমাদের এই মিডলওয়্যার।
  • CORS (Cross Origin Resource Sharing): ব্রাউজারের একটা বিহেভিয়ার হলো যখন ক্লায়েন্ট এবং সার্ভারের ডোমেইন একই না হয় তখন ক্লায়েন্টের সমস্ত রিকোয়েস্ট সার্ভার ব্লক করে দেয়। কারণ দুইটা ভিন্ন সার্ভার হলে ক্রস সার্ভার হয়ে গেলো। তখন সার্ভার ধরে নেয় এখানে হ্যাকিং বা অন্য কোনো সমস্যা হতে যাচ্ছে। তাই সে রিকোয়েস্ট ব্লক করে দেয়। এই মিডলওয়্যার ব্যবহার করে আমরা নির্দিষ্ট করে দিতে পারি যে কোন কোন ডোমেইনকে আমরা আমাদের অ্যাপ্লিকেশনে অ্যালাউ করছি।
  • csrf: ধরেন আপনি একটা ফর্ম সাবমিট করছেন। এই ফর্মের ডাটাকে ম্যানিপুলেট করা যায়। এখন এই মিডলওয়্যার ইউজ করে যখন ফর্ম রেন্ডার করা হয় তখন সাবমিটেড ফর্মের আইডি আর রেন্ডারকৃত ফর্মের আইডি মিলিয়ে দেখা হয়। এটা মাল্টিপেইজ অ্যাপ্লিকেশনের ক্ষেত্রে করতে হয়, সিংগেল পেইজের অ্যাপ্লিকেশনের ক্ষেত্রে করতে হয় না।
  • errorHandler: আমরা সাধারণত ম্যানুয়েলি প্রতিটা রাউটের জন্য এরর হ্যান্ডলিং করে থাকি। তবে কিছু এরর আছে যেগুলো গ্লোবাল। ঐ গ্লোবাল এরর হ্যান্ডেল করার জন্য এই মিডলওয়্যার ব্যবহার করা হয়।
  • method-override: এই মিডলওয়্যার ব্যবহার করে আমরা এক মেথডকে অন্য মেথডে কনভার্ট করতে পারি। যেমন আমরা GET মেথডকে POST মেথডে কনভার্ট করে ফেলতে পারি।
  • morgan: আমরা যে লগার তৈরি করেছিলাম তার মধ্যে কোনো ফাংশনালিটিজই নেই। সেগুলো আমাদের তৈরি করারও কোনো প্রয়োজন নেই। আমরা সেগুলো এই মিডলওয়্যার ব্যবহার করে করতে পারবো।
  • multer: কোন ফর্ম থেকে ডাটা বের করে এনে কোনো একটা ফাইলে সেইভ করে রাখার কাজটা করে multer।
  • response-time: এই মিডলওয়্যার ব্যবহার করে আমরা একটা রেসপন্স জেনারেট হতে কতো সময় লাগে সেটা বের করতে পারি। রেস্পন্স টাইমের মাধ্যমে আমরা বুঝতে পারি আমাদের কোন কন্ট্রোলারকে আরো অপটিমাইজ করতে হবে। মূলত পারফরম্যান্স অপটিমাইজেশনের কাজ করার জন্য এই মিডলওয়্যার ব্যবহার করা হয়।
  • serve-favicon: আমরা যখন রিকোয়েস্ট বা রেসপন্স নিয়ে কাজ করি তখন স্বাভাবিকভাবেই favicon গুলো যায় না। যদি আলাদাভাবে favicon গুলো পাঠাতে হয় সেক্ষেত্রে আমরা এই মিডলওয়্যার ব্যবহার করবো।
  • serve-index: Index ফাইল সার্ভ করার জন্য।
  • serve-static: আমরা যদি কোনো ফাইল পাবলিকলি অ্যাভেইলেবল করতে চাই তাহলে এই মিডলওয়্যার ব্যবহার করতে পারি। ধরি আমাদের প্রজেক্ট ডিরেক্টরিতে আমরা public নামে একটা ডিরেক্টরি ক্রিয়েট করে এতে index.html ফাইল ক্রিয়েট করেছি।
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Hello NodeJS</title>
    </head>
    <body>
        <h1>I am from public directory</h1>
    </body>
</html>

আমি চাইছি ইউজার এই ফাইল দেখবে। কেউ এসে আমাদের এড্রেস লিখে /index.html লিখে এন্টার দিলেই এই ফাইলের মধ্যে যা আছে তা শো করবে। সেটা করার জন্য আমাদের server.js এ এটা পাবলিক করে দিতে হবে app.use(express.static(__dirname + '/public')); লিখে।

// Demo Code
const express = require('express');

const app = express();

app.use(express.static(__dirname + '/public'));

app.listen(8000, () => {
    console.log('Application running on port 8000');
});

এখানে যে আপনাকে public নামই দিতে হবে কথা নেই। আপনি যে নাম দিবেন সেই নামেই কনফিগার করবেন।

  • timeout: ধরেন আমার ইউজার আমাকে রিকোয়েস্ট দিচ্ছে, কিন্তু সেই ডাটাটা আছে আবার অন্য সার্ভারের কাছে, সে আমাকে রেসপন্স দিচ্ছে না। এই অবস্থায় আমি ১০ সেকেন্ড অপেক্ষা করার পর আমার ইউজারকে একটা ম্যাসেজ দিতে পারি যে ডাটাটা এখন পাওয়া যাচ্ছে না, আমরা বিষয়টি দেখছি। এটা এই timeout এর মাধ্যমে করা যায়।

এগুলো গেলো বিল্টইন মিডলওয়্যারের ইউজ কেইস। আরো কিছু ইউজ কেইস আছে রিয়েলটাইম। ধরেন আপনি একটা ফর্ম বানালেন। এখন ইউজার যা খুশি তা ইনপুট দিতে পারে। সে ছবি জায়গায় ভিডিও আপলোড করতে পারে, নামের জায়গায় বয়স লিখতে পারে, ইমেইলের জায়গায় নাম লিখতে পারে। এর উপর আমাদের কন্ট্রোল নেই। আমরা কি করতে পারি? আমরা কিছু Worst case ভেবে আমাদের ভ্যালিডেশন চেক করার জন্য মিডলওয়্যার তৈরি করতে পারি। যে এখানে যা দেয়ার তুমি তাইই দিবে, নাহয় আমি সেটা নিবো না। এটা ইউজারকে ম্যাসেজ দিতে হবে। আমি আমার কন্ট্রোলারে আসার আগেই সব চেক করে শুধু ঠিক জিনিসকেই আসতে দিবো। ভুলভাল কিছু আমার মেইন কন্ট্রোলারের কাছে আমি আসতে দিবো না। এটাই মিডলওয়্যারের কনসেপ্ট।

আবার ধরেন আমাদের অ্যাপ্লিকেশনে সেই লাইক, কমেন্ট করতে পারবে যে লগইন করবে। এখন ইউজার লগড ইন কিনা তা কন্ট্রোলারে আসার আগে চেক করবে মিডলওয়্যার। যদি লগডইন হয় তাহলে সে কন্ট্রোলারে যেতে পারবে, নাহয় না।

এবার পুরো ব্যাকএন্ড ডেভেলপমেন্টকে এক জায়গায় নিয়ে আসি। ইউজার একটা রিকোয়েস্ট দিবে। সেটা একটা মিডলওয়্যারের কাছে যাবে। সেখানে তা ভ্যালিড হলে পরবর্তী মিডলওয়্যারের কাছে যাবে। এভাবে করতে করতে যখন বুঝবে যে এটা একটা ভ্যালিড রিকোয়েস্ট তখনই তা মেইন কন্ট্রোলারের কাছে যাবে। নাহয় যেখানেই ভ্যালিডেশন ফেইল করবে সেখানেই রিকোয়েস্ট শেষ হয়ে যাবে। এটাই ব্যাকএন্ড ডেভেলপমেন্টের কন্সেপ্ট।

এক্সপ্রেসের কনসেপ্ট এখানেই শেষ। এক্সপ্রেসে আর বুঝার তেমন কিছু নেই। এখন যেটা বাকি আছে সেটা হলো প্রজেক্ট স্ট্রাকচার করা আর প্রজেক্ট সেটাপ করা।

Project Structure

একটা প্রজেক্টের ফাইল স্ট্রাকচার করা অনেক কঠিন একটা কাজ। কিভাবে আমরা আমাদের প্রজেক্টের ফাইল স্ট্রাকচার করবো। এর জন্য একটা ডিজাইন প্রিন্সিপাল আছে। এর নাম Clean Code Architecture। এখানে কিভাবে ফাইল স্ট্রাকচার করতে হয় সে বিষয়ে আলোচনা করা হয়েছে। একটা প্রজেক্টের ফাইল স্ট্রাকচার যদি ঠিক না হয় তাহলে আপনি প্রজেক্ট করতে গিয়ে বারবার সমস্যার সম্মুখীন হবেন। যদিও এখনই আমরা ক্লিন কোড আর্কিটেকচারের দিকে ঝুঁকবো না। এটা একটা অ্যাডভান্সড কনসেপ্ট। আমরা পরে এটা নিয়ে আলোচনা করবো। এটা একটা Monolithic application এর জন্য বেস্ট একটা কনসেপ্ট। আমরা এখন যে অ্যাপ্লিকেশন বানাচ্ছি সেটা মনোলিথিক। কারণ সবকিছু এক জায়গায়ই আছে। এই মনোলিথিক আর্কিটেকচারের আভিধানিক নাম Layred architecture. তবে এটা মনোলিথিক আর্কিটেকচার নামেই পরিচিত।

মনোলিথিক অ্যাপ্লিকেশনের জন্য আপাতত কোন প্রজেক্ট স্ট্রাকচার বেস্ট হবে সেটা বলা হচ্ছে না। আগে আমাদের যা যা লাগবে সবকিছুর জন্য আলাদা আলাদা ফোল্ডার বা ফাইল বানিয়ে চলে যাবো। কি কি ফোল্ডার / ফাইল বানাতে হবে এবং কেন চলুন আমরা একটু আলোচনা করি।

  1. app: এই ফোল্ডারে আমরা আমাদের অ্যাপ্লিকেশন রিলেটেড এরর, ডাটাবেজ কানেকশন, অ্যাপ ফাইল, রাউটস (গ্লোবাল, all) এগুলো রাখবো। server.js এর সাথে এই ফোল্ডারের কোনো সম্পর্ক নেই।
  2. routes: আমরা আমাদের সকল রাউট এর মধ্যে রাখবো।
  3. models: আমরা আমাদের সকল ডাটা মডেল এখানে রাখবো।
  4. controller: যেখানে রাউট থাকবে সেখানেই কন্ট্রোলার থাকবে। সকল কন্ট্রোলার থাকবে আমাদের এই ফোল্ডারে।
  5. service: আমরা কখনই আমাদের কন্ট্রোলারকে ডাটাবেইজের সাথে কমিউনিকেশন করতে দিবো না। এজন্য আমরা এই ডিরেক্টরি ক্রিয়েট করেছি ডাটাবেইজের সাথে কমিউনিকেট করার জন্য। যদিও কন্ট্রোলারও কমিউনিকেশন করতে পারে। কিন্তু আমরা ডাটাবেজ কানেকশন লজিক আর বিজনেস লজিক আলাদাভাবে লিখবো ভালভাবে বুঝার জন্য।
  6. middleware: এখানে আমরা আমাদের কাস্টম মিডলওয়্যারগুলো লিখবো।
  7. util: আমাদের যদি কোন ধরণের ইউটিলিটি ফাইল লাগে তা আমরা এই ফোল্ডারে রাখবো।
  8. db: সমস্ত ডাটাবেজ সম্পর্কিত কাজ আমরা এখানে রাখবো।
  9. config: এটা আমরা কনফিগারেশন ম্যানেজ করার জন্য ব্যবহার করবো।
  10. log: সমস্ত অ্যাপ্লিকেশন লগ এখানে থাকবে।
  11. error: আমরা আমাদের সমস্ত কাস্টম এরর এখানে লিখবো।
  12. test: আমাদের অ্যাপ্লিকেশনকে টেস্ট করার কোড আমরা এখানে লিখবো।
  13. server.js: সকল সার্ভার সম্পর্কিত কোড এই ফোল্ডারে থাকবে।
  14. .env & default.env: সকল সিক্রেট ইনফরমেশন যা শুধু আমিই জানবো, আর কাউকে জানতে দিবো না সেগুলো এই ফাইলের মধ্যে থাকবে।

app ফোল্ডারের মধ্যে আমরা app.js নামে একটা ফাইল নিবো। সেখানে আগের মতোই আমাদের অ্যাপ্লিকেশন বানাবো। এখন একটা রাউট সমস্ত অ্যাপ্লিকেশনের API তে থাকতেই হয় যেটা আমরা জানি না এবং বেশির ভাগ ক্ষেত্রে ব্যবহারই করিনা। কিন্তু এটা না থাকলে আমাদের API কে অনেক জায়গায় পারফেক্ট API বলে ধরাই হবে না। এটার কোনো কাজ নাই, কিন্তু এটা থাকতেই হয়। সেটা হলো '/health' রাউট। এটা সাধারণত যখন আমরা kubernetes cluster নিয়ে কাজ করবো, বা থার্ড পার্টি সার্ভিস ব্যবহার করে ডেপ্লয় করবো তখন তারা সাধারণত API চেক করার জন্য '/health' এ একটা রিকোয়েস্ট পাঠায়। যদি তা 200 রিটার্ন না করে তাহলে তারা ধরে নেয় এই API পারফেক্ট না।

const express = require('express');

const app = express();

app.get('/health', (_req, res) => {
    res.status(200).json({ message: 'Success' });
});

module.exports = app;

এখানে দেখুন আমরা আমাদের req প্যারামিটার ব্যবহার করিনি। এরকম আনইউজড ভ্যারিয়েবল থাকলে তার আগে একটা আন্ডারস্কোর (_) বসিয়ে দিবেন। নাহয় কিছু কিছু প্ল্যাটফর্মে তা এরর থ্রো করবে।

আমরা এতক্ষণ পর্যন্ত যত কাজ করেছি সবজায়গায় রাউটের পরে লিসেন করে দিয়েছি। কিন্তু এখন থেকে তা আর করবো না। আমরা app কে এক্সপোর্ট করে দিবো। এর কারন হলো আমরা যে একটা অ্যাপ্লিকেশন তৈরি করলাম আমাদের সেটা টেস্ট করতে হবে। এখন তার জন্য দরকার আমার অ্যাপ্লিকেশনের চেহারা। রানিং অ্যাপ্লিকেশন না। যদি এখানে আমরা লিসেন করতাম তাহলে তো কাজ শেষ। আমরা আর আমাদের অ্যাপ্লিকেশনটাকে পেতাম না। আর না পেলে কিভাবে টেস্ট করতাম? তাই আমরা এখানে শুধু অ্যাপ্লিকেশনের চেহারা রেখেছি। রান করার অপশন এখানে রাখবো না। আমাদের যখন টেস্ট করার দরকার পড়বে আমরা পরে আমাদের টেস্ট ফোল্ডারে ইমপোর্ট করে টেস্ট করতে পারবো। আমরা চাইলে সার্ভারেও ইমপোর্ট করতে পারবো। এবার তাহলে আমাদের সার্ভারটা কেমন হবে?

// server.js

const http = require('http');
const app = require('./app/app');

const server = http.createServer(app);

const PORT = 8000;

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

এখন এখানে আমরা স্ট্যাটিকভাবে দিয়ে দিয়েছি যে 8000 পোর্টে এটা রান হবে। কিন্তু ডেপ্লয় করার সময় যদি পোর্ট চেইঞ্জ হয়ে যায় তখন আমরা কি করবো? এজন্য আমরা একটা কাজ করতে পারি। যখন একটা সার্ভার ডেপ্লয় হয় তখন কিছু সেনসিটিভ ডাটা আছে যা আমরা আমাদের কোডের মধ্যে রাখতে পারি না। আমরা সেগুলো এক্সপোজ করতে দিতে পারি না। সেগুলো process.env ফাইল থেকে আমাদের অ্যাপ্লিকেশনে পাঠানো হয়। এখানে আমরা dotenv নামে একটা প্যাকেজ ইনস্টল করে নিবো। নেয়ার পর আমরা .env ফাইল নামে একটা ফাইল ক্রিয়েট করবো। এবং সেখানে আমরা আমাদের যে ডাটাগুলো এনভায়রনমেন্ট ভ্যারিয়েবলে থাকে সেগুলো রাখবো। যখন আমরা সার্ভারে ডেপ্লয় করি তখন হয় এই এনভায়রনমেন্ট ভ্যারিয়েবলগুলো সেখানে বলে দিই, নাহয় সেখানে একটা .env ফাইল ওপেন করি। এখন আমাদের ডিরেক্টরিতে দুইটা ফাইল ক্রিয়েট করতে হবে। .env এবং default.env। default.env তে আমরা আমাদের .env ফাইলের ভ্যারিয়েবলগুলোই দিবো ডামি ডাটা হিসেবে যাতে কেউ যদি আমার কোডের এক্সেস পেয়েও যায় সে যেন আমার ডাটাগুলো না পায়। এবার .env ফাইলের মধ্যে আমরা নিচের ভ্যারিয়েবল স্টোর করে রাখবো।

PORT = 4444

এবার server.js এ আমরা সরাসরি পোর্ট না লিখে এই পোর্টটা ওখানে নিয়ে ব্যবহার করবো। আমরা আগে চেক করবো process.env.PORT আছে কিনা? যদি না থাকে তাহলে আমরা 8000 দিয়ে দিবো। কোড দেখলে আরো ভালভাবে বুঝবেন।

// server.js
require('dotenv').config();
const http = require('http');
const app = require('./app/app');

const server = http.createServer(app);

const PORT = process.env.PORT || 8000;

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

আমরা app.js এ গিয়েও উপরে এনভায়রনমেন্ট ভ্যারিয়েবলটা রিকোয়ার করে দিবো।

// app.js

require('dotenv').config('../.env');
const express = require('express');
const app = express();

app.get('/health', (_req, res) => {
    res.status(200).json({ message: 'Success' });
});

module.exports = app;

app.js আমাদের এন্ট্রি ফাইল। এই ফাইলে অনেকে দেখা যায় এক হাজার লাইন লিখে ফেলে। যেটা আমার এন্ট্রি ফাইল সেটাতে যদি আমি এক হাজার লাইন লিখে ফেলি তাহলে সেটা মেইনটেইন করা অনেক দুরূহ হয়ে পড়ে। তাই আমাদের উদ্দেশ্য থাকবে এই ফাইলকে সর্বোচ্চ লেভেলে ক্লিয়ার রাখতে। আগে আমরা সব লিখি এরপর কিভাবে ক্লিয়ার রাখতে হবে সেটা দেখাবো। আমরা প্রথমে cors আর morgan ইনস্টল করি। এরপর আমরা সেগুলো app.js এ ইমপোর্ট করে নিই।

// app.js

require('dotenv').config('../.env');
const express = require('express');
const app = express();
const morgan = require('morgan');
const cors = require('cors');

const middleware = [morgan('dev'), cors(), express.json()];

app.get('/health', (_req, res) => {
    throw new Error('Error');
    res.status(200).json({ message: 'Success' });
});

module.exports = app;

ধরি যদি কোনো কারণে /health একটা এরর থ্রো করলো তাহলে তার চেহারা হবে এমন।

error

এখন আমি কি ক্লায়েন্টকে এই এরর শো করবো? কখনোই না। ম্যানুয়েলি যেখানে এরর তৈরি হবে সেখানেই আমরা ফরম্যাটেড এরর ম্যাসেজ শো করবো। আমরা 404 এর জন্য একটা এরর তৈরি করবো আর একটা গ্লোবাল এরর তৈরি করি।

// app.js

require('dotenv').config('../.env');
const express = require('express');
const app = express();
const morgan = require('morgan');
const cors = require('cors');

const middleware = [morgan('dev'), cors(), express.json()];

app.get('/health', (_req, res) => {
    throw new Error('Error');
    res.status(200).json({ message: 'Success' });
});

app.use((_req, _res, next) => {
    const error = new Error('Resource not found');
    error.status = 404;
    next(error);
});

app.use((error, _req, res, _next) => {
    if (error.status) {
        res.status(error.status).json({
            message: error.message,
        });
    }

    res.status(500).json({ message: 'Something went wrong' });
});

module.exports = app;

প্রথমটা শুধু 404 এরর হ্যান্ডেল করবে। যদি কোন রাউট না পায় তাহলে এটা রিটার্ন করবে। আর যদি অন্য কোনো এরর হয় সেটা আমাদের গ্লোবাল এরর হ্যান্ডলার হ্যান্ডেল করবে।

এবার আমরা কোডগুলোকে ডিস্ট্রিবিউট করে ফেলবো আমাদের সুবিধার্থে। না করলেও কিছু যায় আসবে না। কিন্তু যদি কয়েক হাজার লাইন কোড হয়ে সেগুলো ম্যানেজ করতে আমাদের হিমশিম খেয়ে যেতে হবে। তাই আমরা এখন এই কোডগুলোকে ডিস্ট্রিবিউট করবো।

প্রথমে আমাদের মিডলওয়্যার আছে ৩টা। ভবিষ্যতে তো আরো থাকতে পারে। এতগুলো মিডলওয়্যার এখানে থাকলে ফাইলটা অনেক ভারী হয়ে যাবে। সুতরাং আমরা প্রথমে middleware.js নামে একটা ফাইল ক্রিয়েট করবো app ফোল্ডারের ভিতরেই। এবং app.js থেকে মিডলওয়্যারের কোডগুলোকে ওখানে নিয়ে যাবো। এবং পরে তা app.js এ ইমপোর্ট করে আনবো।

// middleware.js

const express = require('express');
const morgan = require('morgan');
const cors = require('cors');

const middleware = [morgan('dev'), cors(), express.json()];

module.exports = middleware;
// app.js

require('dotenv').config('../.env');
const express = require('express');
const app = express();

app.use(require('./middleware'));

app.get('/health', (_req, res) => {
    res.status(200).json({ message: 'Success' });
});

app.use((_req, _res, next) => {
    const error = new Error('Resource not found');
    error.status = 404;
    next(error);
});

app.use((error, _req, res, _next) => {
    if (error.status) {
        res.status(error.status).json({
            message: error.message,
        });
    }

    res.status(500).json({ message: 'Something went wrong' });
});

module.exports = app;

এবার আমরা আমাদের রাউটগুলোকে আলাদা করবো। যদিও এখানে এখন আছে একটি রাউট। সেটা আমরা routes.js নামে একটা ফাইল ক্রিয়েট করে সেখানে রাখবো।

// routes.js

const router = require('express').Router();

router.get('/health', (_req, res) => {
    res.status(200).json({ message: 'Success' });
});

module.exports = router;
// app.js

require('dotenv').config('../.env');
const express = require('express');

const app = express();

app.use(require('./middleware'));
app.use(require('./routes'));

app.use((_req, _res, next) => {
    const error = new Error('Resource not found');
    error.status = 404;
    next(error);
});

app.use((error, _req, res, _next) => {
    if (error.status) {
        res.status(error.status).json({
            message: error.message,
        });
    }

    res.status(500).json({ message: 'Something went wrong' });
});

module.exports = app;

এবার আমরা ক্রিয়েট করবো error.js নামে একটা ফাইল। এবং সেখানে আমরা এরর হ্যান্ডলারগুলো রাখবো।

// error.js

const notFoundHandler = (_req, _res, next) => {
    const error = new Error('Resource not found');
    error.status = 404;
    next(error);
};

const errorHandler = (error, _req, res, _next) => {
    if (error.status) {
        res.status(error.status).json({
            message: error.message,
        });
    }

    res.status(500).json({ message: 'Something went wrong' });
};

module.exports = {
    notFoundHandler,
    errorHandler,
};
// app.js

require('dotenv').config('../.env');
const express = require('express');
const { errorHandler, notFoundHandler } = require('./error');

const app = express();

app.use(require('./middleware'));
app.use(require('./routes'));

app.use(notFoundHandler);

app.use(errorHandler);

module.exports = app;

এখন দেখুন ফাইলটা কত ক্লিন লাগছে। যদি কোনো কিছু ম্যানেজ করতে হয় আমরা ফাইলে গিয়ে গিয়ে ম্যানেজ করতে পারবো। এভাবে যদি আমরা ছোট ছোট ফাইলে সব ভাগ করে নিই তাহলে আমাদের জন্য কাজ করা অনেক সহজ হয়ে যাবে।

Single Page vs Multi Page Application

সিংগেল পেইজ অ্যাপ্লিকেশন বলতে বুঝায় যে অ্যাপ্লিকেশনের মাত্র একটা এইচটিএমএল ফাইল থাকবে, ব্যাকএন্ডে কোনো এইচটিএমএল জেনারেট হবে না, ব্যাকএন্ডের সাথে কমিউনিকেশন হবে শুধু API দ্বারা, আর সমস্ত পেইজ জেনারেট হবে ফ্রন্টএন্ডে ডায়নামিক্যালি জাভাস্ক্রিপ্ট দ্বারা। আর মাল্টিপেইজ অ্যাপ্লিকেশন বলতে বুঝায় যেখানে ব্যাকএন্ডেই html জেনারেট হয় যেমন গত ক্লাসের প্লেলিস্টে যেভাবে আমরা করেছিলাম সেটা মাল্টিপেইজ অ্যাপ্লিকেশন।

Source Code

এই লেকচারের সোর্স কোডসমূহ এই লিংক এ পাবেন।