FastAPI: Path Params, Query, Body ו-HTTPException
- להבין Type Hints ואיך FastAPI משתמש בהם להמרה ולולידציה
- להגדיר Path Parameter ו-Query Parameters (חובה, ברירת מחדל ואופציונלי)
- להגדיר Body עם מודל Pydantic (BaseModel) ולקבל אותו ב-POST
- להחזיר קודים נכונים — 201 ביצירה, 404 עם HTTPException — ולהבין את 422 האוטומטי
מה נלמד?
בשיעור הקודם הפונקציות לא קיבלו שום קלט. עכשיו הן יקבלו את שלושת הסוגים שלמדנו:
| קלט | איך FastAPI יודע |
|---|---|
| Path Parameter | שם בסוגריים מסולסלים בנתיב: "/students/{student_id}" |
| Query Parameter | פרמטר של הפונקציה שלא מופיע בנתיב |
| Body | פרמטר שהטיפוס שלו הוא מודל Pydantic |
ובנוסף: Type Hints, status_code, HTTPException, ושגיאת 422 האוטומטית.
למה צריך את זה?
בלי קלט, שרת יכול רק להחזיר תמיד את אותו דבר. עם קלט, הוא יכול להחזיר סטודנט ספציפי, לסנן, וליצור סטודנטים חדשים. ובונוס ענק: FastAPI בודק ומתרגם את הקלט בשבילנו. בנושא מחשב, מערכת הפעלה ורשת, שרת המחשבון קרס על "hello". כאן, קלט שגוי יקבל אוטומטית 422 עם הסבר מדויק, והשרת ימשיך לעבוד.
הסבר מהבסיס
Type Hints
Type Hint (רמז טיפוס) = הערה בקוד שאומרת מה הטיפוס הצפוי של פרמטר:
def get_student(student_id: int):student_id: int = “הפרמטר הזה אמור להיות int”. ב-Python רגיל, Type Hints לא נאכפים: הם רק תיעוד, שעוזר ל-VS Code ולמפתחים. אבל FastAPI קורא אותם ומשתמש בהם:
- המרה — מה-URL מגיעה המחרוזת
"7", ו-FastAPI ממיר אותה ל-7, כי ביקשנוint. - ולידציה — אם הגיע
"abc", FastAPI לא קורא לפונקציה, אלא מחזיר 422 עם הסבר. - תיעוד —
/docsמציג את הטיפוס של כל פרמטר.
Path Parameter
@app.get("/students/{student_id}")
def get_student(student_id: int):{student_id} בנתיב = “החלק הזה משתנה”. FastAPI מעביר אותו לפרמטר באותו שם בדיוק.
Query Parameters
כל פרמטר של הפונקציה שלא מופיע בנתיב (ואינו מודל) = Query Parameter.
def list_students(city: str | None = None, min_age: int = 0, limit: int = 10):| הגדרה | משמעות |
|---|---|
min_age: int = 0 | אופציונלי, עם ברירת מחדל 0. |
limit: int (בלי =) | חובה. בלעדיו ← 422. |
city: str | None = None | אופציונלי, בלי ערך ברירת מחדל: “או מחרוזת, או None”. None = “לא נשלח, ולכן לא מסננים”. |
str | None נקרא “str או None”. זה Type Hint שאומר שמותרים שני טיפוסים.
Body עם Pydantic
Pydantic היא ספרייה (מותקנת יחד עם FastAPI) שמגדירה מבנה של נתונים עם טיפוסים. יוצרים Class שיורשת מ-BaseModel (Inheritance, נושא OOP!):
from pydantic import BaseModel
class StudentIn(BaseModel):
name: str
age: int
city: str = "Unknown"- כל שורה = שדה, עם טיפוס. שדה עם
=הוא אופציונלי, עם ברירת מחדל. - אין
__init__!BaseModelבונה אותו לבד מתוך השדות. - כשפרמטר של פונקציה הוא מהטיפוס הזה, FastAPI מבין שזה Body: קורא את ה-JSON, בודק שכל השדות קיימים ומהטיפוס הנכון, ובונה אובייקט. ניגשים לשדות עם נקודה:
student.name.
מודל Pydantic הוא טופס מודפס: “שם (חובה, טקסט). גיל (חובה, מספר). עיר (אופציונלי)”. הפקיד (FastAPI) לא מעביר לטיפול (לפונקציה שלכם) טופס שחסר בו שדה חובה, או שכתוב בו “עשרים” בשדה הגיל. הוא מחזיר אותו מיד למגיש (422) עם סימון של השדה הבעייתי. הפונקציה שלכם מקבלת רק טפסים תקינים.
status_code ו-HTTPException
@app.post("/students", status_code=201)— קוד ההצלחה של ה-Endpoint הזה הוא 201 Created, ולא 200.raise HTTPException(status_code=404, detail="...")— עוצר את הפונקציה ומחזיר ל-Client קוד שגיאה עם JSON{"detail": "..."}. זהraiseרגיל (נושא Python!), ו-FastAPI תופס אותו והופך אותו לתשובת HTTP.
תחביר
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class ItemIn(BaseModel): # body model
name: str
price: float
in_stock: bool = True
@app.get("/items/{item_id}") # path parameter
def get_item(item_id: int):
...
raise HTTPException(status_code=404, detail="item not found")
@app.get("/items") # query parameters
def list_items(max_price: float | None = None, limit: int = 10):
...
@app.post("/items", status_code=201) # body
def create_item(item: ItemIn):
item.name, item.price # use the validated fieldsדוגמת קוד ראשונה
API סטודנטים בזיכרון (ב-RAM. עוד אין Database, הוא יגיע בנושא מסדי נתונים ו-MySQL). week-4/hello-api/students_memory.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="Students (in memory)")
students = [
{"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
{"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
{"id": 3, "name": "Noa", "age": 22, "city": "Haifa"},
]
class StudentIn(BaseModel):
name: str
age: int
city: str = "Unknown"
@app.get("/students")
def list_students(city: str | None = None, min_age: int = 0, limit: int = 10):
result = []
for student in students:
if city is not None and student["city"] != city:
continue
if student["age"] < min_age:
continue
result.append(student)
return result[:limit]
@app.get("/students/{student_id}")
def get_student(student_id: int):
for student in students:
if student["id"] == student_id:
return student
raise HTTPException(status_code=404, detail="student " + str(student_id) + " not found")
@app.post("/students", status_code=201)
def create_student(student: StudentIn):
new_id = max(s["id"] for s in students) + 1
new_student = {"id": new_id, "name": student.name, "age": student.age, "city": student.city}
students.append(new_student)
return new_studentהרצה: uvicorn students_memory:app --reload (שימו לב: שם הקובץ השתנה!).
פירוק הקוד שורה אחר שורה
| קוד | הסבר |
|---|---|
students = [...] | “Database” זמני ב-RAM: רשימת dicts (נושא Python). נמחק בכל הפעלה מחדש (נושא מחשב, מערכת הפעלה ורשת!). בנושא מסדי נתונים ו-MySQL נעבור ל-Database אמיתי. |
class StudentIn(BaseModel): | מודל ה-Body: מה ה-Client שולח ביצירה. אין בו id, כי השרת קובע את ה-id. |
city: str = "Unknown" | שדה אופציונלי ב-Body. |
list_students(city: str | None = None, min_age: int = 0, limit: int = 10) | שלושה Query Parameters, כולם אופציונליים. /students?city=Haifa&min_age=23. |
if city is not None and ...: continue | סינון רק אם city נשלח. continue מדלג לסטודנט הבא. |
return result[:limit] | Slicing (נושא Python): לכל היותר limit תוצאות. |
@app.get("/students/{student_id}") + student_id: int | Path Parameter, עם המרה ל-int. |
raise HTTPException(status_code=404, ...) | לא נמצא, ולכן 404 עם הודעה. |
@app.post(..., status_code=201) | יצירה מחזירה 201. |
create_student(student: StudentIn) | הטיפוס הוא מודל, ולכן זה Body. FastAPI מפענח וּבודק. |
student.name, student.age | גישה לשדות של אובייקט (נושא OOP). |
max(s["id"] for s in students) + 1 | מזהה חדש: הגדול ביותר + 1. |
דוגמה נוספת
בדיקה מלאה של כל סוגי הקלט, והתשובות האמיתיות. (אפשר לבדוק גם ב-/docs, או עם curl.)
| בקשה | קוד | תשובה |
|---|---|---|
GET /students | 200 | כל השלושה |
GET /students?city=Haifa | 200 | דנה ונועה |
GET /students?min_age=23&limit=1 | 200 | [{"id": 1, "name": "Dana", ...}] |
GET /students/2 | 200 | {"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"} |
GET /students/42 | 404 | {"detail": "student 42 not found"} |
GET /students/abc | 422 | ראו למטה |
GET /students?min_age=old | 422 | ראו למטה |
POST /students + {"name": "Tom", "age": 27} | 201 | {"id": 4, "name": "Tom", "age": 27, "city": "Unknown"} |
POST /students + {"name": "Rina", "age": "twenty"} | 422 | ראו למטה |
שגיאת 422 — איך היא נראית (GET /students/abc):
{
"detail": [
{
"type": "int_parsing",
"loc": ["path", "student_id"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "abc"
}
]
}קריאת 422:
loc(Location) — איפה הבעיה:["path", "student_id"]. ב-Query היה["query", "min_age"], וב-Body היה["body", "age"].msg— מה הבעיה.input— מה התקבל בפועל.- הפונקציה שלנו לא רצה בכלל. FastAPI עצר את הבקשה לפני כן. אין צורך ב-
try/exceptעלint().
ואחרי POST של Tom: GET /students?city=Unknown מחזיר את Tom, כי ה-city קיבל ברירת מחדל. אבל עצרו את השרת והפעילו מחדש: Tom נעלם. הנתונים היו ב-RAM. זו בדיוק הסיבה ל-Database (מחר!).
טעות נפוצה
הטעות 1: שם לא תואם בין הנתיב לפרמטר.
@app.get("/students/{student_id}")
def get_student(id: int):FastAPI לא מוצא פרמטר בשם student_id, ומתייחס ל-id כ-Query Parameter חובה. כל בקשה ל-/students/7 תחזיר 422 ("loc": ["query", "id"], missing). השם בסוגריים המסולסלים חייב להיות זהה לשם הפרמטר.
הטעות 2: Path Parameter בלי Type Hint.
def get_student(student_id):
for student in students:
if student["id"] == student_id:בלי : int, הפרמטר מגיע כמחרוזת "2", ו-2 == "2" הוא False. התוצאה: תמיד 404, בלי שום שגיאה. Logical Error שקט.
הטעות 3: סדר Routes עם נתיב קבוע ונתיב עם פרמטר.
@app.get("/students/{student_id}")
def get_student(student_id: int): ...
@app.get("/students/top")
def top_students(): ...GET /students/top → 422! FastAPI בודק Routes לפי הסדר, ו-/students/{student_id} “תופס” את top כ-student_id, ונכשל בהמרה ל-int. נתיבים קבועים לפני נתיבים עם פרמטרים.
הטעות 4: return במקום raise ל-HTTPException.
return HTTPException(status_code=404, detail="not found")מחזיר 200, עם אובייקט מוזר בתוך ה-JSON. שגיאה זורקים (raise), ולא מחזירים (נושא Python).
הטעות 5: לכלול id במודל של ה-Body. אם StudentIn כולל id: int, ה-Client חייב לשלוח id, ויכול לשלוח id שכבר קיים. את המזהה השרת קובע. מודל ה-Body מכיל רק את מה שה-Client אמור לשלוח.
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: Path Parameter עם Type Hint
כתבו API עם רשימת סטודנטים בזיכרון (מילונים עם id, name, age, city), ו-Route GET /students/{student_id} שמחזיר את הסטודנט, או 404 עם detail של "student not found".
- מה יקרה בבקשה
GET /students/abc?
הצג פתרון
from fastapi import FastAPI, HTTPException
app = FastAPI(title="Students API")
students = [
{"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
{"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]
@app.get("/students/{student_id}")
def get_student(student_id: int):
for student in students:
if student["id"] == student_id:
return student
raise HTTPException(status_code=404, detail="student not found")GET /students/2→ 200 עם אבי.GET /students/9→ 404{"detail": "student not found"}.GET /students/abc→ 422: ה-Type Hintstudent_id: intגורם ל-FastAPI לבדוק ולהמיר בעצמו. הפונקציה בכלל לא רצה.
תרגיל 2: Query Parameters
הוסיפו Route GET /students שמקבל Query Parameter אופציונלי city (ברירת מחדל None), ומחזיר רק את הסטודנטים מהעיר הזו — או את כולם אם לא נשלחה עיר.
הצג פתרון
from fastapi import FastAPI
app = FastAPI(title="Students API")
students = [
{"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
{"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]
@app.get("/students")
def list_students(city: str = None):
if city is None:
return students
result = []
for student in students:
if student["city"] == city:
result.append(student)
return resultGET /students← שני הסטודנטים.GET /students?city=Haifa← רק דנה.
פרמטר בפונקציה שהוא לא חלק מהנתיב הופך אוטומטית ל-Query Parameter. ערך ברירת מחדל הופך אותו לאופציונלי.
תרגיל 3: POST עם Body
הוסיפו מודל Pydantic בשם StudentIn (name, age, city) ו-Route POST /students שמחזיר 201, נותן לסטודנט id חדש ומוסיף אותו לרשימה. גיל מתחת ל-16 → 400.
הצג פתרון
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="Students API")
MIN_AGE = 16
students = [
{"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
]
class StudentIn(BaseModel):
name: str
age: int
city: str
@app.post("/students", status_code=201)
def create_student(student: StudentIn):
if student.age < MIN_AGE:
raise HTTPException(status_code=400, detail="age must be at least 16")
new_student = {"id": len(students) + 1, "name": student.name,
"age": student.age, "city": student.city}
students.append(new_student)
return new_studentPOST /studentsעם{"name": "Noa", "age": 22, "city": "Eilat"}→ 201 עםid: 2.- גיל 12 → 400. גוף בלי
age→ 422 — Pydantic דוחה מבנה שגוי לפני שהפונקציה רצה.
תרגיל 4: 422 מול 400
- מה ההבדל בין 422 ל-400 ב-FastAPI? מי מחזיר כל אחד?
- לכל בקשה — איזה קוד יחזור?
POST /students {"name": "Noa", "age": "abc", "city": "Eilat"}
POST /students {"name": "Noa", "age": 10, "city": "Eilat"}
POST /students {"name": "Noa", "city": "Eilat"}הצג פתרון
- 422 — FastAPI ו-Pydantic מחזירים אותו אוטומטית כשהמבנה או הטיפוס שגויים. 400 — הקוד שלנו מחזיר אותו כשהערך תקין מבחינת טיפוס, אבל לא עומד בכלל עסקי.
- התשובות:
"age": "abc"→ 422 (גיל אינו מספר)."age": 10→ 400 (מספר תקין, אבל מתחת לגיל המינימלי — הכלל שלנו).- בלי
age→ 422 (שדה חובה חסר).
תרגיל 5: סטטיסטיקה
הוסיפו Route GET /students/stats שמחזיר את מספר הסטודנטים ואת הגיל הממוצע (מעוגל לספרה אחת). איפה צריך להגדיר אותו ביחס ל-GET /students/{student_id}, ולמה?
הצג פתרון
from fastapi import FastAPI, HTTPException
app = FastAPI(title="Students API")
students = [
{"id": 1, "name": "Dana", "age": 24, "city": "Haifa"},
{"id": 2, "name": "Avi", "age": 31, "city": "Tel Aviv"},
]
@app.get("/students/stats")
def students_stats():
total_age = 0
for student in students:
total_age = total_age + student["age"]
return {"count": len(students), "average_age": round(total_age / len(students), 1)}
@app.get("/students/{student_id}")
def get_student(student_id: int):
for student in students:
if student["id"] == student_id:
return student
raise HTTPException(status_code=404, detail="student not found")GET /students/stats → {"count": 2, "average_age": 27.5}.
את /students/stats מגדירים לפני /students/{student_id}. FastAPI בודק Routes לפי הסדר — אילו {student_id} היה ראשון, stats היה נחשב ל-id, ונכשל ב-422 כי הוא לא מספר.
סיכום
- Type Hints (
x: int) — FastAPI ממיר וּבודק לפיהם. קלט לא תקין מקבל 422 אוטומטית, והפונקציה לא רצה. - Path Parameter:
"/students/{student_id}"+student_id: int, באותו שם בדיוק. - Query: כל פרמטר שלא בנתיב.
x: int = 0אופציונלי.x: str | None = Noneאופציונלי בלי ברירת מחדל. בלי=, חובה. - Body: Class שיורשת מ-
BaseModel(Pydantic). פרמטר מהטיפוס הזה = JSON מהבקשה, אחרי ולידציה. status_code=201ליצירה.raise HTTPException(404, ...)ללא נמצא, ו-400 לכלל עסקי.- נתיבים קבועים לפני נתיבים עם פרמטר. ונתונים ב-RAM נמחקים בהפעלה מחדש, ולכן צריך Database.
דוקומנטציה רשמית
- FastAPI — Path Parameters — Path Params
- FastAPI — Query Parameters — Query
- FastAPI — Request Body — Body
- FastAPI — Handling Errors — HTTPException