วิธีสร้าง REST API ด้วย FastAPI Python แบบครบจบ

ในยุคดิจิทัลที่ทุกอย่างเชื่อมโยงกัน การสื่อสารระหว่างแอปพลิเคชันและบริการต่าง ๆ เป็นหัวใจสำคัญที่ขับเคลื่อนนวัตกรรม และหนึ่งในวิธีการสื่อสารที่ได้รับความนิยมสูงสุดก็คือ REST API (Representational State Transfer Application Programming Interface) ครับ สำหรับนักพัฒนา Python ที่กำลังมองหาวิธีสร้าง API ที่รวดเร็ว มีประสิทธิภาพ และใช้งานง่าย วันนี้เราจะพาคุณไปรู้จักกับ FastAPI เฟรมเวิร์กสมัยใหม่ที่จะเข้ามาเปลี่ยนวิธีการสร้าง API ของคุณให้ครบจบในที่เดียว ด้วยความสามารถในการสร้าง API ที่รวดเร็วอย่างไม่น่าเชื่อ พร้อมระบบตรวจสอบความถูกต้องของข้อมูล (data validation) และเอกสาร API (API documentation) ที่สร้างให้อัตโนมัติ บทความนี้จะเจาะลึกทุกขั้นตอน ตั้งแต่การติดตั้งพื้นฐานไปจนถึงการสร้าง API ที่ซับซ้อนพร้อมการเชื่อมต่อฐานข้อมูล การยืนยันตัวตน และการนำไปใช้งานจริง เพื่อให้คุณสามารถสร้าง REST API ด้วย FastAPI ได้อย่างมืออาชีพครับ

สารบัญ

REST API คืออะไร? ทำไมถึงสำคัญ?

ก่อนที่เราจะเริ่มสร้าง API ด้วย FastAPI เรามาทำความเข้าใจพื้นฐานของ REST API กันก่อนครับ REST ย่อมาจาก Representational State Transfer ซึ่งเป็นสถาปัตยกรรม (architectural style) สำหรับการออกแบบระบบเครือข่าย โดยเฉพาะอย่างยิ่งสำหรับเว็บเซอร์วิส ที่เน้นการสื่อสารแบบไร้สถานะ (stateless) และใช้ HTTP Protocol เป็นหลักครับ

หลักการสำคัญของ REST API คือการมองทุกอย่างเป็น “ทรัพยากร” (Resource) ซึ่งสามารถเข้าถึงได้ด้วย URL (Uniform Resource Locator) และมีการดำเนินการกับทรัพยากรเหล่านั้นผ่าน HTTP Methods มาตรฐาน เช่น:

  • GET: ใช้สำหรับดึงข้อมูลทรัพยากร
  • POST: ใช้สำหรับสร้างทรัพยากรใหม่
  • PUT: ใช้สำหรับอัปเดตทรัพยากรทั้งหมด
  • PATCH: ใช้สำหรับอัปเดตทรัพยากรบางส่วน
  • DELETE: ใช้สำหรับลบทรัพยากร

REST API มีความสำคัญอย่างยิ่งในโลกของการพัฒนาซอฟต์แวร์สมัยใหม่ เพราะช่วยให้แอปพลิเคชันต่าง ๆ ไม่ว่าจะเป็นเว็บเบราว์เซอร์, โมบายล์แอป, หรือแม้แต่บริการ Backend ด้วยกัน สามารถสื่อสารและแลกเปลี่ยนข้อมูลกันได้อย่างเป็นมาตรฐาน ทำให้เกิดความยืดหยุ่นในการพัฒนาและสามารถผสานรวมระบบต่าง ๆ เข้าด้วยกันได้อย่างง่ายดายครับ

ทำไมต้อง FastAPI? ข้อได้เปรียบเหนือเฟรมเวิร์กอื่น ๆ

ในบรรดาเฟรมเวิร์ก Python สำหรับการสร้าง API นั้นมีให้เลือกมากมาย เช่น Flask, Django REST Framework แต่ FastAPI ได้รับความนิยมอย่างรวดเร็วและโดดเด่นขึ้นมาด้วยเหตุผลหลายประการครับ

FastAPI สร้างขึ้นบน Starlette สำหรับส่วนของ Web และ Pydantic สำหรับส่วนของการจัดการข้อมูล ทำให้มันได้ประโยชน์จากทั้งสองไลบรารีนี้อย่างเต็มที่ครับ

  • ประสิทธิภาพสูง (High Performance): FastAPI เป็นหนึ่งในเฟรมเวิร์ก Python ที่เร็วที่สุด สามารถรองรับ Request ได้จำนวนมากเทียบเท่ากับ Node.js และ Go ด้วยความสามารถในการทำงานแบบ Asynchronous (async/await) ครับ
  • พัฒนาได้รวดเร็ว (Fast to Code): ช่วยลดเวลาในการพัฒนาลง 20% ถึง 40% เพราะคุณไม่ต้องเขียนโค้ดซ้ำซ้อนสำหรับ Data Validation และ Serialization ครับ
  • ลดข้อผิดพลาด (Fewer Bugs): ด้วยระบบ Type Hint ของ Python และ Pydantic ทำให้มีการตรวจสอบข้อมูลในเวลาคอมไพล์ (compile-time) และรันไทม์ (run-time) ช่วยลดข้อผิดพลาดที่เกี่ยวกับชนิดของข้อมูลได้มากถึง 40% ครับ
  • เอกสาร API อัตโนมัติ (Automatic Docs): FastAPI สร้างเอกสาร API แบบอินเทอร์แอคทีฟให้อัตโนมัติในรูปแบบ Swagger UI และ ReDoc ซึ่งช่วยให้นักพัฒนา Frontend และ Backend ทำงานร่วมกันได้ง่ายขึ้นมากครับ
  • รองรับ Python Type Hints: ใช้ประโยชน์จาก Type Hints ของ Python อย่างเต็มที่ ทำให้โค้ดอ่านง่ายขึ้น มีการตรวจสอบชนิดข้อมูลที่ดีขึ้น และรองรับการทำงานร่วมกับ IDEs ได้ดียิ่งขึ้นครับ
  • Dependency Injection ที่ทรงพลัง: ระบบ Dependency Injection ที่ใช้งานง่ายและมีประสิทธิภาพ ช่วยให้การจัดการ Dependency ของฟังก์ชันต่าง ๆ เป็นไปอย่างราบรื่นและทดสอบได้ง่ายครับ

