
ในยุคที่เทคโนโลยีขับเคลื่อนโลกอย่างรวดเร็ว การเชื่อมต่อและแลกเปลี่ยนข้อมูลระหว่างแอปพลิเคชันและระบบต่าง ๆ ได้กลายเป็นหัวใจสำคัญของการพัฒนาซอฟต์แวร์ยุคใหม่ครับ และ REST API ก็คือพระเอกที่เข้ามาตอบโจทย์นี้ได้อย่างสมบูรณ์แบบ วันนี้เราจะพาทุกท่านดำดิ่งสู่โลกของการสร้าง REST API ด้วย FastAPI เฟรมเวิร์ก Python ที่กำลังมาแรงที่สุดตัวหนึ่ง ด้วยประสิทธิภาพที่เหนือกว่า, การรองรับ Asynchronous เต็มรูปแบบ, การใช้งาน Type Hints ที่ยอดเยี่ยม และการสร้างเอกสาร API โดยอัตโนมัติ ทำให้ FastAPI เป็นตัวเลือกอันดับต้น ๆ สำหรับนักพัฒนาที่ต้องการสร้าง API ที่รวดเร็ว, แข็งแกร่ง และดูแลรักษาง่ายครับ บทความนี้จะครอบคลุมทุกแง่มุม ตั้งแต่การติดตั้งพื้นฐานไปจนถึงการสร้างระบบที่ซับซ้อนพร้อมการเชื่อมต่อฐานข้อมูลและการยืนยันตัวตน เพื่อให้คุณสามารถสร้าง REST API ด้วย FastAPI ได้อย่างครบจบในที่เดียว มาเริ่มกันเลยครับ!
สารบัญ
- บทนำ: ทำไมต้อง FastAPI?
- ความเข้าใจพื้นฐานเกี่ยวกับ REST API
- เตรียมความพร้อม: ติดตั้ง Python และ FastAPI
- เริ่มต้นโปรเจกต์ FastAPI แรกของคุณ
- การสร้าง Endpoint พื้นฐาน (GET)
- การจัดการ Path Parameters
- การจัดการ Query Parameters
- การส่งข้อมูลด้วย Request Body (POST, PUT)
- Data Validation และ Serialization ด้วย Pydantic
- การจัดการ Error และ Exception
- การเชื่อมต่อกับฐานข้อมูล (SQLAlchemy ORM)
- การทำ Authentication และ Authorization (JWT/OAuth2)
- การ Deploy FastAPI Application
- ข้อดีและข้อเสียของ FastAPI (เทียบกับ Flask/Django)
- ตัวอย่างโปรเจกต์จริง: ระบบจัดการสินค้าคงคลัง (Inventory Management)
- FAQ: คำถามที่พบบ่อยเกี่ยวกับ FastAPI
- สรุปและ Call to Action
บทนำ: ทำไมต้อง FastAPI?
ก่อนที่เราจะลงมือสร้าง API กัน เรามาทำความเข้าใจกันก่อนว่าทำไม FastAPI ถึงเป็นตัวเลือกที่น่าสนใจสำหรับการพัฒนา REST API ในปัจจุบันครับ
FastAPI คืออะไร?
FastAPI คือเว็บเฟรมเวิร์กสำหรับ Python ที่ใช้สร้าง RESTful API โดยเฉพาะ สร้างขึ้นบนพื้นฐานของ Starlette (สำหรับเว็บพาร์ท) และ Pydantic (สำหรับ Data Validation และ Serialization) ทำให้ FastAPI มีความสามารถที่โดดเด่นหลายประการ:
- ประสิทธิภาพสูง (High Performance): เป็นหนึ่งในเฟรมเวิร์ก Python ที่เร็วที่สุด โดยมีประสิทธิภาพเทียบเท่ากับ Node.js และ Go ครับ
- เพิ่มผลผลิตให้กับการพัฒนา (Increased Developer Productivity): ด้วยการใช้ Type Hints ของ Python และ Pydantic ทำให้โค้ดมีความชัดเจน, ลดข้อผิดพลาด, และช่วยให้ IDE สามารถตรวจสอบโค้ด (Autocompletion) ได้ดีเยี่ยมครับ
- รองรับ Asynchronous (Async/Await) เต็มรูปแบบ: เหมาะสำหรับการสร้าง API ที่ต้องมีการ I/O Bound Operations สูง เช่น การอ่าน/เขียนฐานข้อมูล, การเรียก API ภายนอก โดยไม่บล็อกการทำงานหลักของเซิร์ฟเวอร์ครับ
- เอกสาร API อัตโนมัติ (Automatic API Documentation): FastAPI สร้างเอกสาร API แบบ Interactive โดยใช้มาตรฐาน OpenAPI (Swagger UI) และ ReDoc ให้คุณโดยอัตโนมัติ ทำให้การทดสอบและทำความเข้าใจ API ง่ายขึ้นมากครับ
- การตรวจสอบข้อมูลอัตโนมัติ (Automatic Data Validation): Pydantic ช่วยให้คุณสามารถกำหนดโครงสร้างข้อมูลที่เข้ามาและออกไปจาก API ได้อย่างง่ายดาย พร้อมการตรวจสอบข้อมูลและแปลงชนิดข้อมูลให้โดยอัตโนมัติครับ
- ความปลอดภัย (Security): มีเครื่องมือสำหรับจัดการความปลอดภัย เช่น การทำ OAuth2, JWT และ HTTP Basic Authentication ในตัวครับ
ด้วยคุณสมบัติเหล่านี้ ทำให้ FastAPI ไม่ได้เป็นเพียงแค่เครื่องมือ แต่เป็นโซลูชันที่ช่วยให้การสร้าง API ที่ซับซ้อนเป็นเรื่องง่ายและมีประสิทธิภาพอย่างไม่น่าเชื่อครับ
ความเข้าใจพื้นฐานเกี่ยวกับ REST API
ก่อนจะลงมือเขียนโค้ด เรามาทบทวนแนวคิดพื้นฐานของ REST API กันสักหน่อยนะครับ การเข้าใจหลักการเหล่านี้จะช่วยให้เราออกแบบและสร้าง API ที่มีประสิทธิภาพและเป็นมาตรฐานครับ
REST (Representational State Transfer) คือสถาปัตยกรรมซอฟต์แวร์สำหรับระบบเครือข่าย โดยเน้นการสื่อสารแบบ Stateless ระหว่าง Client และ Server ผ่านโปรโตคอล HTTP ครับ
หลักการสำคัญของ REST:
- Client-Server: แยก Client (เช่น เว็บเบราว์เซอร์, แอปมือถือ) และ Server ออกจากกันอย่างชัดเจน Client จะส่งคำขอไปยัง Server และ Server จะส่งการตอบกลับมา
- Stateless: แต่ละคำขอจาก Client ไปยัง Server จะต้องมีข้อมูลที่จำเป็นทั้งหมดในการประมวลผลคำขอ โดย Server จะไม่เก็บข้อมูลสถานะของ Client ระหว่างคำขอแต่ละครั้งครับ
- Cacheable: การตอบกลับจาก Server สามารถระบุได้ว่าสามารถ Cache ได้หรือไม่ เพื่อปรับปรุงประสิทธิภาพการทำงานของ Client
- Layered System: Client ไม่จำเป็นต้องรู้ว่ากำลังเชื่อมต่อโดยตรงกับ Server หรือผ่าน Layer อื่น ๆ เช่น Load Balancer หรือ Proxy
- Uniform Interface: เป็นหลักการที่สำคัญที่สุด เพื่อให้ระบบมีความเรียบง่ายและเป็นมาตรฐาน โดยมีส่วนประกอบดังนี้:
- Resources (ทรัพยากร): ทุกสิ่งทุกอย่างใน REST API จะถูกมองว่าเป็นทรัพยากร เช่น ผู้ใช้งาน, สินค้า, คำสั่งซื้อ โดยแต่ละทรัพยากรจะมี Unique Identifier (URI/URL) ครับ
- Standard Methods (HTTP Methods): ใช้ HTTP Methods มาตรฐานในการดำเนินการกับทรัพยากร:
GET: ใช้สำหรับดึงข้อมูลทรัพยากรPOST: ใช้สำหรับสร้างทรัพยากรใหม่PUT: ใช้สำหรับอัปเดตข้อมูลทรัพยากรที่มีอยู่ทั้งหมดPATCH: ใช้สำหรับอัปเดตข้อมูลทรัพยากรบางส่วนDELETE: ใช้สำหรับลบทรัพยากร
- Self-descriptive Messages: ข้อความที่แลกเปลี่ยนระหว่าง Client และ Server ควรมีข้อมูลที่เพียงพอที่จะอธิบายวิธีการประมวลผลข้อความนั้น
- HATEOAS (Hypermedia As The Engine Of Application State): เป็นหลักการที่ Client สามารถนำทางผ่าน API ได้โดยใช้ลิงก์ที่ Server ให้มาใน Response ครับ (ขั้นสูงขึ้นมาหน่อย)
HTTP Status Codes ที่พบบ่อย:
200 OK: การร้องขอสำเร็จ201 Created: สร้างทรัพยากรใหม่สำเร็จ (มักใช้กับ POST)204 No Content: การร้องขอสำเร็จ แต่ไม่มีข้อมูลจะส่งกลับมา (มักใช้กับ DELETE)400 Bad Request: คำขอไม่ถูกต้อง401 Unauthorized: ไม่ได้รับอนุญาตให้เข้าถึง (ต้องมีการยืนยันตัวตน)403 Forbidden: ได้รับอนุญาตแล้ว แต่ไม่มีสิทธิ์เข้าถึงทรัพยากรนั้น ๆ404 Not Found: ไม่พบทรัพยากรที่ร้องขอ422 Unprocessable Entity: คำขอถูกต้องตามโครงสร้าง แต่ไม่สามารถประมวลผลได้ (เช่น Data Validation ล้มเหลว)500 Internal Server Error: ข้อผิดพลาดภายในเซิร์ฟเวอร์
การเข้าใจหลักการเหล่านี้จะทำให้เราออกแบบ API ที่สอดคล้องกับมาตรฐาน RESTful และใช้งานได้ง่ายขึ้นมากครับ
เตรียมความพร้อม: ติดตั้ง Python และ FastAPI
ขั้นแรกของการสร้าง REST API คือการเตรียมสภาพแวดล้อมในการพัฒนาครับ เราจะเริ่มต้นด้วยการติดตั้ง Python และสร้าง Virtual Environment เพื่อแยก Dependencies ของโปรเจกต์เราออกจากระบบหลักครับ
1. ติดตั้ง Python
ก่อนอื่น ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง Python 3.7+ (แนะนำ 3.9 หรือใหม่กว่า) บนเครื่องของคุณแล้วครับ
คุณสามารถตรวจสอบเวอร์ชัน Python ได้โดยเปิด Terminal (macOS/Linux) หรือ Command Prompt/PowerShell (Windows) แล้วพิมพ์:
python3 --version
# หรือบางระบบอาจใช้
python --version
หากยังไม่ได้ติดตั้ง หรือต้องการติดตั้งเวอร์ชันล่าสุด คุณสามารถดาวน์โหลดได้จาก เว็บไซต์ทางการของ Python ครับ สำหรับผู้ใช้งาน macOS/Linux อาจพิจารณาใช้เครื่องมืออย่าง pyenv เพื่อจัดการหลายเวอร์ชันของ Python ได้อย่างง่ายดายครับ
2. สร้างและเปิดใช้งาน Virtual Environment
การใช้ Virtual Environment เป็นสิ่งสำคัญมากในการพัฒนา Python เพื่อป้องกันปัญหา Dependency Conflict ระหว่างโปรเจกต์ต่าง ๆ ครับ
สร้างโฟลเดอร์สำหรับโปรเจกต์ของคุณ:
mkdir my-fastapi-app
cd my-fastapi-app
สร้าง Virtual Environment:
python3 -m venv venv
เปิดใช้งาน Virtual Environment:
- สำหรับ macOS/Linux:
source venv/bin/activate - สำหรับ Windows (Command Prompt):
venv\Scripts\activate.bat - สำหรับ Windows (PowerShell):
venv\Scripts\Activate.ps1
เมื่อเปิดใช้งานแล้ว คุณจะเห็น (venv) นำหน้าบรรทัดคำสั่ง ซึ่งหมายความว่าคุณกำลังอยู่ใน Virtual Environment แล้วครับ
3. ติดตั้ง FastAPI และ Uvicorn
FastAPI ต้องการ ASGI server (เช่น Uvicorn) เพื่อรันแอปพลิเคชันของเราครับ
pip install "fastapi[all]" uvicorn
คำสั่ง "fastapi[all]" จะติดตั้ง FastAPI พร้อมกับ Dependencies ที่แนะนำทั้งหมด เช่น Pydantic, python-multipart (สำหรับการจัดการฟอร์มข้อมูล), jinja2 (สำหรับ templating) และอื่น ๆ ซึ่งสะดวกมากสำหรับเริ่มต้นครับ
ตอนนี้คุณก็พร้อมที่จะสร้าง REST API ตัวแรกด้วย FastAPI แล้วครับ!
เริ่มต้นโปรเจกต์ FastAPI แรกของคุณ
เมื่อเราเตรียมสภาพแวดล้อมเรียบร้อยแล้ว ก็ถึงเวลาสร้างแอปพลิเคชัน FastAPI ตัวแรกของเราครับ
1. สร้างไฟล์ main.py
ในโฟลเดอร์โปรเจกต์ my-fastapi-app ของคุณ ให้สร้างไฟล์ชื่อ main.py และใส่โค้ดต่อไปนี้ครับ
# main.py
from fastapi import FastAPI
# สร้าง instance ของ FastAPI
# app คือ object หลักของ FastAPI ที่เราจะใช้ในการกำหนด routing และ logic ต่างๆ
app = FastAPI()
# กำหนด Path Operation Decorator สำหรับ HTTP GET request ที่ path "/"
# เมื่อมี GET request มายัง root path, ฟังก์ชัน read_root() จะถูกเรียกใช้งาน
@app.get("/")
async def read_root():
"""
Endpoint สำหรับ root path ที่จะส่งคืนข้อความต้อนรับ
"""
# FastAPI จะแปลง Python dictionary นี้ให้เป็น JSON response โดยอัตโนมัติ
return {"message": "Hello, World! Welcome to FastAPI."}
# ตัวอย่างอีกหนึ่ง endpoint สำหรับทดสอบ
@app.get("/hello/{name}")
async def say_hello(name: str):
"""
Endpoint ที่รับชื่อเป็น Path Parameter และส่งคืนข้อความทักทาย
"""
return {"message": f"Hello, {name}! How are you today?"}
อธิบายโค้ด:
from fastapi import FastAPI: นำเข้าคลาสFastAPIจากไลบรารีfastapiครับapp = FastAPI(): สร้างอินสแตนซ์ของFastAPIซึ่งเป็น object หลักที่เราจะใช้ในการสร้าง API ครับ@app.get("/"): นี่คือ Path Operation Decorator ครับ มันบอก FastAPI ว่าฟังก์ชันที่อยู่ใต้ Decorator นี้ควรจะถูกเรียกเมื่อมี HTTPGETrequest เข้ามาที่ Path"/"(root path) ครับasync def read_root():: ฟังก์ชันที่ถูกเรียกเมื่อ Path Operation นี้ถูกทริกเกอร์ FastAPI ถูกออกแบบมาให้ทำงานแบบ Asynchronous ได้อย่างเต็มที่ ดังนั้นเราจึงใช้async defครับ แม้ว่าในกรณีนี้จะไม่ได้มีการรอ (await) อะไรก็ตาม แต่ก็เป็นแนวทางปฏิบัติที่ดีครับreturn {"message": "Hello, World! Welcome to FastAPI."}: ฟังก์ชันนี้จะส่งคืน Python Dictionary ซึ่ง FastAPI จะแปลงเป็น JSON Response ให้โดยอัตโนมัติครับ@app.get("/hello/{name}"): เป็นตัวอย่างการรับค่าnameเป็น Path Parameter ซึ่งจะอธิบายเพิ่มเติมในส่วนถัดไปครับ
2. รันแอปพลิเคชันด้วย Uvicorn
เปิด Terminal (ที่ Virtual Environment ถูก activate อยู่) และรันคำสั่งต่อไปนี้ครับ
uvicorn main:app --reload
อธิบายคำสั่ง:
uvicorn: คือ ASGI server ที่เราติดตั้งไปครับmain:app: บอก Uvicorn ว่าให้หา objectappภายในไฟล์main.pyครับ--reload: โหมดนี้จะทำให้ Uvicorn ตรวจจับการเปลี่ยนแปลงในโค้ดของคุณและรีโหลดเซิร์ฟเวอร์โดยอัตโนมัติ ซึ่งสะดวกมากสำหรับการพัฒนาครับ
คุณจะเห็นข้อความประมาณนี้ใน Terminal ครับ:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [xxxxx]
INFO: Started server process [xxxxx]
INFO: Waiting for application startup.
INFO: Application startup complete.
3. ทดสอบ API ของคุณ
เปิดเว็บเบราว์เซอร์ของคุณแล้วไปที่:
- http://127.0.0.1:8000/
- http://127.0.0.1:8000/hello/SiamLancard (ลองเปลี่ยนชื่อได้เลยครับ)
คุณควรจะเห็น JSON response ในเบราว์เซอร์ครับ
และที่ยอดเยี่ยมกว่านั้น FastAPI ยังสร้างเอกสาร API โดยอัตโนมัติให้คุณด้วยครับ ลองไปที่:
- Swagger UI: http://127.0.0.1:8000/docs
- ReDoc: http://127.0.0.1:8000/redoc
คุณจะเห็นหน้าเอกสาร API แบบ Interactive ที่คุณสามารถทดสอบ Endpoint ของคุณได้โดยตรงจากหน้านั้นเลยครับ นี่คือหนึ่งในคุณสมบัติที่ทรงพลังของ FastAPI ที่ช่วยประหยัดเวลาและลดความซับซ้อนในการทำเอกสารได้อย่างมหาศาลครับ
การสร้าง Endpoint พื้นฐาน (GET)
เราได้สร้าง Endpoint พื้นฐานไปแล้วในส่วนที่แล้ว แต่เพื่อความเข้าใจที่ลึกซึ้งขึ้น เราจะมาดูรายละเอียดเกี่ยวกับการสร้าง Path Operation สำหรับ HTTP GET request กันครับ
Path Operation เป็นการเชื่อมโยง HTTP Method (GET, POST, PUT, DELETE, etc.) กับ URL Path (เช่น /items, /users) เข้ากับฟังก์ชัน Python ที่จะทำงานเมื่อมีการร้องขอเข้ามาครับ
Path Operation Decorator
การสร้าง Path Operation ใน FastAPI ทำได้ง่ายมากด้วย Decorator ครับ
# main.py (เพิ่มโค้ดในไฟล์เดิม)
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "Welcome to FastAPI! This is the root endpoint."}
@app.get("/items/")
async def read_items():
"""
Endpoint สำหรับดึงรายการสินค้าทั้งหมด
"""
return [
{"item_id": 1, "name": "Laptop", "price": 1200},
{"item_id": 2, "name": "Mouse", "price": 25},
{"item_id": 3, "name": "Keyboard", "price": 75},
]
@app.get("/users/")
async def read_users():
"""
Endpoint สำหรับดึงรายการผู้ใช้งาน
"""
return [
{"user_id": 1, "username": "alice"},
{"user_id": 2, "username": "bob"},
]
# คุณสามารถเพิ่ม Metadata ให้กับ Path Operation ได้
@app.get("/health", tags=["Monitoring"], summary="Check API Health Status")
async def health_check():
"""
ตรวจสอบสถานะการทำงานของ API ว่าพร้อมใช้งานหรือไม่
"""
return {"status": "ok", "message": "API is up and running."}
อธิบายโค้ดเพิ่มเติม:
@app.get("/items/"): กำหนดว่าเมื่อมี HTTP GET request มายัง/items/ฟังก์ชันread_itemsจะถูกเรียกใช้ครับ@app.get("/users/"): เช่นเดียวกัน สำหรับ/users/ฟังก์ชันread_usersจะถูกเรียกใช้ครับ- Metadata for Path Operations:
tags=["Monitoring"]: ใช้สำหรับจัดกลุ่ม Endpoint ในเอกสาร Swagger UI ครับ ทำให้ดูเป็นระเบียบมากขึ้นsummary="Check API Health Status": เป็นคำสรุปสั้น ๆ ที่จะแสดงในเอกสาร API"""Docstring""": Docstring ในฟังก์ชันจะถูกใช้เป็นdescriptionของ Endpoint ในเอกสาร API ครับ
การสร้าง Endpoint ด้วย FastAPI นั้นตรงไปตรงมาและเข้าใจง่ายมากครับ คุณเพียงแค่เลือก HTTP Method ที่เหมาะสมกับ Path ของคุณ แล้วกำหนดฟังก์ชันที่จะจัดการ Logic นั้น ๆ ได้เลย
การจัดการ Path Parameters
บ่อยครั้งที่เราต้องการดึงข้อมูลทรัพยากรเฉพาะ เช่น ดึงข้อมูลสินค้าชิ้นเดียวตาม ID ของมัน Path Parameters เข้ามามีบทบาทตรงนี้ครับ
Path Parameters คือค่าที่ถูกฝังอยู่ใน URL Path โดยตรง โดยจะถูกระบุด้วยเครื่องหมายวงเล็บปีกกา {} ใน Path String ครับ
การใช้งาน Path Parameters
# main.py (เพิ่มโค้ดในไฟล์เดิม)
from fastapi import FastAPI
app = FastAPI()
# ... (โค้ดเก่า) ...
@app.get("/items/{item_id}")
async def read_item(item_id: int):
"""
Endpoint สำหรับดึงข้อมูลสินค้าตาม ID
FastAPI จะทำการแปลง `item_id` จาก string ใน URL เป็น integer ให้โดยอัตโนมัติ
"""
return {"item_id": item_id, "name": f"Item {item_id}", "description": "This is a sample item."}
@app.get("/users/{user_id}/orders/{order_id}")
async def read_user_order(user_id: int, order_id: int):
"""
Endpoint สำหรับดึงข้อมูลคำสั่งซื้อของ user คนใดคนหนึ่ง
แสดงให้เห็นการใช้งานหลาย Path Parameters
"""
return {"user_id": user_id, "order_id": order_id, "detail": f"Order {order_id} for User {user_id}"}
# Path Parameter พร้อม Type Hint ที่กำหนดค่าเริ่มต้น
@app.get("/products/{product_name}")
async def get_product_by_name(product_name: str, q: str | None = None):
"""
ตัวอย่าง Path Parameter ที่เป็น string และ Query Parameter
"""
if q:
return {"product_name": product_name, "query": q}
return {"product_name": product_name}
# Path Parameter พร้อม enum (ตัวอย่างขั้นสูงขึ้น)
from enum import Enum
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
"""
Endpoint ที่รับ Path Parameter เป็นค่าจาก Enum
FastAPI จะตรวจสอบว่าค่าที่ส่งมาตรงกับ Enum ที่กำหนดหรือไม่
"""
if model_name is ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeNet is a classic!"}
return {"model_name": model_name, "message": "Have some residuals"}
อธิบายโค้ด:
@app.get("/items/{item_id}"): เรากำหนด Path Parameter ชื่อitem_idใน URL Path ครับasync def read_item(item_id: int)::- เราประกาศ
item_idเป็นพารามิเตอร์ของฟังก์ชันread_itemครับ - สิ่งสำคัญคือการใช้ Type Hint (
item_id: int) ครับ FastAPI จะใช้ Type Hint นี้ในการ:- ตรวจสอบข้อมูล (Data Validation): หากค่าที่ส่งมาใน URL ไม่สามารถแปลงเป็น
intได้ FastAPI จะส่ง HTTP 422 Unprocessable Entity กลับไปโดยอัตโนมัติครับ - แปลงชนิดข้อมูล (Data Conversion): ค่าที่ได้จาก URL จะเป็น String เสมอ แต่ FastAPI จะแปลงเป็น
intให้คุณโดยอัตโนมัติก่อนส่งเข้าฟังก์ชันครับ - สร้างเอกสาร (Documentation): Type Hint จะถูกนำไปใช้ในเอกสาร Swagger UI เพื่อบอกว่าพารามิเตอร์นี้คาดหวังข้อมูลชนิดใดครับ
- ตรวจสอบข้อมูล (Data Validation): หากค่าที่ส่งมาใน URL ไม่สามารถแปลงเป็น
- เราประกาศ
@app.get("/users/{user_id}/orders/{order_id}"): แสดงให้เห็นว่าเราสามารถมี Path Parameters ได้หลายตัวใน Path เดียวกันครับ FastAPI จะจัดการ Type Hint และการแปลงข้อมูลให้ทั้งหมด- Path Parameter with Enum:
- เราสามารถใช้
Enumจาก Python เพื่อจำกัดค่าที่เป็นไปได้สำหรับ Path Parameter ได้ครับ - FastAPI จะตรวจสอบว่าค่าที่ส่งมาตรงกับสมาชิกของ
ModelNameEnum หรือไม่ หากไม่ตรงก็จะส่ง Error 422 กลับไปครับ
- เราสามารถใช้
Path Parameters ทำให้ API ของเรามีความยืดหยุ่นและเข้าถึงทรัพยากรเฉพาะได้อย่างมีประสิทธิภาพครับ ด้วย Type Hints, FastAPI ช่วยให้เรามั่นใจได้ว่าข้อมูลที่รับเข้ามานั้นถูกต้องตามชนิดที่เราต้องการครับ
การจัดการ Query Parameters
นอกจากการระบุข้อมูลใน Path ด้วย Path Parameters แล้ว เรายังสามารถส่งข้อมูลเพิ่มเติมเพื่อกรอง, จัดเรียง, หรือกำหนดเงื่อนไขในการดึงข้อมูลได้ด้วย Query Parameters ครับ
Query Parameters จะอยู่ต่อท้าย URL หลังจากเครื่องหมาย ? และประกอบด้วยคู่ของ key=value คั่นด้วย & เช่น /items?skip=0&limit=10
การใช้งาน Query Parameters
# main.py (เพิ่มโค้ดในไฟล์เดิม)
from fastapi import FastAPI, Query
from typing import Optional, List
app = FastAPI()
# ... (โค้ดเก่า) ...
@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 10):
"""
Endpoint สำหรับดึงรายการสินค้าพร้อม Query Parameters สำหรับการแบ่งหน้า (Pagination)
- `skip`: จำนวนรายการที่จะข้ามไป (ค่าเริ่มต้น 0)
- `limit`: จำนวนรายการสูงสุดที่จะดึงมา (ค่าเริ่มต้น 10)
"""
all_items = [
{"item_id": 1, "name": "Laptop", "price": 1200},
{"item_id": 2, "name": "Mouse", "price": 25},
{"item_id": 3, "name": "Keyboard", "price": 75},
{"item_id": 4, "name": "Monitor", "price": 300},
{"item_id": 5, "name": "Webcam", "price": 50},
]
return all_items[skip : skip + limit]
@app.get("/products/")
async def search_products(q: Optional[str] = None, price_min: float = 0.0, price_max: Optional[float] = None):
"""
Endpoint สำหรับค้นหาสินค้าพร้อม Query Parameters ที่ซับซ้อนขึ้น
- `q`: คำค้นหา (Optional)
- `price_min`: ราคาขั้นต่ำ (ค่าเริ่มต้น 0.0)
- `price_max`: ราคาขั้นสูงสุด (Optional)
"""
results = []
if q:
results.append({"product_name": f"Product with query: {q}"})
if price_min > 0:
results.append({"min_price_filter": price_min})
if price_max:
results.append({"max_price_filter": price_max})
if not results:
return {"message": "No specific search criteria provided."}
return results
# Query Parameter ที่บังคับ (Required Query Parameter)
@app.get("/users_search/")
async def search_users(keyword: str):
"""
Endpoint สำหรับค้นหาผู้ใช้งานด้วย keyword ที่บังคับ
"""
return {"message": f"Searching users with keyword: {keyword}"}
# Query Parameters ที่รับค่าได้หลายค่า (List of Query Parameters)
@app.get("/items_by_tags/")
async def get_items_by_tags(tags: List[str] = Query(None)):
"""
Endpoint สำหรับดึงรายการสินค้าตาม tag ที่ระบุ (สามารถระบุได้หลาย tag)
ตัวอย่าง: /items_by_tags/?tags=electronics&tags=gadgets
"""
if tags:
return {"tags": tags, "message": f"Items filtered by tags: {', '.join(tags)}"}
return {"message": "Please provide one or more tags."}
อธิบายโค้ด:
async def read_items(skip: int = 0, limit: int = 10):- พารามิเตอร์ฟังก์ชันที่ไม่ได้เป็นส่วนหนึ่งของ Path จะถูกตีความว่าเป็น Query Parameters ครับ
- เราใช้ Type Hint (
skip: int,limit: int) เพื่อให้ FastAPI ตรวจสอบและแปลงชนิดข้อมูลให้ครับ - การกำหนดค่าเริ่มต้น (
= 0,= 10) ทำให้ Query Parameter เหล่านี้เป็น Optional ครับ หาก Client ไม่ได้ส่งมา FastAPI จะใช้ค่าเริ่มต้นที่เรากำหนด
q: Optional[str] = None:- ใช้
Optionalจากโมดูลtyping(หรือstr | Noneใน Python 3.10+) เพื่อระบุว่าพารามิเตอร์นี้เป็น Optional และค่าเริ่มต้นคือNoneครับ - FastAPI จะเข้าใจว่าถ้า Client ไม่ได้ส่ง Query Parameter
qมา ก็จะใช้ค่าNoneครับ
- ใช้
keyword: str:- หากเราไม่กำหนดค่าเริ่มต้นให้กับ Query Parameter (เช่น
keyword: strโดยไม่มี= ...) FastAPI จะถือว่า Query Parameter นั้นเป็น Required ครับ - หาก Client ไม่ได้ส่ง
keywordมา FastAPI จะส่ง HTTP 422 Unprocessable Entity กลับไปครับ
- หากเราไม่กำหนดค่าเริ่มต้นให้กับ Query Parameter (เช่น
tags: List[str] = Query(None):- สำหรับการรับ Query Parameter ที่มีชื่อเดียวกันแต่ส่งมาหลายครั้ง (เช่น
?tags=electronics&tags=gadgets) เราสามารถใช้ Type Hint เป็นList[str]ได้ครับ - และใช้
Query(None)เพื่อระบุว่าพารามิเตอร์นี้เป็น Optional และหากไม่มีการส่งค่ามา จะเป็น List ว่าง หรือNoneขึ้นอยู่กับการกำหนด (ในที่นี้None) ครับ - หากเราต้องการให้เป็น Required List เราสามารถใช้
tags: List[str] = Query(...)โดย...หมายถึง Required ครับ
- สำหรับการรับ Query Parameter ที่มีชื่อเดียวกันแต่ส่งมาหลายครั้ง (เช่น
Query Parameters เป็นวิธีที่ยืดหยุ่นในการส่งข้อมูลเพิ่มเติมไปยัง API ของคุณ เพื่อควบคุมการดึงข้อมูลได้อย่างละเอียดครับ FastAPI ทำให้การจัดการ Query Parameters เหล่านี้ง่ายและมีประสิทธิภาพด้วยการผสานรวมกับ Type Hints และ Pydantic ครับ
การส่งข้อมูลด้วย Request Body (POST, PUT)
เมื่อเราต้องการสร้างทรัพยากรใหม่ (ด้วย POST) หรืออัปเดตทรัพยากรที่มีอยู่ (ด้วย PUT/PATCH) เรามักจะต้องส่งข้อมูลจำนวนมากจาก Client ไปยัง Server ครับ ข้อมูลเหล่านี้จะถูกบรรจุอยู่ใน Request Body ซึ่งมักจะอยู่ในรูปแบบ JSON ครับ
FastAPI ใช้ Pydantic ในการจัดการ Request Body ทำให้เราสามารถกำหนดโครงสร้างข้อมูลที่คาดหวังได้อย่างง่ายดาย พร้อมทั้งได้รับการตรวจสอบข้อมูล (Data Validation) และการแปลงชนิดข้อมูล (Serialization) โดยอัตโนมัติครับ
การใช้งาน Request Body ด้วย Pydantic Model
ก่อนอื่น เราต้องสร้าง Pydantic Model เพื่อกำหนดโครงสร้างข้อมูลของ Request Body ครับ
# main.py (เพิ่มโค้ดในไฟล์เดิม)
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from typing import List, Optional
app = FastAPI()
# Pydantic Model สำหรับ Item
# BaseModel เป็น base class ที่ Pydantic ใช้ในการสร้าง data models
class Item(BaseModel):
# Field ที่ต้องการ (Required Field)
name: str = Field(..., example="Laptop Pro") # ใช้ Field เพื่อเพิ่ม metadata เช่น example
description: Optional[str] = Field(None, example="Powerful laptop for professionals.")
price: float = Field(..., gt=0, description="Price must be greater than zero", example=1200.0) # gt=0 คือ greater than 0
tax: Optional[float] = Field(None, example=0.1)
# การเพิ่มตัวอย่างข้อมูลสำหรับเอกสาร Swagger UI
class Config:
schema_extra = {
"example": {
"name": "Smartphone X",
"description": "The latest smartphone with advanced features.",
"price": 999.99,
"tax": 0.08,
}
}
# สมมติฐานว่าเรามีฐานข้อมูลเป็น List ของ Item objects
# ในแอปพลิเคชันจริงจะเชื่อมต่อกับฐานข้อมูลจริง
fake_db = []
next_item_id = 1
@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
"""
Endpoint สำหรับสร้างสินค้าใหม่
- รับ Item object ใน Request Body
- คืน Item object ที่สร้างเสร็จแล้วพร้อม ID (ในกรณีนี้แค่จำลอง)
"""
global next_item_id
item_dict = item.dict() # แปลง Pydantic model เป็น dict
item_dict["id"] = next_item_id
fake_db.append(item_dict)
next_item_id += 1
return item_dict # FastAPI จะแปลง dict นี้กลับเป็น Item model (ตาม response_model) และตรวจสอบข้อมูลก่อนส่งกลับ
@app.get("/items/{item_id}", response_model=Item)
async def get_item(item_id: int):
"""
Endpoint สำหรับดึงข้อมูลสินค้าตาม ID (จำลอง)
"""
for item in fake_db:
if item["id"] == item_id:
return item
raise HTTPException(status_code=404, detail="Item not found")
@app.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: int, item: Item):
"""
Endpoint สำหรับอัปเดตข้อมูลสินค้าที่มีอยู่ทั้งหมด
- รับ item_id เป็น Path Parameter
- รับ Item object ใหม่ใน Request Body
"""
for i, stored_item in enumerate(fake_db):
if stored_item["id"] == item_id:
updated_item_dict = item.dict()
updated_item_dict["id"] = item_id # ให้แน่ใจว่า ID ยังคงเดิม
fake_db[i] = updated_item_dict
return updated_item_dict
raise HTTPException(status_code=404, detail="Item not found for update")
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int):
"""
Endpoint สำหรับลบสินค้าตาม ID
"""
global fake_db
initial_len = len(fake_db)
fake_db = [item for item in fake_db if item["id"] != item_id]
if len(fake_db) == initial_len:
raise HTTPException(status_code=404, detail="Item not found for deletion")
return {"message": "Item deleted successfully"} # FastAPI จะส่ง 204 No Content กลับไปเพราะเรากำหนด status_code ไว้
อธิบายโค้ด:
class Item(BaseModel)::- เราสร้างคลาส
Itemที่สืบทอดมาจากpydantic.BaseModelครับ - ภายในคลาสนี้ เรากำหนด Field ของข้อมูลที่เราคาดหวัง เช่น
name: str,price: float - Type Hints (
str,float,Optional[str]) จะถูกใช้โดย Pydantic เพื่อตรวจสอบชนิดข้อมูลครับ Field(..., example="Laptop Pro"): ใช้Fieldจาก Pydantic เพื่อเพิ่มคุณสมบัติพิเศษให้กับ Field เช่น:...(Ellipsis) หมายถึง Field นี้เป็น Required ครับexample: ใช้สำหรับแสดงตัวอย่างในเอกสาร Swagger UIgt=0: กำหนดเงื่อนไขว่าpriceต้องมากกว่า 0 (Greater Than) Pydantic มี Validator อีกมากมาย เช่นge(greater than or equal),lt(less than),le(less than or equal),min_length,max_length,regexเป็นต้นครับ
class Config: schema_extra: เป็นวิธีที่ใช้เพิ่มตัวอย่าง Request Body สำหรับแสดงในเอกสาร Swagger UI ครับ
- เราสร้างคลาส
@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED):create_item(item: Item): เมื่อเราประกาศพารามิเตอร์ของฟังก์ชันเป็น Pydantic Model (item: Item) FastAPI จะเข้าใจโดยอัตโนมัติว่าข้อมูลที่เข้ามาใน Request Body ควรจะตรงกับโครงสร้างของItemModel ครับ- FastAPI จะทำการ:
- อ่าน JSON จาก Request Body
- แปลง JSON เป็น Python Dictionary
- ส่ง Dictionary นั้นไปให้ Pydantic เพื่อสร้าง
Itemobject - ตรวจสอบข้อมูล (Validation) ตามที่กำหนดใน
ItemModel (เช่นnameต้องเป็นstr,priceต้องเป็นfloatและ> 0) หากมีข้อผิดพลาด Pydantic จะส่ง Error กลับมาทันที (FastAPI จะแปลงเป็น HTTP 422 Unprocessable Entity) - ส่ง
Itemobject ที่ผ่านการตรวจสอบแล้วเข้าสู่ฟังก์ชันcreate_item
response_model=Item: กำหนดว่า Response ที่ส่งกลับไปควรจะเป็นไปตามโครงสร้างของItemModel ครับ FastAPI จะทำการ Serialization (แปลง Python object เป็น JSON) และ Validation ข้อมูลขาออกให้ด้วย ทำให้มั่นใจว่า Client จะได้รับข้อมูลในรูปแบบที่ถูกต้องครับstatus_code=status.HTTP_201_CREATED: กำหนด HTTP Status Code ที่จะส่งกลับเมื่อการสร้างสำเร็จ ตามมาตรฐาน REST การสร้างทรัพยากรใหม่ควรส่ง201 Createdครับ เราใช้status.HTTP_201_CREATEDเพื่อความชัดเจนและป้องกันการพิมพ์ผิดครับ
@app.put("/items/{item_id}", response_model=Item): คล้ายกับPOSTแต่ใช้สำหรับอัปเดตข้อมูลทั้งหมดของทรัพยากรครับ@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT): สำหรับการลบข้อมูล เมื่อลบสำเร็จและไม่มีข้อมูลจะส่งกลับ ควรส่ง204 No Contentครับ
การใช้ Pydantic Model ร่วมกับ FastAPI สำหรับ Request Body ทำให้การจัดการข้อมูลเป็นเรื่องง่าย, ปลอดภัย และมีประสิทธิภาพสูงครับ ไม่ต้องเขียนโค้ดตรวจสอบข้อมูลด้วยตัวเองเลย!
Data Validation และ Serialization ด้วย Pydantic
เราได้เห็น Pydantic ทำงานกับการตรวจสอบข้อมูล (Validation) สำหรับ Request Body ไปแล้วนะครับ แต่ Pydantic มีความสามารถมากกว่านั้นมาก และเป็นส่วนสำคัญที่ทำให้ FastAPI มีความโดดเด่นครับ
Pydantic เป็นไลบรารี Python ที่ช่วยให้เราสามารถกำหนด Schema ของข้อมูลโดยใช้ Python Type Hints และทำการตรวจสอบข้อมูล (Validation) และแปลงชนิดข้อมูล (Parsing) ให้โดยอัตโนมัติครับ
ความสามารถหลักของ Pydantic:
- Data Validation: ตรวจสอบว่าข้อมูลที่เข้ามา (เช่น จาก JSON, Dictionary) ตรงตาม Type Hints และเงื่อนไขที่เรากำหนดหรือไม่
- Data Parsing/Conversion: แปลงข้อมูลจากชนิดหนึ่งไปอีกชนิดหนึ่งโดยอัตโนมัติ (เช่น “123” เป็น 123)
- Data Serialization: แปลง Python Object (Pydantic Model) เป็น Python Dictionary หรือ JSON สำหรับส่งกลับไปยัง Client
- Schema Generation: สร้าง JSON Schema โดยอัตโนมัติ ซึ่ง FastAPI ใช้ในการสร้างเอกสาร OpenAPI (Swagger UI) ครับ
ตัวอย่างการใช้ Pydantic เพิ่มเติม
# main.py (เพิ่มโค้ดในไฟล์เดิม)
from fastapi import FastAPI
from pydantic import BaseModel, Field, EmailStr, HttpUrl
from typing import List, Optional, Set
from datetime import datetime, date, time, timedelta
from uuid import UUID
app = FastAPI()
# ... (โค้ดเก่า) ...
# Pydantic Model ที่ซับซ้อนขึ้น
class UserProfile(BaseModel):
user_id: UUID = Field(..., description="Unique identifier for the user")
username: str = Field(..., min_length=3, max_length=50, regex=r"^[a-zA-Z0-9_]+$")
email: EmailStr = Field(..., example="[email protected]") # Pydantic มี EmailStr type
is_active: bool = True
registration_date: date = Field(default_factory=date.today) # กำหนดค่าเริ่มต้นเป็นวันที่ปัจจุบัน
last_login: Optional[datetime] = None
website: Optional[HttpUrl] = None # Pydantic มี HttpUrl type
class ProductDetails(BaseModel):
product_name: str
description: Optional[str] = None
price: float = Field(..., gt=0)
tags: Set[str] = Field(default_factory=set, description="Set of tags for the product") # ใช้ Set เพื่อให้ unique
class Order(BaseModel):
order_id: int
products: List[ProductDetails] # สามารถมี Pydantic Model ซ้อนกันได้
order_date: datetime = Field(default_factory=datetime.now)
customer_info: UserProfile # อีกหนึ่งตัวอย่างการซ้อน Model
total_amount: float
@app.post("/users/", response_model=UserProfile)
async def create_user(user: UserProfile):
"""
สร้าง User Profile ใหม่ พร้อม Data Validation ที่เข้มงวด
"""
return user
@app.post("/orders/", response_model=Order)
async def create_order(order: Order):
"""
สร้าง Order ใหม่ พร้อม Data Validation ที่ซับซ้อน
"""
# ในโลกจริง อาจจะบันทึกลงฐานข้อมูลและส่งคืน order ที่มี ID จริง
return order
อธิบาย Pydantic Features เพิ่มเติม:
- Built-in Types: Pydantic รองรับ Type Hints มาตรฐานของ Python ทั้งหมด (
str,int,float,bool,List,Dict,Set,Optional,Union) และมี Type พิเศษของตัวเอง เช่นEmailStr: ตรวจสอบว่าเป็นรูปแบบอีเมลที่ถูกต้องหรือไม่HttpUrl: ตรวจสอบว่าเป็น URL ที่ถูกต้องหรือไม่UUID: ตรวจสอบว่าเป็น UUID ที่ถูกต้องหรือไม่datetime,date,time,timedelta: รองรับการตรวจสอบและแปลงชนิดข้อมูลวันที่และเวลา
Field(...): ใช้Fieldเพื่อกำหนดเงื่อนไขเพิ่มเติม:min_length,max_length: สำหรับ Stringgt,lt,ge,le: สำหรับตัวเลขregex: สำหรับการตรวจสอบรูปแบบด้วย Regular Expressiondescription,example: สำหรับข้อมูลในเอกสาร APIdefault_factory: ใช้สำหรับกำหนดค่าเริ่มต้นที่มาจากการเรียกใช้ฟังก์ชัน (เช่นdatetime.nowหรือdate.today)
- Nested Models: Pydantic ช่วยให้เราสามารถสร้าง Model ที่ซับซ้อนโดยการซ้อน Model อื่น ๆ เข้าไปได้ (เช่น
products: List[ProductDetails]และcustomer_info: UserProfileในOrderModel) ซึ่งช่วยให้การออกแบบ Schema มีความยืดหยุ่นและเป็นระเบียบครับ Set[str]: การใช้Setจะบังคับให้ค่าใน List นั้นไม่ซ้ำกันครับ
ด้วย Pydantic, การจัดการข้อมูลที่ซับซ้อนใน API กลายเป็นเรื่องง่ายอย่างไม่น่าเชื่อ ไม่เพียงแต่ลดจำนวนโค้ดที่ต้องเขียนเพื่อตรวจสอบข้อมูล แต่ยังช่วยให้ API ของคุณมีความเสถียรและเชื่อถือได้มากขึ้นด้วยครับ
การจัดการ Error และ Exception
การจัดการข้อผิดพลาดเป็นสิ่งสำคัญในการสร้าง API ที่แข็งแกร่งและเป็นมิตรกับผู้ใช้งานครับ เมื่อเกิดข้อผิดพลาดขึ้นใน Server เราควรส่ง Response ที่มี Status Code และข้อความที่ชัดเจนกลับไปยัง Client เพื่อให้ Client สามารถเข้าใจและจัดการกับข้อผิดพลาดนั้นได้อย่างเหมาะสมครับ
FastAPI มีกลไกในการจัดการ Error และ Exception ที่ใช้งานง่ายและมีประสิทธิภาพครับ
1. การใช้ HTTPException
นี่คือวิธีมาตรฐานในการส่ง HTTP Error Response ใน FastAPI ครับ
# main.py (เพิ่มโค้ดในไฟล์เดิม)
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
# ... (โค้ดเก่า) ...
# ข้อมูลผู้ใช้งานจำลอง
fake_users_db = {
"john": {"full_name": "John Doe", "email": "[email protected]"},
"jane": {"full_name": "Jane Doe", "email": "[email protected]"},
}
@app.get("/users/{username}")
async def read_user(username: str):
"""
Endpoint สำหรับดึงข้อมูลผู้ใช้งานตาม username
จะส่ง HTTP 404 หากไม่พบผู้ใช้งาน
"""
if username not in fake_users_db:
# ยกเว้น HTTPException พร้อม status_code และ detail message
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, # ใช้ status object เพื่อความชัดเจน
detail="User not found",
headers={"X-Error": "There goes my error"}, # สามารถเพิ่ม headers ได้
)
return fake_users_db[username]
@app.post("/items_protected/")
async def create_protected_item(item_name: str, token: str):
"""
ตัวอย่าง Endpoint ที่ต้องการ token แต่ไม่ได้ตรวจสอบจริงจัง
จะส่ง HTTP 401 หาก token ไม่ถูกต้อง
"""
if token != "secret-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return {"message": f"Item '{item_name}' created successfully with token."}
อธิบาย:
- เมื่อคุณยกเว้น
HTTPExceptionFastAPI จะจัดการแปลงมันให้เป็น HTTP Response ที่มี Status Code ที่ระบุและ JSON Body ที่มี"detail"message ครับ - เราใช้
statusโมดูลจากfastapiเพื่อเข้าถึง HTTP Status Codes ที่เป็นมาตรฐาน เช่นstatus.HTTP_404_NOT_FOUNDซึ่งช่วยให้โค้ดอ่านง่ายและลดโอกาสเกิดข้อผิดพลาดครับ - คุณสามารถเพิ่ม
headersเข้าไปในHTTPExceptionได้ด้วย ซึ่งมีประโยชน์สำหรับ Error บางประเภท เช่นWWW-Authenticateสำหรับ 401 Unauthorized ครับ
2. การจัดการ Custom Exception (Custom Exception Handlers)
บางครั้งคุณอาจมี Custom Exception ที่คุณต้องการจัดการในลักษณะเฉพาะ หรือต้องการเปลี่ยนรูปแบบ Error Response สำหรับ Exception บางประเภทครับ
# main.py (เพิ่มโค้ดในไฟล์เดิม)
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.responses import JSONResponse
app = FastAPI()
# ... (โค้ดเก่า) ...
# 1. สร้าง Custom Exception ของเราเอง
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
# 2. สร้าง Custom Exception Handler
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
"""
Custom exception handler สำหรับ UnicornException
จะส่งคืน JSON response ที่กำหนดเอง
"""
return JSONResponse(
status_code=status.HTTP_418_IM_A_TEAPOT, # ตัวอย่าง status code ที่ไม่ธรรมดา
content={"message": f"Oops! {exc.name} did something wrong. But I'm a teapot!"},
)
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
"""
Endpoint ที่จะยกเว้น UnicornException
"""
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}
อธิบาย:
class UnicornException(Exception):: เราสร้างคลาส Custom Exception ที่สืบทอดมาจากExceptionครับ@app.exception_handler(UnicornException): นี่คือ Decorator ที่ใช้ลงทะเบียนฟังก์ชันunicorn_exception_handlerให้เป็นตัวจัดการสำหรับUnicornExceptionครับasync def unicorn_exception_handler(request: Request, exc: UnicornException)::- ฟังก์ชันนี้จะรับ
Requestobject และUnicornExceptionที่ถูกยกเว้นเข้ามาครับ - เราสามารถสร้าง
JSONResponseที่กำหนดเองได้ โดยระบุstatus_codeและcontent(ซึ่งควรเป็น Dictionary ที่จะถูกแปลงเป็น JSON)
- ฟังก์ชันนี้จะรับ
การจัดการ Error อย่างมีประสิทธิภาพไม่เพียงแต่ทำให้ API ของคุณน่าเชื่อถือ แต่ยังช่วยให้ Client สามารถพัฒนาส่วนที่จัดการข้อผิดพลาดได้ง่ายขึ้นด้วยครับ
การเชื่อมต่อกับฐานข้อมูล (SQLAlchemy ORM)
API ส่วนใหญ่จำเป็นต้องมีการจัดเก็บและดึงข้อมูลจากฐานข้อมูลครับ ในโลกของ Python, SQLAlchemy เป็น ORM (Object-Relational Mapper) ที่ได้รับความนิยมและทรงพลังที่สุดตัวหนึ่ง และสามารถทำงานร่วมกับ FastAPI ได้อย่างยอดเยี่ยมครับ
เราจะใช้ SQLAlchemy เพื่อเชื่อมต่อกับฐานข้อมูล SQLite ซึ่งเป็นฐานข้อมูลแบบไฟล์ที่ใช้งานง่าย เหมาะสำหรับการเริ่มต้นและพัฒนาครับ
1. ติดตั้ง Dependencies
ติดตั้ง SQLAlchemy และ sqlite3 (ซึ่งมักจะมาพร้อม Python อยู่แล้ว แต่ถ้าใช้ฐานข้อมูลอื่น เช่น PostgreSQL, MySQL ก็ต้องติดตั้ง Driver เพิ่มเติมครับ)
pip install sqlalchemy
2. กำหนด Database Configuration และ Models
สร้างไฟล์ database.py และ models.py เพื่อแยกโค้ดเกี่ยวกับฐานข้อมูลออกจาก main.py ครับ
database.py:
# database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# URL ของฐานข้อมูล SQLite
# "sqlite:///./sql_app.db" หมายถึงไฟล์ฐานข้อมูลชื่อ sql_app.db จะถูกสร้างในโฟลเดอร์เดียวกันกับ project
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# สร้าง SQLAlchemy Engine
# connect_args={"check_same_thread": False} เป็นสิ่งจำเป็นสำหรับ SQLite
# เพราะ SQLite ไม่ได้ถูกออกแบบมาให้ทำงานพร้อมกันหลายเธรด
# แต่ FastAPI อาจจะใช้หลายเธรดในการจัดการ request
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
# สร้าง SessionLocal class
# แต่ละ instance ของ SessionLocal จะเป็น session ของฐานข้อมูล
# SessionLocal จะถูกใช้เมื่อมีการเรียก request เข้ามา
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# สร้าง Base class สำหรับ declarative models
# ทุก models ของ SQLAlchemy จะต้องสืบทอดมาจาก Base
Base = declarative_base()
models.py:
# models.py
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from .database import Base # นำเข้า Base จาก database.py
# กำหนด SQLAlchemy Model สำหรับ Item
class Item(Base):
__tablename__ = "items" # ชื่อตารางในฐานข้อมูล
id = Column(Integer, primary_key=True, index=True) # Primary Key, มี index เพื่อการค้นหาเร็วขึ้น
name = Column(String, index=True)
description = Column(String, nullable=True) # nullable=True หมายถึง field นี้สามารถเป็น NULL ได้
price = Column(Float)
tax = Column(Float, nullable=True)
is_available = Column(Boolean, default=True) # ค่าเริ่มต้นเป็น True
# สามารถเพิ่มความสัมพันธ์กับตารางอื่นได้ในภายหลัง
# owner_id = Column(Integer, ForeignKey("users.id"))
# owner = relationship("User", back_populates="items")
3. สร้าง Pydantic Models สำหรับ Request/Response
เป็นแนวทางปฏิบัติที่ดีในการแยก Pydantic Models (ที่ใช้สำหรับ API Request/Response) ออกจาก SQLAlchemy Models (ที่ใช้สำหรับฐานข้อมูล) ครับ เพื่อป้องกันการเปิดเผยข้อมูลที่ไม่จำเป็นและเพิ่มความยืดหยุ่น
สร้างไฟล์ schemas.py:
# schemas.py
from pydantic import BaseModel, Field
from typing import Optional
# Pydantic Model สำหรับ Request Body (ใช้ในการสร้าง/อัปเดต Item)
class ItemBase(BaseModel):
name: str = Field(..., min_length=3, max_length=100)
description: Optional[str] = Field(None, max_length=500)
price: float = Field(..., gt=0)
tax: Optional[float] = None
is_available: bool = True
class Config:
schema_extra = {
"example": {
"name": "Wireless Headphone",
"description": "High-quality wireless headphones with noise cancellation.",
"price": 199.99,
"tax": 0.07,
"is_available": True
}
}
# Pydantic Model สำหรับ Response Body (รวมถึง ID ที่สร้างโดย DB)
class ItemCreate(ItemBase):
pass # ItemCreate เหมือนกับ ItemBase แต่แยกไว้เพื่อความชัดเจน
class ItemUpdate(ItemBase):
# สำหรับการอัปเดต อาจจะให้ทุก Field เป็น Optional ก็ได้
name: Optional[str] = Field(None, min_length=3, max_length=100)
description: Optional[str] = Field(None, max_length=500)
price: Optional[float] = Field(None, gt=0)
tax: Optional[float] = None
is_available: Optional[bool] = None
class Item(ItemBase):
id: int # เพิ่ม ID ที่ได้จากฐานข้อมูล
class Config:
orm_mode = True # สำคัญมาก: บอก Pydantic ให้ทำงานกับ ORM objects (SQLAlchemy) ได้
# หรือใน Pydantic v2+ ใช้ from_attributes = True
4. สร้างตารางในฐานข้อมูล
ในไฟล์ main.py หรือไฟล์เริ่มต้นอื่น ๆ ให้เรียกใช้ Base.metadata.create_all(bind=engine) เพื่อสร้างตารางตาม Models ที่เรากำหนดไว้ครับ
main.py (ไฟล์หลัก):
# main.py
from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from . import models, schemas # นำเข้า models และ schemas ที่เราสร้าง
from .database import SessionLocal, engine # นำเข้า SessionLocal และ engine
# สร้างตารางในฐานข้อมูลหากยังไม่มี
# ควรเรียกเพียงครั้งเดียวเมื่อแอปพลิเคชันเริ่มต้น
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# Dependency สำหรับการรับ Database Session
# แต่ละ request จะได้ session ของตัวเอง และจะถูกปิดเมื่อ request เสร็จสิ้น
def get_db():
db = SessionLocal()
try:
yield db # ส่ง db session ไปให้ Path Operation
finally:
db.close() # ปิด db session เมื่อทำงานเสร็จ
5. สร้าง CRUD Endpoints (Create, Read, Update, Delete)
กลับไปที่ main.py และเพิ่ม Path Operations สำหรับการทำงานกับ Item ในฐานข้อมูลครับ
# main.py (เพิ่มโค้ดในไฟล์เดิม)
# ... (โค้ดด้านบน) ...
# Endpoint สำหรับสร้าง Item ใหม่
@app.post("/items/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)
async def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)):
"""
สร้างรายการสินค้าใหม่ในฐานข้อมูล
"""
db_item = models.Item(name=item.name, description=item.description, price=item.price, tax=item.tax, is_available=item.is_available)
db.add(db_item) # เพิ่ม object เข้าสู่ session
db.commit() # บันทึกการเปลี่ยนแปลงลงฐานข้อมูล
db.refresh(db_item) # โหลดข้อมูลล่าสุดจาก DB กลับมายัง object (เช่น ID ที่ถูกสร้างขึ้น)
return db_item
# Endpoint สำหรับดึง Item ทั้งหมด
@app.get("/items/", response_model=List[schemas.Item])
async def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
"""
ดึงรายการสินค้าทั้งหมดจากฐานข้อมูล (พร้อม pagination)
"""
items = db.query(models.Item).offset(skip).limit(limit).all()
return items
# Endpoint สำหรับดึง Item ตาม ID
@app.get("/items/{item_id}", response_model=schemas.Item)
async def read_item(item_id: int, db: Session = Depends(get_db)):
"""
ดึงข้อมูลสินค้าเฉพาะตาม ID
"""
item = db.query(models.Item).filter(models.Item.id == item_id).first()
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return item
# Endpoint สำหรับอัปเดต Item
@app.put("/items/{item_id}", response_model=schemas.Item)
async def update_item(item_id: int, item: schemas.ItemUpdate, db: Session = Depends(get_db)):
"""
อัปเดตข้อมูลสินค้าที่มีอยู่ทั้งหมด
"""
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
if db_item is None:
raise HTTPException(status_code=404, detail="Item not found")
# อัปเดตข้อมูลจาก item ที่ส่งมา
for key, value in item.dict(exclude_unset=True).items(): # exclude_unset=True จะอัปเดตเฉพาะ field ที่ถูกส่งมา
setattr(db_item, key, value)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
# Endpoint สำหรับลบ Item
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int, db: Session = Depends(get_db)):
"""
ลบสินค้าออกจากฐานข้อมูล
"""
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
if db_item is None:
raise HTTPException(status_code=404, detail="Item not found")
db.delete(db_item)
db.commit()
# ไม่ต้อง return อะไรสำหรับ