สัปดาห์ที่ 7

CRUD ครบวงจร — Hono เชื่อมต่อ Supabase (GET/POST/PUT/DELETE)

CLO3
📖 ทฤษฎี

1. ทบทวน REST API Verbs กับ CRUD Operations

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

  • GET — Read: ดึงข้อมูล ไม่เปลี่ยนแปลง State บน Server เช่น GET /api/services ดึงรายการบริการทั้งหมด หรือ GET /api/services/:id ดึงบริการรายการเดียว
  • POST — Create: สร้างข้อมูลใหม่ ส่ง Body เป็น JSON เช่น POST /api/bookings สร้างการจองใหม่
  • PUT / PATCH — Update: แก้ไขข้อมูลที่มีอยู่ PUT แทนที่ทั้ง Resource, PATCH แก้ไขเฉพาะ Field ที่ส่งมา เช่น PATCH /api/bookings/:id เปลี่ยน status การจอง
  • DELETE — Delete: ลบข้อมูล เช่น DELETE /api/services/:id ลบบริการ
CRUD → REST HTTP Method Mapping:

  CRUD      │ HTTP Method │ URL ตัวอย่าง              │ Status Code
  ──────────┼─────────────┼───────────────────────────┼─────────────
  Create    │ POST        │ /api/bookings              │ 201 Created
  Read All  │ GET         │ /api/services              │ 200 OK
  Read One  │ GET         │ /api/services/:id          │ 200 OK
  Update    │ PATCH/PUT   │ /api/bookings/:id          │ 200 OK
  Delete    │ DELETE      │ /api/services/:id          │ 200 OK
  Not Found │ —           │ /api/services/:id (ไม่มี) │ 404 Not Found
  Error     │ —           │ —                          │ 500 Internal Error
          

2. Supabase JS Client: .select(), .insert(), .update(), .delete()

@supabase/supabase-js คือ Official JavaScript SDK ที่ช่วยให้เชื่อมต่อกับ Supabase ได้สะดวกโดยไม่ต้องเขียน HTTP Request เอง SDK ใช้ Query Builder Pattern ที่สามารถต่อ Method แบบ Chaining ได้ ทำให้อ่านโค้ดง่าย

Method หลักที่ต้องรู้จักมี 4 ตัวที่ตรงกับ CRUD:

  • .select() — ดึงข้อมูลจากตาราง รับ Column List เป็น Parameter เช่น .select('id, name, price') หรือ .select('*') สำหรับทุก Column ต่อด้วย .eq('id', value) เพื่อกรอง และ .single() เพื่อรับแถวเดียว
  • .insert() — เพิ่มแถวใหม่ รับ Object หรือ Array ของ Object ต้องต่อด้วย .select().single() เพื่อรับข้อมูลที่เพิ่งสร้างกลับมา
  • .update() — แก้ไขข้อมูล รับ Object ที่มีเฉพาะ Field ที่ต้องการแก้ไข ต้องระบุ Filter ด้วย .eq() เสมอ ไม่เช่นนั้นจะ Update ทุกแถวในตาราง
  • .delete() — ลบแถว ต้องระบุ Filter ด้วย .eq() เสมอ ไม่เช่นนั้นจะลบทุกแถว
ข้อควรระวัง — อย่าลืม Filter

.update() และ .delete() โดยไม่มี .eq() หรือ Filter อื่น จะแก้ไขหรือลบ ทุกแถว ในตาราง เป็นหนึ่งในอุบัติเหตุที่เกิดขึ้นบ่อยที่สุดใน Production ควรตรวจสอบ Filter ก่อนรัน Query ทุกครั้ง

3. เชื่อม Hono Routes กับ Supabase Queries

ใน Hono API แต่ละ Route Handler จะสร้าง Supabase Client ใหม่โดยใช้ Credentials จาก c.env ซึ่งเป็น Environment Variables ที่ Cloudflare Workers ส่งให้ Pattern นี้ทำให้แต่ละ Request มี Client ของตัวเอง ปลอดภัยและไม่มีปัญหา State แชร์กัน