เพื่อให้เห็นภาพชัดเจนยิ่งขึ้น ลองดูตารางเปรียบเทียบ FastAPI กับเฟรมเวิร์กยอดนิยมอื่น ๆ ครับ:

คุณสมบัติ FastAPI Flask Django REST Framework (DRF บน Django)
แนวคิดหลัก API โดยเฉพาะ, Microframework + ORM-agnostic Microframework, Web Application ทั่วไป Full-stack Web Framework + API extension
ประสิทธิภาพ (ความเร็ว) สูงมาก (รองรับ Async/Await) ปานกลาง (สามารถใช้ Async ได้แต่ต้องพึ่ง extension) ปานกลางถึงสูง (รองรับ Async ในเวอร์ชันใหม่ ๆ)
การตรวจสอบข้อมูล (Validation) Pydantic (ในตัว, ทรงพลังมาก) ต้องใช้ไลบรารีภายนอก (เช่น Marshmallow) DRF Serializers (มีประสิทธิภาพ, แต่ต้องกำหนดเอง)
เอกสาร API อัตโนมัติ มีในตัว (Swagger UI, ReDoc) ต้องใช้ไลบรารีภายนอก (เช่น Flask-RESTX) ต้องใช้ไลบรารีภายนอก (เช่น drf-spectacular)
การจัดการ Dependency Dependency Injection ในตัว (ทรงพลัง) ต้องจัดการเอง หรือใช้ไลบรารีภายนอก จัดการได้ผ่าน Views/Serializers, ไม่ได้มี DI โดยตรง
Type Hints ใช้ประโยชน์จาก Type Hints อย่างเต็มที่ รองรับ แต่ไม่ได้บังคับหรือใช้ประโยชน์เท่า FastAPI รองรับ แต่ไม่ได้บังคับหรือใช้ประโยชน์เท่า FastAPI
เหมาะสำหรับ REST APIs ที่ต้องการความเร็วสูง, Microservices Web Applications ขนาดเล็กถึงกลาง, Microservices Web Applications ขนาดใหญ่, Monolithic APIs
ความง่ายในการเรียนรู้ (สำหรับ API) ค่อนข้างง่ายและรวดเร็ว ง่ายสำหรับพื้นฐาน, ซับซ้อนขึ้นเมื่อต้องการ Validation/Docs มี Learning Curve สูงกว่าเล็กน้อย

เตรียมความพร้อมก่อนเริ่มต้น

ก่อนที่เราจะดำดิ่งสู่การเขียนโค้ด คุณต้องแน่ใจว่าได้ติดตั้งเครื่องมือที่จำเป็นสำหรับการพัฒนา FastAPI API ครับ

  • Python 3.7+: FastAPI ใช้ประโยชน์จากคุณสมบัติใหม่ ๆ ของ Python โดยเฉพาะ Type Hints และ Async/Await ดังนั้นต้องเป็น Python เวอร์ชัน 3.7 ขึ้นไปครับ
  • Text Editor / IDE: เช่น VS Code, PyCharm, Sublime Text หรือ Atom
  • Command Line Interface (CLI): สำหรับรันคำสั่งต่าง ๆ ครับ

การตั้งค่าสภาพแวดล้อมการพัฒนา

ติดตั้ง Python

หากคุณยังไม่มี Python 3.7+ บนเครื่อง สามารถดาวน์โหลดและติดตั้งได้จากเว็บไซต์ทางการของ Python (python.org) ครับ

เมื่อติดตั้งแล้ว ให้ลองตรวจสอบเวอร์ชันของ Python ใน Command Line:

python3 --version
# หรือ
python --version

ควรจะได้ผลลัพธ์ประมาณ Python 3.x.x ครับ

สร้าง Virtual Environment

การใช้ Virtual Environment เป็นแนวปฏิบัติที่ดีในการพัฒนา Python เพื่อแยก Dependency ของโปรเจกต์ออกจากกันครับ

สร้างโฟลเดอร์สำหรับโปรเจกต์ของคุณและเข้าไปในโฟลเดอร์นั้น:

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) นำหน้า prompt ใน Command Line ครับ

ติดตั้ง FastAPI และ Uvicorn

Uvicorn เป็น ASGI server (Asynchronous Server Gateway Interface) ที่ FastAPI ใช้ในการรันแอปพลิเคชันของเราครับ

pip install fastapi "uvicorn[standard]"

ตอนนี้คุณพร้อมที่จะสร้าง FastAPI API ตัวแรกแล้วครับ!

สร้าง FastAPI Application ตัวแรก: “Hello, World!”

มาสร้างไฟล์ main.py ในโฟลเดอร์โปรเจกต์ของคุณ และเพิ่มโค้ดต่อไปนี้ครับ

# main.py
from fastapi import FastAPI

# สร้าง Instance ของ FastAPI
app = FastAPI()

# กำหนด Path Operation สำหรับ HTTP GET ที่ root path "/"
@app.get("/")
async def read_root():
    """
    Endpoint สำหรับทดสอบการทำงานของ API
    """
    return {"message": "Hello, World!"}

