সরাসরি পর্বে যান
পর্ব ৪৮ · ৫৫-এর মধ্যে · মডিউল ৭

ওয়েব ব্যাকএন্ড — FastAPI-র শুরু

Web Backends in Python: Requests, Routing and Your First FastAPI Service

Taught in Bangla, this lesson introduces backend development with FastAPI: the request-router-handler-response flow, WSGI and ASGI, Django, Flask and FastAPI compared, a first app with uvicorn and auto-generated /docs, path and query parameters validated by type hints, pydantic request bodies, status codes and a full CRUD API, Depends, testing, databases, security and deployment.

এই পর্বে যা শিখবেন

  • সার্ভারের দিকে একটি request-এর পুরো যাত্রা, আর WSGI ও ASGI কী — standard library দিয়ে নিজের হাতে একটি ছোট router আর WSGI app
  • Django, Flask আর FastAPI-র পার্থক্য, আর কোন কাজে কোনটি বাছবেন
  • FastAPI-র hello world, uvicorn দিয়ে চালানো, /docs-এর স্বয়ংক্রিয় ডকুমেন্টেশন
  • path ও query parameter, pydantic model দিয়ে request body, response model, status code আর HTTPException — একটি দোকানের পুরো CRUD API
  • Depends, async def বনাম def, TestClient দিয়ে টেস্ট, ডেটাবেসের ঝলক, নিরাপত্তার মূল নিয়ম আর deployment

১ · ব্যাকএন্ড কী — অর্ডার নেওয়ার কাউন্টার

পর্ব ৪৭-এ আপনি ছিলেন client-এর আসনে: Python দিয়ে অন্যের সার্ভারে request পাঠিয়েছেন, উত্তর হিসেবে JSON পেয়েছেন। এই পর্বে আসন বদলাচ্ছি। এবার আপনি সেই সার্ভার — যার কাছে অন্যরা request পাঠাবে। মোবাইল অ্যাপ, ওয়েবসাইটের JavaScript, অন্য কোনো প্রোগ্রাম, এমনকি পর্ব ৪৭-এর মতো একটি Python স্ক্রিপ্ট — সবাই আপনার প্রোগ্রামকে প্রশ্ন করবে, আর আপনার প্রোগ্রাম উত্তর দেবে।

সার্ভারের দিকের এই প্রোগ্রামটিকে বলে backend. আর ব্যবহারকারী যা চোখে দেখেন — বোতাম, ফর্ম, পেজ — সেটি frontend. সংজ্ঞাটি খুব সরল: ব্যাকএন্ড হলো একটি প্রোগ্রাম, যে HTTP request-এর অপেক্ষায় বসে থাকে, আর প্রতিটির উত্তর দেয়। এর বেশি রহস্য নেই।

একটি বড় মুদি দোকানের কাউন্টার ভাবুন। ক্রেতা একটি স্লিপে লিখে দেন: “চাল, ৫ কেজি”। কাউন্টারের লোক স্লিপ পড়েন, ঠিক করেন কোন তাকের দায়িত্বে যিনি আছেন তাঁর কাছে পাঠাবেন, সেই কর্মী মাল আনেন বা জানান “নেই”, আর শেষে ক্রেতা হাতে পান একটি রসিদ — তাতে লেখা কী পেলেন, দাম কত, বা কেন পেলেন না। স্লিপটি হলো request, কাউন্টারের বাছাই হলো router, তাকের কর্মী হলেন handler, আর রসিদ হলো response. কাউন্টার সারাদিন খোলা থাকে, একের পর এক স্লিপ আসে — ব্যাকএন্ডও ঠিক তাই।

পর্ব ৪৭-এ দেখেছেন, একটি HTTP request-এ থাকে একটি method (GET, POST, PUT, DELETE…), একটি ঠিকানা বা path (/products/7), কিছু header, আর কখনো একটি body (সাধারণত JSON). Response-এ থাকে একটি status code (200 ঠিক আছে, 404 পাওয়া যায়নি…), header আর body. সার্ভারের দিকে প্রতিটি request-এর যাত্রা চার ধাপের:

  1. Request আসে: নেটওয়ার্ক থেকে কাঁচা বাইট আসে; web server সেগুলো পড়ে method, path, header, body আলাদা করে।
  2. Router মেলায়: routing — “GET /products/7” কোন ফাংশনের দায়িত্বে? path থেকে 7-এর মতো অংশও আলাদা করে।
  3. Handler কাজ করে: আপনার লেখা ফাংশন — ডেটা খোঁজে, হিসাব করে, ডেটাবেসে লেখে। একে বলে endpoint বা route handler.
  4. Response ফেরে: ফলটি JSON-এ রূপান্তর (serialization), সাথে সঠিক status code, তারপর নেটওয়ার্কে ফেরত।

কোনো ফ্রেমওয়ার্ক ছাড়া, শুধু standard library দিয়ে, এই চার ধাপের মাঝের দুটি — router আর handler — নিজের হাতে লিখে দেখি। নেটওয়ার্ক লাগবে না; আমরা request-কে শুধু একটি method আর একটি path হিসেবে ফাংশনে পাঠাব:

import json
import re

products = {7: {"name": "চাল (৫ কেজি)", "price": 420}}
def get_product(product_id: str):
    if not product_id.isdecimal():                # FastAPI এটা নিজেই করে
        return 422, {"detail": "product_id must be an integer"}
    if int(product_id) not in products:
        return 404, {"detail": "প্রোডাক্ট পাওয়া যায়নি"}
    return 200, products[int(product_id)]

routes = [("GET", re.compile(r"/products/(?P<product_id>[^/]+)"), get_product)]
def handle(method: str, path: str):               # ছোট্ট একটি router
    for route_method, pattern, handler in routes:
        match = pattern.fullmatch(path)
        if match and route_method == method:
            return handler(**match.groupdict())
    return 404, {"detail": "Not Found"}

for path in ["/products/7", "/products/abc", "/products/99", "/orders"]:
    status, body = handle("GET", path)
    print(path, "→", status, json.dumps(body, ensure_ascii=False))

আউটপুট চার লাইনের। /products/7 → 200 {"name": "চাল (৫ কেজি)", "price": 420} — router-এর regular expression (পর্ব ২৮) path থেকে product_id="7" বের করল, handler সেটি সংখ্যায় বদলে dict-এ খুঁজে পেল। /products/abc → 422 — path মিলেছে, কিন্তু id সংখ্যা নয়; এটি client-এর ভুল, তাই ৪xx code. /products/99 → 404 {"detail": "প্রোডাক্ট পাওয়া যায়নি"} — সংখ্যা ঠিক, কিন্তু এমন প্রোডাক্ট নেই। /orders → 404 {"detail": "Not Found"} — কোনো route-ই মেলেনি। লক্ষ করুন, এই প্রায় ২০ লাইনের প্রায় অর্ধেক “প্লাম্বিং”: path মেলানো, string থেকে int, ভুল type-এ ৪২২। আসল ব্যবসার যুক্তি মাত্র দুই লাইন। ফ্রেমওয়ার্কের কাজ ঠিক এই প্লাম্বিং আপনার কাঁধ থেকে নামানো।

মূল ধারণা

যেকোনো ব্যাকএন্ড — Facebook-এর হোক বা পাড়ার দোকানের — একই ছাঁচে চলে: request → router → handler → response. ফ্রেমওয়ার্ক বদলায়, ভাষা বদলায়, কিন্তু এই চার ধাপ বদলায় না। নতুন কোনো ফ্রেমওয়ার্ক শিখতে গেলে প্রথমে খুঁজুন: route কোথায় লেখা হয়, handler কী পায়, আর response কীভাবে ফেরে।

২ · ফ্রেমওয়ার্ক কেন — WSGI, ASGI আর Django, Flask, FastAPI

একটি web framework হলো এমন একটি লাইব্রেরি, যা request পড়া, routing, JSON রূপান্তর, যাচাই, error সামলানো, cookie, নিরাপত্তার header — এই সব সাধারণ কাজ আগে থেকেই করে রাখে। আপনি শুধু handler ফাংশনগুলো লেখেন। Library আর framework-এর পার্থক্য নিয়ে একটি পুরোনো কথা আছে — “library-কে আপনি ডাকেন, framework আপনাকে ডাকে”। ব্যাকএন্ডে তা আক্ষরিক সত্য: request এলে ফ্রেমওয়ার্ক আপনার ফাংশন ডাকে।

WSGI আর ASGI — সার্ভার আর অ্যাপের মাঝের চুক্তি

একটি প্রশ্ন থেকে যায়: নেটওয়ার্ক থেকে বাইট পড়ে কে, আর আপনার Python কোড ডাকে কে? এই দুটি কাজ আলাদা প্রোগ্রাম করে। একটি server (যেমন Gunicorn বা Uvicorn) socket খুলে বাইট পড়ে; আর আপনার application (Django, Flask বা FastAPI দিয়ে লেখা) শুধু Python object নিয়ে কাজ করে। দুজনের কথা বলার জন্য একটি নির্দিষ্ট চুক্তি দরকার। Python-এ সেই চুক্তির নাম WSGI (Web Server Gateway Interface, PEP 333, ২০০৩): application হলো একটি সাধারণ ফাংশন, যে একটি dict (environ — request-এর সব তথ্য) আর একটি start_response ফাংশন নেয়, আর body-র বাইট ফেরত দেয়। চুক্তিটি এতই সরল যে নেটওয়ার্ক ছাড়াই নিজে ডেকে দেখা যায়:

import io
import json

def app(environ, start_response):                 # একটি WSGI application
    raw = environ["wsgi.input"].read(int(environ.get("CONTENT_LENGTH") or 0))
    try:
        data = json.loads(raw)
        ok = isinstance(data["price"], (int, float)) and data["price"] > 0
    except (json.JSONDecodeError, KeyError, TypeError):
        ok = False
    status = "201 Created" if ok else "422 Unprocessable Content"
    body = {"created": data} if ok else {"detail": "price: ধনাত্মক সংখ্যা দিন"}
    start_response(status, [("Content-Type", "application/json")])
    return [json.dumps(body, ensure_ascii=False).encode()]

for text in ['{"name": "ডাল", "price": 140}', '{"name": "ডাল", "price": -5}',
             '{"name": "ডাল"}', 'দাম ১৪০ টাকা']:
    raw = text.encode()                           # নেটওয়ার্ক ছাড়াই নকল request
    environ = {"REQUEST_METHOD": "POST", "PATH_INFO": "/products",
               "CONTENT_LENGTH": str(len(raw)), "wsgi.input": io.BytesIO(raw)}
    chunks = app(environ, lambda status, headers: print(status, end=" | "))
    print(b"".join(chunks).decode())

