Read from Supabase
Day 4 - ชั่วโมงที่ 2: Read Issues from Supabase in Next.js
เป้าหมายของชั่วโมงนี้
หลังจบชั่วโมงที่สองของ Day 4 ผู้เรียนควรสามารถ:
- ติดตั้ง Supabase JavaScript client ได้
- สร้าง Supabase server client สำหรับใช้ใน Next.js ได้
- query ข้อมูลจาก table
issuesได้ - map database row แบบ snake_case เป็น TypeScript object แบบ camelCase ได้
- render issue list จาก Supabase แทน mock data ได้
- เข้าใจ error handling เบื้องต้นเมื่อ query database ล้มเหลว
- สร้างหน้า detail
/issues/[id]ที่อ่านข้อมูลจาก Supabase ได้
ไฟล์ที่ใช้ในชั่วโมงนี้
ติดตั้ง package ที่ project root
สร้างหรือแก้ไฟล์:
src/lib/supabase/server.ts
src/lib/issues.ts
src/app/issues/page.tsx
src/app/issues/[id]/page.tsx
src/components/IssueList.tsx
src/types/issue.tsโครงสร้างเวลา 60 นาที
| เวลา | หัวข้อ | รูปแบบ |
|---|---|---|
| 0-5 นาที | Recap Supabase setup | เชื่อมเข้าเนื้อหา |
| 5-15 นาที | ติดตั้ง Supabase client | Demo |
| 15-25 นาที | สร้าง server client | Live coding |
| 25-40 นาที | สร้าง getIssues() และ map data | Live coding |
| 40-50 นาที | render หน้า /issues จาก database | ทำทีละขั้นตอน |
| 50-60 นาที | query issue detail และ recap | Live coding |
Slide 1: Recap ชั่วโมงที่ 1
ตอนนี้เรามีอะไรแล้ว
- Supabase project
- table
issues - seed data 3 rows
- demo RLS policies
.env.localIssuetype ที่ตรงกับ database
เป้าหมายชั่วโมงนี้
เปลี่ยนจาก:
const issues = [...]เป็น:
select * from public.issuesSlide 2: ติดตั้ง Supabase Client
Run in terminal at project root
npm install @supabase/supabase-jsใช้ทำอะไร
@supabase/supabase-js ใช้ให้ Next.js query Supabase ได้ เช่น:
- select
- insert
- update
- delete
หลังติดตั้ง
restart dev server ถ้าจำเป็น
Slide 3: สร้าง Supabase Server Client
File
src/lib/supabase/server.tsตำแหน่งที่วาง
สร้าง folder src/lib/supabase/ ถ้ายังไม่มี แล้วสร้างไฟล์ server.ts ใส่ code นี้เป็นเนื้อหาทั้งไฟล์
Code
import { createClient } from "@supabase/supabase-js";
export function createSupabaseServerClient() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey =
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ??
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseKey) {
throw new Error("Supabase environment variables are missing");
}
return createClient(supabaseUrl, supabaseKey, {
auth: {
persistSession: false,
},
});
}Key Message
เราเรียกไฟล์นี้ว่า server client เพราะตั้งใจใช้จาก Server Component หรือ Server Action
Slide 4: ทำไมไม่ใช้ Service Role Key ตอนนี้
Service role key คืออะไร
key ที่มีสิทธิ์สูงและ bypass RLS ได้
สำหรับ bootcamp นี้
ยังไม่ใช้ service role key ใน Day 4 เพราะ:
- เสี่ยงรั่วถ้านำไปใส่ผิดไฟล์
- ผู้เรียนยังไม่ต้องใช้สิทธิ์สูง
- demo policies อนุญาตให้ anon role ทำ CRUD ได้ชั่วคราว
Key Message
ใช้ publishable/anon key สำหรับช่วงสอนนี้ และย้ำว่า policy วันนี้เป็น demo-only
Slide 5: Database Row Type
File
src/lib/issues.tsตำแหน่งที่วาง
สร้างไฟล์ src/lib/issues.ts แล้ววาง DbIssue ไว้ด้านบนไฟล์ ใต้ import type จาก src/types/issue.ts
Code
import type { Issue, IssueStatus } from "@/types/issue";
type DbIssue = {
id: string;
reporter_name: string;
reporter_email: string;
title: string;
description: string;
status: IssueStatus;
admin_comment: string | null;
created_at: string;
updated_at: string;
};Key Message
DbIssue คือรูปแบบข้อมูลจาก database ส่วน Issue คือรูปแบบที่ UI ใช้
Slide 6: Map DbIssue เป็น Issue
File
src/lib/issues.tsตำแหน่งที่วาง
วาง function mapIssue ต่อจาก type DbIssue และก่อน function query ต่าง ๆ เช่น getIssues
Code
function mapIssue(row: DbIssue): Issue {
return {
id: row.id,
reporterName: row.reporter_name,
reporterEmail: row.reporter_email,
title: row.title,
description: row.description,
status: row.status,
adminComment: row.admin_comment ?? undefined,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}Key Message
Mapping ทำให้ UI ไม่ต้องรู้ว่า database ใช้ snake_case
Slide 7: สร้าง getIssues()
File
src/lib/issues.tsตำแหน่งที่วาง
วาง import createSupabaseServerClient ไว้บนสุดของไฟล์ แล้ววาง function getIssues ต่อจาก mapIssue
Code
import { createSupabaseServerClient } from "@/lib/supabase/server";
export async function getIssues(): Promise<Issue[]> {
const supabase = createSupabaseServerClient();
const { data, error } = await supabase
.from("issues")
.select("*")
.order("created_at", { ascending: false });
if (error) {
throw new Error(`Failed to fetch issues: ${error.message}`);
}
return (data ?? []).map((row) => mapIssue(row as DbIssue));
}Key Message
ตอนนี้ source of truth คือ Supabase ไม่ใช่ mock array แล้ว
Slide 8: หน้า /issues อ่านจาก Supabase
File
src/app/issues/page.tsxตำแหน่งที่แก้
ใช้ code นี้แทนเนื้อหาทั้งไฟล์ src/app/issues/page.tsx จาก placeholder ของ Day 2
Code
import { IssueList } from "@/components/IssueList";
import { getIssues } from "@/lib/issues";
export default async function IssuesPage() {
const issues = await getIssues();
return (
<main className="mx-auto max-w-5xl px-6 py-8">
<IssueList issues={issues} />
</main>
);
}Key Message
Server Component สามารถ await getIssues() ได้โดยตรง
Slide 9: ปรับ IssueList ให้เป็น Server-friendly
File
src/components/IssueList.tsxตำแหน่งที่แก้
แก้ props ของ IssueList ให้เหลือเฉพาะ issues ชั่วคราว ถ้ายังมี props จาก Day 3 เช่น onUpdateStatus หรือ onCloseIssue ให้ถอดออกก่อนสำหรับ server-rendered list ในชั่วโมงนี้
Props สำหรับตอนนี้
type IssueListProps = {
issues: Issue[];
};สำคัญ
ถ้า IssueList ไม่มี useState, useEffect, onClick หรือ onChange ไม่ต้องใส่ "use client"
Key Message
Day 4 เราเริ่มย้ายจาก client state กลับมาใช้ server-rendered data จาก database
Slide 10: เพิ่ม Link ไป Detail
File
src/components/IssueList.tsxตำแหน่งที่วาง
วาง import Link from "next/link"; บนสุดของไฟล์ แล้วใช้ <Link> นี้แทน <a> หรือข้อความ ดูรายละเอียด ภายใน <td> ของแต่ละ row
Code
import Link from "next/link";
<Link
href={`/issues/${issue.id}`}
className="font-semibold text-teal-700 hover:text-teal-900"
>
ดูรายละเอียด
</Link>Key Message
ข้อมูล id จาก database ใช้สร้าง dynamic route ได้ทันที
Slide 11: Query Issue by ID
File
src/lib/issues.tsตำแหน่งที่วาง
วาง function getIssueById ต่อจาก getIssues ในไฟล์เดียวกัน เพราะใช้ createSupabaseServerClient และ mapIssue ร่วมกัน
Code
export async function getIssueById(id: string): Promise<Issue | null> {
const supabase = createSupabaseServerClient();
const { data, error } = await supabase
.from("issues")
.select("*")
.eq("id", id)
.maybeSingle();
if (error) {
throw new Error(`Failed to fetch issue: ${error.message}`);
}
return data ? mapIssue(data as DbIssue) : null;
}Slide 12: หน้า /issues/[id]
File
src/app/issues/[id]/page.tsxตำแหน่งที่แก้
ใช้ code นี้แทนเนื้อหาทั้งไฟล์ src/app/issues/[id]/page.tsx จาก placeholder ของ Day 2
Code
import { notFound } from "next/navigation";
import { getIssueById } from "@/lib/issues";
type IssueDetailPageProps = {
params: Promise<{
id: string;
}>;
};
export default async function IssueDetailPage({ params }: IssueDetailPageProps) {
const { id } = await params;
const issue = await getIssueById(id);
if (!issue) {
notFound();
}
return (
<main className="mx-auto max-w-3xl px-6 py-8">
<h1 className="text-2xl font-bold text-slate-950">{issue.title}</h1>
<p className="mt-2 text-slate-600">{issue.description}</p>
</main>
);
}Slide 13: แสดง Detail เพิ่มเติม
File
src/app/issues/[id]/page.tsxตำแหน่งที่วาง
วาง <dl>...</dl> นี้ต่อจาก paragraph ที่แสดง {issue.description} ภายใน return
Code
<dl className="mt-6 grid gap-4 rounded-lg border border-slate-200 bg-white p-6">
<div>
<dt className="text-sm font-semibold text-slate-500">ผู้แจ้ง</dt>
<dd className="mt-1 text-slate-950">{issue.reporterName}</dd>
</div>
<div>
<dt className="text-sm font-semibold text-slate-500">อีเมล</dt>
<dd className="mt-1 text-slate-950">{issue.reporterEmail}</dd>
</div>
</dl>Slide 14: Error Handling เบื้องต้น
ถ้า query error
if (error) {
throw new Error(`Failed to fetch issues: ${error.message}`);
}ใน development
Next.js จะแสดง error overlay หรือ error page
ใน production
ควรมี:
- error boundary
- logging
- user-friendly error message
Key Message
อย่า ignore database error เงียบ ๆ เพราะจะ debug ยากมาก
Slide 15: Restart Dev Server หลังแก้ Env
ถ้าเจอ error env missing
ตรวจ:
.env.local
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYจากนั้น restart:
npm run devKey Message
Next.js มักต้อง restart dev server เมื่อเพิ่ม env var ใหม่
Slide 16: Read from Supabase
ขั้นตอน
- ติดตั้ง
@supabase/supabase-js - สร้าง
src/lib/supabase/server.ts - สร้าง
src/lib/issues.ts - เขียน
getIssues() - ปรับ
/issuesให้ใช้ข้อมูลจาก Supabase - เพิ่ม link ไป
/issues/[id] - เขียน
getIssueById() - แสดง issue detail จาก database
ผลลัพธ์
หน้า /issues แสดงข้อมูลจาก Supabase จริง และเปิด detail ได้
Slide 17: โค้ดสุดท้ายของ Read Flow จาก Supabase
src/lib/issues.ts
ไฟล์นี้เป็นจุดรวมการอ่านข้อมูล issue จาก Supabase:
import { createSupabaseServerClient } from "@/lib/supabase/server";
import type { Issue } from "@/types/issue";
export async function getIssues(): Promise<Issue[]> {
const supabase = createSupabaseServerClient();
const { data, error } = await supabase
.from("issues")
.select("*")
.order("created_at", { ascending: false });
if (error) {
throw new Error(`Failed to fetch issues: ${error.message}`);
}
return (data ?? []).map((row) => mapIssue(row as DbIssue));
}
export async function getIssueById(id: string): Promise<Issue | null> {
const supabase = createSupabaseServerClient();
const { data, error } = await supabase
.from("issues")
.select("*")
.eq("id", id)
.maybeSingle();
if (error) {
throw new Error(`Failed to fetch issue: ${error.message}`);
}
return data ? mapIssue(data as DbIssue) : null;
}หน้า list และ detail ใช้ function คนละตัว
flowchart LR
step0["src/app/issues/page.tsx"]
step1["await getIssues()"]
step0 --> step1ภาพรวม
Supabase rows
-> mapIssue()
-> Issue[]
-> IssueListSlide 18: Common Mistakes
ข้อผิดพลาดที่พบบ่อย
- ลืมติดตั้ง
@supabase/supabase-js - env var ชื่อไม่ตรง
- ลืม restart dev server
- query table ผิดชื่อ เช่น
issueแทนissues - ลืม map snake_case เป็น camelCase
IssueListยังเป็น Client Component ที่ต้องการ handler จาก Day 3- RLS policy ไม่อนุญาต select
- ใช้
single()แล้ว data ไม่เจอจน error แทนที่จะใช้maybeSingle()
Slide 19: Recap ชั่วโมงที่สองของ Day 4
สิ่งที่ได้เรียน
- ติดตั้ง Supabase client
- สร้าง server client
- query table
issues - map
DbIssueเป็นIssue - render
/issuesจาก database - query issue detail ด้วย id
- error handling เบื้องต้น
ต่อไป
เราจะเปลี่ยน IssueForm ให้สร้างข้อมูลลง Supabase ด้วย Server Action
คำศัพท์สำคัญ
| คำศัพท์ | ความหมาย |
|---|---|
| Supabase client | library สำหรับ query Supabase |
| Server Component | component ที่อ่านข้อมูลฝั่ง server ได้ |
| snake_case | naming style ใน database เช่น created_at |
| camelCase | naming style ใน TypeScript เช่น createdAt |
| Mapping | การแปลงข้อมูลจากรูปแบบหนึ่งเป็นอีกรูปแบบ |
| Query | การขอข้อมูลจาก database |
maybeSingle() | method ที่คืน row เดียว หรือ null ถ้าไม่เจอ |