# คุณสามารถเพิ่ม Path Operation อื่นๆ ได้ที่นี่
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
    """
    Endpoint สำหรับดึงข้อมูล Item ตาม ID
    """
    return {"item_id": item_id, "q": q}

จากโค้ดด้านบน:

  • เรานำเข้า FastAPI จากไลบรารี fastapi ครับ
  • สร้าง instance ของ FastAPI ชื่อ app
  • ใช้ decorator @app.get("/") เพื่อระบุว่าฟังก์ชัน read_root จะถูกเรียกเมื่อมี HTTP GET request เข้ามาที่ root path (/) ครับ
  • ฟังก์ชัน read_root ถูกกำหนดให้เป็น async เพราะ FastAPI ถูกออกแบบมาเพื่อรองรับ Asynchronous Operation โดยธรรมชาติครับ
  • ฟังก์ชันจะคืนค่าเป็น Python dictionary ซึ่ง FastAPI จะแปลงเป็น JSON response อัตโนมัติครับ

การรัน FastAPI Application

เปิด Command Line ของคุณ (ตรวจสอบให้แน่ใจว่า Virtual Environment ยังทำงานอยู่) และรันคำสั่ง Uvicorn:

uvicorn main:app --reload

คำอธิบาย:

  • main: คือชื่อโมดูล Python (ไฟล์ main.py) ครับ
  • app: คือชื่อของ instance FastAPI ที่เราสร้างขึ้นในไฟล์ main.py (app = FastAPI()) ครับ
  • --reload: เป็น flag ที่ทำให้ Uvicorn รีโหลดเซิร์ฟเวอร์อัตโนมัติเมื่อมีการเปลี่ยนแปลงโค้ด ซึ่งมีประโยชน์มากสำหรับการพัฒนาครับ

คุณจะเห็นข้อความคล้าย ๆ กับ:

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.

ทีนี้คุณสามารถเปิดเว็บเบราว์เซอร์ไปที่ http://127.0.0.1:8000 คุณจะเห็น JSON response: {"message": "Hello, World!"} ครับ

สำรวจเอกสาร API อัตโนมัติ (Swagger UI / ReDoc)

ความเจ๋งของ FastAPI คือมันสร้างเอกสาร API ให้อัตโนมัติครับ ลองไปที่ URL เหล่านี้ในเบราว์เซอร์ของคุณ:

  • Swagger UI: http://127.0.0.1:8000/docs
  • ReDoc: http://127.0.0.1:8000/redoc

คุณจะเห็นหน้าเอกสาร API ที่สวยงามและสามารถโต้ตอบได้ ซึ่งช่วยให้คุณทดสอบ Endpoint และทำความเข้าใจโครงสร้าง API ของคุณได้อย่างง่ายดายครับ

การใช้งาน Path Parameters

Path Parameters คือพารามิเตอร์ที่ฝังอยู่ใน URL Path โดยตรง ใช้สำหรับระบุทรัพยากรเฉพาะ เช่น ID ของผู้ใช้ หรือ ID ของสินค้าครับ

ตัวอย่างใน main.py:

# main.py (เพิ่มในไฟล์เดิม)
from fastapi import FastAPI

app = FastAPI()

# ... (โค้ด read_root และ read_item เดิม)

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

@app.get("/users/{user_id}")
async def get_user(user_id: str):
    return {"user_id": user_id}

ในตัวอย่าง read_item เรากำหนด item_id: int ซึ่งหมายความว่า FastAPI จะตรวจสอบให้แน่ใจว่า item_id ที่ส่งมาเป็นตัวเลขจำนวนเต็ม ถ้าไม่ใช่ จะส่งข้อผิดพลาดกลับไปอัตโนมัติครับ นี่คือพลังของ Type Hints และ Pydantic ที่ FastAPI ใช้ครับ

ลองไปที่ http://127.0.0.1:8000/items/5 หรือ http://127.0.0.1:8000/users/john_doe ครับ

การใช้งาน Query Parameters

Query Parameters คือพารามิเตอร์ที่ส่งมากับ URL หลังเครื่องหมายคำถาม (?) มักใช้สำหรับการกรอง, การจัดเรียง หรือการแบ่งหน้า (pagination) ข้อมูลครับ

ตัวอย่างใน main.py:

# main.py (เพิ่มในไฟล์เดิม)
from fastapi import FastAPI
from typing import Optional

app = FastAPI()

# ... (โค้ดเดิม)

@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 10):
    """
    Endpoint สำหรับดึงรายการ Items พร้อม Query Parameters สำหรับ pagination
    """
    return {"message": f"Fetching items with skip={skip} and limit={limit}"}

@app.get("/products/{product_id}")
async def get_product(product_id: int, query: Optional[str] = None, price: float = 0.0):
    """
    Endpoint สำหรับดึงข้อมูลสินค้าพร้อม Query Parameters
    """
    return {"product_id": product_id, "query": query, "price": price}

ในฟังก์ชัน read_items:

  • skip: int = 0 และ limit: int = 10 เป็น Query Parameters ที่มีค่าเริ่มต้น ถ้าไม่มีการระบุค่ามา FastAPI จะใช้ค่าเริ่มต้นเหล่านี้ครับ
  • FastAPI ยังคงใช้ Type Hints เพื่อตรวจสอบชนิดข้อมูลของ Query Parameters อัตโนมัติครับ

ลองทดสอบ:

  • http://127.0.0.1:8000/items/ (ใช้ค่าเริ่มต้น skip=0, limit=10)
  • http://127.0.0.1:8000/items/?skip=20&limit=5
  • http://127.0.0.1:8000/products/123?query=shoes&price=99.99

