FastAPI: প্রথম API

Route, Pydantic model, auto docs

রোডম্যাপের মাস ৩পড়তে প্রায় ১৫ মিনিট

এতদিন যা শিখেছো (function, type hint, Pydantic, async, decorator) সব এক জায়গায় মিলে যায় FastAPI তে। এটা Python এর সবচেয়ে জনপ্রিয় আধুনিক API framework: দ্রুত, type hint দিয়ে চলে, আর নিজে থেকেই API এর documentation বানিয়ে দেয়। Laravel এ যেমন routes/api.php আর Controller, FastAPI তে route function। এই অধ্যায়ে প্রথম API বানাবো আর দেখবো একটা request এর পেছনে FastAPI কী কী করে।

#Setup আর প্রথম API

Terminal
uv init notes-api && cd notes-api
uv add "fastapi[standard]"
Python
# main.py
from fastapi import FastAPI

app = FastAPI(title="Notes API", version="0.1.0")


@app.get("/")
async def root():
    return {"message": "স্বাগতম!"}


@app.get("/hello/{name}")
async def hello(name: str, times: int = 1):
    return {"greeting": f"হ্যালো {name}! " * times}
Terminal
uv run fastapi dev main.py

Browser এ খোলো:

  • http://127.0.0.1:8000/ → {"message": "স্বাগতম!"}
  • http://127.0.0.1:8000/hello/Rahim?times=2
  • http://127.0.0.1:8000/docs → Swagger UI: সব endpoint এর তালিকা, আর browser থেকেই চালিয়ে দেখা যায়

/docs page টা তোমার Flutter developer সত্তার জন্য দারুণ খবর: API কী নেয় আর কী দেয়, সব লেখা থাকে, আর বদল করলে নিজে আপডেট হয়।

#Parameter কোথা থেকে আসে

FastAPI function এর parameter দেখে বুঝে নেয় কোনটা কোথা থেকে আসবে:

Python
from fastapi import FastAPI, Header, Query
from pydantic import BaseModel, Field

app = FastAPI()


class NoteCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    content: str = ""
    tags: list[str] = []


@app.post("/notes/{folder_id}")
async def create_note(
    folder_id: int,                                   # path এ আছে → path parameter
    note: NoteCreate,                                 # Pydantic model → JSON body
    pin: bool = False,                                # সাধারণ type, path এ নেই → query (?pin=true)
    lang: str = Query("bn", pattern="^(bn|en)$"),     # query, যাচাইসহ
    user_agent: str | None = Header(None),            # header
):
    return {"folder": folder_id, "note": note, "pin": pin, "lang": lang, "ua": user_agent}
কোথা থেকে কীভাবে বোঝে
Path (/notes/{folder_id}) URL এ {} এর ভেতরের নামের সাথে মেলে
Query (?pin=true) সাধারণ type (int, str, bool), path এ নেই
Body (JSON) Pydantic model
Header, Cookie Header(), Cookie() দিয়ে বলে দিতে হয়

আর type অনুযায়ী যাচাই আর রূপান্তর নিজে থেকে হয়। folder_id তে "abc" পাঠালে:

JSON
{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["path", "folder_id"],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "abc"
    }
  ]
}

Status 422 Unprocessable Entity, আর তুমি এক লাইনও যাচাইয়ের code লেখোনি।

#একটা request এর যাত্রা FastAPI এর ভেতরে

Request JSON, query Middleware CORS, log Routing কোন function যাচাই Pydantic Dependencies DB session, user তোমার function আসল কাজ response_model বাড়তি field বাদ Response JSON যাচাই ব্যর্থ → 422
তোমার function চলার আগেই যাচাই আর dependency শেষ। ভুল data তোমার code পর্যন্ত পৌঁছায়ই না।

#response_model: কী ফেরত যাবে

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

app = FastAPI()


class NoteCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    content: str = ""


class NoteOut(BaseModel):
    id: int
    title: str
    content: str
    created_at: datetime


# আপাতত memory তে রাখছি; পরের অধ্যায়ে database
_notes: dict[int, dict] = {}
_next_id = 1


