# FastAPI: Path Params, Query, Body ו-HTTPException<no value>

## מה נלמד?

בשיעור הקודם הפונקציות לא קיבלו שום קלט. עכשיו הן יקבלו את שלושת הסוגים שלמדנו:

| קלט | איך FastAPI יודע |
|---|---|
| **Path Parameter** | שם בסוגריים מסולסלים בנתיב: `"/students/{student_id}"` |
| **Query Parameter** | פרמטר של הפונקציה שלא מופיע בנתיב |
| **Body** | פרמטר שהטיפוס שלו הוא **מודל Pydantic** |

ובנוסף: **Type Hints**, **`status_code`**, **`HTTPException`**, ושגיאת **422** האוטומטית.

## למה צריך את זה?

בלי קלט, שרת יכול רק להחזיר תמיד את אותו דבר. עם קלט, הוא יכול להחזיר **סטודנט ספציפי**, **לסנן**, ו**ליצור** סטודנטים חדשים. ובונוס ענק: FastAPI **בודק ומתרגם את הקלט בשבילנו**. בנושא מחשב, מערכת הפעלה ורשת, שרת המחשבון קרס על `"hello"`. כאן, קלט שגוי יקבל אוטומטית **422** עם הסבר מדויק, והשרת ימשיך לעבוד.

## הסבר מהבסיס

### Type Hints

**Type Hint** (רמז טיפוס) = הערה בקוד שאומרת מה **הטיפוס הצפוי** של פרמטר:

```python
def get_student(student_id: int):
```

`student_id: int` = "הפרמטר הזה אמור להיות `int`". ב-Python רגיל, Type Hints **לא נאכפים**: הם רק תיעוד, שעוזר ל-VS Code ולמפתחים. **אבל FastAPI קורא אותם ומשתמש בהם**:

1. **המרה** — מה-URL מגיעה המחרוזת `"7"`, ו-FastAPI ממיר אותה ל-`7`, כי ביקשנו `int`.
2. **ולידציה** — אם הגיע `"abc"`, FastAPI **לא** קורא לפונקציה, אלא מחזיר **422** עם הסבר.
3. **תיעוד** — `/docs` מציג את הטיפוס של כל פרמטר.

### Path Parameter

```python
@app.get("/students/{student_id}")
def get_student(student_id: int):
```

`{student_id}` בנתיב = "החלק הזה משתנה". FastAPI מעביר אותו לפרמטר **באותו שם בדיוק**.

### Query Parameters

**כל פרמטר של הפונקציה שלא מופיע בנתיב (ואינו מודל) = Query Parameter.**

```python
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!):

```python
from pydantic import BaseModel


class StudentIn(BaseModel):
    name: str
    age: int
    city: str = "Unknown"
```

- כל שורה = **שדה**, עם טיפוס. שדה עם `=` הוא אופציונלי, עם ברירת מחדל.
- **אין `__init__`!** `BaseModel` בונה אותו לבד מתוך השדות.
- כשפרמטר של פונקציה הוא מהטיפוס הזה, FastAPI מבין שזה **Body**: קורא את ה-JSON, בודק שכל השדות קיימים ומהטיפוס הנכון, ובונה אובייקט. ניגשים לשדות עם נקודה: `student.name`.

{{< box "analogy" "אנלוגיה: טופס עם שדות חובה" >}}
מודל Pydantic הוא **טופס מודפס**: "שם (חובה, טקסט). גיל (חובה, מספר). עיר (אופציונלי)". הפקיד (FastAPI) לא מעביר לטיפול (לפונקציה שלכם) טופס שחסר בו שדה חובה, או שכתוב בו "עשרים" בשדה הגיל. הוא מחזיר אותו מיד למגיש (**422**) עם סימון של השדה הבעייתי. הפונקציה שלכם מקבלת **רק טפסים תקינים**.
{{< /box >}}

### `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.

## תחביר

```python
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`:

```python
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`):

```json
{
  "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: שם לא תואם בין הנתיב לפרמטר.**

```python
@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.**

```python
def get_student(student_id):
    for student in students:
        if student["id"] == student_id:
```

בלי `: int`, הפרמטר מגיע כ**מחרוזת** `"2"`, ו-`2 == "2"` הוא `False`. התוצאה: תמיד 404, בלי שום שגיאה. **Logical Error שקט.**