การจัดการ Request Body ด้วย Pydantic Model

เมื่อเราต้องการส่งข้อมูลที่ซับซ้อนไปยัง API เช่น การสร้างหรืออัปเดตข้อมูล เราจะใช้ Request Body ครับ FastAPI ใช้ไลบรารี Pydantic สำหรับการกำหนดโครงสร้างข้อมูล การตรวจสอบความถูกต้องของข้อมูล (validation) และการแปลงข้อมูล (serialization/deserialization) ซึ่งเป็นจุดเด่นที่ทำให้ FastAPI มีประสิทธิภาพสูงครับ

สร้าง Pydantic Model

เราจะสร้าง Pydantic Model สำหรับ Item ของเราครับ

# main.py (เพิ่มในไฟล์เดิม)
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

# ... (โค้ดเดิม)

# สร้าง Pydantic Model สำหรับ Item
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

# สร้าง Pydantic Model สำหรับ User (ตัวอย่างเพิ่มเติม)
class User(BaseModel):
    username: str
    email: str
    full_name: Optional[str] = None
    disabled: Optional[bool] = False

ในคลาส Item:

  • name: str: กำหนดให้ name เป็น string และเป็นฟิลด์ที่จำเป็น (required) ครับ
  • description: Optional[str] = None: กำหนดให้ description เป็น string และเป็นฟิลด์ที่ไม่จำเป็น (optional) โดยมีค่าเริ่มต้นเป็น None ครับ
  • price: float: กำหนดให้ price เป็น float และเป็นฟิลด์ที่จำเป็นครับ
  • tax: Optional[float] = None: กำหนดให้ tax เป็น float และเป็นฟิลด์ที่ไม่จำเป็นครับ

Pydantic จะทำการตรวจสอบชนิดข้อมูลและข้อกำหนดอื่น ๆ ให้อัตโนมัติ และถ้าข้อมูลไม่ถูกต้อง จะส่งข้อผิดพลาดที่ชัดเจนกลับไปครับ

ส่งข้อมูลด้วย HTTP POST

ทีนี้เรามาสร้าง Endpoint สำหรับการสร้าง Item ใหม่โดยใช้ HTTP POST และรับ Request Body ที่เป็น Pydantic Model ของเราครับ

# main.py (เพิ่มในไฟล์เดิม)
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

# ... (Pydantic Models และโค้ดเดิม)

@app.post("/items/")
async def create_item(item: Item):
    """
    Endpoint สำหรับสร้าง Item ใหม่
    """
    item_dict = item.dict() # แปลง Pydantic Model เป็น Python dict
    if item.tax:
        price_with_tax = item.price + item.tax
        item_dict.update({"price_with_tax": price_with_tax})
    return item_dict

@app.post("/users/")
async def create_user(user: User):
    """
    Endpoint สำหรับสร้าง User ใหม่
    """
    return user

เมื่อคุณส่ง HTTP POST request ไปที่ /items/ พร้อม JSON body ที่ตรงกับโครงสร้างของ Item model, FastAPI จะ:

  • อ่าน Request Body
  • แปลง JSON เป็น Python dictionary
  • ตรวจสอบข้อมูลใน dictionary นั้นกับ Item model ด้วย Pydantic
  • หากข้อมูลถูกต้อง จะสร้าง instance ของ Item และส่งผ่านไปยังฟังก์ชัน create_item ครับ
  • หากข้อมูลไม่ถูกต้อง (เช่น name ไม่ใช่ string หรือ price ไม่มี) FastAPI จะส่ง HTTP 422 Unprocessable Entity พร้อมรายละเอียดข้อผิดพลาดกลับไปโดยอัตโนมัติครับ

คุณสามารถทดสอบ Endpoint นี้ได้จากหน้า Swagger UI (/docs) โดยเลือกเมนู POST /items/ แล้วคลิก “Try it out” และใส่ JSON body ลงไปครับ ตัวอย่าง Request Body:

{
  "name": "Foo",
  "description": "A very nice Item",
  "price": 35.4,
  "tax": 3.2
}

ทำความเข้าใจ HTTP Methods และการสร้าง CRUD API

การสร้าง REST API มักจะเกี่ยวข้องกับการดำเนินการพื้นฐาน 4 อย่าง หรือที่เรียกว่า CRUD: Create (สร้าง), Read (อ่าน), Update (อัปเดต), และ Delete (ลบ) ครับ เราจะใช้ HTTP Methods ต่าง ๆ เพื่อให้สอดคล้องกับการดำเนินการเหล่านี้

เพื่อความเรียบง่าย เราจะเก็บข้อมูลในหน่วยความจำ (in-memory) ก่อนครับ

# main.py (เพิ่มในไฟล์เดิม)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Item(BaseModel):
    id: Optional[int] = None # เพิ่ม id เข้ามาเพื่อใช้ในการอ้างอิง
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

# เก็บข้อมูล Items ชั่วคราวในหน่วยความจำ
fake_items_db = []
next_item_id = 1

GET: ดึงข้อมูลทั้งหมด

# main.py (เพิ่มในไฟล์เดิม)
@app.get("/items/", response_model=List[Item])
async def read_all_items():
    """
    ดึงข้อมูล Item ทั้งหมด
    """
    return fake_items_db

เราใช้ response_model=List[Item] เพื่อบอก FastAPI ว่า Response ที่ส่งกลับไปควรมีโครงสร้างเป็น List ของ Item ครับ สิ่งนี้ช่วยให้ Swagger UI แสดงเอกสาร Response ที่ถูกต้อง และ FastAPI จะทำการตรวจสอบ Response ด้วย Pydantic ด้วยครับ

GET: ดึงข้อมูลตาม ID

