পাঠ ১৮ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Full-Stack Web Frameworks / রাউটিং

রাউটিং ও রিকোয়েস্ট লাইফসাইকেল

Routing and the request lifecycle
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • রিকোয়েস্ট লাইফসাইকেলের পাঁচটি ধাপ এবং প্রতিটির দায়িত্ব
  • রাউট ম্যাচ না হলে কীভাবে বাকি ধাপগুলো (মিডলওয়্যার, হ্যান্ডলার) এড়িয়ে যাওয়া হয়
  • একটি LifecycleRouter ক্লাস — যা প্রতিটি ধাপ real Python ফাংশন কলের মাধ্যমে সম্পন্ন করে
  • মিডলওয়্যার ও হ্যান্ডলারের মধ্যে দায়িত্বের পার্থক্য — পরবর্তী পাঠ (L19) এই মিডলওয়্যার অংশটিকেই পূর্ণাঙ্গভাবে সম্প্রসারিত করবে

১ · রিকোয়েস্ট লাইফসাইকেলের পাঁচটি ধাপ

যখন একটি HTTP রিকোয়েস্ট সার্ভারে পৌঁছায়, একটি ব্যাক-এন্ড ফ্রেমওয়ার্ক সেটিকে সরাসরি হ্যান্ডলারে পাঠিয়ে দেয় না — বরং নির্দিষ্ট একটি পাইপলাইনের মধ্য দিয়ে পাস করায়। এই পাইপলাইনের প্রতিটি ধাপের একটি নির্দিষ্ট, সংকীর্ণ দায়িত্ব থাকে।

পার্সিং raw request → dict রাউট ম্যাচিং method+path → handler মিডলওয়্যার চেইন ক্রমানুসারে চলে (L19) হ্যান্ডলার আসল বিজনেস লজিক রেসপন্স status+body রাউট না মিললে -- সরাসরি 404, বাকি ধাপ এড়িয়ে যায়
রাউট ম্যাচিং ব্যর্থ হলে মিডলওয়্যার ও হ্যান্ডলার ধাপ কখনো চলে না -- সরাসরি 404 রেসপন্স তৈরি হয়ে যায়।
১. পার্সিং
raw রিকোয়েস্ট লাইন ও হেডার থেকে একটি স্ট্রাকচার্ড request অবজেক্ট/dict তৈরি হয় — method, path, headers, body।
২. রাউট ম্যাচিং
method+path জোড়াটি রেজিস্টার করা রাউটগুলোর সাথে মেলানো হয়। না মিললে বাকি ধাপ বাদ দিয়ে সরাসরি 404।
৩. মিডলওয়্যার চেইন
লগিং, অথ-চেক ইত্যাদি ক্রস-কাটিং কনসার্ন হ্যান্ডলারের আগে ক্রমানুসারে চলে (পূর্ণাঙ্গ প্যাটার্ন L19)।
৪. হ্যান্ডলার
নির্দিষ্ট রুটের জন্য লেখা আসল ফাংশন -- ডেটা প্রসেস করে একটি ফলাফল রিটার্ন করে।

২ · LifecycleRouter — প্রতিটি ধাপ সত্যিকারের ফাংশন কল হিসেবে

নিচের কোডে L01-এর Router ক্লাসটি সম্প্রসারণ করে একটি LifecycleRouter তৈরি করা হয়েছে যার process() মেথড উপরের পাঁচটি ধাপই ধারাবাহিকভাবে চালায় এবং প্রতিটি ধাপ শুরু হওয়ার সময় প্রিন্ট করে দেখায় — একটি সফল রিকোয়েস্ট ও একটি না-মেলা রুটের রিকোয়েস্ট, দুটোই ট্রেস করা হয়েছে।

Python
# ধাপ ১ -- পার্সিং: raw রিকোয়েস্ট লাইন থেকে একটি স্ট্রাকচার্ড request dict
def parse_request(raw_line, headers, body):
    method, path, protocol = raw_line.split(" ")
    return {"method": method, "path": path, "headers": headers, "body": body, "protocol": protocol}