আউটপুট: প্রথম লাইন 201 Created | {"created": {"name": "ডাল", "price": 140}}, আর পরের তিনটি লাইনই 422 Unprocessable Content | {"detail": "price: ধনাত্মক সংখ্যা দিন"}. দ্বিতীয়টিতে দাম ঋণাত্মক, তৃতীয়টিতে price চাবিই নেই (KeyError), চতুর্থটি JSON-ই নয় (JSONDecodeError). আসল server (যেমন Gunicorn) ঠিক এভাবেই environ বানিয়ে app ডাকে — আমরা শুধু নেটওয়ার্কের অংশটি নকল করেছি। দুটি জিনিস খেয়াল করুন। প্রথমত, শুধু একটি field যাচাই করতেই আট লাইন লাগল, আর error বার্তা বলছে না ঠিক কোন field-এ কী ভুল। দ্বিতীয়ত, 201 (নতুন কিছু তৈরি হলো) আর 422 (ডেটা বোঝা গেছে, কিন্তু নিয়ম মানেনি) — সঠিক status code বাছাই ব্যাকএন্ডের দায়িত্ব। অংশ ৫-এ pydantic এই পুরো যাচাই কয়েক লাইনে, field ধরে ধরে বার্তাসহ করবে।

WSGI-র একটি সীমা আছে: প্রতিটি request একটি ফাংশন-ডাক, যা শেষ না হওয়া পর্যন্ত সেই worker আটকে থাকে। একসাথে হাজারো ধীর সংযোগ (chat, WebSocket, অন্য API-র উত্তরের অপেক্ষা) সামলাতে এটি অদক্ষ। তাই এসেছে ASGI (Asynchronous Server Gateway Interface) — একই ধারণা, কিন্তু application একটি async ফাংশন, যা পর্ব ৪২-এর event loop-এ চলে। একটি request যখন ডেটাবেস বা অন্য সার্ভারের উত্তরের অপেক্ষায়, তখন একই process অন্য request-এর কাজ এগিয়ে নিতে পারে। Uvicorn একটি ASGI server; FastAPI একটি ASGI framework.

তিনটি বড় ফ্রেমওয়ার্ক — কোনটি কখন

প্রশ্নDjangoFlaskFastAPI
দর্শনbatteries included — সব একসাথেক্ষুদ্র কেন্দ্র, বাকিটা নিজে বাছুনআধুনিক, type hint দিয়ে চালিত
সাথে যা আসেORM, admin প্যানেল, login, ফর্ম, template, migrationrouting, template (Jinja2); বাকিটা extensionযাচাই (pydantic), স্বয়ংক্রিয় API ডকুমেন্টেশন, dependency injection
চুক্তিWSGI (ASGI-ও সমর্থন করে)WSGIASGI
সবচেয়ে মানানসইপূর্ণ ওয়েবসাইট — খবরের সাইট, ই-কমার্স, স্কুলের ম্যানেজমেন্ট সিস্টেমছোট সাইট, শেখা, যেখানে নিজের মতো সাজাতে চানJSON API — মোবাইল অ্যাপের ব্যাকএন্ড, ML মডেলের API, microservice
শেখার ঢালবেশি, কিন্তু একবার শিখলে অনেক কাজ দ্রুতকমকম, যদি পর্ব ৩৭-এর type hint জানা থাকে

সরল নিয়ম: HTML পেজ আর admin প্যানেলসহ পুরো একটি ওয়েবসাইট দরকার হলে Django; শুধু JSON API — বিশেষ করে মোবাইল অ্যাপ বা AI মডেলের জন্য — হলে FastAPI; ছোট কিছু, বা পুরোনো Flask প্রজেক্টে কাজ হলে Flask. তিনটিই পরিণত, বড় প্রতিষ্ঠানে ব্যবহৃত, আর যেকোনোটিতে ভালো ব্যাকএন্ড বানানো যায়। এই পর্বে FastAPI বেছে নিচ্ছি দুটি কারণে: এটি পর্ব ৩৫-এর decorator আর পর্ব ৩৭-এর type hint-কে সরাসরি কাজে লাগায়, আর এর কোড ছোট বলে পুরো ধারণাটি এক পর্বে দেখানো যায়। ফ্রেমওয়ার্কগুলোর গভীর তুলনা, MVC প্যাটার্ন আর অন্য ভাষার ফ্রেমওয়ার্ক দেখতে পারেন আমাদের Full-Stack Web Frameworks কোর্সে।

৩ · FastAPI-র প্রথম অ্যাপ — hello world, uvicorn আর /docs

এই পর্বের FastAPI কোড এই পেজে চালানো যাবে না: পেজের ভেতরের Python-এ শুধু standard library আছে, আর এখান থেকে কোনো সার্ভারে ঢোকাও যায় না। তাই FastAPI-র কোডের নিচে আমরা আসল আউটপুট (যা FastAPI 0.1xx আর pydantic 2-এ যাচাই করা) দেখিয়ে দিয়েছি। নিজের কম্পিউটারে পর্ব ২৩-এর মতো একটি virtual environment বানিয়ে চালিয়ে দেখুন।

python -m venv .venv
.venv\Scripts\activate            # Linux/macOS: source .venv/bin/activate
pip install "fastapi[standard]"   # FastAPI + uvicorn + fastapi CLI

এবার main.py নামে একটি ফাইল:

from fastapi import FastAPI

app = FastAPI(title="দোকানের প্রোডাক্ট API")

@app.get("/")
def home():
    return {"message": "দোকান খোলা"}

মাত্র পাঁচ লাইন, কিন্তু প্রতিটির অর্থ আছে। app = FastAPI() একটি ASGI application বানাল — এটিই server ডাকবে। @app.get("/") হলো পর্ব ৩৫-এর decorator, তবে decorator factory: app.get("/") ডাকলে একটি decorator ফেরত আসে, যা home ফাংশনটিকে router-এর তালিকায় “GET / এলে একে ডাকো” হিসেবে নিবন্ধন করে, আর ফাংশনটি অপরিবর্তিত ফেরত দেয়। ঠিক পর্ব ৩৫-এর registry pattern. আর handler একটি সাধারণ dict ফেরত দিয়েছে — FastAPI নিজেই সেটিকে JSON-এ বদলাবে আর 200 status দেবে।

চালানোর কমান্ডটি ধাপে ধাপে দেখুন:

uvicorn main :app --reload
  1. uvicorn — ASGI server-কে ডাকা হলো। সে একটি port (স্বাভাবিকভাবে 8000) খুলে নেটওয়ার্কের দিকে কান পাতবে, আর প্রতিটি request-কে Python object বানিয়ে আপনার app-এ দেবে।
  2. main — কোন module-এ app আছে: main.py ফাইল (.py ছাড়া, পর্ব ২২-এর import-এর নিয়মে)। ফাইল shop/api.py হলে লিখতেন shop.api.
  3. :app — module-এর ভেতরে কোন নামের object-টি ASGI application. আমাদের ফাইলে app = FastAPI(), তাই app. ভুল নাম দিলে uvicorn বলবে, সে app খুঁজে পায়নি।
  4. --reload — ফাইল বদলালেই server নিজে থেকে আবার শুরু হবে। লেখার সময় খুব সুবিধার, কিন্তু শুধু নিজের কম্পিউটারে; production-এ এটি বন্ধ রাখুন। নতুন সংস্করণে একই কাজের ছোট রূপ fastapi dev main.py, আর production-এর জন্য fastapi run main.py.

Server চালু হলে ব্রাউজারে http://127.0.0.1:8000/ খুললে দেখবেন {"message":"দোকান খোলা"}. 127.0.0.1 মানে “এই কম্পিউটারই” — আপাতত শুধু আপনিই দেখতে পাচ্ছেন।

/docs — নিজে থেকে তৈরি ডকুমেন্টেশন

এবার খুলুন http://127.0.0.1:8000/docs. আপনি এক লাইনও না লিখে একটি পূর্ণ ইন্টারঅ্যাকটিভ ডকুমেন্টেশন পেজ পাবেন: প্রতিটি endpoint, কী parameter নেয়, কী ফেরত দেয়, আর একটি “Try it out” বোতাম — ব্রাউজার থেকেই request পাঠিয়ে দেখা যায়। এটি তৈরি হয় OpenAPI নামের একটি মানক বর্ণনা থেকে, যা FastAPI আপনার route আর type hint পড়ে /openapi.json-এ বানায়; আর পেজটি দেখায় Swagger UI. বিকল্প চেহারা আছে /redoc-এ।

৪ · Path আর query parameter — type hint থেকেই যাচাই

অংশ ১-এর ছোট router-এ আমরা নিজের হাতে path থেকে id বের করেছিলাম, সংখ্যা কি না দেখেছিলাম, ৪২২ ফেরত দিয়েছিলাম। FastAPI-তে একই কাজ:

products = {7: {"id": 7, "name": "চাল (৫ কেজি)", "price": 420.0, "stock": 12}}

@app.get("/products/{product_id}")
def get_product(product_id: int):
    return products[product_id]          # আপাতত না পাওয়ার ঘটনা বাদ

Path-এর {product_id} অংশটি একটি path parameter — ঠিকানার একটি পরিবর্তনশীল টুকরো। FastAPI দেখে ফাংশনেও একই নামের parameter আছে, আর তার type hint int. পর্ব ৩৭-এ বলেছিলাম, Python নিজে hint যাচাই করে না, কিন্তু কিছু লাইব্রেরি চালানোর সময় hint পড়ে কাজ করে। FastAPI ঠিক সেই লাইব্রেরি: hint পড়ে সে জানে "7" string-কে 7 int-এ বদলাতে হবে, আর না পারলে আপনার ফাংশন ডাকবেই না। দুটি request-এর যাত্রা পাশাপাশি দেখুন:

  1. RequestGET/products/7
  2. Router{product_id}"7" পাওয়া গেল
  3. যাচাইint"7" → 7
  4. Handlerget_product200 + JSON
  5. abc হলে422handler ডাকা হয়নি
নেটওয়ার্ক থেকে আসা requestসব কিছুই লেখা (text)
GET /products/7 HTTP/1.1
Host: 127.0.0.1:8000
Accept: application/json
router-এর route তালিকাউপর থেকে নিচে মেলানো
GET  /                       → home
GET  /products/{product_id}  → get_product   ✔
POST /products               → create_product
product_id = "7"   (এখনো একটি str)
hint: product_id: intpydantic দিয়ে রূপান্তর
"7"  → int → 7      ✔ ফাংশন ডাকা যাবে
get_product(product_id=7)response
HTTP/1.1 200 OK
content-type: application/json

{"id": 7, "name": "চাল (৫ কেজি)", "price": 420.0, "stock": 12}
GET /products/abcযাচাই ব্যর্থ
HTTP/1.1 422 Unprocessable Content