# main.py (เพิ่มในไฟล์เดิม)
@app.get("/items/{item_id}", response_model=Item)
async def read_single_item(item_id: int):
    """
    ดึงข้อมูล Item ตาม ID
    """
    for item in fake_items_db:
        if item.id == item_id:
            return item
    raise HTTPException(status_code=404, detail="Item not found")

หากไม่พบ Item ด้วย ID ที่ระบุ เราจะใช้ raise HTTPException เพื่อส่ง HTTP 404 Not Found กลับไปครับ

POST: สร้างข้อมูลใหม่

# main.py (เพิ่มในไฟล์เดิม)
@app.post("/items/", response_model=Item, status_code=201)
async def create_new_item(item: Item):
    """
    สร้าง Item ใหม่
    """
    global next_item_id
    item.id = next_item_id
    next_item_id += 1
    fake_items_db.append(item)
    return item

เราใช้ status_code=201 เพื่อระบุว่าเมื่อสร้างทรัพยากรสำเร็จ ควรคืนค่า HTTP 201 Created ครับ

PUT: อัปเดตข้อมูลทั้งหมด

PUT ใช้สำหรับอัปเดตทรัพยากรทั้งหมด หรือสร้างใหม่หากไม่มีอยู่ครับ

# main.py (เพิ่มในไฟล์เดิม)
@app.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: int, item: Item):
    """
    อัปเดตข้อมูล Item ทั้งหมดตาม ID
    """
    for i, existing_item in enumerate(fake_items_db):
        if existing_item.id == item_id:
            item.id = item_id # ให้แน่ใจว่า ID ไม่เปลี่ยน
            fake_items_db[i] = item
            return item
    raise HTTPException(status_code=404, detail="Item not found")

PATCH: อัปเดตข้อมูลบางส่วน

PATCH ใช้สำหรับอัปเดตข้อมูลบางส่วนของทรัพยากร โดยปกติจะใช้ร่วมกับ Pydantic model ที่มีฟิลด์เป็น Optional ทั้งหมดครับ

# main.py (เพิ่มในไฟล์เดิม)
from pydantic import BaseModel
from typing import List, Optional

# ... (โค้ดเดิม)

class ItemUpdate(BaseModel):
    name: Optional[str] = None
    description: Optional[str] = None
    price: Optional[float] = None
    tax: Optional[float] = None

@app.patch("/items/{item_id}", response_model=Item)
async def partial_update_item(item_id: int, item_update: ItemUpdate):
    """
    อัปเดตข้อมูล Item บางส่วนตาม ID
    """
    for i, existing_item in enumerate(fake_items_db):
        if existing_item.id == item_id:
            update_data = item_update.dict(exclude_unset=True) # ไม่รวมฟิลด์ที่ไม่ได้ถูกตั้งค่า
            for key, value in update_data.items():
                setattr(existing_item, key, value)
            return existing_item
    raise HTTPException(status_code=404, detail="Item not found")

ใน partial_update_item เราสร้าง ItemUpdate model ที่ทุกฟิลด์เป็น Optional ครับ และใช้ item_update.dict(exclude_unset=True) เพื่อให้ได้เฉพาะฟิลด์ที่มีการส่งค่าเข้ามาใน request body เท่านั้น

DELETE: ลบข้อมูล

# main.py (เพิ่มในไฟล์เดิม)
@app.delete("/items/{item_id}", status_code=204)
async def delete_item(item_id: int):
    """
    ลบ Item ตาม ID
    """
    global fake_items_db
    initial_len = len(fake_items_db)
    fake_items_db = [item for item in fake_items_db if item.id != item_id]
    if len(fake_items_db) == initial_len:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"message": "Item deleted successfully"}

เมื่อลบทรัพยากรสำเร็จ มักจะคืนค่า HTTP 204 No Content ครับ

การตรวจสอบข้อมูลและการจัดการข้อผิดพลาด

FastAPI มีระบบการตรวจสอบข้อมูลและการจัดการข้อผิดพลาดที่มีประสิทธิภาพในตัวครับ

การตรวจสอบข้อมูลด้วย Pydantic

อย่างที่เราได้เห็นไปแล้ว Pydantic ทำการตรวจสอบข้อมูลที่เข้ามาใน Request Body และ Query/Path Parameters โดยอัตโนมัติ ตาม Type Hints ที่เรากำหนดไว้ครับ

ตัวอย่างการเพิ่มเงื่อนไขการตรวจสอบเพิ่มเติม:

# main.py (เพิ่มในไฟล์เดิม)
from fastapi import FastAPI, HTTPException, Body
from pydantic import BaseModel, Field
from typing import List, Optional

app = FastAPI()

# ... (โค้ด Item Model เดิม)

class ItemWithValidation(BaseModel):
    name: str = Field(min_length=3, max_length=50) # ชื่อต้องมีความยาว 3-50 ตัวอักษร
    description: Optional[str] = Field(None, min_length=10, max_length=200)
    price: float = Field(..., gt=0, description="The price must be greater than zero") # ราคาต้องมากกว่า 0
    tax: Optional[float] = Field(None, le=0.2) # ภาษีต้องน้อยกว่าหรือเท่ากับ 0.2

@app.post("/items_with_validation/", response_model=ItemWithValidation)
async def create_item_with_validation(item: ItemWithValidation):
    """
    สร้าง Item พร้อมการตรวจสอบข้อมูลเพิ่มเติม
    """
    return item

ใน ItemWithValidation เราใช้ Field จาก Pydantic เพื่อเพิ่มเงื่อนไขการตรวจสอบที่ละเอียดยิ่งขึ้น เช่น min_length, max_length, gt (greater than), le (less than or equal) ครับ