Flow การทำงานของแต่ละ Request มี 4 ขั้นตอน:

  • Client ส่ง HTTP Request มายัง Hono Route เช่น POST /api/bookings
  • Route Handler สร้าง Supabase Client ด้วย createClient(c.env.SUPABASE_URL, c.env.SUPABASE_KEY)
  • ส่ง Query ไปยัง Supabase ผ่าน SDK และรับผลลัพธ์ { data, error }
  • ถ้ามี error ให้ return JSON Error Response, ถ้าสำเร็จให้ return data
Request Flow — Hono + Supabase:

  Client (Browser/curl)
       │
       │  HTTP Request (GET/POST/PATCH/DELETE)
       ▼
  Cloudflare Workers (Hono)
  ┌────────────────────────────────────────────┐
  │  Route Handler                             │
  │  1. createClient(env.SUPABASE_URL, KEY)    │
  │  2. supabase.from('table').select/insert.. │
  │  3. if (error) return c.json(error, 500)   │
  │  4. return c.json({ success: true, data }) │
  └────────────────────────────────────────────┘
       │
       │  HTTPS (PostgREST API)
       ▼
  Supabase PostgreSQL
          

4. Environment Variables ใน Wrangler (wrangler.toml)

Cloudflare Workers ใช้ wrangler.toml เป็น Configuration File สำหรับ Deploy Environment Variables แบ่งเป็น 2 ประเภทที่สำคัญ:

  • [vars] — ค่าที่เขียนตรงใน wrangler.toml ได้ เช่น URL ที่ไม่เป็นความลับ ค่าเหล่านี้จะถูก Commit เข้า Git ดังนั้นห้ามใส่ค่าที่เป็น Secret
  • wrangler secret put — ใช้สำหรับค่าที่เป็น Secret เช่น API Key หรือ Password ค่านี้จะถูกเข้ารหัสและเก็บใน Cloudflare โดยไม่ถูก Commit ลง Git

ใน Worker Code เข้าถึงทั้ง vars และ secrets ผ่าน c.env.VARIABLE_NAME โดย Hono จะส่ง Env Object มาใน Context ของแต่ละ Request อัตโนมัติ

เคล็ดลับ — Secret Management

ใช้ npx wrangler secret put SUPABASE_KEY เพื่อตั้งค่า anon key แทนการเขียนใน wrangler.toml เพราะ anon key แม้จะไม่ใช่ service_role key แต่ก็ควรเก็บเป็น Secret เพื่อป้องกันการนำไปใช้เกิน Quota หรือโจมตี Rate Limit

5. Error Handling ใน API Layer

การจัดการ Error ที่ดีใน API Layer มีความสำคัญมากเพราะ Client ต้องรู้ว่าเกิดอะไรขึ้น Supabase SDK จะ return { data, error } เสมอ ไม่ throw Exception ดังนั้นต้องตรวจสอบ error ทุกครั้งหลัง await

HTTP Status Code ที่ใช้บ่อยใน CRUD API:

  • 200 OK — Request สำเร็จ (GET, PATCH, DELETE)
  • 201 Created — สร้างข้อมูลสำเร็จ (POST)
  • 400 Bad Request — ข้อมูลที่ส่งมาไม่ถูกต้อง เช่น Missing Field
  • 404 Not Found — ไม่พบ Resource ที่ระบุ เช่น id ไม่มีใน DB
  • 500 Internal Server Error — เกิดปัญหาฝั่ง Server เช่น DB Connection ล้มเหลว

Pattern ที่แนะนำ: ตรวจสอบ error จาก Supabase ก่อนเสมอ ถ้ามี error ให้ return ทันทีพร้อม Status Code ที่เหมาะสม ไม่ควรปล่อยให้โค้ดทำงานต่อเมื่อ query ล้มเหลว

💻 โค้ดตัวอย่าง
Supabase JS Client — CRUD JavaScript
// snippet 1: Supabase CRUD operations
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

// READ - ดึงทุก services
const { data: services } = await supabase
  .from('services').select('*').order('price');

// READ - ดึง service เดียว
const { data: service } = await supabase
  .from('services').select('*').eq('id', id).single();

// CREATE - เพิ่ม booking
const { data: booking } = await supabase
  .from('bookings')
  .insert({ user_id, service_id, booking_date, booking_time, notes })
  .select().single();