**הטעות 3: סדר Routes עם נתיב קבוע ונתיב עם פרמטר.**

```python
@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.**

```python
return HTTPException(status_code=404, detail="not found")
```

מחזיר **200**, עם אובייקט מוזר בתוך ה-JSON. שגיאה **זורקים** (`raise`), ולא מחזירים (נושא Python).

**הטעות 5: לכלול `id` במודל של ה-Body.** אם `StudentIn` כולל `id: int`, ה-Client **חייב** לשלוח id, ויכול לשלוח id שכבר קיים. את המזהה **השרת** קובע. מודל ה-Body מכיל רק את מה שה-Client אמור לשלוח.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

### תרגיל 1: Path Parameter עם Type Hint

כתבו API עם רשימת סטודנטים בזיכרון (מילונים עם `id`, `name`, `age`, `city`), ו-Route `GET /students/{student_id}` שמחזיר את הסטודנט, או 404 עם `detail` של `"student not found"`.

1. מה יקרה בבקשה `GET /students/abc`?

{{< solution >}}
```python
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 Hint `student_id: int` גורם ל-FastAPI לבדוק ולהמיר בעצמו. הפונקציה בכלל לא רצה.
{{< /solution >}}

### תרגיל 2: Query Parameters

הוסיפו Route `GET /students` שמקבל Query Parameter אופציונלי `city` (ברירת מחדל `None`), ומחזיר רק את הסטודנטים מהעיר הזו — או את כולם אם לא נשלחה עיר.

{{< solution >}}
```python
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 result
```

- `GET /students` ‏←‏ שני הסטודנטים.
- `GET /students?city=Haifa` ‏←‏ רק דנה.

פרמטר בפונקציה שהוא **לא** חלק מהנתיב הופך אוטומטית ל-**Query Parameter**. ערך ברירת מחדל הופך אותו לאופציונלי.
{{< /solution >}}

### תרגיל 3: POST עם Body

הוסיפו מודל Pydantic בשם `StudentIn` (`name`, `age`, `city`) ו-Route `POST /students` שמחזיר 201, נותן לסטודנט `id` חדש ומוסיף אותו לרשימה. גיל מתחת ל-16 → 400.

{{< solution >}}
```python
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_student
```

- `POST /students` עם `{"name": "Noa", "age": 22, "city": "Eilat"}` → **201** עם `id: 2`.
- גיל 12 → **400**. גוף בלי `age` → **422** — Pydantic דוחה מבנה שגוי לפני שהפונקציה רצה.
{{< /solution >}}

### תרגיל 4: 422 מול 400

1. מה ההבדל בין 422 ל-400 ב-FastAPI? מי מחזיר כל אחד?
2. לכל בקשה — איזה קוד יחזור?

```text
POST /students  {"name": "Noa", "age": "abc", "city": "Eilat"}
POST /students  {"name": "Noa", "age": 10, "city": "Eilat"}
POST /students  {"name": "Noa", "city": "Eilat"}
```

{{< solution >}}
1. **422** — **FastAPI ו-Pydantic** מחזירים אותו אוטומטית כשהמבנה או הטיפוס שגויים. **400** — **הקוד שלנו** מחזיר אותו כשהערך תקין מבחינת טיפוס, אבל לא עומד בכלל עסקי.
2. התשובות:
   - `"age": "abc"` → **422** (גיל אינו מספר).
   - `"age": 10` → **400** (מספר תקין, אבל מתחת לגיל המינימלי — הכלל שלנו).
   - בלי `age` → **422** (שדה חובה חסר).
{{< /solution >}}

### תרגיל 5: סטטיסטיקה

הוסיפו Route `GET /students/stats` שמחזיר את מספר הסטודנטים ואת הגיל הממוצע (מעוגל לספרה אחת). איפה צריך להגדיר אותו ביחס ל-`GET /students/{student_id}`, ולמה?

{{< solution >}}
```python
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 כי הוא לא מספר.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **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.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## דוקומנטציה רשמית

- [FastAPI — Path Parameters](https://fastapi.tiangolo.com/tutorial/path-params/) — Path Params
- [FastAPI — Query Parameters](https://fastapi.tiangolo.com/tutorial/query-params/) — Query
- [FastAPI — Request Body](https://fastapi.tiangolo.com/tutorial/body/) — Body
- [FastAPI — Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/) — HTTPException

<!-- docs:end -->
