สร้าง API ด้วย FastAPI เร็ว แรง ฟิ้ว !!!
เริ่มต้นเขียน API ด้วย Python และ FastAPI ฟิ้ว ฟิ้ว ๆ
ก่อนอื่น ขอเล่นมุกก่อน
เดาว่าหลายคนอาจจะเคยเห็นโพสนี้ผ่าน feed กันมาบ้างแล้ว
ประมาณว่า มีประกาศรับสมัคร Dev ที่มีประสบการณ์กับ FastAPI เกิน 4 ปี ซึ่งความเป็นจริง FastAPI เพิ่งมาประมาณปี 2019 เอง ทำให้แม้แต่คนที่เป็น Contributor หลักเอง ก็ยังมีประสบการณ์ไม่ถึง ทั้ง ๆ ที่ตัวเองเป็นสร้างเองนะ ฮ่า ๆ
FastAPI คืออะไร
คือ Framework สำหรับการสร้าง API เขียนด้วย Python โดยมี uvicorn เป็นตัว จัดการ run server และด้วยความที่ใช้ uvicorn ที่เป็น ASGI ทำให้รองรับการทำงานแบบ Asynchronous ไปโดยปริยาย
ตัวอย่าง code สร้าง API เริ่มต้นง่าย ๆ
Run server
วิธี run server ก็ง่าย ๆ จะใช้ __main__ หรือใช้ uvicorn command ก็ได้ รวมถึงถ้าใครเคยใช้ gunicorn มาก่อน FastAPI ก็รองรับนะ
จะเห็นได้ว่าเป็นการสร้าง server ที่ Lightweight มาก ๆ และไม่ซับซ้อน แค่ Import เข้ามา เรียก class จากนั้นก็ลุยได้เลย ลักษณะจะคล้าย Flask ถ้าใครเคยเขียน Flask มา จะต้องชอบ FastAPI อย่างแน่นอน (มั้งนะ)
มาดู Feature ของ FastAPI บ้าง
ก่อนอื่นเลย เพื่อให้เห็นภาพมากขึ้น ผมจะ mockup project ขึ้นมา แล้วนำ Feature ของ FastAPI มาใช้ประกอบประมาณ 20% จะได้เห็น code ด้วยไปเลย
เราจะลอง mockup ทำ API สำหรับ movies store อารมณ์ประมาณเป็นร้านซื้อขาย viedeo ง่าย ๆ กัน
โดย code ที่เห็นต่อไปนี้จะเป็นเพียง sample เท่านั้นเพื่อให้เห็นภาพของ Feature มากขึ้น ส่วนใครอยากเห็นของเต็มแบบขนาดเอาไป deploy ใช้ได้เลย สามารถไปดูได้ที่ GitHub ของผมที่นี่
เพราะอีกบทความนึงจะพูดถึงการทำ CI/CD บน Cloud Build ของ Google Cloud เราจะนำ FastAPI ไป deploy แบบ Serverless ของ GCP คลิกอ่านที่นี่
1. HTTP Method ทั่วไป [Request and Response]
เรามาดูตัวอย่าง code สำหรับ method http ที่ใช้เป็นประจำในการทำ API รวมถึงวิธีรับ Request ไม่ว่าจะเป็น Header, Cookie, Body, File และวิธี Response ต่าง ๆ ที่ FastAPI มีให้เราใช้แบบสำเร็จรูป
เริ่มต้นจากสิ่งง่าย ๆ นี้ก่อน
ที่นี้ลองใช้ curl ในการยิง request ดู ก็พบว่าใช้ได้ปกติ เรียบง่าย
ตัวอย่าง code ด้านบนเป็น GET Method ซึ่งเราสามารถกำหนด HTTP Method ผ่าน @app.method ได้เลย ไม่ว่าจะเป็น GET, POST, PUT, PATCH, DELETE
Query String
ในส่วนนี้ผมได้ทำการยิง request ไปและมี query string ชื่อ name=Parasite เพื่อค้นหาข้อมูลด้วยชื่อหนังและให้ return ข้อมูลกลับมา
จะเห็นว่า แค่เพิ่ม argument ใน function ของ API และกำหนด type ก็สามารถใช้ได้แล้ว ส่วน None ก็คือ เรา define default value ของ param นี้ไว้ หรือจะสื่อในทำนองว่า field นี้ non-required นะ
แต่ถ้าอยากทำให้ field นี้ required ล่ะ ทำอย่างไร
เราก็แค่เอา None ออกเท่านั้น เหมือนว่าเราไม่ได้ define default value ของ param ตัวนั้นไว้
โอเคที่นี้มาลองยิง request อีกรอบ ทั้งใส่และไม่ใส่ field name
จะเห็นว่า ถ้าไม่ได้ใส่ name ใน query string, API ก็จะทำการ return error กลับมาให้เลย
Header
ก่อนที่เราจะรับ header เราต้องทำการ import Header เข้ามาก่อน โดยเราสามารถ import จาก FastAPI ได้เลย ตามรูปครับ
เรามาต่อกันที่ API /member โดย path นี้จะมีหน้าที่เช็คว่าลูกค้าที่เข้ามาใช้งานมีข้อมูลในระบบแล้วหรือยัง
จากตรงนี้เราก็จะเห็นว่าการรับ Header นั้นก็ง่าย ๆ สามารถใช้ Header() ได้เลยทันที โดยเรากำหนดให้ชื่อ key ของ Header ตัวนั้น ๆ และกำหนดค่าที่รับมาเป็น type string ได้เลย ส่วนถ้าอยากให้เป็น non-required ใน function ก็ใส่ None ใน Header แบบนี้ Header(…) => Header(None)
Cookie
ต่อจาก Header ก่อนหน้า เราเห็นแล้วว่าถ้าสมมุติต้องการจะรับค่า Header จาก Client เราก็สามารถทำได้ง่าย ๆ ที่นี้เราจะมาดูตัวต่อไปที่สำคัญเช่นกันก็คือ Cookie
ก่อนอื่นให้เรา import Cookie เข้ามาก่อน ตามด้านบน แล้วมาต่อกันที่ check_token จะเห็นว่าลักษณะจะคล้าย ๆ เหมือน function ที่ผ่านมาเกือบทั้งหมด เพียงแค่เปลี่ยน argument ให้รับค่าจาก Cookie เท่านั้น
ลอง curl ดูหน่อย ใช้ได้จริงไหม
Body
เราลองใช้ GET มาเยอะละ ทีนี้ลองเปลี่ยนเป็นฝั่ง POST บ้าง
ก่อนอื่นที่จะทำการรับ body ผมอยากแนะนำ lib ตัวนึงที่ทาง FastAPI มักจะใช้ร่วมกันเพื่อรับ body message โดยมีชื่อว่า pydantic ซึ่งจะถูกนำมาใช้เพื่อให้งานของเราพัฒนาได้ง่ายขึ้นหรือจะมองเป็น class object ก็ได้นะ
เราจะเห็น MoviesObject ที่เป็น class มี field name และ genre ที่เป็น type string อยู่นั้น โดยคราว ๆ ในตอนนี้เราจะใช้มันทำหน้าที่เป็น object เพื่อเราให้สามารถทำการ reference type ได้ จาก param ที่เราได้รับค่าจาก body ซึ่งถ้าใครเขียน Python แล้วสนใจวิธีการสร้าง object ด้วย pydantic ก็สามารถไปหาอ่านเพิ่มเติมได้
เรากลับมาดูตรง insert_movies() ที่เป็น function สำหรับการเพิ่ม ข้อมูล video เข้าไป โดยมี argument req_body ที่จะมี type เป็น MoviesObject และรับค่าจาก Body(…) ในขณะเดียวกันก็มี x_username จาก Header เช่นกัน
ข้อดีของการใช้ BaseModel มาช่วยในการรับ body message คือเราสามารถคุม schema (JSON) ได้ง่ายขึ้น เพราะเรากำหนดได้ว่า field ไหนเป็น required หรือ non-required รวมถึง type ของ field และสามารถกำหนด default value ได้ด้วย
เช่น ถ้า client ส่ง JSON มาไม่ครบ ก็จะ return error กลับไปเลย
ถ้ากรณีเราต้องการกำหนด default value สำหรับ field นั้น ๆ เองก็สามารถทำได้ง่าย ๆ ตาม code ด้านล่าง
จะเห็นว่า field name มี default value เป็น untitled แล้ว ทีนี้ลองยิง request แบบไม่ตรงตาม schema บ้าง โดยผมจะไม่ส่ง field name ไป
จะเห็นว่าไม่มีการ return 422 กลับมาเลย เพราะเราปรับ field ให้เป็น non-required แล้ว แถมก็ตั้ง default value ของ field name ไว้ ทุกอย่างเลยใช้ได้ปกติ
เพิ่มเติมเราจะเห็น genre ที่เป็น field แนวตัวเลือกได้ ก็คงจะนึกถึง Enum กัน โดยตัว Pydantic ก็ support ในส่วนนี้ด้วยลองไปดูกัน
โดยเราจะต้องทำการ import Enum ตามรูป และสร้าง class object ขึ้นมาเพื่อให้เป็น reference สำหรับ ประเภทของ video
ทีนี้ลองมายิง request กันดู
แล้วถ้าสมมุติ gerne ที่ client ส่งมา ไม่ได้มีอยู่ใน list ที่เรากำหนดไว้ล่ะ จะเกิดอะไรขึ้น
ก็จะเห็นว่า client ได้รับ return error กลับไปนั้นเอง
File
มาต่อเนื่องกันกับฝั่ง POST แต่คราวนี้มาดูพวก Body ที่ไม่ใช่ JSON บ้าง
เราจะมาดูที่การอ่าน File ไม่ว่าจะเป็นอะไรก็ตาม ในตัวอย่างนี้จะลอง test ด้วยการ upload รูปเข้าไป แล้วให้ return ชื่อไฟล์ กับ size ตามมา
ก่อนอื่นให้เรา import File และ UploadFile เข้ามาก่อน จากนั้นให้มาดูที่ insert_image_profile() แล้วก็ยิง request ดู
ในตรงนี้ถ้าใครลองแล้วใช้ไม่ได้หรือเกิด error ฝั่ง server ให้ลองเช็คว่าใน environment ของเรามี python-multipart แล้วหรือยัง
Response
ตอนนี้เราเห็น GET / POST แบบเบื้องต้นไปบ้างละ ในหัวข้อนี้จะพูดถึงการปรับแก้ไข Response message บ้าง
โดยต้องย้อนกลับไปนิดนึงว่าตัว FastAPI ใช้ Starlette มา develop ตัว Framework ต่อ ซึ่ง starlette ก็เป็น framework สำหรับทำ web service เหมือนกัน ทำให้เราสามารถเรียกใช้ method ของ starlette ได้เลยโดยไม่ต้องลง package starlette เลย (เพราะถูกลงมาให้ตอนลง FastAPI)
โดยครั้งก่อน /movies ของเราเป็นการ POST เพื่อแก้ไข object ใน server แต่เราอยากเปลี่ยน status code จาก 200 OK ให้เป็น 201 Created สามารถทำง่าย ๆ ด้วยการ import starlette.response เข้ามา แล้วเรียกใช้ JSONResponse แล้วปรับแก้ตรงส่วน return ของ function
ทีนี้ลองยิง request ดู
จะเห็นว่า status code กลายเป็น 201 Created แล้ว
เพิ่มเติมใน starlette.response ไม่ได้มีแค่ JSONResponse อันที่จริงมีมากกว่านั้นเยอะมาก เช่น PlainTextReponse, HTMLResponse, FileResponse, StreamingResponse บลา ๆ เยอะมากให้เราเรียกมาใช้ได้เลยตามความต้องการ
2. API Documents
นับว่าเป็นของดีของเจ้าตัว FastAPI เลย เพราะว่าถ้าหากใครเคยประสบปัญหาเวลาคุยกับทีมหรือเพื่อนร่วมงาน ไม่ว่าจะเป็น Backend ด้วยกันเอง หรือ Frontend ที่จะคอยถามข้อมูลประว่า path ไหนทำอะไร รับอะไร return อะไร ต้อง auth ไหม วิธีการแก้ปัญหาก็มักจะจบที่ทำ document กลางขึ้นมา เพื่อให้ทุกคนในทีมได้รับรู้อย่างเท่ากัน
แต่ประเด็นมันอยู่ตรงที่เราจะทำ document รึเปล่า 5555555
หลายคนอาจจะรู้จักตัว swagger ที่ใช้ YAML มาช่วยเขียน docs ให้เรา ซึ่งก็ช่วยได้ดีระดับนึง แต่พองานเรา scale ใหญ่ขึ้นก็จะมีปัญหาขึ้นมา ประมาณว่า ขี้เกียจ
แต่ถ้าคุณใช้ FastAPI จะไม่เจอปัญหาพวกนั้น เราแค่ทำหน้าที่ dev อย่างเดียว เพราะ FastAPI จะ generate docs ให้เราทันที
แค่เรา run server ขึ้นมาแล้วเปลี่ยน path ไปที่ /docs อย่างของผมเป็น 0.0.0.0:8080/docs เพียงเท่านี้เราก็จะได้หน้า documents ของ API ขึ้นมาแล้วครับ สามารถใช้ Test ได้ด้วย อันนี้โคตรดี
ตัวอย่างแบบชัด ๆ
GET [Query String]
POST [JSON]
POST [File]
ซึ่งใน docs สามารถบ่งบอกลักษณะของ request ในรูป cURL ได้ แล้วก็มีบอกว่า path function นี้จะ return อะไรไปบ้าง ในระดับ object เลยทีเดียว และก็สามารถแต่งเติมได้ตามอิสระ เช่นจะเขียนอธิบาย path ว่าให้รับอะไรบ้าง โดยเราสามารถเขียนได้ใน code ของ function เราเลย
เราสามารถลองปรับแต่งหน้าตา API docs ได้ตามต้องการ ตั้งแต่ app ที่เพิ่ม title , description, version และ insert_movies() เพิ่มคำอธิบายภายใต้ function จากนั้นให้เราไปดูที่ /docs
จะเห็นว่า document ของตัว FastAPI อ้างอิงตาม code เราแทบจะเป๊ะ ๆ ๆ
เพิ่มเติมโดยอันที่จริงปรับแต่งได้มากกว่านี้หรือถ้าใครอยากเข้าไปในระดับ JSON เลยก็ให้ไปที่ /openapi.json ได้เลย
เพิ่มเติมอีกรอบ ถ้าใครเบื่อหน้าตา /docs ให้ลองเปลี่ยนไปที่ /redoc จะได้ style อีกแบบนึง
ในส่วนของการ modify API docs ลูกเล่นค่อนข้างเยอะ ถ้าเราต้องจะปรับแต่งก็สามารถทำได้ หรือลองศึกษา OpenAPI เพิ่มเติมดูครับ
โดย path ทางเข้าของ API docs สามารถเปลี่ยนชื่อได้หรือแม้กระทั่งจะปิดการใช้มันก็สามารถทำได้เช่นกัน
3. Dependencies, Middleware, Router
หลังจากที่เราผ่านการสร้าง API ในและวิธีใช้งาน Document เบื้องต้นแล้ว ผมมี feature ที่อยากจะนำเสนอ คือ Router และ Dependencies
Middleware
แต่ก่อนอื่นอยากให้เห็นวิธีการใช้งาน Middleware ของ FastAPI สักหน่อย
อันนี้เป็นตัวอย่างง่าย ๆ ก็คือเราปรับแต่งตัว middleware ว่าให้ทำการ เพิ่ม Header ชื่อ X-Process-Time ซึ่งก็คือระยะเวลาตั้งแต่ client request เข้ามา จนกระทั่ง response กลับไปหา client
ทดลองยิง request ดู
จะเห็นว่า server return Header ที่ชื่อ x-process-time มาด้วย ซึ่งเป็นหน่วยระยะเวลาของกระบวน request จนกระทั่งจบ response
Router
พอเรางานเริ่ม scale ใหญ่ขึ้นเรื่อย ๆ ก็จะเกิดปัญหาให้ยุ่งเหยิงกันนิดหน่อย ถ้าเราไม่ได้จัดระเบียบไว้ตั้งแต่ตอน design
ก่อนอื่นผมอยากให้เห็น structure ของ project ก่อน
.
├── app
│ ├── api
│ │ ├── member.py
│ │ └── movies.py
│ ├── db.py
│ ├── handlers.py
│ ├── helper.py
│ └── model.py
├── main.py
├── Pipfile
├── Pipfile.lock
├── README.md
├── requirements.txt
└── test_main.py
ผมจะยกตัวอย่างตัว member.py ว่าการประกาศ APIRouter() นั้นทำอย่างไรในเบื้องต้น
เริ่มต้นคือให้เรา Import APIRouter เข้ามา แล้ว assign ใส่ตัวแปรตัวนึงไว้ เพื่อให้เราใช้ในการสร้าง API Path กับ function
และฝั่ง handler.py จะเป็นไฟล์ที่กำหนด Router ซึ่งจะ import api/….py เข้ามา
ถ้าใครอยากเห็น code เต็มใน GitHub คลิกที่นี้เลยครับ
ถ้ามองเป็นภาพจะเห็นเป็นลักษณะตามรูปด้านบน คือเราไม่จำเป็นจะต้องเขียน /api/v1 ที่ function การทำงาน api ของเราในทุก ๆ ครั้ง เราจะให้ router ใน handler จัดการในส่วนนี้รวมถึงการระบุ path prefix ของ API เรา
ซึ่งอ้างอิงจากครั้งก่อน code เราเป็นยังไง API docs เราเป็นอย่างนั้น โดยปริยาย
อย่างภาพด้านบนคือผมได้ทำการแบ่ง member กับ movies เป็นคนละ router กัน
ซึ่งในเคสนี้อาจจะมองเห็นภาพเล็กไปนิดนึง ถ้าสมมุติถ้างานใหญ่ขึ้น ตามรูปด้านล่าง จะเป็นประโยชน์มาก ในการแบ่งหน้าที่ของแต่ละ API service อย่างชัดเจน ไม่ตีกัน ไม่ต้องมาลุ้นว่า code เราจะไปอยู่ผิดที่ผิดทางไหม
Dependencies
ถ้ากรณีที่เรามี function ที่ต้องคอยเช็ค header ในทุก ๆ request ที่เข้ามาใน path นั้น ๆ อยู่แล้ว หรือ function ที่ซ้ำซ้อนแบบใช้ทุกที่ วิธีแก้ไข ก็คงไม่พ้นทำ decorator หรือจะ Inherit class หลักมาก่อนแล้วค่อย implement แต่ถ้าใช้ FastAPI เราไม่ต้องกังวลเรื่องนี้ เพราะอะไรไปดูกัน (เขียนเองยังรู้สึกอวย)
ต่อจากตัวก่อน router ให้ลองคิดตามดูว่าถ้าทุก function API ต้องการ validate header ก่อน execute API นั้น function ที่ว่าควรจะอยู่ตรงไหน
จาก code ตัวอย่างคือ เราต้อง import Depends เข้ามาก่อน แล้วให้เพิ่ม dependencies ให้กับ router ของ movies ซึ่งก็คือ get_x_card_id_token() ส่งผลให้ต่อไปนี่้ทุก ๆ ครั้งที่มีการ request เข้ามาที่ api ของ movies จะทำการ validate header ก่อน ว่ามี X-Card-ID อยู่ใน header หรือไม่ และค่าของ X-Card-ID นั้นถูกต้องตรงกับ database รึเปล่า ถ้าไม่มีหรือมีแล้วก็ไม่ถูกก็จะ return 400 กลับไป
4. Testing
การทำ test ถือว่าเป็นเรื่องจำเป็นในการทำ software อยู่แล้ว ยกเว้นเราจะไม่ทำ ฮ่า ๆ ถ้าใครเคยเขียน test ด้วย python ก็น่าจะคุ้นเคยกับตัว unittest และ pytest ซึ่งก็ใช้กันอย่างแพร่หลาย โดย FastAPI ก็มีตัวช่วยให้ test api ให้ง่ายขึ้นเหมือนกัน (โคตรขาย เอาตรง ๆ ใช้ postman ก็ได้)
เริ่มต้นด้วยการสร้างไฟล์ test_xx.py ตามที่เราต้อง แต่ต้องขึ้นด้วย test_ และภายชื่อ function ก็ต้องขึ้นต้นด้วย test_ เหมือนกันครับ
ต่อด้วย import app ของเรามาก่อนและ fastapi.testclient เพื่อให้เราทำการ mockup app และ request ขึ้นมาได้
จากนั้นก็ทำเขียน test ตาม testcase ของเรา อย่างตัวอย่าง code ด้านบนคือทดสอบว่า /api/v1/member สามารถทำงานได้ถูกต้องตามที่เราต้องการหรือไม่
ทั้งนี้ผมใช้ pytest ช่วยเพื่อให้เห็นภาพมากขึ้นว่า testcase ของเราทำงานเป็นอย่างไรบ้าง ตามรูปด้านล่าง
จะเห็นว่าผ่านหมด 20 testcase ก็ไม่แปลกแหละ ผมเขียนเอง อันไหน error ก็ลบไป ล้อเล่น
5. Deployment
อันนี้มีได้หลายท่ามาก ๆ ให้มองว่า FastAPI เป็นแค่ application ทั่วไปก็ยังได้ ไม่ได้มีอะไรซับซ้อน ซึ่งก็แล้วแต่ความต้องการว่าจะเอา project เราไป host ไว้ที่ไหน ไม่ว่าจะเป็น stateless หรือ stateful ก็ได้ทั้งนั้น จะใช้ gunicorn เหมือน Flask และ Django ก็ได้
แต่ท่าที่ผมจะใช้คือ
Cloud Build ทำ CI/CD แล้ว deploy บน Cloud Run
และทุกอย่างจะอยู่บน Google Cloud ซึ่งก็จะมีพวก GitHub / Docker / Google Cloud บลา ๆ ผสมอยู่ด้วย ตาม flow รูปด้านล่าง
ดังนั้นถ้าใครมีความคุ้นเคย Docker/ Container หรือ GitLab CI/CD, AWS CodeBuild พวกนี้ ก็ตามไปอ่านได้ครับ คลิกที่นี่ ถือว่าเปิดโลกฝั่ง Google Cloud
Conclusion
สิ่งที่ผมเขียนมาทั้งหมดนี้เป็นเพียงประมาณ 10–20 % ของเจ้าตัว FastAPI เท่านั้น ยังไม่ได้ลงลึงอะไรมาก แต่อย่างน้อยคิดว่าน่าจะพอช่วยเหลือคนที่กำลังเริ่มต้นกับ framework ตัวนี้ได้ ด้วยความที่ FastAPI เพิ่งมาไม่นานประมาณปลายปี 2019 ทำให้คนยังไม่รู้จักแพร่หลายเท่า Flask หรือ Django แน่นอน แต่คิดว่าอนาคตมาคงมาแน่ ๆ (เดาล้วน ๆ )
ส่วน Feature อื่น ๆ ที่สำคัญ เช่น WebSocket, GraphQL บลา ๆ ผมจะทดลองใช้ดู เผื่ออนาคตได้ใช้ขึ้นมาจริง ๆแล้วถ้าเวิคจะมาเขียนเป็นบล็อคแชร์ให้ทุกคนอ่านอีกทีครับ
ทั้งนี้ ถ้าบทความนี้ ผิดพลาดในส่วนใด ๆ สามารถทักท้วงหรือคอมเม้นกันได้ครับ
อ่อ ถ้าใครสนใจจะลองทำ CI/CD ต่อ ลองอ่านอันนี้ได้ครับ
ขอบคุณครับ