การจัดการข้อผิดพลาดแบบกำหนดเอง

FastAPI ใช้ HTTPException เพื่อจัดการข้อผิดพลาด HTTP โดยเฉพาะครับ

# main.py (เพิ่มในไฟล์เดิม)
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import JSONResponse

app = FastAPI()

# ... (โค้ดเดิม)

@app.get("/items/{item_id}/info")
async def get_item_info(item_id: int):
    """
    ดึงข้อมูล Item และแสดงข้อผิดพลาดแบบกำหนดเอง
    """
    if item_id == 0:
        # ส่ง HTTP 400 Bad Request
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Item ID cannot be zero")
    if item_id not in [1, 2, 3]: # สมมติว่ามีแค่ item id 1, 2, 3
        # ส่ง HTTP 404 Not Found
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found", headers={"X-Error": "There goes my error"})
    return {"item_id": item_id, "name": f"Item {item_id}"}

# คุณยังสามารถสร้าง Custom Exception Handlers ได้
class CustomException(Exception):
    def __init__(self, name: str):
        self.name = name

@app.exception_handler(CustomException)
async def custom_exception_handler(request, exc: CustomException):
    return JSONResponse(
        status_code=status.HTTP_418_IM_A_TEAPOT, # ตัวอย่างสถานะ HTTP ที่ไม่ปกติ
        content={"message": f"Oops! {exc.name} did something wrong."},
    )

@app.get("/teapot/{name}")
async def tea_time(name: str):
    if name == "coffee":
        raise CustomException(name=name)
    return {"message": f"Enjoy your {name}!"}

HTTPException ช่วยให้คุณสามารถระบุ status_code และ detail (ข้อความผิดพลาด) ได้ตามต้องการ และยังสามารถเพิ่ม headers ได้ด้วยครับ นอกจากนี้ FastAPI ยังให้คุณสร้าง @app.exception_handler สำหรับจัดการกับ Exception ที่คุณสร้างขึ้นเองได้อีกด้วยครับ

Dependency Injection ใน FastAPI

Dependency Injection (DI) เป็นรูปแบบการออกแบบที่ช่วยให้โค้ดของคุณสามารถจัดการกับ Dependencies (สิ่งพึ่งพา) ได้อย่างเป็นระเบียบและง่ายต่อการทดสอบครับ FastAPI มีระบบ DI ที่ทรงพลังในตัว ทำให้คุณสามารถประกาศ Dependencies เป็นฟังก์ชันที่สามารถถูกเรียกใช้ก่อน Path Operation และส่งค่ากลับมาให้ Path Operation ใช้ได้ครับ

ประโยชน์ของ DI:

  • ลดความซับซ้อน: โค้ดหลักของ Path Operation จะสะอาดขึ้น เพราะไม่ต้องจัดการ Dependencies ด้วยตัวเอง
  • นำกลับมาใช้ใหม่ได้: Dependencies สามารถนำกลับมาใช้ใหม่ได้ในหลาย ๆ Path Operation
  • ง่ายต่อการทดสอบ: คุณสามารถ Mock Dependencies ได้ง่ายขึ้นเมื่อทำการทดสอบ Unit Test ครับ

ตัวอย่าง Dependency อย่างง่าย

# main.py (เพิ่มในไฟล์เดิม)
from fastapi import FastAPI, Depends, HTTPException

app = FastAPI()

# ... (โค้ดเดิม)

# Dependency Function
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 10):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items_di/")
async def read_items_di(commons: dict = Depends(common_parameters)):
    """
    Endpoint ที่ใช้ Dependency Injection สำหรับ Query Parameters ทั่วไป
    """
    return commons

ในตัวอย่างนี้ common_parameters เป็นฟังก์ชัน Dependency ที่รับ Query Parameters q, skip, limit และคืนค่าเป็น dictionary ครับ ใน read_items_di เราประกาศว่า commons: dict = Depends(common_parameters) ซึ่งหมายความว่า FastAPI จะเรียก common_parameters ก่อน และส่งค่าที่คืนกลับมาให้ตัวแปร commons ครับ

ลองไปที่ http://127.0.0.1:8000/items_di/?q=search&limit=5 ครับ

Dependency ที่มีพารามิเตอร์

Dependencies สามารถมี Path หรือ Query Parameters ของตัวเองได้ครับ

# main.py (เพิ่มในไฟล์เดิม)
async def verify_token(token: str):
    if token != "mysecrettoken":
        raise HTTPException(status_code=400, detail="Invalid token")
    return token

@app.get("/secure_data/", dependencies=[Depends(verify_token)])
async def read_secure_data():
    """
    Endpoint ที่ต้องมีการยืนยัน Token ก่อนเข้าถึง
    """
    return {"message": "This is secure data!"}

ใน read_secure_data เราใช้ dependencies=[Depends(verify_token)] เพื่อระบุว่า Endpoint นี้ต้องการ Dependency verify_token ครับ ถ้า token ที่ส่งมาไม่ตรงตามที่กำหนด verify_token จะยก HTTPException ขึ้นมา และ request จะไม่ไปถึงฟังก์ชัน read_secure_data ครับ

ลองทดสอบ:

  • http://127.0.0.1:8000/secure_data/?token=wrongtoken (จะได้รับ 400 Bad Request)
  • http://127.0.0.1:8000/secure_data/?token=mysecrettoken (จะได้รับข้อมูล)

Dependency Injection เป็นแนวคิดที่ทรงพลังมาก และเป็นหัวใจสำคัญในการจัดการกับฐานข้อมูล การยืนยันตัวตน และการอนุญาตที่เราจะพูดถึงต่อไปครับ อ่านเพิ่มเติมเกี่ยวกับ Dependency Injection

