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()เสมอ ไม่เช่นนั้นจะลบทุกแถว
.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, ถ้าสำเร็จให้ returndata
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 อัตโนมัติ
ใช้ 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 ล้มเหลว
// 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);
// 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;
# 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 — นำ Supabase ที่ตั้งค่าไว้มาเชื่อมกับ Hono API แทน mock data
ติดตั้ง @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ถูกต้อง
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 ปกติ
แก้ไข 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 ที่เหมาะสม
// แก้ไข 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)
ทดสอบ CRUD Operations ครบทั้ง 4 ปฏิบัติการผ่าน curl เพื่อยืนยันว่า API ทำงานถูกต้องกับ Supabase จริง
- curl GET services → ต้องได้ data 5 รายการจาก Supabase
- curl POST booking → ตรวจใน Supabase Table Editor ว่ามี row ใหม่
- ทดสอบ error case: ส่ง id ที่ไม่มี → ต้องได้ 404
# 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 ไม่ได้