Ansel Ch

[Fastapi] 快速製作一個會員後台API,並用ajax串接

今天要用python來開發API,至於什麼是RESTful API就不贅述,網路上都可以輕易找到!

這次會使用Fastapi並連接MongoDB製作會員系統API,過程相當簡單!

並在最後寫一個簡單的註冊頁面測試API。


前置作業

pip install fastapi[all]==0.75.0
pip install pydantic==1.9
pip install pymongo[srv]==3.11.0
pip install python-dotenv==0.19.2
pip install pytest==7.0.1

fastapi是一個開源的api框架,可以去官網看,且swagger UI支援,不需要加裝第三方套件測試
pydantic 是資料處理的套件
pymongo 是MongoDB出的py連接套件
並準備一個MongoDB帳號,若要用本地端的DB就不用準備,這邊附上官方教學

資料結構

–main.py
–models.py
–routes.py
–.env


Coding

  1. 先設置好.env,連接DB用,ATLAS_URI照著上面官方教學就可以拿到了
ATLAS_URI=mongodb+srv://<usr>:<pwd>@cluster0.mxggl0u.mongodb.net/?retryWrites=true&w=majority
DB_NAME=membership #自己定義資料庫
  1. main.py
from fastapi import FastAPI
from dotenv import dotenv_values
from pymongo import MongoClient
from routes import router as member_router

config = dotenv_values(".env") #讀取.env

app = FastAPI() #設置主要的API

# 設置允許串接ip,等等前端才能連進來
origins = [
    "http://127.0.0.1:5500"]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.on_event("startup") #連接DB
def startup_db_client():
    app.mongodb_client = MongoClient(config["ATLAS_URI"])
    app.database = app.mongodb_client[config["DB_NAME"]]


@app.on_event("shutdown")
def shutdown_db_client():
    app.mongodb_client.close()

# member_router是會員系統的router,若有需求可以設置多個router
app.include_router(member_router, tags=["membership"], prefix="/membership")
  1. models.py #建立資料格式
import uuid
from typing import Optional
from pydantic import BaseModel, Field


class Member(BaseModel):
    account: str = Field(max_length=16,
                         min_length=8, description='最少輸入8個字元,最多16個字元')
    password: str = Field(...)
    name: str = Field(...)
    phone: str = Field(...)
    '''
    Field(...)是必填欄位,Field(None) 是可選填
    Field(None,title="The description of the item",max_length=10,alias='我是替代字')

    '''

    class Config:
        allow_population_by_field_name = True
        schema_extra = {
            "example": {
                "account": "testtest123",
                "password": "testpassword",
                "name": "Ansel",
                "phone": "0912345678"
            }
        }


class MemberUpdate(BaseModel):
    name: Optional[str]
    phone: Optional[str]

    class Config:
        schema_extra = {
            "example": {
                "name": "NewName",
                "phone": "0900123321"
            }
        }
  1. routes.py #這邊只設置 membership的API,有需要可以多設置購物車等等的API
from fastapi import APIRouter, Body, Request, Response, HTTPException, status
from fastapi.encoders import jsonable_encoder
from typing import List

from models import Member, MemberUpdate

router = APIRouter()


@router.post("/", response_description="Create a new account", status_code=status.HTTP_201_CREATED, response_model=Member)
def create_member(request: Request, member: Member = Body(...)):
    # 檢查帳號是否已存在
    existing_member = request.app.database["member_data"].find_one(
        {"account": member.account}
    )
    if existing_member:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail="Account already exists")

    # 新增會員
    member = jsonable_encoder(member)
    new_member = request.app.database["member_data"].insert_one(member)
    created_member = request.app.database["member_data"].find_one(
        {"_id": new_member.inserted_id}
    )

    return created_member


@router.get("/", response_description="List all member", response_model=List[Member])
def list_memberships(request: Request):
    books = list(request.app.database["member_data"].find(limit=100))
    return books


