Python Back-End Development for Beginners

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร

บทความนี้ผู้อ่านจะได้พัฒนา REST API สำหรับ User Service ด้วย Flask ซึ่งเป็น Web Framework ขนาดเล็กใน Python โดยมี API Endpoint ดังต่อไปนี้

  1. GET /api/v1/users
  2. GET /api/v1/users/{user_id}
  3. POST /api/v1/users
  4. PUT /api/v1/users/{user_id}
  5. DELETE /api/v1/users/{user_id}
  6. GET /health

เราจะใช้ HTTP Verb (Method) หลัก ๆ ได้แก่ GET, POST, PUT และ DELETE เพื่อกําหนดการดําเนินการที่เราสามารถทำได้กับ Resource ของ User Service

โดย GET จะถูกใช้เพื่อดึง Resource และ POST จะถูกใช้สำหรับสร้าง Resource ใหม่ ส่วน PUT ถูกใช้สำหรับการ Update แบบแทนที่ และ DELETE สำหรับลบ Resource ที่มีอยู่

แต่ละ Endpoint มีรายละเอียดดังต่อไปนี้

GET /api/v1/users
- สำหรับดึงข้อมูลผู้ใช้ทั้งหมดจากฐานข้อมูล เรียงตาม id จากน้อยไปมาก
- ไม่ต้องส่ง Parameter
- ต้องมี Bearer Token ใน Header
- ส่งคืนข้อมูลเป็น Array ของผู้ใช้

@app.route('/api/v1/users', methods=['GET'])
def get_users():
    # ดึงข้อมูลผู้ใช้ทั้งหมดจากฐานข้อมูล
    # เรียงตามไอดี จากน้อยไปมาก
    query = "SELECT * FROM users ORDER BY id"
    
    # ตัวอย่างผลลัพธ์:
    [
        {
            "id": 1,
            "name": "Nuttachot",
            "email": "[email protected]"
        },
        {
            "id": 2,
            "name": "Poohkan",
            "email": "[email protected]"
        }
    ]

GET /api/v1/users/{user_id}
- สำหรับดึงข้อมูลผู้ใช้ตาม ID ที่ระบุ
- ต้องระบุ user_id ที่ต้องการใน Path
- ต้องมี Bearer Token ใน Header
- ถ้าไม่พบผู้ใช้จะ Return 404