@app.post("/notes", response_model=NoteOut, status_code=status.HTTP_201_CREATED)
async def create_note(data: NoteCreate):
    global _next_id
    note = {"id": _next_id, **data.model_dump(), "created_at": datetime.now(), "owner_secret": "xyz"}
    _notes[_next_id] = note
    _next_id += 1
    return note          # owner_secret বাদ পড়বে, কারণ NoteOut এ নেই


@app.get("/notes", response_model=list[NoteOut])
async def list_notes(limit: int = 20, offset: int = 0):
    return list(_notes.values())[offset : offset + limit]


@app.get("/notes/{note_id}", response_model=NoteOut)
async def get_note(note_id: int):
    note = _notes.get(note_id)
    if note is None:
        raise HTTPException(status_code=404, detail="Note পাওয়া যায়নি")
    return note


@app.delete("/notes/{note_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_note(note_id: int):
    if _notes.pop(note_id, None) is None:
        raise HTTPException(status_code=404, detail="Note পাওয়া যায়নি")

response_model দুইটা কাজ করে:

  1. নিরাপত্তা: model এ যা নেই তা বাদ। Database এর user object থেকে password_hash ভুল করে client এ চলে যাওয়ার ভয় থাকে না।
  2. Documentation: /docs এ দেখায় ঠিক কী ফেরত আসবে। Flutter এ model class বানাতে এটা কাজে লাগে।

#Dependency Injection: Depends

অনেক route এ একই জিনিস লাগে: database session, login করা user, pagination এর parameter। FastAPI তে এগুলো dependency হিসেবে লেখো:

Python
from typing import Annotated
from fastapi import Depends


class Pagination(BaseModel):
    limit: int
    offset: int


def pagination(limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0)) -> Pagination:
    return Pagination(limit=limit, offset=offset)


PageDep = Annotated[Pagination, Depends(pagination)]


@app.get("/notes")
async def list_notes(page: PageDep):
    return list(_notes.values())[page.offset : page.offset + page.limit]

এখন যেকোনো list endpoint এ page: PageDep লিখলেই একই নিয়মে limit (সর্বোচ্চ ১০০) আর offset পাবে। Database session আর current user ও এভাবে আসবে, পরের অধ্যায়গুলোয় দেখবো।

#Async নাকি sync route

Python
@app.get("/a")
async def a():          # event loop এ চলে; ভেতরে শুধু await করা I/O
    ...

@app.get("/b")
def b():                # FastAPI আলাদা thread এ চালায়; sync library চলে
    ...

নিয়ম (Async অধ্যায়ের সারাংশ):

  • ভেতরে async library (asyncpg, httpx.AsyncClient, redis.asyncio) → async def
  • ভেতরে sync library বা blocking কাজ → সাধারণ def
  • কখনো async def এর ভেতরে blocking কাজ না।

#CORS: browser থেকে call করতে দেওয়া

Flutter mobile app এর জন্য CORS লাগে না। কিন্তু Flutter web বা অন্য website থেকে call করলে browser অন্য domain এর API আটকে দেয়, যদি না server অনুমতি দেয়:

Python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com", "http://localhost:3000"],   # "*" production এ না
    allow_methods=["*"],
    allow_headers=["*"],
    allow_credentials=True,
)

#অনুশীলন

#নিজেকে যাচাই করো

FastAPI কীভাবে বোঝে একটা parameter path, query নাকি body থেকে আসবে?

URL path এ {name} থাকলে path parameter। Pydantic model হলে JSON body। সাধারণ type (int, str, bool) যেটা path এ নেই সেটা query parameter। Header বা Cookie এর জন্য আলাদা করে বলতে হয়।

`response_model` এর দুইটা কাজ কী?

Model এ নেই এমন field (যেমন password_hash) response থেকে বাদ দেয়, আর documentation এ দেখায় ঠিক কী ফেরত আসবে।

Input আর output এর জন্য আলাদা Pydantic model কেন?

Client যেন server এর ঠিক করা field (id, created_at, owner) নিজে পাঠাতে না পারে, আর output এ যেন ভেতরের গোপন field চলে না যায়। Update এর জন্য আলাদা model এ সব field ঐচ্ছিক রাখা যায়।

ভুল type এর data পাঠালে FastAPI কী status code দেয়?

422 Unprocessable Entity, সাথে কোন field এ কী সমস্যা তার বিস্তারিত।

চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।