{"detail": [{"type": "int_parsing",
             "loc": ["path", "product_id"],
             "msg": "Input should be a valid integer, unable
                     to parse string as an integer",
             "input": "abc"}]}
  1. Request আসে লেখা হিসেবে। নেটওয়ার্কে সব কিছুই text — 7-ও আসলে "7" অক্ষর। Uvicorn বাইটগুলো পড়ে method (GET) আর path (/products/7) আলাদা করে FastAPI-কে দেয়।
  2. Router path মেলায়। FastAPI-র route তালিকায় /products/{product_id} ছাঁচটি /products/7-এর সাথে মেলে, আর product_id = "7" বের হয়। Method-ও মিলতে হবে — একই path-এ POST এলে অন্য handler, বা 405 Method Not Allowed.
  3. Hint অনুযায়ী রূপান্তর। ফাংশনের product_id: int দেখে FastAPI (ভেতরে pydantic দিয়ে) "7"-কে 7 বানায়। এটি শুধু যাচাই নয়, রূপান্তরও — আপনার ফাংশন পায় একটি সত্যিকারের int, তাই products[product_id] dict-এর int চাবির সাথে মিলে যায়।
  4. Handler আর response. get_product(product_id=7) চলে, একটি dict ফেরত দেয়; FastAPI সেটিকে JSON বানিয়ে 200 OK দিয়ে পাঠায়। আপনি শুধু দুই লাইন লিখেছেন।
  5. /products/abc — handler পর্যন্ত পৌঁছায়ই না। "abc" int হয় না, তাই FastAPI নিজেই 422 ফেরত দেয়, সাথে বিস্তারিত: কোথায় (["path", "product_id"]), কী ভুল, কী এসেছিল। আপনার কোডে এই ভুল সামলানোর একটি লাইনও নেই। (HTTP-র নতুন মানক RFC 9110-এ ৪২২-এর নাম “Unprocessable Content”; পুরোনো লেখায় দেখবেন “Unprocessable Entity”.)

Query parameter — ঐচ্ছিক ছাঁকনি

URL-এর ?-এর পরের অংশ হলো query parameter: /products?max_price=200&limit=5. FastAPI-র নিয়ম খুব সরল — ফাংশনের যে parameter path-এ নেই, সেটি query parameter. Default থাকলে ঐচ্ছিক, না থাকলে বাধ্যতামূলক:

@app.get("/products")
def list_products(max_price: float | None = None, limit: int = 10):
    items = list(products.values())
    if max_price is not None:
        items = [p for p in items if p["price"] <= max_price]
    return items[:limit]

max_price: float | None = None — পর্ব ৩৭-এর union type: দাম দিলে float, না দিলে None, আর তখন ছাঁকনি নেই। limit: int = 10 — না দিলে ১০টি। /products?limit=ten পাঠালে আবার ৪২২, এবার "loc": ["query", "limit"]. ব্যবহার বুঝে বাছুন: নির্দিষ্ট একটি জিনিস চিহ্নিত করতে path (/products/7, /results/123456), তালিকা ছাঁকতে, সাজাতে বা পাতা ভাগ করতে query (?max_price=200, ?page=2).

বাংলা সংখ্যার একটি মজার পার্থক্য: Python-এর নিজের int("৭") দেয় 7, আর "৭".isdecimal() হলো True — Unicode-এর সব দশমিক অঙ্ক Python মেনে নেয়। তাই অংশ ১-এর ছোট router /products/৭-কে 7 হিসেবে খুঁজে পেত। কিন্তু pydantic 2-এর রূপান্তর কঠোর: /products/৭ FastAPI-তে পায় 422. কোনটি চান, সেটি ব্যবসার সিদ্ধান্ত — তবে লক্ষ করুন, “নিজে লেখা যাচাই” আর “লাইব্রেরির যাচাই” একই রকম আচরণ করবে, এমন ধরে নেওয়া যায় না। পরীক্ষা করে দেখতে হয়।

৫ · Request body আর pydantic model — নতুন প্রোডাক্ট যোগ করা

GET শুধু পড়ে। নতুন প্রোডাক্ট যোগ করতে client পাঠায় POST, আর প্রোডাক্টের তথ্য থাকে request-এর body-তে, JSON হিসেবে। এই body-র “আকার” বলে দিতে FastAPI ব্যবহার করে pydantic-এর model — দেখতে পর্ব ৩২-এর dataclass-এর মতো, কিন্তু চালানোর সময় সত্যিই যাচাই করে:

from fastapi import FastAPI, status
from pydantic import BaseModel, Field

class ProductIn(BaseModel):                       # client যা পাঠাবে
    name: str = Field(min_length=1, max_length=50)
    price: float = Field(gt=0)                    # শূন্যের বেশি
    stock: int = Field(default=0, ge=0)           # না দিলে ০

class Product(ProductIn):                         # সার্ভার যা ফেরত দেবে
    id: int

@app.post("/products", status_code=status.HTTP_201_CREATED)
def create_product(data: ProductIn) -> Product:
    global next_id
    product = Product(id=next_id, **data.model_dump())
    products[next_id] = product
    next_id += 1
    return product

এখানে নিয়মটি: parameter-এর type যদি একটি pydantic model হয়, FastAPI ধরে নেয় সেটি আসবে request body থেকে। Field(gt=0) মানে “greater than 0”, ge=0 মানে “শূন্য বা বেশি”। দুটি আলাদা model লক্ষ করুন — ProductIn-এ id নেই, কারণ id দেবে সার্ভার, client নয়। Client যদি নিজে "id": 1 পাঠায়, ProductIn সেটি ফেলে দেবে (pydantic-এর স্বাভাবিক আচরণে অচেনা field উপেক্ষিত হয়)। এটি একটি ছোট কিন্তু গুরুত্বপূর্ণ নিরাপত্তা-নকশা: client যা ঠিক করার অধিকার রাখে না, model-এ সেটি রাখবেন না। তিনটি আলাদা body পাঠালে কী হয়, দেখুন:

1class ProductIn(BaseModel):2    name: str = Field(min_length=1, max_length=50)3    price: float = Field(gt=0)4    stock: int = Field(default=0, ge=0)56@app.post("/products", status_code=201)7def create_product(data: ProductIn) -> Product:8    product = Product(id=next_id, **data.model_dump())9    ...10    return product
body আর যাচাইয়ের ফল
body
data
status
  1. চুক্তি লেখা হলো। Model বলছে: name ১ থেকে ৫০ অক্ষরের string, price শূন্যের বেশি সংখ্যা, stock ঐচ্ছিক (না দিলে ০)। data: ProductIn দেখে FastAPI জানে body-কে এই model দিয়ে যাচাই করতে হবে, আর status_code=201 সফল হলে কোন code যাবে।
  2. বৈধ body — সব ঠিক। name আর price আছে, stock নেই, তাই default ০ বসল। 140 int এসেছিল, কিন্তু hint float, তাই 140.0 হলো। ফাংশন একটি ProductIn object পেল — কোনো dict নয়, ডট দিয়ে পড়া যায় (data.price). model_dump() সেটিকে dict বানায়, আর নতুন id 8. Response: 201 Created.
  3. ঋণাত্মক দাম — ৪২২. price: -5 একটি বৈধ সংখ্যা, কিন্তু gt=0 নিয়ম ভাঙে। FastAPI আপনার ফাংশন ডাকেই না; বার্তায় ঠিক কোন field (["body", "price"]), কোন নিয়ম (greater_than), আর কী এসেছিল (-5). অংশ ২-এর WSGI app-এর আট লাইনের যাচাইয়ের সাথে তুলনা করুন।
  4. Field নেই — ৪২২. name বাধ্যতামূলক (default নেই), তাই "type": "missing", "msg": "Field required". একাধিক field ভুল হলে detail list-এ সবগুলো একসাথে আসে — client একবারেই সব ঠিক করতে পারে। (আসল response-এ পাঠানো body-টিও input হিসেবে ফেরত আসে; এখানে ছোট রাখতে বাদ দিয়েছি।)
  5. "140" string — গৃহীত! pydantic-এর স্বাভাবিক “lax” অবস্থায় সংখ্যার মতো দেখতে string সংখ্যায় রূপান্তরিত হয়, তাই 201. কিন্তু "১৪০" বা "একশো চল্লিশ" হলে ৪২২. এই নমনীয়তা না চাইলে model-এ model_config = ConfigDict(strict=True) দিয়ে কঠোর অবস্থা চালু করা যায়।

Response model — কী ফেরত যাবে, তাও চুক্তি

-> Product return hint শুধু লেবেল নয়। FastAPI এটিকে response model হিসেবে ব্যবহার করে: ফেরত দেওয়া ডেটা ওই model দিয়ে যাচাই আর ছাঁকা হয়, আর /docs-এ response-এর আকার দেখানো হয়। ছাঁকার অংশটিই সবচেয়ে কাজের। ধরুন ভেতরের ডেটায় একটি cost_price (কেনা দাম) আছে, যা ক্রেতার দেখার কথা নয়:

class ProductPublic(BaseModel):
    id: int
    name: str
    price: float

DB = {7: {"id": 7, "name": "চাল", "price": 420.0, "cost_price": 380.0}}

@app.get("/products/{pid}", response_model=ProductPublic)
def get_p(pid: int):
    return DB[pid]          # cost_price-সহ পুরো dict ফেরত দিলাম

# GET /products/7  →  200  {"id": 7, "name": "চাল", "price": 420.0}

Handler পুরো dict ফেরত দিলেও response-এ cost_price নেই — model-এ যা নেই, তা বাইরে যায় না। ব্যবহারকারীর ডেটায় এটি আরও জরুরি: একটি User object-এ password-এর hash থাকে, কিন্তু UserPublic model-এ থাকবে শুধু নাম আর ইমেইল। ভুল করে গোপন তথ্য পাঠানোর বিরুদ্ধে এটি একটি দ্বিতীয় দেয়াল।

৬ · Status code, HTTPException আর পুরো CRUD API

অংশ ৪-এর get_product-এ একটি বাগ রেখে এসেছিলাম: products[product_id] চাবি না পেলে KeyError. FastAPI তখন কী করে? Exception ধরা না পড়লে client পায় 500 Internal Server Error — অর্থাৎ “সার্ভারের দোষ”। অথচ আসলে দোষ কারও নয়, প্রোডাক্টটি শুধু নেই; সঠিক উত্তর 404 Not Found. Status code-গুলো একটি ভাষা, আর client (মোবাইল অ্যাপ, অন্য প্রোগ্রাম) এই ভাষা পড়েই সিদ্ধান্ত নেয় — আবার চেষ্টা করবে, ব্যবহারকারীকে “পাওয়া যায়নি” দেখাবে, নাকি লগইন পেজে পাঠাবে।