การเชื่อมต่อกับฐานข้อมูล (SQLite และ SQLAlchemy)

การเชื่อมต่อ API ของเรากับฐานข้อมูลเป็นขั้นตอนสำคัญในการสร้างแอปพลิเคชันจริง ในที่นี้เราจะใช้ SQLAlchemy ซึ่งเป็น ORM (Object-Relational Mapper) ยอดนิยมสำหรับ Python และ SQLite ซึ่งเป็นฐานข้อมูลแบบไฟล์ที่เหมาะสำหรับการพัฒนาและทดสอบครับ

ติดตั้ง SQLAlchemy และ Pydantic Settings

เราจะต้องติดตั้งไลบรารีเพิ่มเติมครับ:

pip install sqlalchemy "python-dotenv[cli]" # python-dotenv สำหรับจัดการ environment variables

กำหนด Database Models

เราจะสร้างไฟล์ database.py สำหรับการตั้งค่าฐานข้อมูลและโมเดลครับ

# database.py
from sqlalchemy import create_engine, Column, Integer, String, Float, Boolean
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.sql import text
from dotenv import load_dotenv
import os

load_dotenv() # โหลด environment variables จาก .env

DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./sql_app.db") # ใช้ SQLite เป็นค่าเริ่มต้น

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) # สำหรับ SQLite

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

