Students API — CRUD מלא: POST, GET, PUT ו-DELETE מול המסד
- להוסיף POST, PUT ו-DELETE שמחוברים לפונקציות ב-db.py
- לבצע ולידציה עסקית בפונקציה משותפת, ולהחזיר 400
- לתרגם את החוזה של db.py לקודים — 201, 204, 404 ו-409
- להריץ את השרת המלא ולקרוא נכון כל תשובה
מה נלמד?
משלימים את ה-API לפי טבלת המיפוי:
POST /students→ INSERT → 201.PUT /students/{id}→ UPDATE → 200 / 404 / 409.DELETE /students/{id}→ DELETE → 204 / 404.- מודל קלט (Pydantic) וולידציה עסקית משותפת (400).
למה צריך את זה?
זה הפרויקט הקצר שמסכם את Server + Database + CRUD, כמו שהסילבוס מגדיר. בסוף השיעור יש API אמיתי: אפליקציה, Frontend או שירות אחר יכולים ליצור, לקרוא, לעדכן ולמחוק סטודנטים, והנתונים נשמרים במסד.
הסבר מהבסיס
מודל הקלט
class StudentIn(BaseModel):
name: str
age: int
email: strStudentIn מתאר מה ה-Client שולח ב-POST וב-PUT. אין בו id, כי את ה-id נותן המסד (POST) או ה-Path (PUT). השם ...In הוא מוסכמה נפוצה: “המודל של מה שנכנס”.
FastAPI מוודא מבנה וטיפוסים (חסר שדה, טקסט במקום מספר ← 422, אוטומטית).
ולידציה עסקית — 400
כללים (גיל 16–120, שם של לפחות 2 תווים, אימייל עם @) הם שלנו. בודקים אותם בפונקציה אחת, ש-POST ו-PUT קוראות לה (DRY):
def validate_student(student):
if student.age < 16 or student.age > 120:
raise HTTPException(status_code=400, detail="age must be between 16 and 120")שלוש שכבות של בדיקות, וכל אחת עם התפקיד שלה:
| שכבה | בודקת | קוד |
|---|---|---|
| Pydantic (FastAPI) | מבנה וטיפוסים | 422 |
validate_student (main.py) | כללים עסקיים | 400 |
| אילוצי המסד (db.py) | ייחודיות, חובה | 409 (דרך ValueError) |
204 No Content
ב-DELETE מצליח אין מה להחזיר: הסטודנט נמחק. status_code=204 אומר “הצלחתי, בלי גוף”. הפונקציה פשוט לא מחזירה כלום (None), ו-FastAPI שולח תשובה ריקה.
תרגום של ValueError
try:
return db.create_student(...)
except ValueError as error:
raise HTTPException(status_code=409, detail=str(error))str(error) = ההודעה ש-db.py כתבה (“a student with email … already exists”). ההודעה עוברת שתי שכבות: מהמסד (IntegrityError), דרך db.py (ValueError), אל ה-Client (409 + detail).
תחביר
@app.post("/students", status_code=201)
def create_student(student: StudentIn): # body
...
@app.put("/students/{student_id}")
def update_student(student_id: int, student: StudentIn): # path + body together
...
@app.delete("/students/{student_id}", status_code=204)
def delete_student(student_id: int):
... # return nothing on successב-PUT יש גם Path Parameter וגם Body. FastAPI מבחין ביניהם: student_id בנתיב, ו-student (מודל) ב-Body.
דוגמת קוד ראשונה
main.py המלא:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import db
app = FastAPI(title="Students API")
db.init_db()
class StudentIn(BaseModel):
name: str
age: int
email: str
def validate_student(student):
if len(student.name.strip()) < 2:
raise HTTPException(status_code=400, detail="name must have at least 2 characters")
if student.age < 16 or student.age > 120:
raise HTTPException(status_code=400, detail="age must be between 16 and 120")
if "@" not in student.email:
raise HTTPException(status_code=400, detail="invalid email")
@app.get("/students")
def list_students():
return db.get_all_students()
@app.get("/students/{student_id}")
def get_student(student_id: int):
student = db.get_student(student_id)
if student is None:
raise HTTPException(status_code=404, detail="student not found")
return student
@app.post("/students", status_code=201)
def create_student(student: StudentIn):
validate_student(student)
try:
return db.create_student(student.name, student.age, student.email)
except ValueError as error:
raise HTTPException(status_code=409, detail=str(error))
@app.put("/students/{student_id}")
def update_student(student_id: int, student: StudentIn):
validate_student(student)
try:
updated = db.update_student(student_id, student.name, student.age, student.email)
except ValueError as error:
raise HTTPException(status_code=409, detail=str(error))
if updated is None:
raise HTTPException(status_code=404, detail="student not found")
return updated
@app.delete("/students/{student_id}", status_code=204)
def delete_student(student_id: int):
if not db.delete_student(student_id):
raise HTTPException(status_code=404, detail="student not found")הרצה: uvicorn main:app --reload, מתיקיית הפרויקט, עם ה-venv פעיל.
פירוק הקוד שורה אחר שורה
| חלק | הסבר |
|---|---|
validate_student(student) | ולידציה אחת לשני Endpoints. raise HTTPException(400) עוצר את הבקשה לפני שנוגעים במסד. |
| POST | ולידציה ← db.create_student ← מחזיר את הסטודנט מהמסד (עם id) ו-201. ValueError (אימייל קיים) ← 409. |
| PUT | ולידציה ← db.update_student. שני כישלונות שונים: ValueError ← 409, ו-None ← 404. |
updated = ... בתוך try, הבדיקה של None מחוצה לו | ה-try עוטף רק את מה שעלול לזרוק ValueError (נושא Python: try קטן). |
| DELETE | db.delete_student מחזירה False ← 404. הצלחה ← אין return ← 204 בלי גוף. |
if not db.delete_student(...) | קריאה ובדיקה בשורה אחת. המחיקה מתבצעת בכל מקרה, ו-not הופך את False ל"נכשל". |
סך הכול: קובץ של כ-60 שורות, שמכיל API מלא. כל Route קצר, כי כל העבודה הכבדה (SQL, אילוצים) נמצאת ב-db.py, וכל ההמרות (JSON, טיפוסים) ב-FastAPI.
דוגמה נוספת
כל התשובות האמיתיות של השרת — רצף בקשות על מסד ריק (נבדק עם הקוד למעלה):
| # | בקשה | Body | קוד | תשובה |
|---|---|---|---|---|
| 1 | POST /students | {"name": "Dana", "age": 24, "email": "dana@mail.com"} | 201 | {"id": 1, "name": "Dana", "age": 24, "email": "dana@mail.com"} |
| 2 | POST /students | {"name": "Avi", "age": 31, "email": "avi@mail.com"} | 201 | {"id": 2, ...} |
| 3 | POST /students | {"name": "Dana2", "age": 20, "email": "dana@mail.com"} | 409 | {"detail": "a student with email dana@mail.com already exists"} |
| 4 | POST /students | {"name": "Kid", "age": 7, "email": "kid@mail.com"} | 400 | {"detail": "age must be between 16 and 120"} |
| 5 | POST /students | {"name": "Noa"} | 422 | {"detail": [{"type": "missing", "loc": ["body", "age"], ...}, {... "email" ...}]} |
| 6 | POST /students | {"name": "Noa", "age": "abc", "email": "n@m.com"} | 422 | {"detail": [{"type": "int_parsing", "loc": ["body", "age"], ...}]} |
| 7 | GET /students | — | 200 | [{"id": 1, ...Dana}, {"id": 2, ...Avi}] |
| 8 | GET /students/99 | — | 404 | {"detail": "student not found"} |
| 9 | GET /students/abc | — | 422 | {"detail": [{"type": "int_parsing", "loc": ["path", "student_id"], ...}]} |
| 10 | PUT /students/2 | {"name": "Avi Levi", "age": 32, "email": "avi@mail.com"} | 200 | {"id": 2, "name": "Avi Levi", "age": 32, "email": "avi@mail.com"} |
| 11 | PUT /students/99 | {"name": "Ghost", "age": 30, "email": "g@mail.com"} | 404 | {"detail": "student not found"} |
| 12 | DELETE /students/1 | — | 204 | (ריק) |
| 13 | DELETE /students/1 | — | 404 | {"detail": "student not found"} |
| 14 | GET /students | — | 200 | [{"id": 2, "name": "Avi Levi", "age": 32, "email": "avi@mail.com"}] |
ולוג השרת (ב-Terminal של uvicorn) מציג שורה לכל בקשה, למשל:
INFO: 127.0.0.1:49197 - "POST /students HTTP/1.1" 201 Created
INFO: 127.0.0.1:49198 - "GET /students/999 HTTP/1.1" 404 Not Foundקריאה נכונה של הטבלה: לכל אחת מהשורות יש סיבה בשכבה אחרת:
- 422 (שורות 5, 6, 9) — FastAPI ו-Pydantic. הקוד שלנו לא רץ.
- 400 (שורה 4) —
validate_student. - 409 (שורה 3) — המסד (
UNIQUE), דרךdb.py. - 404 (שורות 8, 11, 13) —
db.pyהחזירהNone/False.
טעות נפוצה
הטעות 1: לשכוח status_code=201 ב-POST, או 204 ב-DELETE. הכול “עובד”, אבל עם 200. ה-Client (ובדיקות אוטומטיות) מצפים לקודים הנכונים.
הטעות 2: להחזיר תוכן מ-DELETE עם 204.
@app.delete("/students/{student_id}", status_code=204)
def delete_student(student_id: int):
db.delete_student(student_id)
return {"message": "deleted"}לפי תקן HTTP, ב-204 אסור שיהיה גוף. גרסאות שונות של FastAPI מטפלות בזה אחרת (שגיאה, או גוף שנזרק). או 204 בלי return, או 200 עם הודעה. לא שניהם.
הטעות 3: לשכוח את הבדיקה של None ב-PUT. return db.update_student(...) מחזיר 200 עם null לסטודנט שלא קיים.
הטעות 4: except Exception ב-Route.
try:
return db.create_student(...)
except Exception:
raise HTTPException(status_code=409, detail="conflict")באג אמיתי (למשל טבלה חסרה) יוחזר כ"409 Conflict", ויטעה את כולם. תופסים רק ValueError, וכל השאר יגיע כ-500, כדי שנראה אותו ונתקן.
הטעות 5: ולידציה כפולה ושונה. בדיקת גיל ב-POST “מעל 16”, וב-PUT “מעל 18”. פונקציה אחת (validate_student).
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: POST — יצירה
הוסיפו ל-main.py מודל StudentIn (name, age, email) ו-POST /students שמחזיר 201. אימייל כפול (ValueError מ-db.create_student) → 409.
הצג פתרון
main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import db
app = FastAPI(title="Students API")
db.init_db()
class StudentIn(BaseModel):
name: str
age: int
email: str
@app.post("/students", status_code=201)
def create_student(student: StudentIn):
try:
return db.create_student(student.name, student.age, student.email)
except ValueError as error:
raise HTTPException(status_code=409, detail=str(error))- יצירה מוצלחת ← 201 עם הסטודנט (כולל
id). - אותו אימייל שוב ← 409 עם
"a student with email ... already exists". - גוף בלי
email→ 422, עוד לפני שהפונקציה רצה.
תרגיל 2: כללי ולידציה
כתבו validate_student(student) שזורקת HTTPException עם 400 אם: השם קצר מ-2 תווים, הגיל מחוץ ל-16 עד 120, או שאין @ באימייל. הפעילו אותה ב-POST לפני השמירה.
הצג פתרון
main.py (בתוספת)
MIN_NAME_LENGTH = 2
MIN_AGE = 16
MAX_AGE = 120
def validate_student(student):
if len(student.name.strip()) < MIN_NAME_LENGTH:
raise HTTPException(status_code=400, detail="name too short")
if student.age < MIN_AGE or student.age > MAX_AGE:
raise HTTPException(status_code=400, detail="age must be 16-120")
if "@" not in student.email:
raise HTTPException(status_code=400, detail="invalid email")
@app.post("/students", status_code=201)
def create_student(student: StudentIn):
validate_student(student)
try:
return db.create_student(student.name, student.age, student.email)
except ValueError as error:
raise HTTPException(status_code=409, detail=str(error))הבדיקות שלנו רצות לפני שנוגעים במסד: קלט פסול לא מגיע לשכבת הנתונים בכלל.
תרגיל 3: PUT — עדכון
הוסיפו PUT /students/{student_id}: ולידציה (400), סטודנט שלא קיים (404), אימייל שתפוס על ידי סטודנט אחר (409), ואחרת 200 עם הסטודנט המעודכן.
הצג פתרון
main.py (בתוספת)
@app.put("/students/{student_id}")
def update_student(student_id: int, student: StudentIn):
validate_student(student)
try:
updated = db.update_student(student_id, student.name, student.age, student.email)
except ValueError as error:
raise HTTPException(status_code=409, detail=str(error))
if updated is None:
raise HTTPException(status_code=404, detail="student not found")
return updatedכל תוצאה של db.update_student מתורגמת לקוד אחר: מילון ← 200, None ← 404, ValueError ← 409.
תרגיל 4: DELETE — 204
הוסיפו DELETE /students/{student_id} שמחזיר 204 כשהמחיקה הצליחה, ו-404 כשאין סטודנט כזה. למה ב-204 לא מחזירים גוף?
הצג פתרון
main.py (בתוספת)
@app.delete("/students/{student_id}", status_code=204)
def delete_student(student_id: int):
if not db.delete_student(student_id):
raise HTTPException(status_code=404, detail="student not found")204 No Content אומר בדיוק “הצלחתי, ואין מה להחזיר”. לכן הפונקציה לא מחזירה כלום. מחיקה שנייה של אותו id → 404.
תרגיל 5: מי מחליט על איזה קוד?
לכל בקשה — איזה קוד יחזור, ומי מחזיר אותו: Pydantic, הקוד שלנו (validate_student), או התרגום מ-db?
1. POST /students {"name": "Noa", "age": "abc", "email": "noa@mail.com"}
2. POST /students {"name": "Noa", "age": 10, "email": "noa@mail.com"}
3. POST /students {"name": "Noa", "age": 22, "email": "dana@mail.com"} (taken)
4. PUT /students/99 {"name": "Noa", "age": 22, "email": "noa@mail.com"}
5. DELETE /students/1 (exists)הצג פתרון
| בקשה | קוד | מי |
|---|---|---|
| 1 | 422 | Pydantic — גיל אינו מספר |
| 2 | 400 | validate_student — כלל עסקי |
| 3 | 409 | תרגום של ValueError מ-db — כפילות במסד |
| 4 | 404 | db.update_student החזירה None |
| 5 | 204 | הצלחה בלי גוף |
סיכום
- POST →
db.create_student→ 201 + הסטודנט.ValueError→ 409. - PUT (Path + Body) →
db.update_student→ 200.None→ 404.ValueError→ 409. - DELETE →
db.delete_student→ 204 בלי גוף.False→ 404. - שלוש שכבות בדיקה: 422 (Pydantic, מבנה), 400 (
validate_student, כללים), 409 (המסד, ייחודיות). - ולידציה בפונקציה אחת ל-POST ול-PUT. תופסים רק
ValueError, וכל השאר נשאר 500. main.pyשל כ-60 שורות = API מלא, כי כל שכבה עושה את שלה.
דוקומנטציה רשמית
- FastAPI — Request Body — POST ו-PUT עם Body
- FastAPI — Response Status Code — status_code=201
- FastAPI — Body - Updates — PUT — עדכון רשומה