অধ্যায় 4 · আপনার প্রথম ব্যাকএন্ড
ব্যাকএন্ড কী করে: রিকোয়েস্ট, রাউট ও রেসপন্স
- পৃষ্ঠা 16 / 22
- 20 মিনিট পড়া
গত কয়েকটা পাতায় আপনি ছিলেন ক্লায়েন্ট: curl আর requests দিয়ে BitByte Practice API-তে রিকোয়েস্ট পাঠিয়েছেন, আর সে উত্তর দিয়েছে। এবার আপনি অন্য পাশে যাবেন। ব্যাকএন্ড (backend) হলো সেই প্রোগ্রাম, যা সার্ভারে বসে রিকোয়েস্টের অপেক্ষা করে, প্রতিটা রিকোয়েস্টের মানে বোঝে, আসল কাজটা করে (ডেটাবেস পড়া, মডেল চালানো, কোনো LLM প্রোভাইডারকে কল করা) আর রেসপন্স ফেরত পাঠায়। আপনি যে AI প্রোডাক্টই বানান, তাতে একটা ব্যাকএন্ড থাকবে: চ্যাট উইন্ডো, মোবাইল অ্যাপ বা Slack বট, সবাই আসলে তার ক্লায়েন্ট।
ফ্রেমওয়ার্ক ব্যবহারের আগে আপনি শুধু Python-এর স্ট্যান্ডার্ড লাইব্রেরি দিয়ে একটা ছোট্ট ব্যাকএন্ড লিখবেন। এটা ইচ্ছে করেই একটু আনাড়ি। নিজের হাতে URL পার্স করা, রিকোয়েস্ট বডি পড়া, স্ট্যাটাস কোড বাছাই আর ক্র্যাশ সামলানোর পর আপনি পরিষ্কার বুঝবেন পরের পাতায় FastAPI আপনার হয়ে ঠিক কী কী করে দেয়, আর ভেতরে কিছু গোলমাল হলে সেটা ডিবাগও করতে পারবেন।
যা শিখবেন
- ব্যাকএন্ড আর API সার্ভার কী, আর ফ্রন্টএন্ড থেকে এরা কোথায় আলাদা।
- একটা রিকোয়েস্টের পুরো জীবন: ক্লায়েন্টের কানেকশন থেকে আপনার হ্যান্ডলার হয়ে আবার ফেরত।
- রাউটিং, পাথ আর কোয়েরি প্যারামিটার, রিকোয়েস্ট বডি আর JSON সিরিয়ালাইজেশন, নিজের হাতে।
- কখন কোন স্ট্যাটাস কোড পাঠাবেন, এনভায়রনমেন্ট ভেরিয়েবল থেকে কনফিগারেশন, লগিং আর এরর হ্যান্ডলিং।
- Sync বনাম async: আসলে এর মানে কী, আর AI ব্যাকএন্ডে কখন এটা গুরুত্বপূর্ণ।
ফ্রন্টএন্ড, ব্যাকএন্ড আর API সার্ভার
ফ্রন্টএন্ড (frontend) হলো যেটা ইউজার হাতে ছুঁয়ে দেখেন: ব্রাউজারের ওয়েব পেজ, মোবাইল অ্যাপ, কমান্ড-লাইন টুল। এটা চলে ইউজারের ডিভাইসে, তাই সিক্রেট বা নিয়মকানুনের ব্যাপারে এর ওপর ভরসা করা যায় না। ব্যাকএন্ড চলে আপনার নিয়ন্ত্রণে থাকা মেশিনে। API কি থাকে এখানেই, ডেটাবেস আর মডেলের সঙ্গে কথা বলে এটাই, কে কী করতে পারবে সেটা ঠিক করে এটাই, আর ব্যবসার নিয়মগুলো এক জায়গায় রাখে, যাতে সব ক্লায়েন্ট একই আচরণ পায়।
API সার্ভার এমন এক ব্যাকএন্ড, যার ক্লায়েন্ট HTML পড়া মানুষ নয়, অন্য প্রোগ্রাম। এটা কথা বলে HTTP-তে, আর (প্রায় সবসময়) JSON-এ। আধুনিক বেশিরভাগ AI প্রোডাক্ট ঠিক এভাবেই বানানো: একটা React বা মোবাইল ফ্রন্টএন্ড একটা JSON API-কে কল করে, আর API কল করে মডেলকে।
| অংশ | কোথায় চলে | AI প্রোডাক্টে সাধারণ কাজ | সিক্রেট রাখা যায়? |
|---|---|---|---|
| ফ্রন্টএন্ড (ওয়েব, মোবাইল, CLI) | ইউজারের ডিভাইসে | চ্যাট দেখানো, প্রশ্ন নেওয়া, উত্তর দেখানো | না: এর কোড আর ট্রাফিক যে কেউ পড়তে পারে |
| ব্যাকএন্ড / API সার্ভার | আপনার সার্ভার বা ক্লাউডে | ইউজার যাচাই, ইনপুট ভ্যালিডেশন, মডেল বা LLM কল, ফলাফল সেভ, ব্যবহার গোনা | হ্যাঁ: কি থাকে এখানে, এনভায়রনমেন্ট ভেরিয়েবলে |
| ডেটাবেস | আপনার সার্ভার বা কোনো ম্যানেজড সার্ভিসে | ইউজার, কথোপকথন, ডকুমেন্ট, ফিডব্যাক | শুধু ব্যাকএন্ডের মাধ্যমে পৌঁছানো যায় |
| মডেল / LLM প্রোভাইডার | আপনার GPU মেশিন, অথবা OpenAI, Anthropic ইত্যাদি | ইনপুট থেকে প্রেডিকশন বা উত্তর বানানো | শুধু ব্যাকএন্ড কল করে, নিজের কি দিয়ে |
শেষ কলামটাই ব্যাকএন্ড থাকার সবচেয়ে বড় কারণ। মোবাইল অ্যাপ যদি সরাসরি LLM প্রোভাইডারকে কল করত, প্রোভাইডারের কি অ্যাপের ভেতরেই চলে যেত, আর কয়েক দিনের মধ্যেই কেউ না কেউ সেটা বের করে ফেলত।
একটা রিকোয়েস্টের জীবন
ক্লায়েন্ট যখন GET /courses?level=beginner পাঠায়, সার্ভারের দিকে পরপর যা ঘটে:
ক্লায়েন্ট (curl, ব্রাউজার, অ্যাপ)
| ১. সার্ভারের হোস্ট আর পোর্টে একটা TCP কানেকশন খোলে
| ২. রিকোয়েস্টের টেক্সট পাঠায়: মেথড, পাথ, হেডার, দরকার হলে বডি
v
সার্ভার প্রসেস (একটা পোর্টে শুনছে, যেমন 8000)
| ৩. পার্স: method=GET path=/courses query={"level": ["beginner"]} হেডার বডি
| ৪. রাউট: "GET /courses" কোন হ্যান্ডলারের? কারও না হলে -> 404
| ৫. হ্যান্ডেল: ইনপুট যাচাই -> আসল কাজ (ডেটাবেস, মডেল, LLM) -> ফলাফল তৈরি
| ৬. সিরিয়ালাইজ: Python dict -> JSON টেক্সট -> বাইট, সঙ্গে স্ট্যাটাস কোড আর হেডার
| ৭. লগ: "GET /courses?level=beginner" 200
v
ক্লায়েন্ট পায়: স্ট্যাটাস লাইন, হেডার, JSON বডি৩, ৪, ৬ আর ৭ নম্বর ধাপ আজ পর্যন্ত লেখা সব ব্যাকএন্ডেই এক। আপনার আসল প্রোডাক্ট শুধু ৫ নম্বর ধাপটা। ফ্রেমওয়ার্কের পক্ষে পুরো যুক্তি এটাই: ৩, ৪, ৬ আর ৭ তারা আপনার হয়ে ঠিকঠাক করে দেয়, আপনি সময় দেন ৫-এ।
শুধু স্ট্যান্ডার্ড লাইব্রেরি দিয়ে একটা ছোট্ট ব্যাকএন্ড
প্রতিটা পাতা শুরু হয় নতুন একটা হোম ফোল্ডারে, তাই এই পাতার জন্য একটা প্রজেক্ট ফোল্ডার বানান:
mkdir -p ~/projects/tiny-backend
cd ~/projects/tiny-backend
python --versionPython 3.12.3আপনার Python আরও নতুন হতে পারে (কোর্সে আমরা 3.14 ধরে চলি); এখানকার সবকিছু 3.12 বা তার পরের ভার্সনে চলে। এবার এই ফাইলটা tiny_server.py নামে সেভ করুন। ধীরে ধীরে পড়ুন: ডায়াগ্রামের প্রতিটা নম্বর দেওয়া ধাপ এর কোথাও না কোথাও আছে।
# file: tiny_server.py
"""শুধু Python-এর স্ট্যান্ডার্ড লাইব্রেরি দিয়ে বানানো ছোট্ট একটা JSON ব্যাকএন্ড (কোনো ফ্রেমওয়ার্ক ছাড়া)।"""
import json
import logging
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlsplit
# কনফিগারেশন আসে এনভায়রনমেন্ট ভেরিয়েবল থেকে, নিরাপদ ডিফল্টসহ।
APP_NAME = os.environ.get("APP_NAME", "tiny-backend")
PORT = int(os.environ.get("PORT", "8000"))
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
logging.basicConfig(level=LOG_LEVEL, format="%(levelname)s %(name)s: %(message)s")
log = logging.getLogger(APP_NAME)
COURSES = [
{"id": 1, "title": "How AI Works", "level": "beginner", "hours": 3},
{"id": 2, "title": "Python for AI", "level": "beginner", "hours": 12},
{"id": 3, "title": "RAG Systems", "level": "advanced", "hours": 10},
]
POSITIVE = {"good", "great", "love", "excellent", "helpful", "clear"}
NEGATIVE = {"bad", "slow", "boring", "confusing", "hate", "broken"}
def classify(text):
"""আসল ML মডেলের বদলি: খুশি আর অখুশি শব্দ গুনে দেখে।"""
words = text.lower().replace(".", " ").replace(",", " ").split()
score = sum(w in POSITIVE for w in words) - sum(w in NEGATIVE for w in words)
return "positive" if score > 0 else "negative" if score < 0 else "neutral"
class Handler(BaseHTTPRequestHandler):
# ---- ধাপ ৬: Python অবজেক্টকে HTTP রেসপন্সে সিরিয়ালাইজ করা
def send_json(self, status, data):
body = json.dumps(data).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
# ---- ধাপ ৭: বিল্ট-ইন অ্যাক্সেস লগকে logging মডিউলের মধ্য দিয়ে পাঠানো
def log_message(self, format, *args):
log.info(format, *args)
# ---- এরর হ্যান্ডলিং: এক জায়গা, যা যেকোনো ক্র্যাশকে পরিষ্কার 500-এ বদলে দেয়
def safely(self, route):
try:
route()
except Exception:
log.exception("unhandled error on %s %s", self.command, self.path)
self.send_json(500, {"detail": "Internal server error"})
def do_GET(self):
self.safely(self.route_get)
def do_POST(self):
self.safely(self.route_post)
# ---- ধাপ ৩ ও ৪: URL পার্স করা আর রাউট করা
def route_get(self):
url = urlsplit(self.path) # "/courses?level=beginner" -> পাথ + কোয়েরি
query = parse_qs(url.query) # "level=beginner" -> {"level": ["beginner"]}
parts = url.path.strip("/").split("/")
if url.path == "/health":
self.send_json(200, {"status": "ok", "app": APP_NAME})
elif url.path == "/courses":
level = query.get("level", [None])[0]
items = [c for c in COURSES if level is None or c["level"] == level]
self.send_json(200, {"count": len(items), "items": items})
elif url.path == "/stats":
level = query.get("level", [None])[0]
hours = [c["hours"] for c in COURSES if c["level"] == level]
self.send_json(200, {"level": level, "average_hours": sum(hours) / len(hours)})
elif len(parts) == 2 and parts[0] == "courses":
if not parts[1].isdigit():
self.send_json(400, {"detail": "Course id must be a whole number"})
return
course = next((c for c in COURSES if c["id"] == int(parts[1])), None)
if course is None:
self.send_json(404, {"detail": f"Course {parts[1]} not found"})
else:
self.send_json(200, course)
else:
self.send_json(404, {"detail": f"No route for GET {url.path}"})
def route_post(self):
if self.path != "/sentiment":
self.send_json(404, {"detail": f"No route for POST {self.path}"})
return
length = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(length) # বডি আসে বাইট হিসেবে
try:
data = json.loads(raw) # JSON-এর বাইট -> Python অবজেক্ট
except json.JSONDecodeError:
self.send_json(400, {"detail": "Body must be valid JSON"})
return
text = data.get("text") if isinstance(data, dict) else None
if not isinstance(text, str) or not text.strip():
self.send_json(422, {"detail": "Field 'text' is required and must be a non-empty string"})
return
log.debug("classifying %d characters", len(text))
self.send_json(200, {"label": classify(text), "model": "keyword-v1"})
if __name__ == "__main__":
server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
log.info("%s listening on http://127.0.0.1:%d", APP_NAME, PORT)
server.serve_forever()এবার চালু করুন। সার্ভার চলতেই থাকে যতক্ষণ না আপনি থামান, তাই নিজের মেশিনে আপনি এটা দ্বিতীয় একটা টার্মিনালে চালিয়ে সেখানেই লগ দেখতেন। এই পাতায় আমরা এটা ব্যাকগ্রাউন্ডে চালু করি (&), আউটপুট পাঠাই server.log-এ, প্রসেস আইডি মনে রাখি SERVER_PID-এ, আর curl --retry-কে বলি সার্ভার সাড়া না দেওয়া পর্যন্ত অপেক্ষা করতে:
APP_NAME=bitbyte-tiny PORT=8000 python tiny_server.py > server.log 2>&1 &
export SERVER_PID=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 http://localhost:8000/health{"status": "ok", "app": "bitbyte-tiny"}APP_NAME=… PORT=… python … লিখলে এনভায়রনমেন্ট ভেরিয়েবলগুলো শুধু ওই একটা কমান্ডের জন্য সেট হয়। /health এন্ডপয়েন্ট প্রতিটা ব্যাকএন্ডের প্রথম এন্ডপয়েন্ট হওয়া উচিত: লোড ব্যালান্সার, Docker আর আপটাইম মনিটর এটা কল করে জানতে চায়, "সার্ভার চালু আছে তো?"
রাউটিং: কোন রিকোয়েস্ট কোন কোড সামলাবে
রাউটিং (routing) একটা মেথড আর একটা পাথের জোড়াকে একটা হ্যান্ডলারের সঙ্গে মেলায়। এমন প্রতিটা জোড়া একেকটা এন্ডপয়েন্ট, যেমন GET /courses। tiny_server.py-তে রাউটার হলো একটা if/elif শৃঙ্খল, আর যা সে চেনে না সেটা গড়িয়ে গিয়ে 404 পায়:
curl -s http://localhost:8000/courses
echo
curl -s http://localhost:8000/teachers
echo
curl -s -X DELETE http://localhost:8000/courses/1{"count": 3, "items": [{"id": 1, "title": "How AI Works", "level": "beginner", "hours": 3}, {"id": 2, "title": "Python for AI", "level": "beginner", "hours": 12}, {"id": 3, "title": "RAG Systems", "level": "advanced", "hours": 10}]}
{"detail": "No route for GET /teachers"}
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 501</p>
<p>Message: Unsupported method ('DELETE').</p>
<p>Error code explanation: 501 - Server does not support this operation.</p>
</body>
</html>শেষ রেসপন্সটা এসেছে স্ট্যান্ডার্ড লাইব্রেরির কাছ থেকে: 501 (not implemented) লেখা একটা HTML পেজ, কারণ হ্যান্ডলার ক্লাসে do_DELETE মেথড নেই। JSON API-র উচিত এখানে JSON-এ 405 Method Not Allowed দেওয়া, কারণ পাথটা আছে, শুধু মেথডটা ভুল। নিজের হাতে লেখা সার্ভারে এমন অসংখ্য খুঁটিনাটি ভুল হয়, যদি না আপনি প্রতিটা মনে রাখেন; এটা তার একটা ছোট উদাহরণ।
পাথ প্যারামিটার, কোয়েরি প্যারামিটার আর বডি
একটা রিকোয়েস্ট তিন জায়গায় ইনপুট বয়ে আনতে পারে। প্রতিটা, আর যে কোড সেটা পড়ে:
| কোথায় | উদাহরণ | কীসের জন্য | tiny_server.py-তে যা দিয়ে পড়া হয় |
|---|---|---|---|
| পাথ প্যারামিটার | /courses/2 | কোন রিসোর্স: একটা আইডি বা নাম | url.path.split("/") |
| কোয়েরি প্যারামিটার | /courses?level=beginner | ঐচ্ছিক ফিল্টার, সাজানো, পেজিং | parse_qs(url.query) |
| বডি | {"text": "Great course"} | কিছু তৈরি বা প্রসেস করার জন্য পাঠানো ডেটা | self.rfile.read(length), তারপর json.loads |
curl -s http://localhost:8000/courses/2
echo
curl -s http://localhost:8000/courses/99
echo
curl -s http://localhost:8000/courses/two
echo
curl -s "http://localhost:8000/courses?level=advanced"{"id": 2, "title": "Python for AI", "level": "beginner", "hours": 12}
{"detail": "Course 99 not found"}
{"detail": "Course id must be a whole number"}
{"count": 1, "items": [{"id": 3, "title": "RAG Systems", "level": "advanced", "hours": 10}]}খেয়াল করুন, URL-এর সবকিছু আসে টেক্সট হিসেবে। /courses/2-এর 2 ততক্ষণ স্ট্রিং "2", যতক্ষণ না আপনার কোড সেটা যাচাই করে (isdigit()) আর রূপান্তর করে (int())। যাচাইটা ভুলে গেলে /courses/two হ্যান্ডলারকে ক্র্যাশ করাবে। আরেকটা ব্যাপার: parse_qs প্রতিটা কি-র জন্য একটা লিস্ট দেয়, কারণ ?tag=a&tag=b-ও বৈধ; এজন্যই কোডে লেখা query.get("level", [None])[0]।
বডি আসল ইনপুটের জন্য, যেমন যে টেক্সট ক্লাসিফাই করতে হবে। ঠিক Content-Type দিয়ে JSON পাঠান:
curl -s -X POST http://localhost:8000/sentiment \
-H "Content-Type: application/json" \
-d '{"text": "Great course, the examples are clear and helpful."}'
echo
curl -s -X POST http://localhost:8000/sentiment -d 'not json'
echo
curl -s -X POST http://localhost:8000/sentiment -H "Content-Type: application/json" -d '{"txt": "typo in the key"}'{"label": "positive", "model": "keyword-v1"}
{"detail": "Body must be valid JSON"}
{"detail": "Field 'text' is required and must be a non-empty string"}তিনটা রিকোয়েস্ট, তিন রকম উত্তর, আর প্রতিটা ক্লায়েন্টকে স্পষ্ট বলে দেয় কী ঠিক করতে হবে। হ্যান্ডলার নিজের হাতে যাচাই করেছে: বডি JSON কি না, সেটা অবজেক্ট কি না, text আছে কি না, সেটা স্ট্রিং কি না, আর খালি কি না। পনেরোটা ফিল্ডের রিকোয়েস্টের জন্য এসব যাচাই লেখার কথা ভাবুন। দুই পাতা পরে Pydantic এই কাজ করবে শুধু একটা ক্লাসের সংজ্ঞা দেখে।
JSON সিরিয়ালাইজেশন
সিরিয়ালাইজেশন (serialisation) Python অবজেক্টকে পাঠানোর উপযোগী বাইটে বদলায়; ডিসিরিয়ালাইজেশন পাওয়া বাইটকে আবার অবজেক্টে ফেরায়। JSON-এ মান মাত্র ছয় রকমের (অবজেক্ট, অ্যারে, স্ট্রিং, সংখ্যা, true/false, null), তাই সব Python অবজেক্ট এতে খাপ খায় না:
import json
from datetime import datetime
reply = {"label": "positive", "score": 0.93, "cached": False, "tags": ("ai", "nlp"), "error": None}
print(json.dumps(reply))
try:
json.dumps({"created": datetime(2026, 10, 11, 9, 30)})
except TypeError as exc:
print("TypeError:", exc)
print(json.dumps({"created": datetime(2026, 10, 11, 9, 30).isoformat()}))
print(json.dumps({"note": "café"}), json.dumps({"note": "café"}, ensure_ascii=False)){"label": "positive", "score": 0.93, "cached": false, "tags": ["ai", "nlp"], "error": null}
TypeError: Object of type datetime is not JSON serializable
{"created": "2026-10-11T09:30:00"}
{"note": "caf\u00e9"} {"note": "café"}যা খেয়াল করবেন: False হয়েছে false, None হয়েছে null, আর টাপলটা হয়েছে JSON অ্যারে। datetime নেওয়া হয়নি, তাই API তারিখ পাঠায় ISO 8601 স্ট্রিং হিসেবে। ইংরেজি ছাড়া অন্য অক্ষর ডিফল্টভাবে এস্কেপ হয়; দুটো রূপই বৈধ JSON, আর ডিকোড করলে একই স্ট্রিং পাওয়া যায়। ফ্রেমওয়ার্ক এসব (তারিখ, ডেসিমাল, UUID, আপনার নিজের ক্লাস) আপনার হয়ে সামলায়।
আপনার ব্যাকএন্ড যে স্ট্যাটাস কোড পাঠায়
ক্লায়েন্টের দিকে আপনি স্ট্যাটাস কোড পড়েন। সার্ভারের দিকে আপনি সেটা বাছাই করেন, আর এই বাছাই আপনার API-র চুক্তির অংশ: ক্লায়েন্টের কোড এর ওপর ভিত্তি করে সিদ্ধান্ত নেয়, আর রিট্রাই লজিক (যেমনটা আপনি লিখেছেন নির্ভরযোগ্য API কল: টাইমআউট, রিট্রাই, পেজিনেশন ও LLM API পাতায়) এর ওপর ভরসা করে।
| আপনার হ্যান্ডলারে যা ঘটেছে | পাঠান | কেন |
|---|---|---|
| কাজ হয়েছে, এই নিন ডেটা | 200 OK | স্বাভাবিক সাফল্য |
| নতুন রিসোর্স তৈরি হয়েছে | 201 Created | ক্লায়েন্টকে জানায় যে নতুন কিছু তৈরি হলো |
| কাজ হয়েছে, ফেরত দেওয়ার কিছু নেই (যেমন ডিলিট) | 204 No Content | কোনো বডিই নেই |
| রিকোয়েস্টটাই ভাঙাচোরা (JSON নয়, URL-এ ভুল টাইপ) | 400 Bad Request | ক্লায়েন্টকে রিকোয়েস্ট বদলাতে হবে। আমাদের হাতে লেখা সার্ভার 400 দেয়; FastAPI বডি বৈধ JSON না হলে বা মান ভুল হলে 422 দেয় (পরের পাতাগুলোতে), আর কিছু অন্য ফ্রেমওয়ার্ক 400 ব্যবহার করে |
| ক্রেডেনশিয়াল নেই বা ভুল / ক্রেডেনশিয়াল ঠিক কিন্তু অনুমতি নেই | 401 / 403 | অথেনটিকেশন বনাম অথরাইজেশন |
| যা চাওয়া হয়েছে তা নেই | 404 Not Found | অজানা আইডি বা রাউট |
| গঠন ঠিক, কিন্তু ফিল্ডগুলো অবৈধ | 422 Unprocessable Content | ভ্যালিডেশন এররের জন্য FastAPI এটাই ব্যবহার করে |
| ক্লায়েন্ট খুব দ্রুত রিকোয়েস্ট পাঠাচ্ছে | 429 Too Many Requests | সঙ্গে Retry-After দিন |
| আপনার কোড ক্র্যাশ করেছে | 500 Internal Server Error | এটা কখনো ক্লায়েন্টের দোষ নয়; লগ করুন, খুঁটিনাটি লুকান |
| আপনি যে সার্ভিসের ওপর নির্ভর করেন (DB, LLM প্রোভাইডার) সেটা বন্ধ | 502 / 503 | ক্লায়েন্টকে জানায়, পরে আবার চেষ্টা করলে কাজ হতে পারে |
সোনালি নিয়ম: 4xx মানে "ক্লায়েন্টকে রিকোয়েস্ট ঠিক করতে হবে"; 5xx মানে "সার্ভারকে নিজের সমস্যা ঠিক করতে হবে"। ভেতরে {"error": "..."} রেখে 200 ফেরত দিলে প্রতিটা ক্লায়েন্টের রিট্রাই আর মনিটরিং লজিক ভেঙে যায়।
লগিং আর এরর হ্যান্ডলিং
ব্যাকএন্ডের কোনো স্ক্রিন নেই। রাত ৩টায় কিছু গোলমাল হলে লগই আপনার একমাত্র ভরসা। tiny_server.py এতক্ষণ যা লিখেছে, দেখুন:
cat server.logINFO bitbyte-tiny: bitbyte-tiny listening on http://127.0.0.1:8000
INFO bitbyte-tiny: "GET /health HTTP/1.1" 200 -
INFO bitbyte-tiny: "GET /courses HTTP/1.1" 200 -
INFO bitbyte-tiny: "GET /teachers HTTP/1.1" 404 -
INFO bitbyte-tiny: code 501, message Unsupported method ('DELETE')
INFO bitbyte-tiny: "DELETE /courses/1 HTTP/1.1" 501 -
INFO bitbyte-tiny: "GET /courses/2 HTTP/1.1" 200 -
INFO bitbyte-tiny: "GET /courses/99 HTTP/1.1" 404 -
INFO bitbyte-tiny: "GET /courses/two HTTP/1.1" 400 -
INFO bitbyte-tiny: "GET /courses?level=advanced HTTP/1.1" 200 -
INFO bitbyte-tiny: "POST /sentiment HTTP/1.1" 200 -
INFO bitbyte-tiny: "POST /sentiment HTTP/1.1" 400 -
INFO bitbyte-tiny: "POST /sentiment HTTP/1.1" 422 -প্রতিটা রিকোয়েস্টের জন্য এক লাইন: মেথড, পাথ, স্ট্যাটাস। (আসল সার্ভার সময়ও লগ করে; এখানকার ফরম্যাটে সময় বাদ রাখা হয়েছে, যাতে আপনার আউটপুট পাতার সঙ্গে মেলে। প্রোডাকশনে %(asctime)s যোগ করুন।) এবার একটা আসল বাগ ঘটান। /stats একটা গড় হিসাব করে, আর কোর্স নেই এমন লেভেলের কথা কেউ ভাবেনি:
curl -s "http://localhost:8000/stats?level=beginner"
echo
curl -s -w "\nstatus: %{http_code}\n" "http://localhost:8000/stats?level=expert"{"level": "beginner", "average_hours": 7.5}
{"detail": "Internal server error"}
status: 500ক্লায়েন্ট পেয়েছে ছোট, নিরাপদ একটা মেসেজ আর 500। খুঁটিনাটি গেছে লগে, যেখানে আপনি দেখতে পান কিন্তু কোনো আক্রমণকারী পায় না:
tail -n 9 server.logERROR bitbyte-tiny: unhandled error on GET /stats?level=expert
Traceback (most recent call last):
File "/home/nadia/projects/tiny-backend/tiny_server.py", line 50, in safely
route()
File "/home/nadia/projects/tiny-backend/tiny_server.py", line 76, in route_get
self.send_json(200, {"level": level, "average_hours": sum(hours) / len(hours)})
~~~~~~~~~~~^~~~~~~~~~~~
ZeroDivisionError: division by zero
INFO bitbyte-tiny: "GET /stats?level=expert HTTP/1.1" 500 -ট্রেসব্যাক পড়ুন নিচ থেকে ওপরে: প্রথমে এরর (ZeroDivisionError), তারপর যে লাইনে এরর হলো, তারপর যেসব কলের পথ ধরে সেখানে পৌঁছানো। বাইরে বন্ধুসুলভ মেসেজ, ভেতরে লগে পুরো বিবরণ: প্রোডাকশনের সব API এই নিয়ম মানে। ক্লায়েন্টকে কখনো ট্রেসব্যাক পাঠাবেন না: এতে ফাইলের পাথ, লাইব্রেরির ভার্সন, কখনো কখনো ডেটাও ফাঁস হয়। সিক্রেটও কখনো লগ করবেন না: API কি, পাসওয়ার্ড আর ইউজারের পুরো প্রম্পট লগ ফাইলে থাকার জিনিস নয়।
এনভায়রনমেন্ট থেকে কনফিগারেশন
একই কোড চলে আপনার ল্যাপটপে, CI-তে আর প্রোডাকশনে, ভিন্ন ভিন্ন সেটিং নিয়ে: পোর্ট, লগ লেভেল, ডেটাবেস URL, মডেলের নাম, LLM প্রোভাইডারের API কি। এগুলো থাকে এনভায়রনমেন্ট ভেরিয়েবলে, কোডে কখনো নয় (এদের সঙ্গে পরিচয় হয়েছে এনভায়রনমেন্ট ভেরিয়েবল, .env ফাইল ও ভার্চুয়াল এনভায়রনমেন্ট পাতায়)। সার্ভার থামিয়ে আরও বিস্তারিত লগিং দিয়ে আবার চালু করুন:
kill $SERVER_PID
while kill -0 $SERVER_PID 2>/dev/null; do sleep 0.1; done
APP_NAME=bitbyte-debug LOG_LEVEL=DEBUG PORT=8000 python tiny_server.py > server.log 2>&1 &
export SERVER_PID=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 http://localhost:8000/health
echo
curl -s -X POST http://localhost:8000/sentiment -H "Content-Type: application/json" -d '{"text": "Too slow and confusing"}'
echo
cat server.log{"status": "ok", "app": "bitbyte-debug"}
{"label": "negative", "model": "keyword-v1"}
INFO bitbyte-debug: bitbyte-debug listening on http://127.0.0.1:8000
INFO bitbyte-debug: "GET /health HTTP/1.1" 200 -
DEBUG bitbyte-debug: classifying 22 characters
INFO bitbyte-debug: "POST /sentiment HTTP/1.1" 200 -while kill -0 লাইনটা অপেক্ষা করে যতক্ষণ না পুরোনো প্রসেস সত্যিই বন্ধ হয়, যাতে নতুনটা পোর্টটা নিতে পারে। একই কোড, ভিন্ন আচরণ: নতুন নাম, আর একটা DEBUG লাইন যা আগে দেখেননি। সেটিং বদলাতে কখনো কোড বদলাতে হয় না, আর সিক্রেট কখনো Git-এ ঢোকে না।
| কাজ | bash (macOS, Linux, Git Bash) | PowerShell |
|---|---|---|
| শুধু একটা কমান্ডের জন্য ভেরিয়েবল সেট | PORT=8000 python tiny_server.py | এভাবে হয় না; আগে সেট করে নিন |
| এই টার্মিনালের জন্য সেট | export LOG_LEVEL=DEBUG | $env:LOG_LEVEL = "DEBUG" |
| মান পড়া | echo $LOG_LEVEL | $env:LOG_LEVEL |
Sync বনাম async, এক অংশে
ব্যাকএন্ডের বেশিরভাগ সময় যায় অপেক্ষায়: ডেটাবেসের জন্য, অন্য API-র জন্য, আর সবচেয়ে বেশি LLM-এর জন্য, যার প্রতিটা উত্তরে কয়েক সেকেন্ড লাগতে পারে। অপেক্ষার সময় সার্ভার কী করে, তার ওপরই নির্ভর করে সে কতজন ইউজারকে সামলাতে পারবে।
- Synchronous (sync) কোড অপেক্ষা করে আটকে থেকে: উত্তর না আসা পর্যন্ত থ্রেডটা থেমে থাকে। একসঙ্গে কয়েকজন ইউজারকে সামলাতে সার্ভারের দরকার কয়েকটা থ্রেড বা প্রসেস।
ThreadingHTTPServerপ্রতিটা রিকোয়েস্টকে আলাদা থ্রেড দেয়; সাধারণHTTPServerএকবারে একটাই রিকোয়েস্ট সামলায়। - Asynchronous (async) কোড অপেক্ষা করে সরে দাঁড়িয়ে:
awaitনিয়ন্ত্রণ ফিরিয়ে দেয় একটা ইভেন্ট লুপকে (event loop), যে উত্তর আসা পর্যন্ত অন্য কাজ চালায়। একটা থ্রেডেই অনেকগুলো ধীর রিকোয়েস্ট একসঙ্গে চলতে পারে।
নিচে তিনটা নকল "মডেল কল", প্রতিটা এক সেকেন্ড অপেক্ষা করে, দুইভাবে করা:
import asyncio
import time
def call_model_sync(name):
time.sleep(1) # আটকে রাখে: এই থ্রেডে আর কিছু চলে না
return f"{name}: done"
async def call_model_async(name):
await asyncio.sleep(1) # সরে দাঁড়ায়: ইভেন্ট লুপ অন্য কাজ চালায়
return f"{name}: done"
start = time.perf_counter()
print([call_model_sync(n) for n in ("q1", "q2", "q3")])
print(f"sync, one after another: about {round(time.perf_counter() - start)} s")
async def main():
return await asyncio.gather(*(call_model_async(n) for n in ("q1", "q2", "q3")))
start = time.perf_counter()
print(asyncio.run(main()))
print(f"async, waiting together: about {round(time.perf_counter() - start)} s")['q1: done', 'q2: done', 'q3: done']
sync, one after another: about 3 s
['q1: done', 'q2: done', 'q3: done']
async, waiting together: about 1 sআপাতত যে নিয়মগুলো জানা দরকার:
- Async সাহায্য করে যখন আপনি async লাইব্রেরি দিয়ে I/O-র জন্য অপেক্ষা করছেন (নেটওয়ার্ক, ডিস্ক, ডেটাবেস)। CPU-র কাজ, যেমন scikit-learn মডেল চালানো, এতে একটুও দ্রুত হয় না।
asyncকোডের ভেতরে একটা ব্লকিং কল (time.sleep,requests.get) পুরো ইভেন্ট লুপকে জমিয়ে দেয়, আর বাকি সব ইউজার অপেক্ষায় থাকেন।- FastAPI-তে দুটোই লেখা যায়:
async defএন্ডপয়েন্ট চলে ইভেন্ট লুপে, আর সাধারণdefএন্ডপয়েন্ট চলে একটা থ্রেড পুলে, তাই সেখানে ব্লকিং কোড নিরাপদ। নিশ্চিত না হলেdefলিখুন। অ্যাডভান্সড টিউটোরিয়ালে এ নিয়ে আরও গভীরে যাওয়া হবে।
ফ্রেমওয়ার্ক আপনার জন্য যা করে
tiny_server.py-এর দিকে আবার তাকান: প্রায় একশো লাইন, যার বেশিরভাগই প্লাম্বিং। পাশে দেখুন, পরের পাতায় FastAPI এর বদলে কী দেয়:
| আপনি নিজের হাতে করেছেন | FastAPI যা দিয়ে করে |
|---|---|
if/elif রাউটিং, হাতে লেখা 404, 405 নেই | @app.get("/courses") ডেকোরেটর; 404 আর 405 আপনা থেকেই |
পাথ ভাগ করা, isdigit(), int() | ফাংশনের সিগনেচারে course_id: int |
parse_qs আর [None])[0] | level: str | None = None |
বাইট পড়া, json.loads, প্রতিটা ফিল্ড যাচাই | একটা Pydantic মডেল; এরর হয়ে যায় বিস্তারিত 422 |
json.dumps, হেডার, Content-Length | একটা dict বা মডেল return করলেই হয় |
safely() র্যাপার | বিল্ট-ইন: না-সামলানো এরর 500 হয়ে যায় আর লগ হয় |
| শুধু থ্রেড | def আর async def এন্ডপয়েন্ট, দুটোই ভালোভাবে চলে |
| কোনো ডকুমেন্টেশন নেই | আপনার কোড থেকে বানানো ইন্টারঅ্যাক্টিভ ডক, /docs-এ |
আসল AI কাজে এটা কোথায় দেখবেন
- LLM ফিচার: চ্যাট ফ্রন্টএন্ড ইউজারের মেসেজ পাঠায় আপনার ব্যাকএন্ডে; ব্যাকএন্ড ইউজারের কোটা দেখে, সিস্টেম প্রম্পট যোগ করে, গোপনে রাখা কি দিয়ে প্রোভাইডারকে কল করে, টোকেন ব্যবহার লগ করে আর উত্তর ফেরত দেয়।
- মডেল সার্ভিং: একটা
/predictএন্ডপয়েন্ট ট্রেইন করা মডেলকে ঠিক সেভাবেই মুড়ে রাখে, যেভাবে/sentimentমুড়ে রেখেছেclassify()-কে: ইনপুট যাচাই, মডেল চালানো, মডেলের ভার্সনসহ JSON ফেরত। - হেলথ চেক আর লগ: কোনো কন্টেইনারের
/healthসাড়া দেওয়া বন্ধ করলে হোস্টিং প্ল্যাটফর্ম সেটা রিস্টার্ট করে, আর কেন এমন হলো তা জানার উপায় লগ। - কনফিগারেশন: সস্তা মডেল থেকে ভালো মডেলে যাওয়া, বা স্টেজিং থেকে প্রোডাকশনে যাওয়া, এনভায়রনমেন্ট ভেরিয়েবলের পরিবর্তন, কোডের নয়।
সাধারণ ভুল
- ফ্রন্টএন্ড থেকে সরাসরি LLM প্রোভাইডারকে কল করা। API কি পৌঁছে যায় প্রতিটা ইউজারের ব্রাউজার বা ফোনে। সমাধান: ফ্রন্টএন্ড কল করবে আপনার ব্যাকএন্ডকে; কি থাকবে শুধু ব্যাকএন্ডে।
- "আমাদের অ্যাপ তো এমন পাঠায় না" ভেবে ইনপুটে ভরসা করা। যে কেউ
curlদিয়ে আপনার API কল করতে পারে, ঠিক যেমন আপনি এইমাত্র করলেন। সমাধান: প্রতিটা পাথ প্যারামিটার, কোয়েরি প্যারামিটার আর বডির ফিল্ড সার্ভারে যাচাই করুন। - এররের জন্য 200, বা ক্লায়েন্টের ভুলের জন্য 500 ফেরত দেওয়া। ক্লায়েন্ট 5xx পেলে রিট্রাই করে আর 4xx পেলে হাল ছাড়ে, তাই ভুল কোড মানে অকারণ রিট্রাই বা চাপা পড়া ব্যর্থতা। সমাধান: ওপরের টেবিল মেনে চলুন।
- ক্লায়েন্টকে ট্রেসব্যাক বা সিক্রেট পাঠানো, কিংবা লগে সিক্রেট লেখা। সমাধান: ক্লায়েন্টের জন্য সাধারণ মেসেজ, লগে পুরো ট্রেসব্যাক, আর কি, পাসওয়ার্ড বা টোকেন কখনো লগ নয়।
- Async কোডের ভেতরে ব্লকিং কল।
asyncফাংশনে একটাtime.sleepবাrequests.getসব রিকোয়েস্টকে আটকে দেয়। সমাধান:awaitসহ async লাইব্রেরি ব্যবহার করুন, নয়তো সাধারণdefএন্ডপয়েন্ট লিখুন।
নিজে চেষ্টা করুন
আগের অংশ থেকে ছোট্ট সার্ভারটা এখনও চলছে। মাঝারি আর কঠিন কাজের জন্য tiny_server.py এডিট করুন, তারপর সার্ভার রিস্টার্ট করুন (থামান, অপেক্ষা করুন, চালু করুন, ঠিক কনফিগারেশন অংশের মতো) আর curl দিয়ে পরিবর্তনটা পরীক্ষা করুন।
- সহজ:
curl -s -o /dev/null -w "%{http_code}\n"দিয়েGET /courses/3,GET /courses/30আরGET /nothing-এর শুধু স্ট্যাটাস কোড প্রিন্ট করুন। - মাঝারি:
/stats-এর বাগটা ঠিক করুন, যাতে কোর্স নেই এমন লেভেলের জন্য ক্র্যাশ না করে{"detail": "No courses for level 'expert'"}সহ404আসে।levelএকদমই না থাকলে কোন স্ট্যাটাস কোড বেছে নিতেন, আর কেন? - কঠিন:
/sentiment-এ একটা সর্বোচ্চ দৈর্ঘ্যের নিয়ম যোগ করুন: ২০০ অক্ষরের বেশি টেক্সট পাবে স্পষ্ট মেসেজসহ422। মডেল লম্বা ইনপুট সামলাতে পারলেও AI এন্ডপয়েন্টে এমন সীমা কেন দরকার?
উত্তর
for path in /courses/3 /courses/30 /nothing; do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000$path
done200
404
404মাঝারি কাজের জন্য, ভাগ করার আগে যাচাই করুন। /stats-এর কোডটুকু এটা দিয়ে বদলে দিন:
elif url.path == "/stats":
level = query.get("level", [None])[0]
hours = [c["hours"] for c in COURSES if c["level"] == level]
if not hours:
self.send_json(404, {"detail": f"No courses for level {level!r}"})
return
self.send_json(200, {"level": level, "average_hours": sum(hours) / len(hours)})level না থাকা মানে রিসোর্স না পাওয়া নয়, রিকোয়েস্টটাই অসম্পূর্ণ; তাই সৎ উত্তর 400 (অথবা 422, প্রয়োজনীয় কোয়েরি প্যারামিটার না থাকলে FastAPI যেটা পাঠাত)। কঠিন কাজের জন্য আগের text যাচাইয়ের ঠিক পরে একটা চেক যোগ করুন:
if len(text) > 200:
self.send_json(422, {"detail": "Field 'text' must be at most 200 characters"})
returnসীমা আপনাকে রক্ষা করে: LLM-এ পাঠানো প্রতিটা অক্ষরে টোকেন (মানে টাকা) খরচ হয়, সময় লাগে, আর বিশাল বডি আপনার সার্ভারকে আটকে রাখতে পারে। অপব্যবহার আর হঠাৎ বড় বিলের বিরুদ্ধে প্রথম প্রতিরক্ষা হলো ইনপুটের সীমা।
কাজ শেষ হলে সার্ভার থামান:
kill $SERVER_PIDসারসংক্ষেপ
- ব্যাকএন্ড চলে আপনার নিয়ন্ত্রণে থাকা মেশিনে: ইনপুট যাচাই করে, সিক্রেট রাখে, ডেটাবেস আর মডেলের সঙ্গে কথা বলে, আর একটা API দিয়ে অনেক ক্লায়েন্টকে সেবা দেয়।
- প্রতিটা রিকোয়েস্ট যায় পার্স, রাউট, হ্যান্ডেল, সিরিয়ালাইজ আর লগের মধ্য দিয়ে; এর মধ্যে শুধু "হ্যান্ডেল" আপনার আসল প্রোডাক্ট।
- ইনপুট আসে পাথে, কোয়েরি স্ট্রিংয়ে বা বডিতে, সবসময় টেক্সট বা বাইট হিসেবে; তাই যাচাই আর রূপান্তর করতে হয়।
- স্ট্যাটাস কোড ভেবেচিন্তে বাছুন: 2xx সাফল্য, 4xx ক্লায়েন্টের ভুল, 5xx সার্ভারের সমস্যা। বিবরণ লগ করুন, ফেরত দিন নিরাপদ মেসেজ।
- কনফিগারেশন আর সিক্রেট আসে এনভায়রনমেন্ট ভেরিয়েবল থেকে। I/O-র অপেক্ষায় async সাহায্য করে; async কোডে ব্লকিং কল সবার ক্ষতি করে।
এরপর: আপনার প্রথম FastAPI অ্যাপ। এই সার্ভারটাই আপনি আবার বানাবেন অনেক কম কোডে, টাইপ করা প্যারামিটার, স্বয়ংক্রিয় ভ্যালিডেশন আর ইন্টারঅ্যাক্টিভ ডকুমেন্টেশনসহ, একটা ঠিকঠাক uv প্রজেক্টে।