Codeঅর্থকখন ফেরত দেবেন
200 OKসফলসাধারণ GET, PUT
201 Createdনতুন কিছু তৈরি হলোসফল POST
204 No Contentসফল, কিন্তু ফেরত দেওয়ার কিছু নেইসফল DELETE
400 Bad Requestrequest-ই ভাঙাব্যবসার নিয়ম ভাঙা, যেমন স্টকের বেশি অর্ডার
401 / 403পরিচয় নেই / অনুমতি নেইলগইন ছাড়া, বা অন্যের ডেটায় হাত দিলে
404 Not Foundএমন কিছু নেইভুল id
422ডেটা নিয়ম মানেনিFastAPI নিজেই দেয় (যাচাই ব্যর্থ)
500সার্ভারের ভেতরের ভুলকখনো ইচ্ছা করে নয় — এটি একটি বাগের লক্ষণ

নিয়মটি মনে রাখুন: ৪xx মানে client-এর দিক থেকে সমস্যা, ৫xx মানে সার্ভারের দিক থেকে। FastAPI-তে ইচ্ছা করে একটি ৪xx উত্তর দিতে HTTPException raise করুন — পর্ব ২৪-এর মতো একটি সাধারণ exception, যা FastAPI ধরে সঠিক response বানায়:

1@app.get("/products/{product_id}")2def get_product(product_id: int) -> Product:3    if product_id not in products:4        raise HTTPException(status_code=404,5                            detail="প্রোডাক্ট পাওয়া যায়নি")6    return products[product_id]
অবস্থা
product_id
products-এ আছে?
কোথায়
  1. Request পৌঁছাল। GET /products/99 — router মেলাল, "99" int-এ বদলাল, তাই যাচাই পাস। এখান পর্যন্ত সব ঠিক; সমস্যাটি ব্যবসার — এমন প্রোডাক্ট আছে কি না।
  2. আগে পরীক্ষা। if product_id not in products — পর্ব ১৩-এর guard clause. ভুল ঘটনাটি আগে সামলে বেরিয়ে যাওয়া, যাতে নিচের “সুখী পথ” পরিষ্কার থাকে।
  3. HTTPException raise. এটি সাধারণ Python exception — ফাংশন সঙ্গে সঙ্গে থেমে যায়, return-এ পৌঁছায় না। কোনো try/except লিখতে হয়নি। detail-এ যা দেবেন, client তা-ই দেখবে — তাই এখানে ভেতরের তথ্য (ফাইলের নাম, SQL) নয়, শুধু ব্যবহারকারীর বোঝার মতো বার্তা।
  4. FastAPI ধরে response বানায়। FastAPI-র ভেতরের একটি exception handler HTTPException ধরে, আর 404 status আর {"detail": "..."} body দিয়ে উত্তর পাঠায়। Client-এর অ্যাপ এখন জানে, “পাওয়া যায়নি” বার্তা দেখাতে হবে — আবার চেষ্টা করে লাভ নেই।
  5. সুখী পথ। GET /products/7 হলে পরীক্ষা পার হয়ে return, আর 200. লক্ষ করুন, KeyError-এর সম্ভাবনা আর নেই — আর থাকলেও, সেটি হতো 500, যা একটি বাগের সংকেত, ইচ্ছাকৃত উত্তর নয়।

পুরো CRUD — দোকানের প্রোডাক্ট API

ডেটা নিয়ে চারটি মৌলিক কাজ — Create, Read, Update, Delete — একসাথে CRUD. REST নামের নকশা-রীতিতে এগুলো HTTP method-এর সাথে মেলে: একটি “resource” (/products), আর method বলে কী করতে হবে। সব একসাথে, ডেটা আপাতত একটি dict-এ (server বন্ধ হলে মুছে যাবে — অংশ ৮-এ ডেটাবেস):

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field

app = FastAPI(title="দোকানের প্রোডাক্ট API")

class ProductIn(BaseModel):
    name: str = Field(min_length=1, max_length=50)
    price: float = Field(gt=0)
    stock: int = Field(default=0, ge=0)

class Product(ProductIn):
    id: int

products: dict[int, Product] = {
    7: Product(id=7, name="চাল (৫ কেজি)", price=420, stock=12),
}
next_id = 8

def find_or_404(product_id: int) -> Product:
    if product_id not in products:
        raise HTTPException(status_code=404, detail="প্রোডাক্ট পাওয়া যায়নি")
    return products[product_id]

@app.get("/products")                                       # Read (তালিকা)
def list_products(max_price: float | None = None, limit: int = 10) -> list[Product]:
    items = [p for p in products.values() if max_price is None or p.price <= max_price]
    return items[:limit]

@app.get("/products/{product_id}")                          # Read (একটি)
def get_product(product_id: int) -> Product:
    return find_or_404(product_id)

@app.post("/products", status_code=status.HTTP_201_CREATED) # Create
def create_product(data: ProductIn) -> Product:
    global next_id
    product = Product(id=next_id, **data.model_dump())
    products[next_id] = product
    next_id += 1
    return product

@app.put("/products/{product_id}")                          # Update
def update_product(product_id: int, data: ProductIn) -> Product:
    find_or_404(product_id)
    products[product_id] = Product(id=product_id, **data.model_dump())
    return products[product_id]

@app.delete("/products/{product_id}", status_code=204)      # Delete
def delete_product(product_id: int) -> None:
    find_or_404(product_id)
    del products[product_id]

এই API-তে পরপর কয়েকটি request পাঠালে (FastAPI 0.143 আর pydantic 2.14-এ TestClient দিয়ে যাচাই করা আসল উত্তর):

POST   /products  {"name": "ডাল", "price": 140}   → 201 {"name": "ডাল", "price": 140.0, "stock": 0, "id": 8}
GET    /products?max_price=200                   → 200 [{"name": "ডাল", "price": 140.0, "stock": 0, "id": 8}]
PUT    /products/8 {"name": "মসুর ডাল", "price": 150, "stock": 5}
                                                 → 200 {"name": "মসুর ডাল", "price": 150.0, "stock": 5, "id": 8}
DELETE /products/8                               → 204 (body নেই)
DELETE /products/8                               → 404 {"detail": "প্রোডাক্ট পাওয়া যায়নি"}

শেষ দুই লাইন লক্ষ করুন। একই DELETE দ্বিতীয়বার পাঠালে ৪০৪ — কিন্তু সার্ভারের অবস্থা দুইবারেই এক (প্রোডাক্ট ৮ নেই)। এই গুণকে বলে idempotent: একই request একবার পাঠানো আর দশবার পাঠানোর ফল সার্ভারের দিক থেকে এক। GET, PUT, DELETE idempotent হওয়ার কথা; POST নয় — দুবার পাঠালে দুটি প্রোডাক্ট তৈরি হয়। দুর্বল নেটওয়ার্কে মোবাইল অ্যাপ যখন request আবার পাঠায়, এই পার্থক্য বড় হয়ে ওঠে: bKash-এ টাকা পাঠানোর মতো কাজ দুবার হয়ে যাওয়া মানে বিপদ, তাই পেমেন্ট API-গুলো আলাদা “idempotency key” ব্যবহার করে।

একই ছাঁচে আরও দুটি API

🎓 পরীক্ষার ফল খোঁজা

GET /results/{roll} — roll: int path parameter, ফল না পেলে 404 "এই রোলের ফল নেই". শুধু পড়ার API, তাই একটি মাত্র endpoint. ফলের দিনে হাজারো মানুষ একসাথে খোঁজেন — তখন গতি আর cache-ই আসল প্রশ্ন, আর ডেটা বদলানোর কোনো endpoint বাইরে খোলা রাখা চলবে না।

✅ To-do তালিকা

TodoIn(title: str, done: bool = False) model, আর পুরো CRUD: GET /todos, POST /todos, PUT /todos/{id}, DELETE /todos/{id}. Query parameter ?done=false দিয়ে শুধু বাকি কাজগুলো। দোকানের API-র সাথে প্রায় হুবহু এক — ছাঁচটি একবার বুঝলে নতুন resource যোগ করা কয়েক মিনিটের কাজ।

৭ · Depends, async def আর টেস্টিং

Dependency injection — handler নিজে জিনিস বানায় না, চেয়ে নেয়

বাস্তব ব্যাকএন্ডে প্রায় প্রতিটি handler-এর কিছু “সরঞ্জাম” লাগে: একটি ডেটাবেস সংযোগ, বর্তমান ব্যবহারকারী কে, সেটিংস। প্রতিটি ফাংশনে নিজে নিজে সংযোগ খোলা আর বন্ধ করা মানে একই কোড বারবার, আর কোথাও বন্ধ করতে ভুলে যাওয়ার ঝুঁকি। FastAPI-র সমাধান dependency injection: handler শুধু বলে “আমার একটি db লাগবে”, আর ফ্রেমওয়ার্ক সেটি বানিয়ে দেয়, কাজ শেষে গুছিয়ে নেয়। পরীক্ষার ফলের API দিয়ে দেখুন:

1def get_db():2    db = ResultDB()           # সংযোগ খোলা3    try:4        yield db5    finally:6        db.close()            # সবসময় বন্ধ78@app.get("/results/{roll}")9def result(roll: int, db: ResultDB = Depends(get_db)):10    r = db.get_result(roll)11    if r is None:12        raise HTTPException(404, "এই রোলের ফল নেই")13    return r
একটি request-এর জীবন
request
db
ফল
  1. Handler চাহিদা জানায়। db: ResultDB = Depends(get_db) মানে: “এই parameter-এর মান পেতে get_db চালাও”। এটি path বা query parameter নয় — client এটি পাঠায় না; FastAPI নিজে জোগায়।
  2. Dependency চলে — yield পর্যন্ত। Request এলে FastAPI handler-এর আগে get_db() চালায়। এটি পর্ব ৩৪-এর generator: yield db-এ থেমে সংযোগটি তুলে দেয়। প্রতিটি request নিজের আলাদা db পায়।
  3. Handler কাজ করে। result(roll=123456, db=...) — handler-এর কাছে সংযোগ খোলা বা বন্ধের কোনো কোড নেই, শুধু ব্যবসার যুক্তি: ফল খোঁজা, ফেরত দেওয়া। 200.
  4. ভুল রোল — exception হলেও… /results/1-এ ফল নেই, HTTPException উঠল। Handler মাঝপথে থামল, কিন্তু db সংযোগ ঝুলে থাকবে না…
  5. …finally সবসময় চলে। Handler শেষ হলে — সফলভাবে হোক বা exception দিয়ে — FastAPI generator-কে শেষ পর্যন্ত চালায়, তাই finally: db.close() সফল আর ব্যর্থ — দুই ক্ষেত্রেই চলে। এটি পর্ব ৩৬-এর context manager-এর ধারণা। আর টেস্টে app.dependency_overrides[get_db] দিয়ে আসল db-র বদলে একটি নকল db বসানো যায় — dependency injection-এর সবচেয়ে বড় লাভ এটাই।

async def না def?