class LifecycleRouter:
    def __init__(self):
        self.routes = {}
        self.middlewares = []

    def use(self, middleware_fn):
        self.middlewares.append(middleware_fn)

    def route(self, method, path):
        def decorator(handler_fn):
            self.routes[(method, path)] = handler_fn
            return handler_fn
        return decorator

    def process(self, raw_line, headers, body):
        # ধাপ ১ -- পার্সিং
        print("ধাপ ১ -- পার্সিং")
        request = parse_request(raw_line, headers, body)
        print(f"   parsed: {request['method']} {request['path']}")

        # ধাপ ২ -- রাউট ম্যাচিং
        print("ধাপ ২ -- রাউট ম্যাচিং")
        handler = self.routes.get((request["method"], request["path"]))
        if handler is None:
            print("   কোনো রাউট মেলেনি -- বাকি ধাপ এড়িয়ে সরাসরি 404")
            return {"status": 404, "headers": {}, "body": "Not Found"}
        print(f"   মিলেছে: {handler.__name__}()")

        # ধাপ ৩ -- মিডলওয়্যার চেইন (ক্রমানুসারে)
        print("ধাপ ৩ -- মিডলওয়্যার চেইন")
        for mw in self.middlewares:
            print(f"   চলছে: {mw.__name__}")
            mw(request)

        # ধাপ ৪ -- হ্যান্ডলার এক্সিকিউশন
        print("ধাপ ৪ -- হ্যান্ডলার এক্সিকিউশন")
        result = handler(request)
        print(f"   হ্যান্ডলার রিটার্ন করেছে: {result}")

        # ধাপ ৫ -- রেসপন্স ফরম্যাটিং
        print("ধাপ ৫ -- রেসপন্স ফরম্যাটিং")
        response = {"status": 200, "headers": {"Content-Type": "application/json"}, "body": result}
        print(f"   status={response['status']}")
        return response


def log_middleware(request):
    print(f"     [log] {request['method']} {request['path']} রিকোয়েস্ট এসেছে")


app = LifecycleRouter()
app.use(log_middleware)

@app.route("GET", "/users/42")
def get_user(request):
    return {"id": 42, "name": "রবিন"}


print("=== ট্রেস ১: মেলা রুট ===")
response1 = app.process("GET /users/42 HTTP/1.1", {"Accept": "application/json"}, None)
print("চূড়ান্ত রেসপন্স:", response1)

print("\n=== ট্রেস ২: না-মেলা রুট ===")
response2 = app.process("GET /users/999 HTTP/1.1", {"Accept": "application/json"}, None)
print("চূড়ান্ত রেসপন্স:", response2)

    
লক্ষ্য করুন ট্রেস ২-এ ধাপ ৩, ধাপ ৪, ও ধাপ ৫-এর প্রিন্ট লাইনগুলো একেবারেই দেখা যায় না — কারণ process() মেথডে রাউট ম্যাচিং ব্যর্থ হলে return স্টেটমেন্ট সাথে সাথে ফাংশন থেকে বেরিয়ে যায়। এটিই লাইফসাইকেলের একটি গুরুত্বপূর্ণ বৈশিষ্ট্য — প্রতিটি ধাপ পরবর্তী ধাপে যাওয়ার একটি "গেটকিপার"।

৩ · কেন প্রতিটি ধাপ আলাদা রাখা হয়

এই পাঁচটি ধাপকে আলাদা রাখার সুবিধা হলো প্রতিটির দায়িত্ব একক ও স্পষ্ট (single responsibility) — parse_request() শুধু raw ডেটাকে গঠন দেয়, রাউট ম্যাচিং শুধু কোন হ্যান্ডলার চলবে তা ঠিক করে, মিডলওয়্যার ক্রস-কাটিং কনসার্ন (লগিং, অথ) সামলায়, হ্যান্ডলার শুধু বিজনেস লজিক লেখে, আর রেসপন্স ফরম্যাটিং সব হ্যান্ডলারের জন্য একই কনসিস্টেন্ট আউটপুট আকার নিশ্চিত করে। প্রতিটি Express, Django, Rails, Laravel বা Spring-এর মতো ফ্রেমওয়ার্কই এই একই পাঁচটি ধাপ অনুসরণ করে, শুধু সিনট্যাক্স ও ধাপগুলোর নাম আলাদা হতে পারে।

মূল কথা · Key takeaway

একটি HTTP রিকোয়েস্ট সরাসরি হ্যান্ডলারে পৌঁছায় না — এটি পার্সিং, রাউট ম্যাচিং, মিডলওয়্যার চেইন, হ্যান্ডলার এক্সিকিউশন, ও রেসপন্স ফরম্যাটিং — এই পাঁচটি ধাপের একটি নির্দিষ্ট পাইপলাইন পার হয়। যেকোনো ধাপ (বিশেষত রাউট ম্যাচিং বা মিডলওয়্যার) পরবর্তী ধাপগুলো এড়িয়ে সরাসরি একটি রেসপন্স তৈরি করে দিতে পারে — এই "শর্ট-সার্কিট" আচরণটি পরের পাঠে (L19) মিডলওয়্যার প্যাটার্নে আরও গভীরভাবে দেখা যাবে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ রাউট ম্যাচিং ধাপটি মিডলওয়্যার চেইনের আগে কেন রাখা হয়, পরে নয়?