# Database Model
class DBItem(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    description = Column(String, nullable=True)
    price = Column(Float)
    tax = Column(Float, nullable=True)

# สร้างตารางในฐานข้อมูล (ถ้ายังไม่มี)
def create_db_tables():
    Base.metadata.create_all(bind=engine)

# สร้างไฟล์ .env
# .env
# DATABASE_URL="sqlite:///./sql_app.db"

และในไฟล์ main.py เราจะสร้าง Pydantic Models สำหรับการรับ Request และ Response:

# main.py (ปรับปรุง Pydantic Item Model)
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional
from sqlalchemy.orm import Session
import database # นำเข้า database.py

# สร้างตารางในฐานข้อมูลเมื่อแอปเริ่มทำงาน
database.create_db_tables()

# Pydantic Model สำหรับ Item ที่รับเข้ามาจาก Request Body
class ItemCreate(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

# Pydantic Model สำหรับ Item ที่ส่งกลับไป (รวม ID)
class Item(BaseModel):
    id: int
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

    class Config: # กำหนดให้ Pydantic สามารถอ่านจาก ORM objects ได้
        orm_mode = True

app = FastAPI()

สร้าง CRUD Operations กับฐานข้อมูล

เราจะสร้างฟังก์ชันสำหรับ CRUD operations ใน main.py โดยใช้ SQLAlchemy Session:

# main.py (เพิ่มในไฟล์เดิม)

# Dependency เพื่อรับ Database Session
def get_db():
    db = database.SessionLocal()
    try:
        yield db
    finally:
        db.close()

# POST: สร้าง Item ใหม่ในฐานข้อมูล
@app.post("/db_items/", response_model=Item, status_code=status.HTTP_201_CREATED)
async def create_db_item(item: ItemCreate, db: Session = Depends(get_db)):
    db_item = database.DBItem(**item.dict())
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

# GET: ดึง Item ทั้งหมดจากฐานข้อมูล
@app.get("/db_items/", response_model=List[Item])
async def read_db_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    items = db.query(database.DBItem).offset(skip).limit(limit).all()
    return items

# GET: ดึง Item ตาม ID จากฐานข้อมูล
@app.get("/db_items/{item_id}", response_model=Item)
async def read_db_item(item_id: int, db: Session = Depends(get_db)):
    item = db.query(database.DBItem).filter(database.DBItem.id == item_id).first()
    if item is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
    return item

# PUT: อัปเดต Item ในฐานข้อมูล
@app.put("/db_items/{item_id}", response_model=Item)
async def update_db_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):
    db_item = db.query(database.DBItem).filter(database.DBItem.id == item_id).first()
    if db_item is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")

    for key, value in item.dict(exclude_unset=True).items(): # ใช้ exclude_unset สำหรับ PATCH หรือ PUT
        setattr(db_item, key, value)
    
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

# DELETE: ลบ Item จากฐานข้อมูล
@app.delete("/db_items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_db_item(item_id: int, db: Session = Depends(get_db)):
    db_item = db.query(database.DBItem).filter(database.DBItem.id == item_id).first()
    if db_item is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
    
    db.delete(db_item)
    db.commit()
    return None

ในโค้ดข้างต้น:

  • ฟังก์ชัน get_db เป็น Dependency ที่สร้าง SQLAlchemy Session และปิด Session หลังจาก Request ถูกประมวลผลเสร็จสิ้นครับ
  • เราใช้ db: Session = Depends(get_db) ในแต่ละ Path Operation เพื่อรับ Database Session ครับ
  • db.query(database.DBItem) ใช้สำหรับสร้าง Query
  • .filter(...) สำหรับการกรองข้อมูล
  • .first() สำหรับดึงข้อมูลเดียว
  • .all() สำหรับดึงข้อมูลทั้งหมด
  • db.add(db_item), db.commit(), db.refresh(db_item) เป็นขั้นตอนมาตรฐานในการบันทึกข้อมูลและอัปเดต object ด้วยข้อมูลล่าสุดจากฐานข้อมูลครับ

คุณสามารถรันแอปพลิเคชันและทดสอบ Endpoint เหล่านี้ได้ผ่าน Swagger UI ครับ ระบบจะสร้างไฟล์ sql_app.db ขึ้นมาในโฟลเดอร์โปรเจกต์ของคุณโดยอัตโนมัติครับ

การยืนยันตัวตน (Authentication) และการอนุญาต (Authorization) ด้วย JWT

การยืนยันตัวตนและอนุญาตเป็นส่วนสำคัญของ API ส่วนใหญ่ เราจะใช้ JSON Web Tokens (JWT) ซึ่งเป็นมาตรฐานเปิดสำหรับการส่งข้อมูลระหว่างภาคีอย่างปลอดภัย โดยข้อมูลจะอยู่ในรูปของ JSON object ครับ

ภาพรวม JWT

JWT ประกอบด้วยสามส่วนที่คั่นด้วยจุด (.): header, payload, และ signature ครับ

  • Header: ระบุประเภทของโทเค็น (JWT) และอัลกอริทึมที่ใช้ในการเซ็นชื่อ (เช่น HS256)
  • Payload: ประกอบด้วย Claims ซึ่งเป็นข้อมูลเกี่ยวกับ entity (เช่น ผู้ใช้) และ metadata อื่น ๆ
  • Signature: ใช้สำหรับตรวจสอบว่าโทเค็นไม่ได้ถูกเปลี่ยนแปลงระหว่างทาง

เมื่อผู้ใช้ล็อกอินสำเร็จ เซิร์ฟเวอร์จะสร้าง JWT และส่งกลับไปให้ไคลเอนต์ ไคลเอนต์จะเก็บ JWT ไว้และส่งกลับมาใน Header ของทุก ๆ Request ที่ต้องการการยืนยันตัวตนครับ

ติดตั้งไลบรารีที่จำเป็น

pip install python-jose[cryptography] passlib[bcrypt]
  • python-jose[cryptography]: สำหรับการสร้างและตรวจสอบ JWT
  • passlib[bcrypt]: สำหรับการแฮชรหัสผ่านอย่างปลอดภัย

สร้าง User Model

เพิ่ม Pydantic Models สำหรับผู้ใช้ใน main.py (หรือในไฟล์ schemas.py แยกต่างหากก็ได้ครับ)

# main.py (เพิ่มในไฟล์เดิม)
from datetime import datetime, timedelta
from typing import Optional

from jose import JWTError, jwt
from passlib.context import CryptContext

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

app = FastAPI()

# ... (โค้ด Item และ DB Item Model เดิม)

# Pydantic Models สำหรับ User
class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None

class UserInDB(User):
    hashed_password: str

# JWT Configuration
SECRET_KEY = "your-secret-key" # ควรเก็บใน Environment Variable
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# Password Hashing Context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2PasswordBearer สำหรับดึง token จาก Authorization header
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# ฟังก์ชันช่วยสำหรับ JWT
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
    return pwd_context.hash(password)

# จำลองฐานข้อมูลผู้ใช้ (ในความเป็นจริงควรเชื่อมกับ DB)
fake_users_db = {
    "john_doe": {
        "username": "john_doe",
        "email": "[email protected]",
        "full_name": "John Doe",
        "hashed_password": get_password_hash("secret"),
        "disabled": False,
    },
    "jane_smith": {
        "username": "jane_smith",
        "email": "[email protected]",
        "full_name": "Jane Smith",
        "hashed_password": get_password_hash("anothersecret"),
        "disabled": True,
    },
}

def get_user_from_db(username: str):
    if username in fake_users_db:
        user_dict = fake_users_db[username]
        return UserInDB(**user_dict)
    return None

async def authenticate_user(username: str, password: str):
    user = get_user_from_db(username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        user = get_user_from_db(username)
        if user is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    return user

async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

สร้าง Endpoint สำหรับ Login และ Protected Route

# main.py (เพิ่มในไฟล์เดิม)

# Endpoint สำหรับ Login และรับ Access Token
@app.post("/token", response_model=dict)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = await authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

# Endpoint ที่ต้องการการยืนยันตัวตน
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """
    ดึงข้อมูลผู้ใช้ปัจจุบันที่ล็อกอินอยู่
    """
    return current_user

# ตัวอย่าง Endpoint ที่ต้องการการยืนยันตัวตนและเป็นผู้ดูแลระบบ
@app.get("/admin/data")
async def read_admin_data(current_user: User = Depends(get_current_active_user)):
    # ในความเป็นจริงควรมี logic ตรวจสอบ role/permission ของ user
    if current_user.username != "john_doe": # ตัวอย่างง่ายๆ สำหรับ admin
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
    return {"message": f"Welcome, admin {current_user.username}! Here is your secret data."}

เพื่อทดสอบ:

  1. ไปที่ http://127.0.0.1:8000/docs
  2. ที่มุมขวาบน จะมีปุ่ม "Authorize" คลิกแล้วใส่ Username (เช่น john_doe) และ Password (secret) ใน Endpoint /token เพื่อรับ Access Token มาครับ
  3. คัดลอก Access Token ที่ได้ (เฉพาะส่วนที่เป็นตัวอักษรยาว ๆ หลัง "access_token": ")
  4. คลิกปุ่ม "Authorize" ที่มุมขวาบนอีกครั้ง และใส่ Bearer <your_access_token> ลงไปในช่อง value แล้วกด Authorize ครับ
  5. ลองเรียก Endpoint /users/me

จัดส่งรวดเร็วส่งด่วนทั่วประเทศ
รับประกันสินค้าเคลมง่าย มีใบรับประกัน
ผ่อนชำระได้บัตรเครดิต 0% สูงสุด 10 เดือน
สะสมแต้ม รับส่วนลดส่วนลดและคะแนนสะสม

© 2026 SiamLancard — จำหน่ายการ์ดแลน อุปกรณ์ Server และเครื่องพิมพ์ใบเสร็จ

SiamLancard
Logo
Free Forex EA Download — XM Signal · EA Forex ฟรี
iCafeForex.com - สอนเทรด Forex | SiamCafe.net
Shopping cart