@router.get("/{account}", response_description="Get member by id", response_model=Member)
def find_member(account: str, request: Request):
    if (member := request.app.database["member_data"].find_one({"account": account})) is not None:
        return member

    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                        detail=f"Member with account {account} not found")


@router.patch("/{account}", response_description="Update member info", response_model=Member)
def update_member(account: str, request: Request, member: MemberUpdate = Body(...)):
    member = {k: v for k, v in member.dict().items() if v is not None}

    if len(member) >= 1:
        update_result = request.app.database["member_data"].update_one(
            {"account": account}, {"$set": member}
        )

        if update_result.modified_count == 0:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND, detail=f"Member with ID {account} not found")

    if (
        existing_book := request.app.database["member_data"].find_one({"account": account})
    ) is not None:
        return existing_book

    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                        detail=f"Member with ID {account} not found")


@router.delete("/{account}", response_description="Delete account")
def delete_member(account: str, request: Request, response: Response):
    delete_result = request.app.database["member_data"].delete_one(
        {"account": account})

    if delete_result.deleted_count == 1:
        response.status_code = status.HTTP_204_NO_CONTENT
        return response

    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                        detail=f"Member with ID {account} not found")

這邊我用啟動後的畫面比較好懂
swagger

上面可以看到用
GET /membership/:得到DB內的所有資料
POST /membership/:建立會員資料
GET /membership/{account}:{account}內帶入要查詢的帳號
DELETE /membership/{account}:{account}內帶入要刪除的帳號
PATCH /membership/{account}:{account}內帶入要更新的帳號

至於啟動的部分會用到uvicorn來跑這隻程式,在終端機輸入

python -m uvicorn main:app --reload

跑起來會出現

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [9560] using watchgod
INFO:     Started server process [9244]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

之後再瀏覽器輸入http://127.0.0.1:8000/docs 就會進入剛才的swagger UI了,便可以開始做測試

不想開啟也可以用curl指令進行測試,不過這邊不贅述。

網頁前端

註冊畫面
<script>

      $(document).ready(function () {
         $('#formId').submit(function (event) {
            // 防止提交默認
            event.preventDefault();

            // 獲取表單數據
            var account = $('#account').val();
            var password = $('#password').val();
            var name = $('#name').val();
            var contactNumber = $('#phone').val();

            // POST Requests with JSON
            $.ajax({
               url: 'http://127.0.0.1:8000/membership/',
               type: 'POST',
               contentType: 'application/json',
               data: JSON.stringify({
                  account: account,
                  password: password,
                  name: name,
                  phone: contactNumber
               }),

               success: function (response) {
                  console.log(response);
               },
               error: function (xhr, status, error) {
                  console.error(error);
               }
            });
         });
      });

   </script>

   <form class="transfot" id='formId'>
        <div class="col-md-12">
        <span>Sign up</span>
        <h3>Let start!</h3>
        </div>
        <div class="col-md-12">
        <input class="col-md-12" id="account" placeholder="Account" type="text"
            name="Account">
        </div>
        <div class="col-md-12">
        <input class="col-md-12" id="password" placeholder="Password" type="text"
            name="Password">
        </div>
        <div class="col-md-12">
        <input class="col-md-12" id="name" placeholder="Name" type="text" name="Name">
        </div>
        <div class="col-md-12">
        <input class="col-md-12" id="phone" placeholder="Contact Number" type="text"
            name="Contact Number">
        </div>
        <div class="col-md-12">
        <button type='submit' id='submitButton'>Sign up</button>
        </div>
    </form>

AJAX程式碼大致是這樣


postdata 填好資料後送出
postsuccess

可以看到console回傳JSON格式,且FAST API的terminal顯示POST Created
之後再進到Swagger用GET memberlist驗證


before
after

左邊是傳送前只有一筆資料,右邊可以看到剛剛POST進來的第二筆。

簡單的前後端連接就完成啦!

Ref