FastAPI-তে handler দুইভাবেই লেখা যায়, আর দুটিই ঠিক — কিন্তু নিয়ম না জানলে বিপদ:

  • async def — handler সরাসরি event loop-এ চলে (পর্ব ৪২)। ভেতরে শুধু await-যোগ্য কাজ করুন: async ডেটাবেস ড্রাইভার, httpx.AsyncClient দিয়ে অন্য API ডাকা, asyncio.sleep.
  • def — FastAPI handler-টিকে একটি আলাদা thread pool-এ চালায়, যাতে event loop আটকে না যায়। সাধারণ (blocking) লাইব্রেরি — sqlite3, requests, ফাইল পড়া — ব্যবহার করলে এটিই নিরাপদ।

সবচেয়ে সাধারণ ভুল: async def-এর ভেতরে একটি blocking call — যেমন time.sleep(2), requests.get(...), বা সাধারণ sqlite3 query. তখন পুরো event loop দুই সেকেন্ড আটকে থাকে, আর সেই সময় সব ব্যবহারকারীর request দাঁড়িয়ে থাকে। দশজন একসাথে এলে শেষজন অপেক্ষা করবেন বিশ সেকেন্ড। সন্দেহ হলে সাধারণ def লিখুন — সেটি কখনো এভাবে সবাইকে আটকায় না।

টেস্টিং — server না চালিয়েই

পর্ব ৩৯-এর pytest এখানে সরাসরি কাজে লাগে। FastAPI-র TestClient নেটওয়ার্ক বা আসল server ছাড়াই আপনার app-কে request পাঠায় — অংশ ২-এ আমরা যেমন নকল environ দিয়ে WSGI app ডেকেছিলাম, অনেকটা তেমন:

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_get_existing_product():
    r = client.get("/products/7")
    assert r.status_code == 200
    assert r.json()["name"] == "চাল (৫ কেজি)"

def test_missing_product_is_404():
    assert client.get("/products/999").status_code == 404

def test_negative_price_rejected():
    r = client.post("/products", json={"name": "ডাল", "price": -5})
    assert r.status_code == 422
$ pytest -q
...                                                    [100%]
3 passed

লক্ষ করুন, তিনটির মধ্যে দুটি টেস্ট ব্যর্থতার পথ যাচাই করছে — ৪০৪ আর ৪২২. ব্যাকএন্ডে সুখী পথ সাধারণত নিজে থেকেই কাজ করে; বাগ লুকিয়ে থাকে ভুল id, খালি field, ঋণাত্মক দাম আর অনুমতিহীন request-এ। (TestClient চালাতে httpx প্যাকেজ লাগে, যা fastapi[standard]-এর সাথে আসে।)

৮ · ডেটাবেস, নিরাপত্তা আর deployment — খেলনা থেকে আসল সার্ভিস

ডেটাবেসের ঝলক

আমাদের dict server বন্ধ হলেই মুছে যায়, আর একাধিক worker থাকলে প্রত্যেকের dict আলাদা। আসল ডেটা থাকে একটি database-এ। শুরুর জন্য সবচেয়ে সহজ পর্ব ২৭-এ দেখা SQLite — Python-এর সাথেই আসা sqlite3 module, একটি ফাইলেই পুরো ডেটাবেস। def handler-এ (thread pool, তাই blocking চলবে) একটি query দেখতে এমন:

import sqlite3

@app.get("/products/{product_id}")
def get_product(product_id: int):
    with sqlite3.connect("shop.db") as conn:
        row = conn.execute(
            "SELECT id, name, price FROM products WHERE id = ?", (product_id,)
        ).fetchone()
    if row is None:
        raise HTTPException(status_code=404, detail="প্রোডাক্ট পাওয়া যায়নি")
    return {"id": row[0], "name": row[1], "price": row[2]}

? চিহ্নটি গুরুত্বপূর্ণ: মানটি আলাদা করে পাঠানো হয় (parameterized query), f-string দিয়ে SQL-এ জোড়া হয় না — তাই SQL injection-এর পথ বন্ধ। (এখানে product_id আগেই int, কিন্তু string parameter-এ এই নিয়ম জীবন বাঁচায়।) প্রজেক্ট বড় হলে সাধারণত একটি ORM ব্যবহার হয় — SQL না লিখে Python class দিয়ে টেবিল। FastAPI-র নির্মাতারই বানানো SQLModel একই class-কে pydantic model আর ডেটাবেস টেবিল দুটোই বানায়; আরও প্রচলিত বিকল্প SQLAlchemy. টেবিলের নকশা, ORM আর migration-এর গভীরে যেতে দেখুন ORM প্যাটার্ন.

নিরাপত্তার মূল নিয়ম — রক্ষণাত্মক দৃষ্টিতে

আপনার API ইন্টারনেটে গেলে যে কেউ যেকোনো request পাঠাতে পারে — আপনার অ্যাপ দিয়ে নয়, সরাসরি। তাই প্রথম নিয়ম: client-কে কখনো বিশ্বাস করবেন না। মোবাইল অ্যাপে দাম ঋণাত্মক দেওয়া যায় না বলে সার্ভারে যাচাই বাদ দেওয়া চলবে না; কেউ অ্যাপ ছাড়াই {"price": -5} পাঠাতে পারেন। পর্ব ৫০-এ নিরাপদ Python নিয়ে বিস্তারিত আসবে; ব্যাকএন্ডের জন্য এখনই যা মানবেন:

Deployment — এক অনুচ্ছেদে