যদি কোনো পাথের জন্য কোনো হ্যান্ডলারই নিবন্ধিত না থাকে, তাহলে মিডলওয়্যার চালানোর কোনো অর্থ নেই — লগিং বা অথ-চেক করেও শেষে 404-ই ফেরত যাবে। রাউট ম্যাচিং আগে করে ফ্রেমওয়ার্ক অপ্রয়োজনীয় কাজ (মিডলওয়্যার চালানো) এড়িয়ে যায় এবং দ্রুত 404 রিটার্ন করে। এটি একটি পারফরম্যান্স ও স্পষ্টতা — দুটোরই সিদ্ধান্ত।

প্র ০২ উপরের কোডে parse_request() কেন LifecycleRouter ক্লাসের ভেতরের মেথড না হয়ে আলাদা একটি ফাংশন?

পার্সিং লজিকটি রাউটিং-নির্দিষ্ট কিছু নয় -- এটি শুধু raw টেক্সট থেকে একটি স্ট্রাকচার্ড dict তৈরি করে, যা Router-এর কোনো state (যেমন self.routes) স্পর্শ করে না। এটিকে আলাদা রাখলে ফাংশনটি স্বতন্ত্রভাবে টেস্ট করা যায় এবং প্রয়োজনে অন্য কোনো কনটেক্সটেও পুনরায় ব্যবহার করা যায় -- এটি "একক দায়িত্ব" নীতিরই একটি ব্যবহারিক উদাহরণ।

প্র ০৩ যদি log_middleware রাউট ম্যাচিং-এর আগে রাখা হতো, তাহলে ট্রেস ২-এ কী পার্থক্য দেখা যেত?

তাহলে না-মেলা রুটের জন্যও লগিং প্রিন্ট হতো, কারণ মিডলওয়্যার এখন রাউট ম্যাচিং-নির্ভর নয়। বাস্তব ফ্রেমওয়ার্কগুলোতেও কিছু মিডলওয়্যার (যেমন রিকোয়েস্ট-লগিং) ইচ্ছাকৃতভাবে রাউট ম্যাচিং-এর আগে রাখা হয়, যাতে 404 রিকোয়েস্টগুলোও লগ হয় -- এটি একটি ডিজাইন সিদ্ধান্ত, নির্দিষ্ট নিয়ম নয়।

অনুশীলন

  1. চিন্তা করুন: "ধাপ ৫ -- রেসপন্স ফরম্যাটিং" ধাপটি কেন হ্যান্ডলারের ভেতরেই না করে আলাদা রাখা হয়েছে? হ্যান্ডলার নিজেই কেন সরাসরি {"status": 200, ...} রিটার্ন করে না?

    যদি প্রতিটি হ্যান্ডলারকে নিজে থেকে status/headers গঠন করতে হতো, তাহলে প্রতিটি হ্যান্ডলারে একই বয়লারপ্লেট কোড বারবার লিখতে হতো এবং কোনো ভুলে দুটো হ্যান্ডলার ভিন্ন আকারে রেসপন্স ফরম্যাট করতে পারত। আলাদা রেসপন্স-ফরম্যাটিং ধাপ রাখলে হ্যান্ডলার শুধু "আসল ফলাফল" (যেমন {"id": 42, ...}) রিটার্ন করে, আর ফ্রেমওয়ার্ক নিশ্চিত করে প্রতিটি রেসপন্স একই কনসিস্টেন্ট আকারে ক্লায়েন্টের কাছে যায়।

  2. পরীক্ষা করুন: উপরের কোড সেলে app-এ একটি নতুন রুট @app.route("GET", "/health") যোগ করে একটি health_check(request) ফাংশন লিখুন যা {"status": "ok"} রিটার্ন করে। তারপর app.process("GET /health HTTP/1.1", {}, None) কল করে Run চেপে দেখুন সবগুলো ধাপ ঠিকভাবে প্রিন্ট হচ্ছে কি না।

    নতুন রুট রেজিস্টার হলে self.routes-এ ("GET", "/health"): health_check এন্ট্রি যুক্ত হয়। তখন app.process(...) কল করলে ধাপ ১ থেকে ৫ পর্যন্ত সবগুলো প্রিন্ট লাইন দেখা যাবে (যেহেতু রুট মিলেছে), এবং চূড়ান্ত রেসপন্সে "body": {"status": "ok"} দেখা যাবে -- ঠিক যেমন /users/42 রুটের জন্য হয়েছিল।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
Context API ও বিকল্প স্টেট প্যাটার্ন