// UPDATE - เปลี่ยน status
const { data } = await supabase
  .from('bookings').update({ status: 'confirmed' }).eq('id', bookingId);

// DELETE - ลบ service
const { error } = await supabase
  .from('services').delete().eq('id', serviceId);
src/index.js JavaScript
// snippet 2: Hono routes เชื่อม Supabase — src/index.js
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { createClient } from '@supabase/supabase-js';

const app = new Hono();
app.use('/api/*', cors({ origin: '*' }));

// GET /api/services
app.get('/api/services', async (c) => {
  const supabase = createClient(c.env.SUPABASE_URL, c.env.SUPABASE_KEY);
  const { data, error } = await supabase.from('services').select('*');
  if (error) return c.json({ error: error.message }, 500);
  return c.json({ success: true, data });
});

// POST /api/bookings
app.post('/api/bookings', async (c) => {
  const supabase = createClient(c.env.SUPABASE_URL, c.env.SUPABASE_KEY);
  const body = await c.req.json();
  const { data, error } = await supabase
    .from('bookings').insert(body).select().single();
  if (error) return c.json({ error: error.message }, 400);
  return c.json({ success: true, data }, 201);
});

// DELETE /api/services/:id
app.delete('/api/services/:id', async (c) => {
  const supabase = createClient(c.env.SUPABASE_URL, c.env.SUPABASE_KEY);
  const { error } = await supabase
    .from('services').delete().eq('id', c.req.param('id'));
  if (error) return c.json({ error: error.message }, 400);
  return c.json({ success: true });
});

export default app;
wrangler.toml TOML
# snippet 3: wrangler.toml — environment variables
name = "bookeasy-api"
main = "src/index.js"
compatibility_date = "2024-01-01"

[vars]
SUPABASE_URL = "https://xxxx.supabase.co"

# สำหรับ secret (key ที่ไม่ควรเขียนใน file):
# npx wrangler secret put SUPABASE_KEY

🧪 ปฏิบัติการ Lab 6 — BookEasy: เชื่อม Hono กับ Supabase

นำ Hono API ที่สร้างไว้มาเชื่อมต่อกับ Supabase จริง แทนที่ mock data ด้วย Query จาก PostgreSQL และทดสอบ CRUD ครบทุก Operation

ต่อยอดจาก Lab 5

ต่อยอดจาก Lab 5 — นำ Supabase ที่ตั้งค่าไว้มาเชื่อมกับ Hono API แทน mock data

1
เพิ่ม Supabase ใน bookeasy-api

ติดตั้ง @supabase/supabase-js และตั้งค่า Environment Variables ใน Wrangler

  • เปิด Terminal ใน folder bookeasy-api แล้วรัน npm install @supabase/supabase-js
  • เปิดไฟล์ wrangler.toml เพิ่ม Section [vars] และตั้ง SUPABASE_URL ด้วย Project URL จาก Supabase Dashboard
  • รัน npx wrangler secret put SUPABASE_KEY แล้วพิมพ์ anon key เมื่อ Prompt ถาม
  • ตรวจสอบว่า wrangler.toml มี [vars] Section และ SUPABASE_URL ถูกต้อง
Terminal bash
cd bookeasy-api
npm install @supabase/supabase-js
npx wrangler secret put SUPABASE_KEY
# พิมพ์ anon key เมื่อ Prompt ถาม แล้วกด Enter
เกณฑ์การผ่าน

npm install สำเร็จและ @supabase/supabase-js อยู่ใน package.json ไฟล์ wrangler.toml มี SUPABASE_URL ใน [vars] และรัน npx wrangler secret put SUPABASE_KEY สำเร็จโดยไม่มี Error

คำแนะนำ

ห้ามเขียน SUPABASE_KEY ลงใน wrangler.toml โดยตรง ให้ใช้ wrangler secret put เท่านั้น หาก Deploy บน Cloudflare แล้ว Secret จะถูกเข้ารหัสและเข้าถึงได้ผ่าน c.env.SUPABASE_KEY เหมือนกับ vars ปกติ

2
แทนที่ mock data ด้วย Supabase queries

แก้ไข src/index.js ให้ทุก Route ดึงข้อมูลจาก Supabase จริง แทนการใช้ Array mock data

  • เพิ่ม import { createClient } from '@supabase/supabase-js' ที่ด้านบนของไฟล์
  • แก้ GET /api/services ให้ดึงข้อมูลจากตาราง services ใน Supabase จริง เรียงตาม price
  • แก้ GET /api/services/:id ให้ดึง service เดียวจาก Supabase โดยใช้ .eq('id', id).single()
  • เพิ่ม POST /api/bookings ที่รับ JSON Body แล้ว insert ลงตาราง bookings ใน Supabase
  • ทุก Route ต้องตรวจสอบ error จาก Supabase และ return Error Response ที่เหมาะสม
src/index.js — แก้ GET /api/services JavaScript
// แก้ไข GET /api/services ให้ดึงจาก Supabase จริง
app.get('/api/services', async (c) => {
  const supabase = createClient(c.env.SUPABASE_URL, c.env.SUPABASE_KEY);
  const { data, error } = await supabase
    .from('services')
    .select('*')
    .order('price', { ascending: true });
  if (error) return c.json({ error: error.message }, 500);
  return c.json({ success: true, data });
});

// เพิ่ม GET /api/services/:id
app.get('/api/services/:id', async (c) => {
  const supabase = createClient(c.env.SUPABASE_URL, c.env.SUPABASE_KEY);
  const { data, error } = await supabase
    .from('services')
    .select('*')
    .eq('id', c.req.param('id'))
    .single();
  if (error) return c.json({ error: 'ไม่พบบริการ' }, 404);
  return c.json({ success: true, data });
});
เกณฑ์การผ่าน

รัน npx wrangler dev แล้วทดสอบด้วย curl หรือ Browser: GET /api/services ต้องได้ Array 5 รายการจาก Supabase จริง และ POST /api/bookings ต้องได้ 201 Created พร้อมข้อมูลที่เพิ่งสร้าง

คำแนะนำ

ถ้าได้ Error "supabase_key is not defined" ให้ตรวจสอบว่า wrangler secret put SUPABASE_KEY สำเร็จ และชื่อ Environment Variable ใน code ตรงกับชื่อ Secret ที่ตั้งไว้ (case-sensitive)

3
ทดสอบ CRUD ครบ

ทดสอบ CRUD Operations ครบทั้ง 4 ปฏิบัติการผ่าน curl เพื่อยืนยันว่า API ทำงานถูกต้องกับ Supabase จริง

  • curl GET services → ต้องได้ data 5 รายการจาก Supabase
  • curl POST booking → ตรวจใน Supabase Table Editor ว่ามี row ใหม่
  • ทดสอบ error case: ส่ง id ที่ไม่มี → ต้องได้ 404
Terminal — ทดสอบด้วย curl bash
# GET ทุก services (ต้องได้ 5 รายการ)
curl http://localhost:8787/api/services

# GET service เดียว (ใส่ UUID จริงจาก Supabase)
curl http://localhost:8787/api/services/<uuid>

# GET service ที่ไม่มี (ต้องได้ 404)
curl http://localhost:8787/api/services/00000000-0000-0000-0000-000000000000

# POST สร้าง booking ใหม่
curl -X POST http://localhost:8787/api/bookings \
  -H "Content-Type: application/json" \
  -d '{
    "service_id": "<service-uuid>",
    "booking_date": "2025-07-01",
    "booking_time": "10:00",
    "notes": "ทดสอบจาก curl"
  }'
เกณฑ์การผ่าน

GET /api/services ตอบกลับ JSON ที่มี Array 5 รายการจาก Supabase จริง POST /api/bookings ตอบกลับ 201 Created และใน Supabase Table Editor เมนู bookings มี row ใหม่ปรากฏขึ้น GET service id ที่ไม่มีอยู่ต้องได้ Status Code 404

คำแนะนำ

ใช้ npx wrangler dev เพื่อรัน Local Development Server ที่ Port 8787 ถ้าต้องการทดสอบผ่าน Browser ให้เปิด http://localhost:8787/api/services ส่วน POST ต้องใช้ curl หรือ Tool เช่น Postman / Bruno เพราะ Browser ส่ง POST ตรงจาก URL Bar ไม่ได้