নিজের কম্পিউটার থেকে ইন্টারনেটে নিতে দরকার একটি সব সময় চালু থাকা কম্পিউটার: একটি VPS (ভাড়া করা Linux সার্ভার, যেখানে সব নিজে সাজান) অথবা একটি PaaS (প্ল্যাটফর্ম, যেখানে কোড দিলেই চালিয়ে দেয়)। সেখানে --reload ছাড়া uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 বা fastapi run চলে, প্রায়ই একটি Docker container-এর ভেতরে; সামনে থাকে Nginx বা Caddy-র মতো একটি reverse proxy, যে HTTPS-এর সার্টিফিকেট (যেমন Let's Encrypt-এর বিনামূল্যের সার্টিফিকেট) সামলায়। সব secret আর সেটিংস যায় environment variable-এ, আর log যায় একটি নির্দিষ্ট জায়গায়, যাতে রাত তিনটায় কিছু ভাঙলে সকালে কারণ খুঁজে পাওয়া যায়। Container, CI/CD আর secret ব্যবস্থাপনার বিস্তারিত পাবেন environment ও secret ব্যবস্থাপনা পর্বে।

C/C++ জানলে

C-তে একটি সার্ভার মানে নিজে socket(), bind(), listen(), accept() ডেকে বাইট পড়া, আর HTTP-র লেখা নিজে ভাঙা — আমাদের TCP সকেট পর্বে ঠিক এটাই করা হয়েছে। Python-এর ব্যাকএন্ডে সেই অংশটি করে Uvicorn বা Gunicorn-এর মতো server, আর আপনি থাকেন ওপরের স্তরে — Python object আর ফাংশন নিয়ে। মজার ব্যাপার, গতির জন্য Uvicorn-এর ভেতরেও C-তে লেখা অংশ (যেমন uvloop, httptools) ব্যবহার হতে পারে, আর pydantic 2-এর যাচাইয়ের কেন্দ্র লেখা Rust-এ।

পরীক্ষা ও ইন্টারভিউতে
  • WSGI আর ASGI-র পার্থক্য? — WSGI synchronous: প্রতিটি request একটি ফাংশন-ডাক; ASGI asynchronous: application একটি async ফাংশন, তাই একই process-এ অনেক অপেক্ষমাণ সংযোগ সামলানো যায়, WebSocket-ও।
  • Path আর query parameter-এর পার্থক্য? — path একটি নির্দিষ্ট resource চিহ্নিত করে (/products/7); query তালিকা ছাঁকে বা সাজায় (?max_price=200), সাধারণত ঐচ্ছিক।
  • FastAPI কখন ৪২২ দেয়? — যখন request-এর ডেটা type hint বা pydantic model-এর নিয়ম মানে না; handler তখন ডাকা হয় না।
  • PUT আর POST-এর পার্থক্য? — POST নতুন resource বানায় (idempotent নয়); PUT একটি নির্দিষ্ট resource প্রতিস্থাপন করে (idempotent).
  • ৪০৪ আর ৫০০-এর পার্থক্য? — ৪০৪: চাওয়া জিনিস নেই (client-এর দিক); ৫০০: সার্ভারের ভেতরে অপ্রত্যাশিত ভুল, অর্থাৎ একটি বাগ।

৯ · AI-এর যুগে — AI-এর লেখা API যাচাই করা

“একটি FastAPI CRUD API লিখে দাও” — AI কয়েক সেকেন্ডে লিখে দেবে, আর তা চলবেও। সমস্যা হলো, “চলে” আর “নিরাপদে, সঠিকভাবে চলে” এক জিনিস নয়। AI-এর প্রশিক্ষণের ডেটায় পুরোনো Flask আর FastAPI-র টিউটোরিয়াল প্রচুর, আর অনেক টিউটোরিয়াল ইচ্ছা করেই যাচাই আর নিরাপত্তা বাদ দেয়, যাতে উদাহরণ ছোট থাকে। ফলে কিছু ভুল বারবার দেখা যায়:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
pydantic 1-এর .dict(), orm_mode, @validator; বা @app.on_event("startup")পুরোনো API; deprecated বা নতুন সংস্করণে বদলে গেছে.model_dump(), from_attributes, @field_validator, lifespan
body নেয় dict বা request.json() দিয়েকোনো যাচাই নেই; যেকোনো আকারের ডেটা ঢোকেpydantic model আর Field-এর সীমা
ভুলেও 200 ফেরত — {"error": "not found"}client ভাবে সফল; অ্যাপ ভুল আচরণ করেHTTPException(404), সঠিক ৪xx
SECRET_KEY = "abc123" কোডেইgit-এ গেলেই ফাঁসenvironment variable
async def-এর ভেতরে requests, time.sleep, সাধারণ sqlite3event loop আটকে সবার request থামেasync লাইব্রেরি, নয়তো সাধারণ def
f-string দিয়ে SQL বানানোSQL injectionparameterized query বা ORM
allow_origins=["*"], debug চালু, traceback client-এঅপ্রয়োজনে দরজা খোলা, ভেতরের তথ্য ফাঁসনির্দিষ্ট domain; সাধারণ error বার্তা
নিজের বানানো token বা password-এর সাধারণ hashসহজে ভাঙা যায়পরীক্ষিত লাইব্রেরি, মানক OAuth2/JWT প্রবাহ

AI-কে ব্যাকএন্ড লিখতে বলার সময় শর্তগুলো আগেই দিন। নিচের কাঠামোটি কপি করে নিজের প্রজেক্ট বসান:

আমি Python 3.12+, FastAPI-র সাম্প্রতিক সংস্করণ আর pydantic 2 ব্যবহার করি। একটি ছোট দোকানের প্রোডাক্ট API লিখে দাও (তালিকা, একটি দেখা, যোগ, বদল, মুছে ফেলা)। শর্ত: ১. শুধু pydantic 2-এর API: model_dump, field_validator, ConfigDict. pydantic 1-এর .dict(), orm_mode, @validator ব্যবহার করবে না; startup-এর জন্য lifespan. ২. প্রতিটি body-র জন্য আলাদা input model (id ছাড়া) আর output model; Field দিয়ে সীমা দাও (দাম > 0, নামের দৈর্ঘ্য ১–৫০)। ৩. সঠিক status code: তৈরি হলে 201, মুছলে 204, না পেলে HTTPException(404). কোনো error-এ 200 ফেরত দেবে না। ৪. secret বা ডেটাবেসের ঠিকানা কোডে লিখবে না; environment variable থেকে পড়বে। ৫. blocking লাইব্রেরি (sqlite3, requests) থাকলে handler সাধারণ def রাখো; async def-এর ভেতরে কোনো blocking call নয়। ৬. SQL লাগলে শুধু parameterized query. ৭. client-এর কাছে কোনো traceback বা ভেতরের তথ্য যাবে না। ৮. TestClient আর pytest দিয়ে টেস্ট দাও — অন্তত একটি 404 আর একটি 422-এর টেস্টসহ। শেষে বলো কোন অংশ production-এর আগে বদলাতে হবে।

AI-এর লেখা ব্যাকএন্ড পেলে — যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১CGI থেকে WSGI, তারপর ASGI — ওয়েব সার্ভার আর Python কোডের মাঝের “চুক্তি” তিনবার কেন বদলাল?

১৯৯০-এর দশকের শুরুতে ওয়েব ছিল প্রায় পুরোটাই স্থির HTML ফাইল। পেজে কিছু হিসাব করে দেখাতে চাইলে (ফর্মের উত্তর, খোঁজার ফল) একটি উপায় দরকার ছিল, যাতে web server একটি প্রোগ্রাম চালিয়ে তার আউটপুট ব্রাউজারে পাঠাতে পারে। ১৯৯৩ সালের দিকে NCSA-র web server-কে ঘিরে এল CGI (Common Gateway Interface): প্রতিটি request-এ server একটি নতুন process চালায়, request-এর তথ্য দেয় environment variable আর stdin দিয়ে, আর প্রোগ্রাম stdout-এ যা লেখে সেটিই response. পরে এটি RFC 3875 (২০০৪) হিসেবে লিখিত হয়। নিয়মটি এত সরল যে যেকোনো ভাষায় — C, Perl, Python — CGI প্রোগ্রাম লেখা যেত।

দাম ছিল গতি। প্রতিটি request-এ নতুন process মানে প্রতিবার Python interpreter চালু করা, module import, ডেটাবেস সংযোগ — তারপর সব ফেলে দেওয়া। ভিড় বাড়লে সার্ভার হাঁপিয়ে ওঠে। FastCGI, Apache-র mod_python-এর মতো সমাধান এল, যেখানে প্রোগ্রাম চালু থাকে, কিন্তু প্রতিটি Python ফ্রেমওয়ার্ক ভিন্ন ভিন্ন server-এর সাথে ভিন্ন ভিন্নভাবে জুড়ত। একটি ফ্রেমওয়ার্ক বাছাই মানে server-ও বাছাই — এক জট।

২০০৩ সালে Phillip J. Eby লেখেন PEP 333 — WSGI. ধারণাটি ছিল একটি মাঝের সরল চুক্তি: application মানে একটি callable, যা environ dict আর start_response নেয় আর বাইটের iterable ফেরত দেয় — অংশ ২-এর runnable উদাহরণে ঠিক এটাই দেখেছেন। এতে যেকোনো WSGI server (পরে Gunicorn, uWSGI, mod_wsgi) যেকোনো WSGI ফ্রেমওয়ার্ক (Django, Flask) চালাতে পারল। Python 3-এর bytes আর str-এর পার্থক্য মেটাতে ২০১০ সালে এল PEP 3333. WSGI-র সাফল্য অন্য ভাষাকেও প্রভাবিত করেছে — Ruby-র Rack এই ধারণা থেকেই অনুপ্রাণিত বলে জানা যায়।

কিন্তু WSGI-র নকশা ছিল “একটি request, একটি ফাংশন-ডাক, তারপর শেষ”। ২০১০-এর দশকে WebSocket, দীর্ঘ সংযোগ আর অন্য সার্ভিসের উত্তরের অপেক্ষা সাধারণ হয়ে উঠল, আর Python-এ এল asyncio (৩.৪) আর async/await (৩.৫)। Django Channels প্রজেক্টের কাজের ভেতর থেকে Andrew Godwin-এর নেতৃত্বে তৈরি হয় ASGI: application একটি async callable, যা scope, receive আর send নিয়ে একটি সংযোগে একাধিক বার্তা আদান-প্রদান করতে পারে। Uvicorn, Starlette, FastAPI এর ওপর দাঁড়িয়ে; Django ৩.০ (২০১৯) থেকে ASGI সমর্থন করে।

মূল উপলব্ধি: প্রতিটি পরিবর্তন এসেছে একটি নির্দিষ্ট ব্যথা থেকে — CGI-তে প্রতি request-এ process-এর খরচ, WSGI-র আগে server আর ফ্রেমওয়ার্কের জট, WSGI-তে অপেক্ষমাণ সংযোগের অদক্ষতা। আর প্রতিবার সমাধান ছিল একটি ছোট, মানক চুক্তি, যা দুই পক্ষকে স্বাধীনভাবে উন্নত হতে দেয়।

প্র ০২Django জন্মেছিল একটি স্থানীয় সংবাদপত্রের অফিসে। খবরের ঘরের তাড়া কীভাবে একটি ফ্রেমওয়ার্কের দর্শন গড়ে দিল?

২০০৩ সালের দিকে যুক্তরাষ্ট্রের Kansas অঙ্গরাজ্যের Lawrence শহরের পত্রিকা Lawrence Journal-World-এর ওয়েব দলে কাজ করতেন Adrian Holovaty আর Simon Willison. খবরের ঘরের কাজের ধরন সফটওয়্যারের জন্য কঠিন: সম্পাদক সকালে একটি নতুন বিভাগ চান — নির্বাচনের ফল, খেলার স্কোর, স্থানীয় অনুষ্ঠানের তালিকা — আর সেটি চাই সেদিন বিকেলের মধ্যেই, কখনো কয়েক ঘণ্টায়। তাঁরা PHP-তে কাজ করতেন, কিন্তু প্রতিটি নতুন সাইটে একই কোড — ডেটাবেস, URL, ফর্ম, লগইন — আবার লিখতে লিখতে ক্লান্ত হয়ে Python-এ নিজেদের একটি কাঠামো বানাতে শুরু করেন। পরে Jacob Kaplan-Moss-সহ আরও কয়েকজন যোগ দেন।

২০০৫ সালের জুলাইয়ে তাঁরা এটিকে open source হিসেবে প্রকাশ করেন, নাম দেন জ্যাজ গিটারবাদক Django Reinhardt-এর নামে। ২০০৮ সালে Django 1.0 আসে, আর একই বছর গঠিত হয় Django Software Foundation, যা আজও প্রজেক্টটির দেখাশোনা করে।

খবরের ঘরের ছাপ Django-র প্রতিটি অংশে দেখা যায়। তাড়াহুড়োর জন্য সবকিছু এক বাক্সে: ORM, URL routing, template, ফর্ম, authentication — বাইরে খুঁজতে হয় না (“batteries included”). সবচেয়ে বিখ্যাত উদাহরণ admin প্যানেল: model লিখলেই একটি সম্পাদনা-ইন্টারফেস তৈরি হয়ে যায়, যাতে প্রোগ্রামার কোড লেখার সাথে সাথেই সম্পাদক ও রিপোর্টাররা ডেটা ঢোকাতে শুরু করতে পারেন। তাদের স্লোগানও তাই — “The web framework for perfectionists with deadlines”. আর খবরের সাইটে আক্রমণের ঝুঁকি বেশি বলে, CSRF সুরক্ষা আর template-এ স্বয়ংক্রিয় escaping-এর মতো নিরাপত্তা শুরু থেকেই চালু থাকে।

এই দর্শনের একটি দাম আছে, আর সেখান থেকেই অন্য ফ্রেমওয়ার্কের জায়গা। Django একটি নির্দিষ্ট পথে চলতে চায় — নিজের ORM, নিজের template, নিজের প্রজেক্ট-কাঠামো। পথটি মানলে কাজ খুব দ্রুত; না মানলে লড়াই। শুধু JSON API চাইলে Django-র অনেক অংশ অপ্রয়োজনীয় লাগতে পারে — যদিও Django REST framework (Tom Christie-র তৈরি, যিনি পরে Starlette আর Uvicorn-ও বানান) এই কাজে বহুল ব্যবহৃত। Instagram-এর মতো খুব বড় সার্ভিসও Django দিয়ে শুরু করেছিল বলে সুপরিচিত।

মূল উপলব্ধি: একটি ফ্রেমওয়ার্কের নকশা তার জন্মের পরিবেশের সমস্যার উত্তর। Django-র “সব একসাথে” আর admin প্যানেল এসেছে সময়সীমার চাপ থেকে। ফ্রেমওয়ার্ক বাছার সময় প্রশ্ন করুন: আমার সমস্যা কি তার জন্মের সমস্যার মতো?

প্র ০৩Flask নাকি একটি এপ্রিল ফুলের রসিকতা থেকে জন্মেছিল। ঘটনাটি কী, আর “micro-framework” ধারণা কী শেখায়?

Armin Ronacher ছিলেন অস্ট্রিয়ার একজন তরুণ Python প্রোগ্রামার, আর Pocoo নামের একটি open source দলের সদস্য। এই দল আগেই দুটি লাইব্রেরি বানিয়েছিল: Werkzeug (WSGI-র সহায়ক টুলকিট — request, response, routing, debugger) আর Jinja2 (HTML template ইঞ্জিন)। সে সময় Python-এ “ছোট” ফ্রেমওয়ার্কের একটি জোয়ার চলছিল — এক ফাইলেই পুরো ফ্রেমওয়ার্ক, যেমন web.py আর Bottle.

২০১০ সালের ১লা এপ্রিল Armin একটি রসিকতা প্রকাশ করেন: “Denied” নামের একটি “micro-framework”, যা আসলে Werkzeug আর Jinja2-কে একটি ফাইলে গুঁজে দেওয়া, সাথে একটি বাড়াবাড়ি প্রচারের ওয়েবসাইট আর ভিডিও। উদ্দেশ্য ছিল এক-ফাইল ফ্রেমওয়ার্কের হুজুগ নিয়ে মজা করা। কিন্তু অপ্রত্যাশিতভাবে অনেকে রসিকতাটিকে গুরুত্ব দিয়ে নেন — আসলেই তাঁরা এমন একটি ছোট, পরিষ্কার ফ্রেমওয়ার্ক চাইছিলেন। Armin পরে এই অভিজ্ঞতার কথা নিজেই লিখেছেন, আর সেই আগ্রহ দেখে কয়েক সপ্তাহের মধ্যে একটি সত্যিকারের প্রজেক্ট হিসেবে প্রকাশ করেন Flask. (রসিকতার খুঁটিনাটি বিভিন্ন বর্ণনায় একটু ভিন্ন হয়; মূল কাহিনি — এপ্রিল ফুলের প্রকল্পে মানুষের সত্যিকারের আগ্রহ থেকে Flask — Armin-এর নিজের বক্তব্যে আছে।)

“Micro” মানে ছোট অ্যাপ নয়, বরং ছোট কেন্দ্র। Flask নিজে দেয় routing, request/response, template আর একটি পরিষ্কার @app.route decorator — FastAPI-র @app.get এই ধরনেরই উত্তরসূরি। ডেটাবেস, ফর্ম, লগইন — কোনোটিই বাধ্যতামূলক নয়; extension দিয়ে যোগ করুন, অথবা নিজের পছন্দের লাইব্রেরি নিন। এটি Django-র ঠিক উল্টো দর্শন: “সিদ্ধান্ত আমরা নিয়েছি” বনাম “সিদ্ধান্ত আপনার”। ছোট প্রজেক্টে আর শেখার জন্য এটি দারুণ; বড় প্রজেক্টে দলকে নিজেদের কাঠামো আর নিয়ম নিজেদেরই ঠিক করতে হয়। Flask 1.0 এসেছে ২০১৮ সালে, আর এখন এটি Pallets নামের একটি সম্প্রদায়-চালিত সংগঠনের তত্ত্বাবধানে।

মূল উপলব্ধি: একটি রসিকতা সফল হয়েছিল কারণ এটি একটি সত্যিকারের চাহিদা ছুঁয়ে গিয়েছিল — ছোট, বোঝা যায় এমন, নমনীয় সরঞ্জাম। “Micro-framework” শেখায়, কম জিনিস দেওয়াও একটি নকশা-সিদ্ধান্ত, আর তার দামও আছে: স্বাধীনতার সাথে আসে দায়িত্ব।

প্র ০৪FastAPI প্রায় নতুন কিছু আবিষ্কার করেনি — Starlette আর pydantic-এর ওপর দাঁড়িয়ে। তাহলে কয়েক বছরের মধ্যে এত জনপ্রিয় হলো কীভাবে?

Sebastián Ramírez (অনলাইনে “tiangolo”) কলম্বিয়ার একজন প্রোগ্রামার, যিনি বিভিন্ন কোম্পানিতে API আর ডেটা-ভিত্তিক সিস্টেম বানাতেন। বছরের পর বছর তিনি Flask আর তার নানা extension, Django REST framework, আর অন্যান্য টুল মিলিয়ে কাজ করেছেন — যাচাইয়ের জন্য একটি লাইব্রেরি, ডকুমেন্টেশনের জন্য আরেকটি, serialization-এর জন্য আরেকটি, আর তিনটির মধ্যে একই তথ্য তিনবার লেখা। FastAPI-র ডকুমেন্টেশনে তিনি নিজেই একটি দীর্ঘ “Alternatives, Inspiration and Comparisons” অংশ লিখেছেন, যেখানে কোন ধারণা কোথা থেকে নেওয়া, তা খোলাখুলি স্বীকার করা।

২০১৮ সালের ডিসেম্বরে তিনি FastAPI প্রকাশ করেন। নিচের দুটি স্তর অন্যদের তৈরি। Starlette — Tom Christie-র লেখা একটি হালকা ASGI toolkit, যা routing, request/response, WebSocket আর middleware দেয়; FastAPI আসলে Starlette-এর একটি subclass. pydantic — Samuel Colvin-এর লাইব্রেরি (২০১৭), যা type hint দেখে চালানোর সময় ডেটা যাচাই আর রূপান্তর করে। ২০২৩ সালে pydantic 2-এ যাচাইয়ের কেন্দ্র Rust-এ নতুন করে লেখা হয় (pydantic-core), যাতে গতি অনেক গুণ বাড়ে।

তাহলে FastAPI-র নিজের অবদান কী? একটি ধারণা: একই type hint থেকে সব. product_id: int একবার লিখলেই (১) editor autocomplete আর mypy যাচাই পায়, (২) চালানোর সময় রূপান্তর আর ৪২২ পায়, (৩) OpenAPI বর্ণনা আর /docs পায়। তথ্য একবার লেখা, তিন জায়গায় কাজ — তাই ডকুমেন্টেশন আর কোড কখনো আলাদা হয়ে পড়ে না। এর সাথে যোগ হয় Depends-এর dependency injection আর async-এর স্বাভাবিক সমর্থন। আর সময়টিও অনুকূলে ছিল: Python 3.6-এর পর type hint সাধারণ হয়ে উঠছিল (পর্ব ৩৭), asyncio পরিণত হচ্ছিল, আর machine learning মডেলকে API হিসেবে চালানোর চাহিদা বিস্ফোরিত হচ্ছিল — ডেটা বিজ্ঞানীরা Python জানেন, তাঁদের দরকার ছিল দ্রুত একটি নির্ভরযোগ্য API. অসাধারণ বিস্তারিত ডকুমেন্টেশনও জনপ্রিয়তার একটি বড় কারণ হিসেবে প্রায়ই উল্লেখ হয়।

একটি সতর্কতাও আছে। প্রজেক্টটি দীর্ঘদিন মূলত একজন রক্ষণাবেক্ষণকারীর ওপর নির্ভর করেছে, আর সংস্করণ এখনো 0.x — ছোট সংস্করণের ধাপেও কখনো কিছু বদলায়। তাই production-এ সংস্করণ নির্দিষ্ট করে রাখা (পর্ব ২৩-এর pinning) আর release notes পড়া জরুরি। Sebastián পরে একই দর্শনে SQLModel আর Typer (CLI-এর জন্য — পর্ব ৪৯-এ CLI টুলের প্রসঙ্গে মনে রাখার মতো) বানিয়েছেন।

মূল উপলব্ধি: উদ্ভাবন সবসময় নতুন যন্ত্রাংশ নয়; কখনো ভালো যন্ত্রাংশগুলো একটি পরিষ্কার ধারণার চারপাশে জোড়া লাগানো। FastAPI-র ধারণা — “একটি type hint, তিনটি কাজ” — পুনরাবৃত্তি সরিয়েছে, আর সেটিই তার জনপ্রিয়তার মূলে।

প্র ০৫REST-ই কি API বানানোর একমাত্র উপায়? GraphQL কেন এল, আর কখন কোনটি বাছবেন?

REST শব্দটি এসেছে Roy Fielding-এর ২০০০ সালের PhD গবেষণাপত্র থেকে (University of California, Irvine). Fielding নিজে HTTP/1.1 মানকের অন্যতম লেখক ছিলেন, আর তাঁর গবেষণায় তিনি ব্যাখ্যা করেন কেন ওয়েব এত বড় হয়েও টিকে আছে: সব কিছু “resource” (ঠিকানা দিয়ে চিহ্নিত), অল্প কয়েকটি মানক কাজ (HTTP method), প্রতিটি request নিজেই সম্পূর্ণ (stateless — সার্ভার আগের কথা মনে রাখে না), আর উত্তর cache করা যায়। আজ “REST API” বলতে সাধারণত যা বোঝায় — GET /products/7, POST /products, JSON — তা Fielding-এর পুরো তত্ত্বের একটি সরল, ব্যবহারিক রূপ। এই পর্বের দোকানের API এই ধরনেরই।

REST-এর একটি পরিচিত ব্যথা আছে। ধরুন মোবাইল অ্যাপের একটি পর্দায় দেখাতে হবে একজন ক্রেতা, তাঁর শেষ পাঁচটি অর্ডার, আর প্রতিটি অর্ডারের প্রোডাক্টের নাম। REST-এ এর মানে হতে পারে অনেকগুলো আলাদা request (/customers/5, /customers/5/orders, তারপর প্রতিটি প্রোডাক্ট) — একে বলে under-fetching; আবার প্রতিটি উত্তরে এমন অনেক field আসে যা পর্দায় লাগে না — over-fetching. ধীর মোবাইল নেটওয়ার্কে দুটিই কষ্টের। Facebook ২০১২ সালে তাদের মোবাইল অ্যাপের জন্য ঠিক এই সমস্যা সমাধানে ভেতরে ভেতরে বানায় GraphQL, আর ২০১৫ সালে প্রকাশ্যে আনে; ২০১৮ সালে এটি Linux Foundation-এর অধীনে GraphQL Foundation-এ যায়।

GraphQL-এ সাধারণত একটিই endpoint, আর client একটি query-তে বলে দেয় ঠিক কোন কোন field চায়, সম্পর্কসহ — সার্ভার ঠিক সেই আকারের JSON ফেরত দেয়। একটি schema-তে সব type লেখা থাকে, তাই এখানেও স্বয়ংক্রিয় ডকুমেন্টেশন আর যাচাই আছে। কিন্তু দামও আছে: যেহেতু client যেকোনো আকারের query পাঠাতে পারে, একটি অতিরিক্ত গভীর query সার্ভারকে ডুবিয়ে দিতে পারে, তাই query-র জটিলতার সীমা লাগে; HTTP-র সাধারণ cache সহজে কাজ করে না; আর ছোট API-তে পুরো কাঠামোটি বাড়াবাড়ি। Python-এ Strawberry বা Graphene-এর মতো লাইব্রেরি দিয়ে GraphQL সার্ভার বানানো যায়, FastAPI-র সাথেও।

বাছাইয়ের সরল নিয়ম: অল্প কয়েকটি স্পষ্ট resource, অনেক ধরনের client, cache গুরুত্বপূর্ণ — REST. অনেক সম্পর্কযুক্ত ডেটা, প্রতিটি পর্দার আলাদা চাহিদা, frontend দল দ্রুত বদলাতে চায় — GraphQL বিবেচনা করুন। বিস্তারিত তুলনা দেখুন GraphQL বনাম REST পর্বে।

মূল উপলব্ধি: API-র নকশা নির্ভর করে কে ব্যবহার করবে আর কোন নেটওয়ার্কে তার ওপর। REST ওয়েবের নিজের নীতি থেকে এসেছে, GraphQL এসেছে মোবাইল অ্যাপের ব্যথা থেকে — কোনোটিই সব ক্ষেত্রে সেরা নয়।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।

  1. Status code অনুমান করুন: অংশ ৬-এর পুরো CRUD API চালু আছে, আর শুধু প্রোডাক্ট ৭ আছে। প্রতিটির status code বলুন: (ক) GET /products/7, (খ) GET /products/seven, (গ) POST /products body {"name": "", "price": 50}, (ঘ) PUT /products/42 বৈধ body সহ, (ঙ) DELETE /products/7, (চ) PATCH /products/7.

    (ক) 200 — আছে। (খ) 422 — "seven" int নয়; handler ডাকাই হয় না। (গ) 422 — name-এর min_length=1 ভাঙা হয়েছে (খালি string-ও বৈধ string, কিন্তু নিয়ম মানেনি)। (ঘ) 404 — body ঠিক আছে বলে যাচাই পার হলো, কিন্তু find_or_404 প্রোডাক্ট ৪২ খুঁজে পেল না। ক্রম লক্ষ করুন: আগে যাচাই, পরে ব্যবসার পরীক্ষা — তাই body ভুল হলে ৪০৪-এর আগে ৪২২ আসত। (ঙ) 204 — মুছে গেল, body নেই। (চ) 405 Method Not Allowed — path মেলে, কিন্তু ওই path-এ PATCH-এর কোনো route নেই।

  2. বাংলা অঙ্কের রহস্য: অংশ ১-এর runnable ছোট router-এ "/products/৭" পাঠালে কী আসবে? FastAPI-তে একই request পাঠালে কী আসে? পার্থক্যের কারণ কী, আর একটি সত্যিকারের দোকানের জন্য কোন আচরণ আপনি বাছবেন, কেন?

    ছোট router-এ 200 আর চালের তথ্য — কারণ "৭".isdecimal() হলো True আর int("৭") দেয় 7; Python Unicode-এর সব দশমিক অঙ্ক মানে। FastAPI-তে 422 (int_parsing), কারণ pydantic 2-এর রূপান্তর শুধু ASCII অঙ্ক মানে। বাছাই নির্ভর করে ব্যবহারকারীর ওপর: বাংলা কিবোর্ডে কেউ রোল নম্বর বা ফোন নম্বর টাইপ করলে বাংলা অঙ্ক আসতেই পারে। ভালো নকশা হলো সীমানায় একটি স্পষ্ট নিয়ম — frontend-এ বা একটি pydantic validator-এ বাংলা অঙ্ককে ইংরেজিতে বদলানো — আর সেই নিয়মের একটি টেস্ট লেখা। মূল শিক্ষা: নিজে লেখা যাচাই আর লাইব্রেরির যাচাই এক রকম আচরণ করবে, এমন ধরে নেবেন না।

  3. AI-এর কোডে বাগ খুঁজুন: একটি AI এই endpoint দিয়েছে। অন্তত চারটি সমস্যা খুঁজুন: @app.post("/login") async def login(body: dict): → ভেতরে user = requests.get(f"{AUTH_URL}/users/{body['email']}").json(), if user["password"] == body["password"]: return {"token": "secret123"}, return {"error": "wrong password"}.

    (১) body: dict — কোনো যাচাই নেই; email না থাকলে KeyError → ৫০০. একটি LoginIn pydantic model লাগবে। (২) async def-এর ভেতরে requests.get — blocking call, পুরো event loop আটকাবে; httpx.AsyncClient দিয়ে await, নয়তো সাধারণ def. (৩) password সরাসরি তুলনা — মানে কোথাও password সাধারণ লেখা হিসেবে জমা আছে; password রাখতে হয় একটি মানক password-hashing লাইব্রেরি দিয়ে, আর তুলনাও সেই লাইব্রেরির ফাংশন দিয়ে। (৪) "secret123" — hard-coded, সবার জন্য একই token; মানক JWT/OAuth2 প্রবাহ আর environment variable-এর secret দরকার। (৫) ভুল password-এ 200 আর {"error": ...} — হওয়া উচিত HTTPException(401). (৬) user না পেলে user["password"] ভাঙবে; আর “user নেই” ও “password ভুল”-এর জন্য আলাদা বার্তা দিলে আক্রমণকারী জেনে যায় কোন ইমেইল নিবন্ধিত — একই সাধারণ বার্তা দিন।

  4. একটি endpoint লিখুন (একমাত্র কোড লেখার কাজ): পরীক্ষার ফলের API-তে GET /results যোগ করুন, যা query parameter min_gpa (float, ০ থেকে ৫, ঐচ্ছিক) নেয় আর সেই GPA বা বেশি পাওয়া রোলগুলোর তালিকা দেয়। ডেটা: results = {123456: 5.0, 123457: 4.25, 123458: 3.5}. min_gpa=7 পাঠালে ৪২২ আসা উচিত।

    একটি সম্ভাব্য সমাধান:

    from typing import Annotated
    from fastapi import FastAPI, Query
    
    app = FastAPI()
    results = {123456: 5.0, 123457: 4.25, 123458: 3.5}
    
    @app.get("/results")
    def list_results(min_gpa: Annotated[float | None, Query(ge=0, le=5)] = None):
        return [{"roll": r, "gpa": g} for r, g in results.items()
                if min_gpa is None or g >= min_gpa]
    
    # GET /results?min_gpa=4   → 200 [{"roll": 123456, "gpa": 5.0}, {"roll": 123457, "gpa": 4.25}]
    # GET /results?min_gpa=7   → 422 ("Input should be less than or equal to 5")

    Annotated[float | None, Query(ge=0, le=5)] হলো query parameter-এ সীমা বসানোর আধুনিক ধরন — type আর অতিরিক্ত নিয়ম একসাথে (pydantic model-এর Field-এর মতো)। পুরোনো কোডে দেখবেন min_gpa: float | None = Query(None, ge=0, le=5); সেটিও চলে। ফেরত তালিকার ক্রম dict-এর insertion order অনুযায়ী (পর্ব ১৬)। একটি test_min_gpa_out_of_range টেস্ট লিখে ৪২২ যাচাই করলে কাজটি সম্পূর্ণ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমFastAPI অ্যাপে GET /products/7 request আসার পর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📨 Uvicorn নেটওয়ার্ক থেকে request-এর বাইট পড়ে
  2. 🧭 Router path-টি /products/{product_id} route-এর সাথে মেলায়
  3. 🔎 type hint অনুযায়ী "7" থেকে int 7 হয়
  4. ⚙️ handler ফাংশন চলে আর ফল ফেরত দেয়
  5. 📦 ফল JSON-এ রূপান্তরিত হয়ে status code-সহ client-এর কাছে যায়

FastAPI-তে @app.get("/products/{product_id}") আর def get_product(product_id: int) সংজ্ঞায়িত। Client যদি GET /products/abc পাঠায়, কী ঘটে?

  • handler চলে আর ভেতরে ValueError হয়ে 500 আসে
  • 404 Not Found আসে, কারণ এমন path নেই
  • handler ডাকা হয় না; 422 আর কোন field-এ কী ভুল তার বিবরণ আসে
  • product_id-তে "abc" string হিসেবেই handler চলে

WSGI আর ASGI-র মূল পার্থক্য কোনটি?

  • WSGI Python-এর জন্য, ASGI JavaScript-এর জন্য
  • WSGI synchronous ফাংশন-ডাকের চুক্তি; ASGI async, অপেক্ষার সময় অন্য request এগোতে পারে
  • WSGI একটি ডেটাবেস, ASGI একটি web server
  • WSGI Python 3-এ আর চলে না, তাই ASGI এসেছে

একটি REST API-তে নতুন resource সফলভাবে তৈরি হলে কোন status code সবচেয়ে উপযুক্ত?

  • 204 No Content
  • 200 OK
  • 422 Unprocessable Content
  • 201 Created

FastAPI-তে একটি async def endpoint-এর ভেতরে time.sleep(2) লিখলে কী সমস্যা হয়?

  • event loop আটকে যায়, তাই সেই সময় অন্য সব request-ও অপেক্ষা করে
  • SyntaxError আসে, কারণ async def-এ time.sleep নিষেধ
  • FastAPI নিজে থেকে এটিকে asyncio.sleep-এ বদলে দেয়
  • শুধু সেই request দুই সেকেন্ড দেরি করে, অন্যদের কোনো প্রভাব নেই

একটি API-তে ডেটাবেসের password কোথায় রাখা সবচেয়ে নিরাপদ অভ্যাস?

  • main.py-এর শুরুতে একটি constant হিসেবে
  • environment variable-এ, কোড সেখান থেকে পড়ে
  • frontend-এর JavaScript-এ, যাতে অ্যাপ সরাসরি ডেটাবেসে ঢুকতে পারে
  • একটি ফাংশনের docstring-এ, যাতে দলের সবাই জানে

FastAPI-তে একটি handler-এর parameter db = Depends(get_db), যেখানে get_db একটি generator যা yield db করে আর finally-এ db.close() ডাকে। handler যদি HTTPException(404) raise করে, তাহলে কী হয়?

  • db.close() আর চলে না, সংযোগ খোলা থেকে যায়
  • client 500 পায়, কারণ dependency-র ভেতরে exception হয়েছে
  • client 404 পায়, আর finally-র db.close() চলে
  • পরের সব request-এ একই বন্ধ db পাঠানো হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ব্যাকএন্ড = অপেক্ষারত কাউন্টার: request → router → handler → response. ফ্রেমওয়ার্ক প্লাম্বিং সামলায়; server (Uvicorn, Gunicorn) আর app কথা বলে WSGI বা ASGI চুক্তিতে।
  2. ফ্রেমওয়ার্ক বাছাই: পুরো ওয়েবসাইট আর admin — Django; ছোট ও নমনীয় — Flask; JSON API, type hint, async — FastAPI.
  3. Type hint-ই যাচাই: path (/products/{id}) আর query (?max_price=) parameter-এর hint থেকে রূপান্তর আর ৪২২; pydantic model দিয়ে body; response model দিয়ে বাইরে যাওয়া ডেটা ছাঁকা; /docs নিজেই তৈরি।
  4. সঠিক status code: ২০১ তৈরি, ২০৪ মুছে ফেলা, না পেলে HTTPException(404); ৪xx client-এর দিক, ৫০০ মানে বাগ। Depends দিয়ে সরঞ্জাম জোগানো; blocking কাজ থাকলে সাধারণ def; TestClient দিয়ে ব্যর্থতার পথও টেস্ট।
  5. নিরাপত্তা: client-কে বিশ্বাস নয়, traceback বাইরে নয়, secret environment variable-এ, parameterized query, CORS নির্দিষ্ট domain-এ, authentication পরীক্ষিত লাইব্রেরিতে, আর শুধু HTTPS.

পরের পর্বে: এতক্ষণ অন্যদের request-এর উত্তর দিয়েছেন। এবার Python দিয়ে নিজের প্রতিদিনের কাজ করিয়ে নেবেন — শত শত ফাইলের নাম বদলানো, রিপোর্ট বানানো, আর argparse দিয়ে এমন কমান্ড-লাইন টুল, যা অন্যরাও ব্যবহার করতে পারে: অটোমেশন আর CLI টুল.

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান