অধ্যায় 4 · আপনার প্রথম ব্যাকএন্ড
আপনার প্রথম FastAPI অ্যাপ
- পৃষ্ঠা 17 / 22
- 15 মিনিট পড়া
আগের পাতায় আপনি নিজের হাতে একটা ব্যাকএন্ড লিখেছেন আর দেখেছেন তার কতটা অংশই প্লাম্বিং: পাথ ভাগ করা, স্ট্রিংকে সংখ্যায় বদলানো, JSON যাচাই, স্ট্যাটাস কোড বাছাই, হেডার লেখা। FastAPI এই প্লাম্বিংয়ের সবটা আপনার হয়ে করে দেয়। আপনি টাইপ হিন্টসহ সাধারণ Python ফাংশন লেখেন, আর FastAPI সেগুলোকে এমন HTTP এন্ডপয়েন্টে বদলে দেয়, যারা নিজের ইনপুট পার্স ও যাচাই করে, আউটপুটকে JSON-এ রূপান্তর করে আর নিজেদের ডকুমেন্টেশন নিজেরাই বানায়।
আজকাল Python-এ AI ব্যাকএন্ডের জন্য সবচেয়ে প্রচলিত পছন্দ FastAPI: মডেল সার্ভিংয়ের এন্ডপয়েন্ট, LLM গেটওয়ে, RAG সার্ভিস আর এজেন্টের টুল প্রায়ই FastAPI অ্যাপ। এই পাতায় আপনি uv দিয়ে একটা ঠিকঠাক প্রজেক্ট বানাবেন, প্রথম এন্ডপয়েন্টগুলো লিখবেন, আর curl, Python ও ব্রাউজার দিয়ে সেগুলো কল করবেন।
যা শিখবেন
- FastAPI কেন, আর Flask ও Django-র সঙ্গে এর তুলনা।
- uv দিয়ে (আর pip দিয়েও) FastAPI প্রজেক্ট সেট আপ করা, আর
fastapi devওfastapi run-এর পার্থক্য। - টাইপসহ পাথ প্যারামিটার, কোয়েরি প্যারামিটার আর রিকোয়েস্ট বডি নিয়ে GET ও POST এন্ডপয়েন্ট লেখা।
- FastAPI কীভাবে আপনার হয়ে ইনপুট যাচাই করে, আর তার এরর রেসপন্স দেখতে কেমন।
- স্বয়ংক্রিয় ডকুমেন্টেশন কোথায় থাকে (
/docs,/redoc,/openapi.json), আরcurlওrequestsদিয়ে আপনার API কল করা।
FastAPI কেন
Python-এ তিনটা ওয়েব ফ্রেমওয়ার্কের সঙ্গে আপনার বারবার দেখা হবে। তিনটাই ভালো; বানানো হয়েছে ভিন্ন ভিন্ন কাজের জন্য।
| FastAPI | Flask | Django | |
|---|---|---|---|
| যার জন্য বানানো | JSON API | ছোট ওয়েব অ্যাপ আর API | ডেটাবেস, অ্যাডমিন আর টেমপ্লেটসহ পূর্ণাঙ্গ ওয়েবসাইট |
| ইনপুট যাচাই | স্বয়ংক্রিয়, টাইপ হিন্ট থেকে (Pydantic) | হাতে, বা এক্সটেনশন দিয়ে | ফর্ম আর সিরিয়ালাইজার (API-র জন্য Django REST framework) |
| API ডক | স্বয়ংক্রিয়ভাবে তৈরি (OpenAPI) | এক্সটেনশন | এক্সটেনশন |
| Async | নেটিভ: def আর async def দুটোই চলে | করা যায়, তবে মূল জিনিস নয় | আংশিক async |
| AI-তে সাধারণ ব্যবহার | মডেল সার্ভিং, LLM ব্যাকএন্ড, RAG আর এজেন্ট সার্ভিস | পুরোনো ML ডেমো আর ছোট টুল | অনেক পেজ, ইউজার আর অ্যাডমিন স্ক্রিনওয়ালা প্রোডাক্ট |
FastAPI দাঁড়িয়ে আছে দুটো লাইব্রেরির ওপর: Starlette (ওয়েবের অংশ: রাউটিং, রিকোয়েস্ট, রেসপন্স) আর Pydantic (ডেটা যাচাই)। একে চালায় একটা সার্ভার প্রোগ্রাম, Uvicorn। FastAPI এখনও 0.x ভার্সনে (২০২৬ সালের অক্টোবরে 0.143), তাই আপগ্রেড করার সময় রিলিজ নোট পড়ে নিন।
uv দিয়ে প্রজেক্ট বানানো
uv-এর সঙ্গে আপনার পরিচয় হয়েছে এনভায়রনমেন্ট ভেরিয়েবল, .env ফাইল ও ভার্চুয়াল এনভায়রনমেন্ট পাতায়। এখন থেকে প্রতিটা প্রজেক্টই uv প্রজেক্ট। এই পাতার জন্য একটা বানান:
mkdir -p ~/projects
cd ~/projects
uv init --no-package bitbyte-api
cd bitbyte-api
uv add -q "fastapi[standard]"
ls -AInitialized project `bitbyte-api` at `/home/nadia/projects/bitbyte-api`
.git
.gitignore
.python-version
.venv
README.md
main.py
pyproject.toml
uv.lockuv init প্রজেক্টের ফাইলগুলো বানিয়েছে: pyproject.toml, শুরুর একটা main.py, একটা README.md, Python ভার্সনের নাম লেখা .python-version ফাইল, আর একটা Git রিপোজিটরি, যার .gitignore আগে থেকেই .venv বাদ রাখে; --no-package মানে "এটা একটা অ্যাপ্লিকেশন, প্রকাশ করার মতো লাইব্রেরি নয়"। uv add ভার্চুয়াল এনভায়রনমেন্ট .venv বানিয়েছে, FastAPI ইনস্টল করেছে, সেটা pyproject.toml-এ লিখে রেখেছে আর প্রতিটা প্যাকেজের হুবহু ভার্সন আটকে রেখেছে uv.lock-এ। [standard] এক্সট্রা ডেভেলপমেন্টের জন্য দরকারি জিনিসগুলো নিয়ে আসে: Uvicorn, fastapi কমান্ড আর কয়েকটা সহায়ক লাইব্রেরি।
cat pyproject.toml[project]
name = "bitbyte-api"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi[standard]>=0.143.0",
]আপনার requires-python-এ দেখাবে আপনার নিজের Python ভার্সন। আপনি যদি সাধারণ pip পছন্দ করেন, বা কোনো প্রজেক্ট আগে থেকেই pip ব্যবহার করে, তাহলে সমতুল্য কমান্ডগুলো এই:
| ধাপ | uv | pip আর venv |
|---|---|---|
| প্রজেক্ট বানানো | uv init --no-package bitbyte-api | mkdir bitbyte-api, cd bitbyte-api, python -m venv .venv |
| venv অ্যাকটিভেট করা | লাগে না: uv run নিজেই ব্যবহার করে | source .venv/bin/activate (PowerShell: .venv\Scripts\Activate.ps1) |
| FastAPI ইনস্টল | uv add "fastapi[standard]" | pip install "fastapi[standard]" |
| ভার্সন লিখে রাখা | স্বয়ংক্রিয়: pyproject.toml + uv.lock | pip freeze > requirements.txt |
| কমান্ড চালানো | uv run fastapi dev main.py | fastapi dev main.py (venv অ্যাকটিভ থাকলে) |
| অন্য মেশিনে আবার বানানো | uv sync | pip install -r requirements.txt |
আপনার প্রথম এন্ডপয়েন্ট
uv init একটা "hello" main.py রেখে গেছে। সেটা এটা দিয়ে বদলে দিন:
# file: main.py
from fastapi import FastAPI
app = FastAPI(title="BitByte Course API")
@app.get("/")
def home():
return {"name": "BitByte Course API", "docs": "/docs"}
@app.get("/health")
def health():
return {"status": "ok"}এটাই একটা সম্পূর্ণ ব্যাকএন্ড। app হলো অ্যাপ্লিকেশন। @app.get("/health") একটা পাথ অপারেশন ডেকোরেটর (path operation decorator): ঠিক নিচের ফাংশনটাকে এটা GET /health-এর হ্যান্ডলার হিসেবে রেজিস্টার করে। ফাংশন একটা dict ফেরত দেয়, আর FastAPI সেটাকে 200 স্ট্যাটাসসহ JSON রেসপন্সে বদলে দেয়। tiny_server.py-এর রাউটিং আর send_json-এর চল্লিশ লাইনের সঙ্গে তুলনা করুন।
fastapi dev আর fastapi run
fastapi কমান্ড আপনার অ্যাপকে Uvicorn দিয়ে চালু করে। এর দুটো মোড:
fastapi dev main.py | fastapi run main.py | |
|---|---|---|
| কীসের জন্য | নিজের মেশিনে কোড লেখার সময় | প্রোডাকশন, Docker, আর এই পাতা |
| ফাইল সেভ করলে নিজে থেকে রিলোড | হ্যাঁ | না |
| কোথায় শোনে | 127.0.0.1 (শুধু আপনার মেশিন) | 0.0.0.0 (সব নেটওয়ার্ক ইন্টারফেস) |
| ডিফল্ট পোর্ট | 8000 | 8000 |
নিজের মেশিনে প্রজেক্ট ফোল্ডারে দ্বিতীয় একটা টার্মিনাল খুলে সেখানে ডেভ সার্ভার চালান; এডিট করার সময় চলতে দিন, আর থামাতে Ctrl+C চাপুন:
uv run fastapi dev main.pyএটা আপনার অ্যাপ আর তার ডকের URL প্রিন্ট করে, তারপর প্রতিটা রিকোয়েস্টের জন্য একটা করে লগ লাইন। main.py যতবার সেভ করবেন, ততবার নিজে থেকেই রিস্টার্ট হবে। এই পাতায়, আগের পাতার মতোই, সার্ভার চলে ব্যাকগ্রাউন্ডে, fastapi run দিয়ে, যাতে প্রতিটা ধাপ সবার জন্য একইভাবে কাজ করে:
uv run fastapi run main.py --port 8000 > server.log 2>&1 &
export SERVER_PID=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 http://localhost:8000/health
echo
curl -s http://localhost:8000/
echo
grep "Uvicorn running" server.log{"status":"ok"}
{"name":"BitByte Course API","docs":"/docs"}
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)FastAPI কমপ্যাক্ট JSON পাঠায় (কোনো ফাঁকা জায়গা ছাড়া)। ব্রাউজারে http://localhost:8000/ খুললেও একই জিনিস দেখবেন। tiny_server.py যেখানে ভুল করেছিল, সেটাও চেষ্টা করুন:
curl -s http://localhost:8000/teachers
echo
curl -s -X DELETE http://localhost:8000/health{"detail":"Not Found"}
{"detail":"Method Not Allowed"}অজানা পাথের জন্য JSON 404, আর চেনা পাথে ভুল মেথডের জন্য JSON 405, আপনার এক লাইন কোডও ছাড়া।
কোর্স API
এবার একটা আসল API: কোর্সের তালিকা আর ফিল্টার, আইডি দিয়ে একটা কোর্স, আর একটা সেন্টিমেন্ট এন্ডপয়েন্ট, যা ML মডেলের জায়গা নিচ্ছে। main.py-কে পূর্ণ সংস্করণ দিয়ে বদলে দিন আর একবার পড়ে নিন; নিচের অংশগুলো এটাকে টুকরো টুকরো করে বোঝাবে।
# file: main.py
"""BitByte Course API: আপনার প্রথম FastAPI অ্যাপ।"""
from typing import Annotated
from fastapi import FastAPI, HTTPException, Path, Query
from pydantic import BaseModel
app = FastAPI(title="BitByte Course API", version="0.1.0")
COURSES = [
{"id": 1, "title": "How AI Works", "level": "beginner", "hours": 3},
{"id": 2, "title": "Python for AI", "level": "beginner", "hours": 12},
{"id": 3, "title": "Maths for AI", "level": "beginner", "hours": 8},
{"id": 4, "title": "RAG Systems", "level": "advanced", "hours": 10},
{"id": 5, "title": "AI Agents", "level": "advanced", "hours": 12},
]
POSITIVE = {"good", "great", "love", "excellent", "helpful", "clear"}
NEGATIVE = {"bad", "slow", "boring", "confusing", "hate", "broken"}
class SentimentRequest(BaseModel):
text: str
@app.get("/")
def home():
return {"name": "BitByte Course API", "docs": "/docs"}
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/courses")
def list_courses(
level: Annotated[str | None, Query(description="beginner or advanced")] = None,
limit: Annotated[int, Query(ge=1, le=50)] = 10,
):
"""কোর্সের তালিকা, চাইলে লেভেল দিয়ে ফিল্টার করা।"""
items = [c for c in COURSES if level is None or c["level"] == level]
return {"count": len(items[:limit]), "items": items[:limit]}
@app.get("/courses/{course_id}")
def get_course(course_id: Annotated[int, Path(ge=1)]):
"""id দিয়ে একটা কোর্স আনা।"""
for course in COURSES:
if course["id"] == course_id:
return course
raise HTTPException(status_code=404, detail=f"Course {course_id} not found")
@app.post("/sentiment")
def sentiment(body: SentimentRequest):
"""টেক্সটকে positive, negative বা neutral লেবেল দেওয়া (আসল মডেলের বদলি)।"""
words = body.text.lower().replace(".", " ").replace(",", " ").split()
score = sum(w in POSITIVE for w in words) - sum(w in NEGATIVE for w in words)
label = "positive" if score > 0 else "negative" if score < 0 else "neutral"
return {"label": label, "model": "keyword-v1"}fastapi run রিলোড করে না, তাই সার্ভার রিস্টার্ট করুন: থামান, পুরোনো প্রসেস চলে যাওয়া পর্যন্ত অপেক্ষা করুন, আবার চালু করুন। (fastapi dev হলে শুধু ফাইল সেভ করলেই হতো।)
kill $SERVER_PID
while kill -0 $SERVER_PID 2>/dev/null; do sleep 0.1; done
uv run fastapi run main.py --port 8000 > server.log 2>&1 &
export SERVER_PID=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 http://localhost:8000/health{"status":"ok"}টাইপসহ পাথ প্যারামিটার
"/courses/{course_id}" একটা পাথ প্যারামিটার ঘোষণা করে: URL-এর ওই অংশে যা থাকে, সেটা একই নামের ফাংশন আর্গুমেন্টে চলে যায়। বাকিটা করে টাইপ হিন্ট:
curl -s http://localhost:8000/courses/2
echo
curl -s http://localhost:8000/courses/42
echo
curl -s http://localhost:8000/courses/two
echo
curl -s http://localhost:8000/courses/0{"id":2,"title":"Python for AI","level":"beginner","hours":12}
{"detail":"Course 42 not found"}
{"detail":[{"type":"int_parsing","loc":["path","course_id"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"two"}]}
{"detail":[{"type":"greater_than_equal","loc":["path","course_id"],"msg":"Input should be greater than or equal to 1","input":"0","ctx":{"ge":1}}]}চারটা রিকোয়েস্ট, চারটা শিক্ষা:
course_id: intআপনার ফাংশন চলার আগেই"2"-কে পূর্ণসংখ্যা2-তে বদলে দিয়েছে। কোনোisdigit()নেই, কোনোint()নেই।- কোর্স না থাকাটা আপনার সিদ্ধান্ত, তাই আপনার কোড
HTTPException(status_code=404, detail=...)raise করে। FastAPI সেটাকে ওই স্ট্যাটাস আর{"detail": ...}-সহ রেসপন্সে বদলে দেয়। twoপূর্ণসংখ্যা নয়, তাই FastAPI আপনার ফাংশন না ডেকেই422দিয়েছে। বডি বলে দেয় সমস্যা কোথায় ("loc": ["path", "course_id"]), কী ভুল (msg), আর সে কী পেয়েছে (input)।Path(ge=1)একটা নিয়ম যোগ করে: ১-এর সমান বা বড়। শূন্য সেটা ভাঙে, তাই আরেকটা 422, আর নিয়মটা দেখা যায়ctx-এ।
Annotated[int, Path(ge=1)]-কে পড়ুন এভাবে: "একটা int, সঙ্গে FastAPI-র জন্য এই বাড়তি তথ্য"। নতুন সব কোডের জন্য FastAPI-র ডকুমেন্টেশন এই স্টাইলই সুপারিশ করে। ব্লগ পোস্টে এখনও পুরোনো স্টাইল course_id: int = Path(ge=1) দেখবেন; সেটাও চলে, কিন্তু আপনি Annotated রূপটাই লিখুন।
কোয়েরি প্যারামিটার
যে ফাংশন আর্গুমেন্ট পাথে নেই, সেটা হয়ে যায় কোয়েরি প্যারামিটার। ডিফল্ট মান থাকলে সেটা ঐচ্ছিক; str | None = None মানে "ঐচ্ছিক, না থাকলে None":
curl -s "http://localhost:8000/courses?level=advanced"
echo
curl -s "http://localhost:8000/courses?level=beginner&limit=2"
echo
curl -s "http://localhost:8000/courses?limit=500"{"count":2,"items":[{"id":4,"title":"RAG Systems","level":"advanced","hours":10},{"id":5,"title":"AI Agents","level":"advanced","hours":12}]}
{"count":2,"items":[{"id":1,"title":"How AI Works","level":"beginner","hours":3},{"id":2,"title":"Python for AI","level":"beginner","hours":12}]}
{"detail":[{"type":"less_than_equal","loc":["query","limit"],"msg":"Input should be less than or equal to 50","input":"500","ctx":{"le":50}}]}শেলে &-ওয়ালা URL কোটেশনের মধ্যে রাখতে ভুলবেন না, নইলে শেল কমান্ডটা ওখানেই কেটে ফেলে। limit পূর্ণসংখ্যায় রূপান্তরিত হয়েছে আর ge=1, le=50 দিয়ে যাচাই হয়েছে: যে ক্লায়েন্ট ৫০০টা কোর্স চায়, সে বিশাল রেসপন্সের বদলে পায় একটা স্পষ্ট 422। AI API-তে এ ধরনের নিয়মই ঠিক করে দেয় একটা রিকোয়েস্টে ক্লায়েন্ট মডেলের কাছে সর্বোচ্চ কয়টা আইটেম পাঠাতে পারবে।
| যেভাবে ঘোষণা | যা হয় | উদাহরণ |
|---|---|---|
পাথের {}-এর ভেতরের নাম | পাথ প্যারামিটার, সবসময় বাধ্যতামূলক | course_id: Annotated[int, Path(ge=1)] |
| ডিফল্টসহ সাধারণ টাইপ | ঐচ্ছিক কোয়েরি প্যারামিটার | limit: Annotated[int, Query(ge=1, le=50)] = 10 |
| ডিফল্ট ছাড়া সাধারণ টাইপ | বাধ্যতামূলক কোয়েরি প্যারামিটার | q: str |
| একটা Pydantic মডেল | JSON রিকোয়েস্ট বডি | body: SentimentRequest |
রিকোয়েস্ট বডি
যে ফাংশন আর্গুমেন্টের টাইপ একটা Pydantic মডেল (BaseModel থেকে ইনহেরিট করা ক্লাস), সেটা পড়া হয় JSON বডি থেকে। SentimentRequest বলছে, বডি হতে হবে একটা অবজেক্ট, যাতে text নামে একটা স্ট্রিং ফিল্ড আছে:
curl -s -X POST http://localhost:8000/sentiment \
-H "Content-Type: application/json" \
-d '{"text": "Great course, the examples are clear."}'
echo
curl -s -X POST http://localhost:8000/sentiment -H "Content-Type: application/json" -d '{"txt": "typo"}'
echo
curl -s -X POST http://localhost:8000/sentiment -H "Content-Type: application/json" -d '{"text": 42}'{"label":"positive","model":"keyword-v1"}
{"detail":[{"type":"missing","loc":["body","text"],"msg":"Field required","input":{"txt":"typo"}}]}
{"detail":[{"type":"string_type","loc":["body","text"],"msg":"Input should be a valid string","input":42}]}ফাংশনের ভেতরে body একটা আসল Python অবজেক্ট: body.text যে আছে আর স্ট্রিং, সেটা নিশ্চিত, তাই আপনার কোড সরাসরি ব্যবহার করতে পারে। ফিল্ড না থাকা আর ভুল টাইপ, দুটোই পায় নিখুঁত 422, আর loc এবার শুরু হয় "body" দিয়ে। পরের পাতা পুরোটাই এই মডেলগুলো নিয়ে: ফিল্ডের সীমা, আরও ভালো এরর, আর রেসপন্সের জন্যও মডেল।
def নাকি async def? এই এন্ডপয়েন্টগুলো সাধারণ
defফাংশন। FastAPI এগুলো চালায় একটা থ্রেড পুলে, তাই ভেতরের ধীর বা ব্লকিং কোড (একটাrequestsকল, একটা scikit-learn প্রেডিকশন) অন্য রিকোয়েস্টকে আটকে দেয় না।async defব্যবহার করুন কেবল তখন, যখন ভেতরে যা কিছুর জন্য অপেক্ষা করেন, সবই async লাইব্রেরি দিয়ে await করা হয়। নিশ্চিত না হলেdefলিখুন।
স্বয়ংক্রিয় ডকুমেন্টেশন
আপনার পাথ, টাইপ, ডিফল্ট আর ডকস্ট্রিং থেকে FastAPI আপনার API-র বর্ণনা বানায় একটা OpenAPI ডকুমেন্টে, আর সেটার তিনটা রূপ দেখায়:
| URL | এটা কী |
|---|---|
/docs | Swagger UI: ইন্টারঅ্যাক্টিভ পেজ, যেখানে প্রতিটা এন্ডপয়েন্ট চালিয়ে দেখা যায় ("Try it out", তারপর "Execute") |
/redoc | ReDoc: একই তথ্য, পরিচ্ছন্ন আর সহজে পড়ার মতো রেফারেন্স হিসেবে |
/openapi.json | কাঁচা OpenAPI ডকুমেন্ট: টুল, ক্লায়েন্ট জেনারেটর আর AI এজেন্ট যা পড়ে |
ব্রাউজারে http://localhost:8000/docs খুলুন আর সেখান থেকে POST /sentiment চালিয়ে দেখুন। টার্মিনাল থেকে পেজগুলো আছে কি না দেখে নিতে পারেন:
curl -s http://localhost:8000/docs | grep -o "<title>.*</title>"
curl -s http://localhost:8000/redoc | grep -o "<title>.*</title>"<title>BitByte Course API - Swagger UI</title>
<title>BitByte Course API - ReDoc</title>Python থেকে আপনার API কল করা
আপনার API আর দশটা ওয়েব API-র মতোই, তাই Python-এর requests দিয়ে API কল পাতার requests কোড কোনো পরিবর্তন ছাড়াই কাজ করে। এই কোড OpenAPI ডকুমেন্ট পড়ে প্রতিটা এন্ডপয়েন্টের তালিকাও দেখায়:
import requests
BASE = "http://localhost:8000"
r = requests.get(f"{BASE}/courses", params={"level": "advanced", "limit": 1}, timeout=5)
print(r.status_code, r.headers["content-type"])
print(r.json())
r = requests.post(f"{BASE}/sentiment", json={"text": "Too slow and boring"}, timeout=5)
print(r.status_code, r.json())
r = requests.get(f"{BASE}/courses/42", timeout=5)
print(r.status_code, r.json())
spec = requests.get(f"{BASE}/openapi.json", timeout=5).json()
print(spec["info"])
for path, operations in spec["paths"].items():
for method, operation in operations.items():
print(f"{method.upper():5} {path:20} {operation['summary']}")200 application/json
{'count': 1, 'items': [{'id': 4, 'title': 'RAG Systems', 'level': 'advanced', 'hours': 10}]}
200 {'label': 'negative', 'model': 'keyword-v1'}
404 {'detail': 'Course 42 not found'}
{'title': 'BitByte Course API', 'version': '0.1.0'}
GET / Home
GET /health Health
GET /courses List Courses
GET /courses/{course_id} Get Course
POST /sentiment SentimentFastAPI প্রতিটা এন্ডপয়েন্টের সারাংশ বানিয়েছে ফাংশনের নাম থেকে (list_courses হয়েছে "List Courses"), আর আপনার ডকস্ট্রিংকে ব্যবহার করেছে বর্ণনা হিসেবে। ভালো ফাংশন নাম আর ডকস্ট্রিং মানে বিনা খরচে ডকুমেন্টেশন। এর গুরুত্ব প্রতি বছর বাড়ছে: AI এজেন্ট আর কোড অ্যাসিস্ট্যান্ট কোনো API কীভাবে কল করতে হয় তা শেখে /openapi.json পড়ে।
সবশেষে, সার্ভারের লগে প্রতিটা রিকোয়েস্ট তার স্ট্যাটাস কোডসহ দেখা যায় (লগ লাইনের শুরুতে INFO: আর ক্লায়েন্টের ঠিকানাও থাকে, এখানে grep -o সেটা কেটে দিয়েছে):
grep -oE '"(GET|POST) /courses.*' server.log"GET /courses/2 HTTP/1.1" 200 OK
"GET /courses/42 HTTP/1.1" 404 Not Found
"GET /courses/two HTTP/1.1" 422 Unprocessable Entity
"GET /courses/0 HTTP/1.1" 422 Unprocessable Entity
"GET /courses?level=advanced HTTP/1.1" 200 OK
"GET /courses?level=beginner&limit=2 HTTP/1.1" 200 OK
"GET /courses?limit=500 HTTP/1.1" 422 Unprocessable Entity
"GET /courses?level=advanced&limit=1 HTTP/1.1" 200 OK
"GET /courses/42 HTTP/1.1" 404 Not FoundUvicorn এখনও 422-এর পুরোনো নাম "Unprocessable Entity" প্রিন্ট করে; বর্তমান HTTP স্ট্যান্ডার্ডে এর নাম "Unprocessable Content"। কোড একই, মানেও একই।
আসল AI কাজে এটা কোথায় দেখবেন
- মডেল সার্ভিং: Pydantic বডিসহ একটা
POST /predictএন্ডপয়েন্ট, বেশিরভাগ টিম এভাবেই ট্রেইন করা মডেলকে API-র পেছনে রাখে;/sentimentঠিক সেই আকারের। - LLM ব্যাকএন্ড: একটা FastAPI অ্যাপ চ্যাট মেসেজ নেয়, প্রোভাইডারকে কল করে, আর উত্তর ফেরত দেয় বা স্ট্রিম করে; অ্যাডভান্সড টিউটোরিয়ালে এমন একটা বানানো হবে।
- এজেন্টের টুল: কোনো এজেন্ট ফ্রেমওয়ার্ক বা MCP সার্ভার আপনার এন্ডপয়েন্ট কল করতে পারে, আর প্রতিটার কী দরকার তা OpenAPI স্কিমা থেকে জেনে নেয়।
- ইন্টারনাল API: ডেটা টিম কোনো ডেটাবেস কোয়েরি বা ফিচার হিসাবকে ছোট একটা FastAPI সার্ভিসে মুড়ে দেয়, যাতে অন্য টিম নিরাপদে ব্যবহার করতে পারে।
সাধারণ ভুল
- রিস্টার্ট করতে ভুলে যাওয়া।
fastapi run(বা--reloadছাড়া শুধুuvicorn) এডিট ধরতে পারে না, ফলে আপনি পুরোনো কোড টেস্ট করেন। সমাধান: ডেভেলপ করার সময়fastapi devব্যবহার করুন। - প্রোডাকশনে
fastapi devচালানো। রিলোড ফাইল নজরে রাখে আর রিসোর্স নষ্ট করে, আর এটা শোনে শুধু127.0.0.1-এ, ফলে কন্টেইনারের বাইরে থেকে পৌঁছানো যায় না। সমাধান: Docker আর সার্ভারেfastapi run। - 200 স্ট্যাটাসসহ এররের dict ফেরত দেওয়া।
return {"error": "not found"}তবুও 200 পাঠায়। সমাধান:raise HTTPException(status_code=404, detail="...")। - পাথ আর ফাংশন আর্গুমেন্টের নাম না মেলা।
@app.get("/courses/{id}")-এর সঙ্গেdef get_course(course_id: int)লিখলেcourse_idহয়ে যায় বাধ্যতামূলক কোয়েরি প্যারামিটার, আর প্রতিটা কল 422 দিয়ে ব্যর্থ হয়। সমাধান: দুই জায়গায় একই নাম ব্যবহার করুন। - "Address already in use"। অন্য কোনো সার্ভার (প্রায়ই ভুলে যাওয়া পুরোনো একটা) আগে থেকেই 8000 পোর্টে শুনছে। সমাধান: সেটার টার্মিনালে Ctrl+C দিয়ে থামান, অথবা আপনারটা
--port 8001দিয়ে চালু করুন।
নিজে চেষ্টা করুন
উত্তরগুলো ছোট আর চালানোর উপযোগী রাখতে এগুলো দ্বিতীয় একটা ফাইল, answers.py, থেকে একই app-এ এন্ডপয়েন্ট যোগ করে, আর 8001 পোর্টে চলে। নিজের প্রজেক্টে আপনি সেগুলো সরাসরি main.py-তে যোগ করবেন।
- সহজ:
GET /levelsযোগ করুন, যা আলাদা আলাদা লেভেলগুলোর সাজানো তালিকা ফেরত দেয়:{"levels": ["advanced", "beginner"]}। - মাঝারি:
GET /searchযোগ করুন, যাতে একটা বাধ্যতামূলক কোয়েরি প্যারামিটারq(কমপক্ষে ২ অক্ষর) থাকবে, আর ফেরত দেবে সেই কোর্সগুলো, যাদের শিরোনামেqআছে (ছোট-বড় হাতের অক্ষর না দেখে)।qনা দিলে কী হয়? - কঠিন:
POST /sentiment/batchযোগ করুন, যা{"texts": ["...", "..."]}নেয় আর প্রতিটা টেক্সটের জন্য একটা করে লেবেল ফেরত দেয়, আগেরsentimentফাংশনটাই আবার ব্যবহার করে।requestsদিয়ে দুটো টেক্সট পাঠান।
উত্তর
# file: answers.py
from typing import Annotated
from fastapi import Query
from pydantic import BaseModel
from main import COURSES, SentimentRequest, app, sentiment
@app.get("/levels")
def levels():
return {"levels": sorted({c["level"] for c in COURSES})}
@app.get("/search")
def search(q: Annotated[str, Query(min_length=2)]):
hits = [c for c in COURSES if q.lower() in c["title"].lower()]
return {"query": q, "count": len(hits), "items": hits}
class BatchRequest(BaseModel):
texts: list[str]
@app.post("/sentiment/batch")
def sentiment_batch(body: BatchRequest):
return {"labels": [sentiment(SentimentRequest(text=t))["label"] for t in body.texts]}uv run fastapi run answers.py --port 8001 > answers.log 2>&1 &
curl -s --retry 20 --retry-connrefused --retry-delay 0 http://localhost:8001/levels
echo
curl -s "http://localhost:8001/search?q=ai"
echo
curl -s "http://localhost:8001/search"{"levels":["advanced","beginner"]}
{"query":"ai","count":4,"items":[{"id":1,"title":"How AI Works","level":"beginner","hours":3},{"id":2,"title":"Python for AI","level":"beginner","hours":12},{"id":3,"title":"Maths for AI","level":"beginner","hours":8},{"id":5,"title":"AI Agents","level":"advanced","hours":12}]}
{"detail":[{"type":"missing","loc":["query","q"],"msg":"Field required","input":null}]}ডিফল্ট ছাড়া প্যারামিটার বাধ্যতামূলক, তাই q না থাকলে আসে "type": "missing"-সহ 422। কঠিন কাজের জন্য নতুন এন্ডপয়েন্টটা Python থেকে কল করুন:
r = requests.post(
"http://localhost:8001/sentiment/batch",
json={"texts": ["I love the clear examples", "The video is broken"]},
timeout=5,
)
print(r.status_code, r.json())200 {'labels': ['positive', 'negative']}AI API-তে ব্যাচ এন্ডপয়েন্ট খুব প্রচলিত: অনেকগুলো ইনপুট নিয়ে একটা রিকোয়েস্ট, একটা করে ইনপুট নিয়ে অনেকগুলো রিকোয়েস্টের চেয়ে অনেক সস্তা। আসল API-তে লিস্টের দৈর্ঘ্যেরও একটা সীমা দিতেন (কীভাবে, তা পরের পাতায় দেখবেন)।
সারসংক্ষেপ
- FastAPI টাইপসহ Python ফাংশনকে ডকুমেন্টেশনসহ HTTP এন্ডপয়েন্টে বদলে দেয়; Python-এ AI ব্যাকএন্ডের জন্য এটাই সাধারণ পছন্দ।
- প্রজেক্ট শুরু করুন
uv initআরuv add "fastapi[standard]"দিয়ে (অথবাpython -m venvআরpip install); ডেভেলপ করুনfastapi devদিয়ে, ডিপ্লয় করুনfastapi runদিয়ে। - পাথ প্যারামিটার আসে পাথের
{name}থেকে, কোয়েরি প্যারামিটার অন্য সাধারণ আর্গুমেন্ট থেকে, আর JSON বডি আসে Pydantic মডেল টাইপের আর্গুমেন্ট থেকে। বাড়তি নিয়ম লিখুনAnnotated[..., Path(...)]আরAnnotated[..., Query(...)]দিয়ে। - ভুল ইনপুট আপনার কোড চলার আগেই পায় অবস্থান, মেসেজ আর ইনপুটসহ 422; আপনার নিজের "পাওয়া যায়নি" হলো
raise HTTPException(status_code=404, ...)। - প্রতিটা অ্যাপে আছে
/docs,/redocআর/openapi.json, যা তৈরি হয় আপনার কোড, নাম আর ডকস্ট্রিং থেকে।
এরপর: Pydantic মডেল, ভ্যালিডেশন ও এরর রেসপন্স। Pydantic দিয়ে ডেটার নিখুঁত বর্ণনা দেবেন, প্রতিটা ফিল্ডে সীমা বসাবেন, রেসপন্স থেকে ফিল্ড লুকাবেন, আর তৈরি, মোছা ও "পাওয়া যায়নি"-র জন্য ঠিক স্ট্যাটাস কোড পাঠাবেন।