@app.route('/api/v1/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    # ดึงข้อมูลผู้ใช้ตาม ID ที่ระบุ
    query = "SELECT * FROM users WHERE id = %s"
    
    # ตัวอย่างผลลัพธ์:
    {
        "id": 1,
        "name": "Nuttachot",
        "email": "[email protected]"
    }

POST /api/v1/users
- สำหรับสร้างผู้ใช้ใหม่จากข้อมูลที่ส่งมา
- ต้องส่งข้อมูล name และ email มาในรูปแบบ JSON
- ต้องมี Bearer Token ใน Header
- email ต้องไม่ซ้ำกับที่มีอยู่แล้ว
- ส่งคืนข้อมูลผู้ใช้ที่สร้างใหม่พร้อม ID

@app.route('/api/v1/users', methods=['POST'])
def create_user():
    # สร้างผู้ใช้ใหม่จากข้อมูลที่ส่งมา
    
    # ตัวอย่างข้อมูลที่ต้องส่ง:
    {
        "name": "Nuttachot",
        "email": "[email protected]"
    }
    
    # ตัวอย่างผลลัพธ์:
    {
        "id": 1,  # ID จะถูกสร้างอัตโนมัติ
        "name": "Nuttachot",
        "email": "[email protected]"
    }

PUT /api/v1/users/{user_id}
- สำหรับอัพเดทข้อมูลผู้ใช้ทั้งหมดตาม ID ที่ระบุ
- ต้องระบุ user_id ที่ต้องการแก้ไขใน Path
- ต้องส่งข้อมูลใหม่ทั้ง name และ email
- ต้องมี Bearer Token ใน header
- ถ้าไม่พบผู้ใช้จะ Return 404
- email ใหม่ต้องไม่ซ้ำกับคนอื่น
- ส่งคืนข้อมูลผู้ใช้ที่อัพเดทแล้ว

@app.route('/api/v1/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    # อัพเดทข้อมูลผู้ใช้ตาม ID ที่ระบุ
    
    # ตัวอย่างข้อมูลที่ต้องส่ง:
    {
        "name": "Nuttachot Promrit",
        "email": "[email protected]"
    }
    
    # ตัวอย่างผลลัพธ์:
    {
        "id": 1,
        "name": "Nuttachot Promrit",
        "email": "[email protected]"
    }

DELETE /api/v1/users/{user_id}
- สำหรับลบผู้ใช้ตาม ID ที่ระบุ
- ต้องระบุ user_id ที่ต้องการลบใน Path
- ต้องมี Bearer Token ใน Header
- ถ้าไม่พบผู้ใช้จะ Return 404
- ส่งคืนข้อความยืนยันการลบสำเร็จ

@app.route('/api/v1/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    # ลบผู้ใช้ตาม ID ที่ระบุ
    
    # ตัวอย่างผลลัพธ์:
    {
        "message": "User deleted successfully"
    }

GET /health
- สำหรับตรวจสอบการเชื่อมต่อกับฐานข้อมูล
- ไม่ต้องใช้ Bearer Token

@app.route('/health', methods=['GET'])
def health_check():
    # ตรวจสอบการเชื่อมต่อกับฐานข้อมูล
    
    # ตัวอย่างผลลัพธ์ (กรณีปกติ):
    {
        "status": "healthy",
        "database": "connected"
    }
    
    # ตัวอย่างผลลัพธ์ (กรณีมีปัญหา):
    {
        "detail": "Database connection failed: error message"
    }

Project นี้จะมีการพัฒนาบน Github Codespaces และ VS Code Editor ซึ่งเป็น Linux-based Environment สำหรับนักพัฒนาที่ง่ายในการทดลอง Deploy API และ Database Server บน Docker Container โดยจะมีการสร้าง Git Repository 3 Repo บน Github ได้แก่

  1. userservice Repo สำหรับเก็บ Codebase ของ REST API
  2. userdatabase Repo สำหรับเก็บ Codebase ของการ Config PostgreSQL
  3. backend Repo สำหรับการสร้าง Codespace

นอกจากนี้ยังมีรูปแบบในการจัดการ Branch ใน Git ที่มีลักษณะการพัฒนา Code แบบรวมศูนย์บน Branch หลัก (main Branch) ที่เรียกว่า Trunk-Based Development ซึ่งหลาย ๆ บริษัท เช่น Google, Facebook, Netflix และ Amazon นำมาปรับใช้เพื่อให้สามารถพัฒนาและ Deploy Codeใหม่ ๆ ได้อย่างต่อเนื่องและรวดเร็ว

โดยนักพัฒนาทุกคนในทีมจะทำงานและ Merge Code เข้ามายัง Trunk โดยตรงอย่างสม่ำเสมอ ซึ่งต่างจาก Git flow ทั่วไปที่อาจมี Branch หลายระดับ เช่น main, develop หรือ feature Branch

ดังนั้นบนความนี้ไม่ใช่มีเนื้อหาเฉพาะการพัฒนา REST API เท่านั้น แต่ยังแสดงตัวอย่าง Workflow ของการพัฒนา Software แบบสมัยใหม่ที่เน้นการทำงานร่วมกันเป็นทีมด้วยแนวทางแบบ Trunk-Based Development ซึ่งผู้พัฒนาจะได้ใช้ VS Code ร่วมกับ Github Codespaces ในการทดสอบการ Deploy Software บน Linux-based Environment ก่อนนำขึ้น Production ต่อไป

สร้าง Github Codespaces

Codespaces เป็น Platform ที่ให้บริการนักพัฒนา Software ในรูปแบบของ Cloud-based จาก GitHub โดยเราสามารถสร้าง Environment ที่มีเครื่องมือและ Resource พร้อมใช้งานสำหรับการพัฒนา Software ได้อย่างรวดเร็ว นักพัฒนาสามารถสร้าง Codespace ที่เป็น Linux-based OS และเครื่องมือต่าง ๆ เช่น Git, Docker และ Python เป็นต้น

  • กด New เพื่อสร้าง Repository ตั้งชื่อเป็น backend เลือกชนิด Repo แบบ Private และเลือก Add a README file แล้วกด Create repository
  • เลือกเมนู Codespace
  • กด New Codespace
  • ที่หน้าสร้าง Codespace เลือก Repository เป็น backend จากที่สร้างไว้ แล้วเลือก Region และ Machine type ตามที่ต้องการ แล้วกด Create codespace
  • Codespace จะเปิด VS Code บน Browser ให้ทำงาน ดู Version ของ OS ด้วยคำสั่งต่อไปนี้
cat /etc/os-release

เพื่อการใช้งาน Codespace และทดสอบ Code ที่ Seamless ขึ้น เหมือนการจำลอง Environment dev มาไว้บน Localhost เราจะรัน VS Code จากเครื่องเราเองโดยไม่ใช้ Browser แต่ก่อนอื่นให้ปิดหน้าต่าง VS Code บน Browser ก่อน ป้องกันการลืมปิด ซึ่ง Github จะนับจำนวนชั่วโมงการให้ Resource เราตาม Quota ที่มี (มีทั้งแบบ Free Plan และ GitHub Pro)

  • ไปที่เมนู Codespace อีกครั้ง แล้วกดที่ปุ่ม 3 จุด
  • เลือก Open in Visual Studio Code
  • กด Allow
  • เลือก Open
  • ดู Version ของ Git และ Docker ด้วยคำสั่งต่อไปนี้
git version

docker version

Config PostgreSQL และ Deploy บน Docker Container

เราจะใช้แนวทางในการพัฒนา Software แบบ Trunk-Based Development (TBD) ซึ่งมีหลักการสำคัญ คือ

  • การ Commit และ Merge บ่อย ๆ นักพัฒนาจะ Commit และ Merge การเปลี่ยนแปลงของตัวเองเข้ากับ Trunk (Branch หลัก) บ่อยครั้ง อาจจะเป็นรายวันหรือบ่อยกว่านั้น การรวม Code บ่อย ๆ ช่วยให้ทีมทำงานร่วมกันได้อย่างคล่องตัวและลดโอกาสการเกิด Merge Conflicts ขนาดใหญ่
  • ขนาดงานที่เล็กและแตก Branch สั้น ๆ ใน TBD งานแต่ละชิ้นควรจะเล็กพอที่จะทำเสร็จได้ภายในระยะสั้น ๆ นักพัฒนาอาจแตก Branch ย่อยขึ้นมาเพื่อทำงานในบางงาน แต่ Branch ย่อยนั้นควรจะใช้เวลาไม่นานก่อนจะ Merge กลับไปยัง Trunk ไม่มี Branch ระยะยาว

โดยเราจะสร้าง userdatabase Repo สำหรับเก็บ Codebase ของการ Config PostgreSQL ที่มีโครงสร้าง Project ดังต่อไปนี้

.
├── README.md
├── backup
│   ├── Dockerfile
│   └── backup.sh
├── backups
├── docker
│   ├── Dockerfile
│   └── init.sql
├── .env
├── .gitignore
└── docker-compose.yml
  • สร้าง Folder userdatabase เข้าไปใน Folder นี้แล้วเริ่มต้นใช้งาน Git ด้วยคำสั่งต่อไปนี้
git init
  • ไปที่ Github.com สร้าง Git Repo ชื่อ userdatabase เลือกชนิด Repo แบบ Private แล้วกด Create repository
  •  เลือก SSH แล้วกด Copy URL
  • เชื่อมต่อ Codespace กับ Git Repo ด้วยคำสั่ง git remote add origin ตามด้วย URL ที่ได้ Copy มา เช่น
git remote add origin [email protected]:promritn/userdatabase.git
  • สร้าง SSH Key บน Codespace สำหรับการ Push และ Pull Code กับ Git Repo ที่ได้เชื่อมต่อ โดยไม่ต้องป้อน Username และ Password ทุกครั้ง
สร้าง ssh-keygen
ssh-keygen -t ed25519 -C "[email protected]"

แสดง public key บน Linux
cat ~/.ssh/id_ed25519.pub
  • ใช้คำสั่ง cat ~/.ssh/id_ed25519.pub เพื่อแสดง Public Key ดังเช่นตัวอย่างต่อไปนี้แล้ว Copy ไว้
ssh-ed25519 ABCAC3NzaC1lZFI1NTE3AAAAIMmPOcXyJu+c/2Ork3pmgBU9FBl1iwxBr97Bh1MxI6sB [email protected]
  • นำ Public Key ไปเพิ่มบน Github Repo โดยไปที่รูป Profile เลือก Your organizations เลือกเมนู SSH and GPG keys แล้วกด New SSH key
  • ตั้งชื่อ userdatabase และนำ Public key ที่ Copy ไปวาง แล้วกด Add SSH key
  • ยืนยันตัวตน
  • กลับมาที่ Codespace สร้างไฟล์ README.md ใน Folder userdatabase
  touch README.md
  • พิมพ์หัวข้อ PostgreSQL config แบบหัวเรื่องระดับ 1 ในไฟล์ README.md ด้วย Tag ของ Markdown แล้ว Save
# PostgreSQL config
  • Commit เข้า Git ด้วยคำสั่งต่อไปนี้
git add .

git commit -m 'first commit'
  • Push Code ที่ Commit ขึ้น Github Server
git push origin main
  • ไปที่ Browser แล้ว Refresh หน้าต่าง userdatabase Repo จะเห็นหัวข้อ PostgreSQL config แบบตัวใหญ่

เราจะ Config PostgreSQL บน Branch ย่อย ซึ่งตามหลักการของ Trunk-Based Developmen แล้วนักพัฒนาควรจะสร้าง Branch ย่อยเฉพาะงานของตัวเอง (Short-Lived Branch) จาก Trunk ซึ่งงานควรมีขนาดเล็กและสามารถทำให้เสร็จได้ภายในเวลาอันสั้น (ไม่เกิน 1 วัน) เพื่อให้ง่ายต่อการ Merge กลับไปยัง Trunk อย่างรวดเร็ว

  • สร้าง Short-Lived Branch ชื่อ feature/config-postgresql
git checkout -b feature/config-postgresql
  • สร้างไฟล์และ Folder ของ Project ตามโครงสร้างดังต่อไปนี้
.
├── README.md
├── backup
│   ├── Dockerfile
│   └── backup.sh
├── backups
├── docker
│   ├── Dockerfile
│   └── init.sql
├── .env
├── .gitignore
└── docker-compose.yml
  • แก้ไขไฟล์ docker-compose.yml สำหรับการ Deploy PosgreSQL, PGAdmin และ Backup Container
# docker-compose.yml

services:
  db:
    build: ./docker
    container_name: user_postgres
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backups:/backups  # volume สำหรับเก็บไฟล์ backup
    ports:
      - "${POSTGRES_PORT}:5432"
    networks:
      - database_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  pgadmin:
    image: dpage/pgadmin4
    container_name: user_pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
    volumes:
      - pgadmin_data:/var/lib/pgadmin
    ports:
      - "${PGADMIN_PORT}:80"
    networks:
      - database_network
    restart: unless-stopped
    depends_on:
      - db

  backup:
    build: ./backup
    container_name: postgres_backup
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_HOST: db
      BACKUP_RETENTION_DAYS: 7  # เก็บ backup ไว้ 7 วัน
      BACKUP_SCHEDULE: "0 0 * * *"  # ทำ backup ทุกวันเวลาเที่ยงคืน
    volumes:
      - ./backups:/backups
    networks:
      - database_network
    depends_on:
      - db

networks:
  database_network:
    name: database_network
    driver: bridge

volumes:
  postgres_data:
  pgadmin_data:
docker-compose.yml
  • แก้ไขไฟล์ .env
# .env
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres123
POSTGRES_PORT=5432
[email protected]
PGADMIN_DEFAULT_PASSWORD=admin123
PGADMIN_PORT=5050
.env
  • แก้ไขไฟล์ .gitignore
# .gitignore
.env

# Ignore backup files
/backups/
*.backup
*.backup.gz
*.dump
*.sql
*.gz
.gitignore
  • แก้ไขไฟล์ Dockerfile ใน Folder docker
# Dockerfile
FROM postgres:17-alpine

# Copy initialization scripts
COPY init.sql /docker-entrypoint-initdb.d/

# Set locale (optional)
ENV LANG en_US.utf8
docker/Dockerfile
  • แก้ไขไฟล์ init.sql ใน Folder docker
-- สร้างตาราง users
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- สร้าง function สำหรับอัพเดท updated_at โดยอัตโนมัติ
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = now();
    RETURN NEW;
END;
$$ language 'plpgsql';

-- สร้าง trigger สำหรับอัพเดท updated_at
CREATE TRIGGER update_users_modtime
    BEFORE UPDATE ON users
    FOR EACH ROW
    EXECUTE FUNCTION update_modified_column();

-- สร้าง indexes เพื่อเพิ่มประสิทธิภาพ
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_name ON users(name);

-- เพิ่มข้อมูลตัวอย่าง
INSERT INTO users (name, email) VALUES 
    ('ณัฐโชติ พรหมฤทธิ์', '[email protected]'),
    ('สัจจาภรณ์ ไวจรรยา', '[email protected]'),
    ('สมศรี มีสุข', '[email protected]');
docker/init.sql
  • แก้ไขไฟล์ Dockerfile ใน Folder backup
# backup/Dockerfile
FROM postgres:17-alpine

# ติดตั้ง dependencies ที่จำเป็น
RUN apk add --no-cache \
    bash \
    curl \
    pigz \
    tar \
    dcron \
    tzdata

# Copy backup script
COPY backup.sh /backup.sh
RUN chmod +x /backup.sh

# สร้าง directory สำหรับ log
RUN mkdir -p /var/log/cron && \
    touch /var/log/cron/cron.log && \
    chmod 0644 /var/log/cron/cron.log

# Create a script to setup and run cron
RUN echo '#!/bin/sh' > /entrypoint.sh && \
    echo 'printenv | grep -v "no_proxy" >> /etc/environment' >> /entrypoint.sh && \
    echo 'crond -f -d 8 >> /var/log/cron/cron.log 2>&1' >> /entrypoint.sh && \
    chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
backup/Dockerfile
  • แก้ไขไฟล์ backup.sh ใน Folder backup
#!/bin/bash
# backup/backup.sh

# กำหนดตัวแปร
BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${TIMESTAMP}"

# สร้างโฟลเดอร์ backup ถ้ายังไม่มี
mkdir -p ${BACKUP_DIR}

# ทำ Database Backup
echo "Starting backup of PostgreSQL database: ${POSTGRES_DB}"
PGPASSWORD=${POSTGRES_PASSWORD} pg_dump -h ${POSTGRES_HOST} -U ${POSTGRES_USER} -d ${POSTGRES_DB} -F c -b -v -f "${BACKUP_DIR}/${BACKUP_FILE}.backup"

# บีบอัดไฟล์ backup
echo "Compressing backup file..."
pigz "${BACKUP_DIR}/${BACKUP_FILE}.backup"

# ลบไฟล์ backup เก่า
echo "Removing old backups..."
find ${BACKUP_DIR} -type f -name "*.backup.gz" -mtime +${BACKUP_RETENTION_DAYS} -delete

# ตรวจสอบสถานะการทำงาน
if [ $? -eq 0 ]; then
    echo "Backup completed successfully: ${BACKUP_FILE}.backup.gz"
else
    echo "Backup failed!"
    exit 1
fi

# สร้าง symlink ไปยัง backup ล่าสุด
ln -sf "${BACKUP_DIR}/${BACKUP_FILE}.backup.gz" "${BACKUP_DIR}/latest.backup.gz"

ก่อน Commit Code เข้า Git นักพัฒนาต้องทดสอบก่อนว่ามันทำงานได้

  • Deploy PostgreSQL, PGAdmin และ Backup Container
docker-compose up -d
  • ตรวจสอบ Container ที่ Deploy
docker-compose ps
  • ดู Logs ของ 3 Container ที่รัน
docker-compose logs
  • ทดลอง Backup Database
docker exec postgres_backup /backup.sh
  • สำหรับการ Restore เราจะใช้คำสั่งดังต่อไปนี้
# Latest backup
docker exec -it user_postgres pg_restore -U [user] -d [dbname] -v /backups/latest.backup.gz

# Specific backup
docker exec -it user_postgres pg_restore -U [user] -d [dbname] -v /backups/backup_[timestamp].backup.gz

ทดลอง Query ข้อมูลผ่าน PG Admin ดังนี้

  • ไปยัง  URL http://localhost:5050 ใส่ Username และ Password ตามที่ได้กำหนดไว้ในไฟล์ .env
  • กด Add New Server
  • ตั้งชื่อ Connection
  • กำหนดค่า Connection ต่างๆ
  • คลิ๊กขวาที่ postgres เลือก Query Tool
  • ทดสอบ Query ข้อมูลใน Table users ด้วยคำสั่งต่อไปนี้ แล้วกดปุ่ม Play
select * from users
  • เมื่อทดสอบการ Deploy, Backup Database และ Query ข้อมูลแล้วจึง Commit Code เข้า Git ด้วยคำสั่งต่อไปนี้
git add .

git commit -m 'config postgresql, backup container and pgadmin'
  • ดู History ที่ Commit
git log --oneline

Rebase โดยนำโค้ดใน branch ของเรามาอัปเดตให้ตรงกับ Trunk ล่าสุด

  • แต่ก่อน Rebase ควรดึงการเปลี่ยนแปลงล่าสุดจาก Trunk เพื่อให้แน่ใจว่า Branch ของเราอยู่ในสถานะล่าสุด เหมือนบน Github Server
git checkout main

git pull origin main
  • Rebase เพื่อนำโค้ดใน Branch ของเรามา Update ให้ตรงกับ Trunk ล่าสุด
git checkout feature/config-postgresql

git rebase main
  • เมื่อ Code ใน Branch feature/config-postgresql ตรงกับ Trank ล่าสุดแล้ว จึง Merge กลับไปยัง Trunk (ถ้ามี Conflict ตอน Rebase ให้แก้ไข Conflict ก่อน Merge)
git checkout main

git merge feature/config-postgresql

*อาจ Push Branch feature/config-postgresql ขึ้น Github Server เพื่อเปิด Pull Request (PR) ให้ทีมตรวจสอบก่อนก็ได้

**ขั้นตอนการแก้ไข Conflict หลังจาก Rebase

1. ดูไฟล์ที่เกิด Conflict
2. เปิดไฟล์ที่มี Conflict และแก้ไขความขัดแย้ง
3. บันทึกไฟล์ที่แก้ไขแล้ว
4. ใช้ git add กับไฟล์ที่แก้ไข
5. ใช้ git rebase --continue เพื่อดำเนินการ Rebase ต่อ
6. หากยังมี Conflict ให้ทำซ้ำขั้นตอนจนกว่าจะเสร็จสิ้น

ยกเลิกการ Rebase (ถ้าจำเป็น)
git rebase --abort

  • Push Code ขึ้น Github Server
git push origin main
  • ลบ Branch ย่อยที่เสร็จแล้ว
git branch -d feature/config-postgresql
  • ดู History ทั้งหมด
git log --oneline

Trunk ของเราจะมี History เป็นเส้นตรงสวยงาม

พัฒนา API เชื่อมต่อกับ PostgreSQL และ Deploy บน Docker Container

เราจะสร้าง userservice Repo สำหรับเก็บ Codebase ของ REST API โดยมีโครงสร้างของ Project ดังต่อไปนี้

.
├── .env
├── .gitignore
├── Dockerfile
├── README.md
├── docker-compose.yml
├── main.py
└── requirements.txt
  • สร้าง Folder userservice เข้าไปใน Folder นี้แล้วเริ่มต้นใช้งาน Git ด้วยคำสั่งต่อไปนี้
git init
  • ไปที่ Github.com สร้าง Git Repo ชื่อ userservice เลือกชนิด Repo แบบ Private แล้วกด Create repository
  • เลือก SSH แล้วกด Copy URL
  • เชื่อมต่อ Codespace กับ Git Repo ด้วยคำสั่ง git remote add origin ตามด้วย URL ที่ได้ Copy มา เช่น
git remote add origin [email protected]:promritn/userservice.git
  • สร้างไฟล์ README.md ใน Folder userservice
touch README.md
  • พิมพ์หัวข้อ REST API Project แบบหัวเรื่องระดับ 1 ในไฟล์ README.md ด้วย Tag ของ Markdown แล้ว Save
# REST API Project
  • Commit เข้า Git ด้วยคำสั่งต่อไปนี้
git add .

git commit -m 'first commit'
  • Push Code ที่ Commit ขึ้น Github Server
git push origin main

สร้าง Short-Lived Branch ชื่อ feature/restapi-dev

git checkout -b feature/restapi-dev
  • สร้างไฟล์และ Folder ของ Project ตามโครงสร้างดังต่อไปนี้
.
├── .env
├── .gitignore
├── Dockerfile
├── README.md
├── docker-compose.yml
├── main.py
└── requirements.txt
  • แก้ไขไฟล์ docker-compose.yml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_NAME=${DB_NAME}
      - DB_HOST=host.docker.internal
      - DB_PORT=${DB_PORT:-5432}
      - API_TOKEN=${API_TOKEN}
    extra_hosts:
      - "host.docker.internal:host-gateway"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 5s
    restart: unless-stopped  # จะรีสตาร์ท container อัตโนมัติถ้า health check ไม่ผ่าน
docker-compose.yml
  • แก้ไขไฟล์ .env
# .env
API_PORT=8000
DB_USER=postgres
DB_PASSWORD=postgres123
DB_NAME=postgres
API_TOKEN=fjwfji3399
.env
  • แก้ไขไฟล์ .gitignore
# .gitignore
.env
.gitignore
  • แก้ไขไฟล์ Dockerfile
FROM python:3.11-slim

WORKDIR /app

# ติดตั้ง system dependencies และ curl สำหรับ health check
RUN apt-get update && apt-get install -y \
    gcc \
    libpq-dev \
    curl \
    && rm -rf /var/lib/apt/lists/*

# คัดลอกไฟล์ requirements.txt
COPY requirements.txt .

# ติดตั้ง Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# คัดลอกโค้ดทั้งหมด
COPY . .

# แก้ปัญหา permission denied สำหรับ non-root user
RUN useradd -m myuser
RUN chown -R myuser:myuser /app
USER myuser

# รัน Flask ด้วย gunicorn
CMD ["gunicorn", "-b", "0.0.0.0:8000", "main:app", "--access-logfile", "-", "--error-logfile", "-"]
  • แก้ไข main.py
from flask import Flask, request, jsonify, abort
from flask_httpauth import HTTPTokenAuth
from flask_cors import CORS
import psycopg2
import psycopg2.extras
import os

# ดึงค่า config จาก environment variables
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_NAME = os.getenv("DB_NAME")
DB_HOST = os.getenv("DB_HOST", "localhost")  # ค่าเริ่มต้นคือ localhost
DB_PORT = os.getenv("DB_PORT", "5432")       # ค่าเริ่มต้นคือ 5432
API_TOKEN = os.getenv("API_TOKEN")

# สร้าง Flask app
app = Flask(__name__)

# ตั้งค่า CORS
CORS(app, resources={r"/api/v1/*": {"origins": "http://localhost:3000"}})

# สร้าง authentication instance
auth = HTTPTokenAuth(scheme='Bearer')

# ฟังก์ชันตรวจสอบ token
@auth.verify_token
def verify_token(token):
    return token == API_TOKEN

# ฟังก์ชันสำหรับเชื่อมต่อฐานข้อมูล
def get_db_connection():
    conn = psycopg2.connect(
        host=DB_HOST,
        port=DB_PORT,
        dbname=DB_NAME,
        user=DB_USER,
        password=DB_PASSWORD
    )
    return conn

# สร้าง API Blueprint สำหรับ version 1
from flask import Blueprint
api_v1 = Blueprint('api_v1', __name__, url_prefix='/api/v1')

# Routes ภายใต้ Blueprint api_v1
@api_v1.route('/users', methods=['GET'])
@auth.login_required
def get_users():
    """ดึงข้อมูลผู้ใช้ทั้งหมด"""
    conn = get_db_connection()
    cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
    cursor.execute('SELECT * FROM users')
    users = cursor.fetchall()
    cursor.close()
    conn.close()
    users_list = [dict(user) for user in users]
    return jsonify(users_list)

@api_v1.route('/users/<int:user_id>', methods=['GET'])
@auth.login_required
def get_user(user_id):
    """ดึงข้อมูลผู้ใช้ตาม ID"""
    conn = get_db_connection()
    cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
    cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))
    user = cursor.fetchone()
    cursor.close()
    conn.close()
    if not user:
        abort(404, description="ไม่พบผู้ใช้")
    return jsonify(dict(user))

@api_v1.route('/users', methods=['POST'])
@auth.login_required
def create_user():
    """สร้างผู้ใช้ใหม่"""
    data = request.get_json()
    name = data.get('name')
    email = data.get('email')
    if not name or not email:
        return jsonify({'error': 'Name and email are required'}), 400

    conn = get_db_connection()
    cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
    try:
        cursor.execute(
            'INSERT INTO users (name, email) VALUES (%s, %s) RETURNING *',
            (name, email)
        )
        new_user = cursor.fetchone()
        conn.commit()
    except psycopg2.errors.UniqueViolation:
        conn.rollback()
        cursor.close()
        conn.close()
        return jsonify({'error': 'อีเมลนี้ถูกใช้งานแล้ว'}), 400
    except Exception as e:
        conn.rollback()
        cursor.close()
        conn.close()
        abort(500, description="เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์")
    cursor.close()
    conn.close()
    return jsonify(dict(new_user)), 201

@api_v1.route('/users/<int:user_id>', methods=['PUT'])
@auth.login_required
def update_user(user_id):
    """อัพเดทข้อมูลผู้ใช้"""
    data = request.get_json()
    name = data.get('name')
    email = data.get('email')
    if not name or not email:
        return jsonify({'error': 'Name and email are required'}), 400

    conn = get_db_connection()
    cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
    cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))
    user = cursor.fetchone()
    if not user:
        cursor.close()
        conn.close()
        abort(404, description="ไม่พบผู้ใช้")

    try:
        cursor.execute(
            'UPDATE users SET name = %s, email = %s WHERE id = %s RETURNING *',
            (name, email, user_id)
        )
        updated_user = cursor.fetchone()
        conn.commit()
    except psycopg2.errors.UniqueViolation:
        conn.rollback()
        cursor.close()
        conn.close()
        return jsonify({'error': 'อีเมลนี้ถูกใช้งานแล้ว'}), 400
    except Exception as e:
        conn.rollback()
        cursor.close()
        conn.close()
        abort(500, description="ไม่สามารถอัพเดทข้อมูลได้")
    cursor.close()
    conn.close()
    return jsonify(dict(updated_user))

@api_v1.route('/users/<int:user_id>', methods=['DELETE'])
@auth.login_required
def delete_user(user_id):
    """ลบผู้ใช้"""
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))
    user = cursor.fetchone()
    if not user:
        cursor.close()
        conn.close()
        abort(404, description="ไม่พบผู้ใช้")
    try:
        cursor.execute('DELETE FROM users WHERE id = %s', (user_id,))
        conn.commit()
    except Exception as e:
        conn.rollback()
        cursor.close()
        conn.close()
        abort(500, description="เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์")
    cursor.close()
    conn.close()
    return jsonify({"message": "ลบผู้ใช้สำเร็จ"})

# Health check route ที่ระดับ root (นอก blueprint)
@app.route('/health', methods=['GET'])
def health_check():
    """ตรวจสอบสถานะของ API และการเชื่อมต่อฐานข้อมูล"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute('SELECT 1')
        cursor.fetchone()
        cursor.close()
        conn.close()
        return jsonify({"status": "healthy", "database": "connected"})
    except Exception:
        abort(503, description="Database connection failed")

# ลงทะเบียน Blueprint
app.register_blueprint(api_v1)

แก้ไข requirement.txt

# requirements.txt

Flask==2.3.2
Flask-HTTPAuth==4.8.0
Flask-CORS==3.0.10
psycopg2-binary==2.9.7
python-dotenv==1.0.1
gunicorn==23.0.0

ก่อน Commit Code เข้า Git นักพัฒนาต้องทดสอบก่อนว่ามันทำงานได้

  • Deploy API
docker-compose up -d
  • ตรวจสอบ Container ที่ Deploy
docker-compose ps
  • ดู Logs ของ Container ที่รัน
docker-compose logs
  • ทดลองยิง API เส้น /health ผ่าน curl
curl http://localhost:8000/health
  • ทดลองยิง API เส้น /health ผ่าน Postman
  • ทดลองดึงข้อมูล users ทั้งหมดจาก Database ผ่าน Postman ด้วย Method GET (ต้องมี Bearer Token ใน Header)
http://localhost:8000/api/v1/users
  • ทดลองดึงข้อมูล users คนที่ 1 จาก Database ผ่าน Postman ด้วย Method GET (ต้องมี Bearer Token ใน Header)
http://localhost:8000/api/v1/users/1
  • ทดลองเพิ่ม users ใหม่ลง Database โดยรับข้อมูลแบบ JSON Format ผ่าน Postman ด้วย Method POST (ต้องมี Bearer Token ใน Header)
http://localhost:8000/api/v1/users
{
        "name": "apple",
        "email": "[email protected]"
}
JSON Format
  • ทดลองแก้ไข Email ของ User คนที่ 1 เป็น [email protected] ผ่าน JSON Format ด้วย Method PUT (ต้องมี Bearer Token ใน Header)
http://localhost:8000/api/v1/users/1
{
        "name": "nuttachot promrit",
        "email": "[email protected]"
}
  • ทดลองลบ User คนที่ 3 ด้วย Method DELETE (ต้องมี Bearer Token ใน Header)
http://localhost:8000/api/v1/users/3
  • เมื่อทดสอบการยิง API แต่ละเส้นแล้วจึง Commit Code เข้า Git ด้วยคำสั่งต่อไปนี้
git add .

git commit -m 'rest api dev'

ดู History ที่ Commit

git log --oneline

Rebase โดยนำโค้ดใน Branch ของเรามา Update ให้ตรงกับ Trunk ล่าสุด

  • แต่ก่อน Rebase ควรดึงการเปลี่ยนแปลงล่าสุดจาก Trunk เพื่อให้แน่ใจว่า Branch ของเราอยู่ในสถานะล่าสุด เหมือนบน Github Server
git checkout main

git pull origin main
  • Rebase เพื่อนำโค้ดใน Branch ของเรามา Update ให้ตรงกับ Trunk ล่าสุด
git checkout feature/restapi-dev

git rebase main
  • เมื่อ Code ใน Branch feature/restapi-dev ตรงกับ Trank ล่าสุดแล้ว จึง Merge กลับไปยัง Trunk (ถ้ามี Conflict ให้แก้ไข Conflict ก่อน Merge)
git checkout main
git merge feature/restapi-dev
  • Push Code ขึ้น Github Server
git push origin main
  • ลบ Branch ย่อยที่เสร็จแล้ว
git branch -d feature/restapi-dev
  • ดู History ทั้งหมด
git log --oneline

Expose API ให้เข้าถึงจาก Internet

  • ติดตั้ง Cloudflared
sudo apt update

sudo wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64

sudo mv cloudflared /usr/local/bin/

sudo chmod +x /usr/local/bin/cloudflared

cloudflared --version

  • สร้าง Cloudflare Tunnel
cloudflared tunnel --url http://0.0.0.0:8000
  • Copy URL ที่ได้นำไปเปิดบน Browser โดยเพิ่ม Path /health เช่น
https://creates-ignored-digest-filled.trycloudflare.com/health