เราจะมาสร้าง 'URL Shortener' หรือเว็บย่อลิงก์ง่ายๆ ด้วยกัน ตั้งแต่ต้นจนจบเลย จะได้เห็นภาพรวมการทำงานของเว็บแอปฯ ตั้งแต่หน้าบ้านยันหลังบ้าน (ใช้ Next.js กับ Prisma นี่แหละ) ไม่ต้องกลัวยากนะครับ ผมจะแบ่งออกเป็นหลายๆ episode ซึ่งบทความนี้คือ EP.1 ของ URL Shorter ผมจะอธิบายแบบเพื่อนสอนเพื่อน ทำตามได้แน่นอน
ทำความเข้าใจเรื่อง URL แบบง่ายๆกันก่อน
เอาล่ะครับทุกคน! ก่อนที่เราจะกระโดดเข้าไปเขียนโค้ด Next.js เท่ๆ หรือจัดการกับฐานข้อมูลด้วย Prisma ORM กันเนี่ย มีเรื่องนึงที่ผมอยากชวนคุยให้เข้าใจตรงกันก่อนครับ นั่นคือเรื่องของ URL นี่เอง
"เอ๊ะ! แล้วมันเกี่ยวอะไรกับโปรเจคที่เราจะทำล่ะ?" คุณอาจจะกำลังคิดในใจ 🤔
คำตอบคือ... เกี่ยวข้อง เต็มๆ เลยครับ! เพราะโปรเจค URL Shortener หรือ "ตัวย่อลิงก์" ของเราเนี่ย มันคือการ "เล่น" กับ URL โดยตรงเลย ถ้าเราเข้าใจพื้นฐานตรงนี้แน่นๆ นะครับ ผมรับรองเลยว่าตอนที่เราไปเขียนโค้ดจริงๆ มันจะไหลลื่น เข้าใจง่ายขึ้นเยอะเลย ไม่ต้องมานั่งเกาหัวว่า "เอ๊ะ โค้ดส่วนนี้มันทำงานยังไงนะ?" 😉
URL คืออะไรกันนะ? 🤔
พูดแบบบ้านๆ เลยนะครับ URL (Uniform Resource Locator) ก็เหมือน "ที่อยู่" ของสิ่งต่างๆ บนโลกอินเทอร์เน็ตนั่นแหละครับ

ลองนึกภาพตามนะครับ... เวลาเราอยากจะไปบ้านเพื่อน เราก็ต้องรู้ "ที่อยู่" ใช่ไหมครับ? ไม่งั้นก็คงไปไม่ถูก บนโลกออนไลน์ก็หลักการเดียวกันเลยครับ! ถ้าเราอยากจะเข้าเว็บไซต์ไหน ดูรูปภาพ ฟังเพลง หรือเข้าถึงข้อมูลอะไรก็ตาม เราก็ต้องใช้ URL นี่แหละครับ เป็นตัวบอกเบราว์เซอร์ (Browser) อย่าง Google Chrome, Firefox, หรือ Safari ที่เราใช้กันอยู่ทุกวันว่า "เฮ้! พาฉันไปที่นี่หน่อยสิ!"
มาลองชำแหละส่วนประกอบ URL กัน (แบบเข้าใจง่าย)
ทีนี้ URL ที่เราเห็นกันยาวๆ เนี่ย จริงๆ แล้วมันมีส่วนประกอบย่อยๆ ซ่อนอยู่ข้างในนะครับ เรามาลองดูกัน
สมมติว่าเรามี URL ตัวอย่างนี้: https://chat.mengsokool.space/blog/5efksa
เราสามารถแบ่งมันออกเป็นส่วนๆ ได้ประมาณนี้ครับ:

- Scheme (โพรโทคอล):
https://
เจ้าตัวนี้เปรียบเสมือน "ภาษา" หรือ "วิธีการสื่อสาร" ที่เราจะใช้คุยกับเซิร์ฟเวอร์ครับhttps
(ตัว s ต่อท้ายย่อมาจาก Secure) หมายถึงการสื่อสารแบบปลอดภัย มีการเข้ารหัสข้อมูล ทำให้ข้อมูลที่ส่งไปมายากต่อการถูกดักฟัง ส่วนhttp://
(แบบไม่มี s) คือแบบธรรมดา ซึ่งเดี๋ยวนี้เว็บไซต์ส่วนใหญ่ก็หันมาใช้https
กันเกือบหมดแล้ว เพื่อความปลอดภัยของผู้ใช้งานอย่างเราๆ นี่แหละครับ - Domain/Host (ชื่อบ้าน):
chat.mengsokool.space
นี่คือ "ชื่อ" ของเซิร์ฟเวอร์ หรือถ้าเปรียบเทียบง่ายๆ ก็คือ "ชื่อบ้าน" หรือ "ชื่ออาคาร" ของเว็บไซต์ที่เราต้องการจะไปเยือนนั่นเองครับ มันเป็นเหมือนชื่อเฉพาะที่ทำให้เรารู้ว่ากำลังจะไปที่ไหน - Path (เส้นทางในบ้าน):
/blog/5efksa
และนี่แหละครับ! พระเอก ⭐ ของโปรเจค URL Shortener ของเรา! ส่วนนี้คือ "เส้นทาง" ที่ระบุเจาะจงลงไปอีกว่า เราต้องการจะไปที่หน้าไหน หรือต้องการข้อมูลส่วนไหน ภายใน เว็บไซต์นั้นๆ ครับ เหมือนกับเราเดินเข้าบ้าน (Domain) แล้วก็ต้องเดินต่อไปตามทางเดิน (Path) เพื่อไปยังห้องที่ต้องการ
ในโปรเจคของเรา ไอ้เจ้า Path สั้นๆ ที่เราจะสร้างขึ้นมา (เช่น /AmhrT
ในตัวอย่าง https://short.gg/AmhrT
) เราจะเรียกมันเท่ๆ ว่า "Slug" ครับ! เจ้า Slug นี่แหละ จะทำหน้าที่เป็นเหมือน "กุญแจ" หรือ "รหัสลับ" ที่เราจะใช้ในการค้นหา URL ยาวๆ ตัวจริงที่ถูกเก็บซ่อนไว้อีกที
(💡 เกร็ดเล็กเกร็ดน้อย: จริงๆ แล้ว URL อาจจะมีส่วนประกอบอื่นๆ อีกนะครับ เช่น Query Parameters (ส่วนที่อยู่หลังเครื่องหมาย ?
เช่น ?user=meng&status=active
) ที่ใช้ส่งข้อมูลเพิ่มเติมไปกับ URL หรือ Fragment (ส่วนที่อยู่หลังเครื่องหมาย #
เช่น #section-2
) ที่ใช้บอกเบราว์เซอร์ให้เลื่อนหน้าจอไปยังส่วนที่ระบุไว้ แต่สำหรับโปรเจคพื้นฐานของเราตอนนี้ รู้จักแค่ 3 ส่วนหลัก (Scheme, Domain, Path) นี้ก็เอาอยู่แล้วครับ!)
แล้วทำไมต้องย่อ URL ล่ะ? 🤔
ก็ดู URL ยาวๆ แบบในรูปด้านล่างสิ...

เห็นแล้วรู้สึกยังไงครับ? 😅
- จำยาก: ใครจะไปจำ URL ยาวเหยียดขนาดนั้นได้ไหวล่ะครับ!
- แชร์ลำบาก: ลองส่งให้เพื่อนในแชทดูสิครับ ยาวเต็มหน้าจอไปหมด
- ดูไม่สวยงาม: เวลาเอาไปแปะในเอกสาร, Presentation, หรือบนรูปภาพ มันก็ดูรกหูรกตา ไม่คลีนเลย
การที่เราย่อ URL ให้มันสั้นลง (เช่น ให้เหลือแค่ https://short.gg/AmhrT
) มันเลยมีประโยชน์มากๆ ครับ:
- จำง่ายขึ้นเยอะ: โดยเฉพาะถ้าเราสามารถกำหนด Slug เองได้ (อันนี้เป็นฟีเจอร์ขั้นสูงขึ้นไป)
- แชร์สะดวก: ไม่ว่าจะในโซเชียลมีเดีย, อีเมล, หรือบอกต่อด้วยปากเปล่า ก็ง่ายกว่าเยอะ
- ดูสะอาดตา: ทำให้ข้อความหรือเอกสารของเราดูเป็นระเบียบเรียบร้อยมากขึ้น
- (ขั้นสูงขึ้นไปอีกนิด): บางทีผู้ให้บริการ URL Shortener ยังสามารถเก็บสถิติได้ด้วยนะครับ ว่ามีคนคลิกที่ลิงก์ย่อของเรากี่ครั้ง มาจากไหนบ้าง ซึ่งมีประโยชน์มากในการวัดผลทางการตลาด (แต่ในโปรเจคพื้นฐานของเราตอนนี้ เราจะยังไม่ทำส่วนนี้นะครับ 😉)

URL Shortener ทำงานยังไง (ภาพรวมแบบไวๆ) ⚡
หลักการทำงานของมันจริงๆ แล้วง่ายนิดเดียวเลยครับ แบ่งเป็น 2 ขั้นตอนหลักๆ คือ:
1. ตอนที่เราสร้างลิงก์ย่อ (การจัดเก็บ):

- รับ URL ยาวๆ มา: คุณ (ผู้ใช้) เอา URL ยาวๆ ที่ต้องการย่อ มาใส่ในระบบของเรา
- สร้าง Slug สั้นๆ: ระบบของเราจะสร้าง "รหัสลับ" หรือ "Slug" สั้นๆ ที่ไม่ซ้ำกับใครขึ้นมา (เช่น
AmhrT
) - จดบันทึก: เราจะเก็บข้อมูลคู่นี้ไว้ใน ฐานข้อมูล (Database) ครับ ว่า "Slug
AmhrT
นี้ มันคู่กับ URL ยาวๆ อันนั้นนะ!"
2. ตอนที่มีคนคลิกลิงก์ย่อ (การเรียกใช้งาน):

- มีคนเรียกใช้: พอมีคนคลิกหรือเข้าเว็บด้วย URL สั้นๆ ของเรา (เช่น
https://short.gg/AmhrT
) - ค้นหา: ระบบของเราจะรีบวิ่งไปเปิด "สมุดบันทึก" (Database) เพื่อดูว่า "เอ๊ะ! Slug
AmhrT
เนี่ย มันคือ URL ยาวๆ อันไหนกันนะ?" - ส่งต่อไป (Redirect): พอเจอ URL ยาวๆ ตัวจริงที่คู่กันแล้ว ระบบก็จะรีบ "ส่งต่อ" (หรือที่เรียกว่า Redirect) คนที่คลิกมานั้น ไปยัง URL ยาวๆ ตัวจริงทันทีเลยครับ! ปิ๊ง! ✨ ผู้ใช้ก็จะไปถึงหน้าเว็บปลายทางที่ต้องการได้ในที่สุด
เห็นไหมครับ? พอเราเข้าใจภาพรวมและส่วนประกอบต่างๆ ของ URL แบบนี้แล้ว ทีนี้ตอนที่เราไปเขียนโค้ด Next.js เพื่อดึงค่า Path (หรือ Slug) จาก URL ที่คนส่งเข้ามา แล้วเอา Slug นั้นไป Query หรือค้นหาในฐานข้อมูล (ด้วย Prisma) เพื่อหา URL จริง ก่อนจะสั่ง Redirect ด้วยคำสั่งของ Next.js มันจะดูสมเหตุสมผลและเข้าใจง่ายขึ้นเยอะเลยใช่ไหมครับ!
เอาล่ะ! พื้นฐานเราแน่นพอประมาณแล้ว ทีนี้ก็ถึงเวลาไปลุยโค้ดกันจริงๆ แล้วล่ะครับ! ไปกันเลย! 💪
เริ่มต้นด้วยการเตรียมโปรเจค Next.js กันก่อน
เปิด Terminal หรือ Command Prompt แล้วรันคำสั่งนี้:
npx create-next-app ai-chatbot
คำสั่งนี้จะสร้างโปรเจกต์ Next.js ใหม่ชื่อว่า ai-chatbot ให้เราครับ
จากนั้น จะมีคำถามให้เราตั้งค่าโปรเจกต์เล็กน้อย:

- Would you like to use TypeScript? เลือก Yes หรือ No ได้ตามความถนัด (ถ้าไม่เคยใช้ TypeScript มาก่อน เลือก No ก็ได้ครับ)
- Would you like to use ESLint? เลือก Yes
- Would you like to use Tailwind CSS? เลือก Yes (Tailwind CSS จะช่วยให้เราจัดหน้าเว็บได้ง่ายและสวยงาม)
- Would you like to use src/ directory? เลือก No
- Would you like to use App Router? (recommended) เลือก Yes
- Would you like to customize the default import alias? เลือก No
ดีไซน์หน้าแอป
เป้าหมายของเราคือแอป URL Shorter ที่ใช้ง่ายๆ ผมจึงลองดีไซน์คร่าวๆดังตัวอย่างด้านล่างนี้

- ผู้ใช้เปิดแอป: เปิดแอปย่อลิงก์ขึ้นมา
- วางลิงก์ยาว: ผู้ใช้อยากย่อลิงก์อะไร ก็เอาลิงก์ยาวๆ ไปวางในช่องที่ให้ใส่ URL (Input field)
- กดปุ่ม "ย่อ": พอกรอกลิงก์เสร็จ ก็กดปุ่ม "ย่อ" หรือปุ่มอะไรก็ตามที่เราตั้งชื่อไว้ (เช่น "ย่อเลย!", "Shorten!")
- แอปทำงาน:
- แอปรับลิงก์ยาวๆ ที่ผู้ใช้ใส่มา
- แอปจัดการแปลงลิงก์ยาวๆ นั้นให้กลายเป็นลิงก์สั้นๆ
- แสดงผลลัพธ์:
- แอปแสดงลิงก์สั้นๆ ที่สร้างขึ้นมา ให้ผู้ใช้เห็น
- (อาจจะ) มีปุ่ม "คัดลอก" (Copy) โผล่มาให้กดง่ายๆ จะได้เอาลิงก์ไปใช้ได้เลย
- (ถ้าต้องการย่อลิงก์อื่นอีก) ผู้ใช้กดปุ่ม "ย่อลิงก์อันต่อไป" หรือปุ่มอะไรที่เราตั้งชื่อไว้ เพื่อเริ่มกระบวนการใหม่ตั้งแต่ข้อ 2
สรุป: เปิดแอป > วางลิงก์ > กด "ย่อ" > ได้ลิงก์สั้นๆ > (ถ้าอยากได้อีก) กด "ย่อลิงก์อันต่อไป"
มาเตรียม 'บ้าน' เก็บข้อมูลกัน: Setup ฐานข้อมูลด้วย Prisma 🏠
เอาล่ะครับ โปรเจคของเราต้องมีที่เก็บข้อมูล URL ยาวๆ กับ Slug สั้นๆ ที่คู่กันใช่ไหมครับ? เราจะใช้เครื่องมือสุดเจ๋งที่ชื่อว่า Prisma มาช่วยจัดการเรื่องฐานข้อมูลให้เราครับ มันจะทำให้ชีวิตเราง่ายขึ้นเยอะเลย
มาเริ่มเตรียมฐานข้อมูลให้พร้อมใช้งานกันเลยครับ!
1. เริ่มต้นใช้งาน Prisma:
เปิด Terminal หรือ Command Prompt ของคุณขึ้นมา แล้วพิมพ์คำสั่งนี้ลงไปเลยครับ:
npx prisma init
คำสั่งนี้เหมือนเป็นการบอกว่า "เฮ้ Prisma! มาช่วยจัดการฐานข้อมูลในโปรเจคนี้หน่อย!"
ถ้าทุกอย่างเรียบร้อยดี คุณจะเห็นข้อความประมาณนี้:

สังเกตไหมครับว่า Prisma มันใจดีมากๆ มันสร้างของสำคัญให้เรา 2 อย่าง:
- โฟลเดอร์
prisma
: ข้างในจะมีไฟล์schema.prisma
ซึ่งเป็นเหมือน "พิมพ์เขียว" หรือ "แบบแปลน" ฐานข้อมูลของเราครับ เราจะมากำหนดโครงสร้างข้อมูลกันในไฟล์นี้ - ไฟล์
.env
: ไฟล์นี้เอาไว้เก็บ "ความลับ" ของโปรเจคครับ เช่น ข้อมูลสำหรับเชื่อมต่อฐานข้อมูล ซึ่งเราไม่ควรเปิดเผยให้ใครเห็น (เดี๋ยวเราจะมาแก้ไขไฟล์นี้กัน)

2. เลือกใช้ฐานข้อมูล (แบบง่ายๆ ก่อน):
เปิดไฟล์ prisma/schema.prisma
ขึ้นมาครับ คุณจะเห็นโค้ดหน้าตาประมาณนี้:

เห็นตรง provider = "postgresql"
ไหมครับ? นี่คือค่าเริ่มต้นที่ Prisma ตั้งมาให้ แต่สำหรับโปรเจคแรกของเรา เพื่อให้ง่ายต่อการทดสอบและไม่ต้องติดตั้งอะไรเพิ่มเติม เราจะเปลี่ยนไปใช้ SQLite ก่อนครับ ซึ่งมันจะเก็บฐานข้อมูลทั้งหมดไว้ในไฟล์ไฟล์เดียว ง่ายมากๆ!
ให้คุณแก้จาก postgresql
เป็น sqlite
แบบนี้ครับ:
datasource db {
provider = "sqlite" // <--- แก้ตรงนี้
url = env("DATABASE_URL")
}
3. บอก Prisma ว่าไฟล์ฐานข้อมูลเราอยู่ที่ไหน:
จำไฟล์ .env
ที่ Prisma สร้างให้ได้ไหมครับ? เปิดไฟล์นั้นขึ้นมาเลยครับ มันจะมีหน้าตาประมาณนี้:
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
เราต้องแก้ค่า DATABASE_URL
ให้ชี้ไปที่ไฟล์ SQLite ที่เราจะสร้างครับ ให้แก้ทั้งบรรทัดเป็นแบบนี้:
DATABASE_URL="file:./dev.db"
ความหมายก็คือ "เฮ้ Prisma! ไฟล์ฐานข้อมูล SQLite ของเราจะชื่อ dev.db
นะ แล้วก็เก็บไว้ที่เดียวกับโปรเจคนี่แหละ" (เดี๋ยว Prisma จะสร้างไฟล์นี้ให้เราเองตอนรันคำสั่งถัดไปครับ)
4. สร้างตัวช่วยเชื่อมต่อฐานข้อมูล (Prisma Client):
เพื่อให้โค้ดส่วนอื่นๆ ในโปรเจคของเราเรียกใช้ Prisma ได้ง่ายๆ เราจะสร้างไฟล์สำหรับจัดการ Prisma Client กันครับ
- สร้างโฟลเดอร์ใหม่ชื่อ
lib
ขึ้นมาในโปรเจคของคุณ (ถ้ายังไม่มี) - ข้างในโฟลเดอร์
lib
ให้สร้างไฟล์ใหม่ชื่อprisma.ts

- จากนั้น คัดลอกโค้ดนี้ไปวางในไฟล์
lib/prisma.ts
ครับ:
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
// บรรทัดนี้เป็นการประกาศตัวแปร global สำหรับเก็บ PrismaClient (เพื่อประสิทธิภาพตอน dev)
const globalForPrisma = global as unknown as { prisma: PrismaClient };
// สร้าง PrismaClient instance หรือใช้ instance ที่มีอยู่แล้ว (ถ้าอยู่ใน global)
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
// log: ["query"], // ถ้าอยากเห็น log ตอน query database ให้ uncomment บรรทัดนี้
});
// ในโหมด development ให้เก็บ PrismaClient instance ไว้ใน global เพื่อไม่ให้สร้าง connection ใหม่บ่อยๆ
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
โค้ดส่วนนี้อาจจะดูซับซ้อนนิดหน่อยนะครับ แต่หลักๆ คือมันช่วยให้เรามี prisma
object เพียงตัวเดียวสำหรับใช้เชื่อมต่อฐานข้อมูลทั่วทั้งโปรเจค โดยเฉพาะตอนที่เราพัฒนาระบบ (Development Mode) มันจะช่วยไม่ให้เกิดการสร้างการเชื่อมต่อฐานข้อมูลใหม่ซ้ำๆ ซึ่งดีต่อประสิทธิภาพครับ
5. กำหนดโครงสร้างข้อมูล (Model):
ย้อนกลับไปที่ไฟล์ prisma/schema.prisma
อีกครั้งครับ ตอนนี้เราจะมาสร้าง "แบบแปลน" หรือ Model สำหรับเก็บข้อมูล URL ของเรากัน
เลื่อนลงมาด้านล่างสุดของไฟล์ แล้วเพิ่มโค้ดส่วนนี้เข้าไปครับ:
// ...โค้ดด้านบน (generator, datasource) เหมือนเดิม...
// สร้างโมเดลสำหรับเก็บข้อมูล URL
model url {
slug String @id @unique // Slug สั้นๆ (เป็น Primary Key และต้องไม่ซ้ำใคร)
url String // URL ยาวๆ ตัวจริง
}
อธิบายง่ายๆ คือ:
model url
: เรากำลังสร้างตารางข้อมูลชื่อurl
นะslug String @id @unique
: ในตารางนี้จะมีคอลัมน์ชื่อslug
เก็บข้อมูลประเภทข้อความ (String) และมันจะเป็น Primary Key (@id
) คือตัวบ่งชี้ข้อมูลแต่ละแถว และต้อง ไม่ซ้ำกัน (@unique
) ด้วยนะurl String
: มีอีกคอลัมน์ชื่อurl
เก็บข้อมูลประเภทข้อความ (String) เหมือนกัน เอาไว้เก็บ URL ยาวๆ

6. สร้างฐานข้อมูลจริงตามแบบแปลน:
ขั้นตอนสุดท้ายของการเตรียมฐานข้อมูลครับ! ให้รันคำสั่งนี้ใน Terminal:
npx prisma db push
คำสั่งนี้จะอ่าน "แบบแปลน" (Model) ที่เรากำหนดไว้ใน schema.prisma
แล้วไปสร้างไฟล์ฐานข้อมูล SQLite (dev.db
) พร้อมกับตาราง url
ตามที่เราออกแบบไว้ให้เลยครับ
ถ้าไม่มีอะไรผิดพลาด คุณควรจะเห็นข้อความประมาณนี้:

เย้! 🎉 ตอนนี้ฐานข้อมูล SQLite ของเราก็พร้อมใช้งานแล้วครับ มีตาราง url
รอให้เราใส่ข้อมูลเรียบร้อย!
สร้างสมองให้โปรเจค: มาเขียน API กัน! 🧠
ตอนนี้เรามีหน้าบ้าน (เดี๋ยวจะทำกัน) กับฐานข้อมูลแล้ว ขาดก็แต่ "สมอง" หรือส่วน API (Application Programming Interface) ที่จะคอยเชื่อมทั้งสองอย่างเข้าด้วยกันครับ API นี่แหละที่จะรับคำสั่งจากหน้าบ้าน (เช่น "ช่วยย่อลิงก์นี้หน่อย!") แล้วไปจัดการกับฐานข้อมูลให้ ก่อนจะส่งผลลัพธ์กลับไป
ใน Next.js (ด้วย App Router) การสร้าง API นั้นง่ายมากๆ ครับ เราแค่สร้างไฟล์ route.ts
(หรือ route.tsx
) ไว้ในโฟลเดอร์ app/api/
หรือโฟลเดอร์ย่อยลงไปอีกตามเส้นทางที่เราต้องการ
โปรเจค URL Shortener ของเราจะมี API หลักๆ 2 ส่วนครับ:
1. API สำหรับ "ย่อลิงก์" (สร้าง Slug ใหม่)
ส่วนนี้จะทำงานเมื่อคุณกดปุ่ม "Shorten" ที่หน้าเว็บครับ ขั้นตอนการทำงานของมันจะเป็นแบบนี้:
- รับคำสั่ง: หน้าบ้าน (Frontend) จะส่ง URL ยาวๆ ที่คุณกรอกเข้ามาให้ API ตัวนี้
- สร้างรหัสลับ: API จะสร้าง "Slug" หรือรหัสสั้นๆ แบบสุ่มขึ้นมา (เช่น
aBc1Xy
) - เช็คกันซ้ำ: API จะต้องเช็คก่อนว่า Slug ที่เพิ่งสร้างมาเนี่ย มีใครใช้ไปแล้วหรือยังในฐานข้อมูล ถ้าซ้ำ ก็ต้องสร้างใหม่ไปเรื่อยๆ จนกว่าจะได้อันที่ไม่ซ้ำใคร (อันนี้สำคัญมาก!)
- บันทึกข้อมูล: เมื่อได้ Slug ที่ไม่ซ้ำแล้ว API ก็จะเก็บข้อมูลคู่นี้ (Slug สั้นๆ กับ URL ยาวๆ) ลงไปในฐานข้อมูลของเรา (ผ่าน Prisma นั่นเอง)
- ส่งผลลัพธ์กลับ: สุดท้าย API จะส่งเฉพาะ Slug สั้นๆ ที่สร้างเสร็จกลับไปให้หน้าบ้าน เพื่อเอาไปแสดงผลเป็นลิงก์ย่อให้คุณเห็น
มาสร้างไฟล์ API กัน:
ให้คุณสร้างโฟลเดอร์ api
ขึ้นมาใน app
ก่อน (ถ้ายังไม่มี) จากนั้นสร้างโฟลเดอร์ shorten
ข้างใน api
อีกที แล้วสร้างไฟล์ชื่อ route.ts
ไว้ข้างในครับ (หรือที่เหล่าเซียน Next.js เขาเรียกกันติดปากว่า app/api/shorten/route.ts
นั่นแหละครับ 😉)

จากนั้น เอาโค้ดนี้ไปใส่ในไฟล์ app/api/shorten/route.ts
ได้เลย:
import { prisma } from "@/lib/prisma"; // Import ตัวช่วยเชื่อมต่อ DB ของเรา
import { NextRequest, NextResponse } from "next/server"; // Import เครื่องมือจัดการ Request/Response จาก Next.js
// ฟังก์ชันสำหรับสร้าง Slug แบบสุ่ม (เดี๋ยวอธิบายข้างล่างนะ)
function generateRandomSlug(length: number): string {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let slug = "";
for (let i = 0; i < length; i++) {
slug += characters.charAt(Math.floor(Math.random() * characters.length));
}
return slug;
}
// ฟังก์ชันหลักที่จะทำงานเมื่อมี Request แบบ POST ส่งมาที่ /api/shorten
export async function POST(req: NextRequest) {
try {
// 1. ดึง URL ยาวๆ ที่หน้าบ้านส่งมา (อยู่ใน body ของ request)
const body = await req.json();
const longUrl = body.url; // คาดหวังว่าหน้าบ้านจะส่งมาใน key ชื่อ 'url'
// ตรวจสอบก่อนว่ามี URL ส่งมาจริงไหม
if (!longUrl) {
return NextResponse.json({ error: "ไม่พบ URL ที่ต้องการย่อ" }, { status: 400 }); // Bad Request
}
// 2. สร้าง Slug สั้นๆ และเช็คว่าไม่ซ้ำ
let slug = "";
let isSlugUnique = false;
let attempts = 0; // ใส่ตัวนับเผื่อกรณีเกิดเหตุการณ์ไม่คาดฝัน (วนลูปไม่รู้จบ)
while (!isSlugUnique && attempts < 10) { // พยายามสัก 10 ครั้งพอ
slug = generateRandomSlug(6); // สร้าง Slug 6 ตัวอักษร
// ลองค้นหาใน DB ว่ามี Slug นี้อยู่หรือยัง
const existingUrl = await prisma.url.findUnique({
where: { slug: slug },
});
if (!existingUrl) {
// ถ้าไม่เจอ แสดงว่า Slug นี้ว่าง! ใช้ได้เลย
isSlugUnique = true;
}
attempts++;
}
// ถ้าพยายาม 10 ครั้งแล้วยังหา Slug ที่ไม่ซ้ำไม่ได้ อาจจะมีปัญหา
if (!isSlugUnique) {
console.error("ไม่สามารถสร้าง Slug ที่ไม่ซ้ำได้หลังจากพยายาม 10 ครั้ง");
return NextResponse.json({ error: "เกิดข้อผิดพลาดในการสร้างรหัสย่อ โปรดลองอีกครั้ง" }, { status: 500 });
}
// 4. บันทึกข้อมูล Slug และ URL ยาวๆ ลง Database
await prisma.url.create({
data: {
slug: slug, // Slug สั้นๆ ที่ได้มา
url: longUrl, // URL ยาวๆ ที่รับมา
},
});
// 5. ส่งเฉพาะ Slug ที่สร้างได้กลับไปให้หน้าบ้าน
// หน้าบ้านจะต้องเอา Slug นี้ไปต่อกับ Domain ของเว็บเองนะ
return NextResponse.json({ slug: slug }, { status: 201 }); // 201 Created
} catch (error) {
// ดักจับ Error ที่อาจเกิดขึ้นระหว่างการทำงาน
console.error("เกิดข้อผิดพลาดใน API /api/shorten:", error);
return NextResponse.json(
{ error: "เกิดข้อผิดพลาดบางอย่างที่ Server" },
{ status: 500 } // Internal Server Error
);
}
}
อธิบายโค้ดเพิ่มเติม:
generateRandomSlug(length)
: เป็นฟังก์ชันง่ายๆ ที่ผมเขียนขึ้นมาเพื่อสุ่มตัวอักษร (A-Z, a-z, 0-9) ออกมาตามจำนวนlength
ที่ระบุครับexport async function POST(req: NextRequest)
: นี่คือหัวใจหลักครับ ฟังก์ชันนี้จะทำงานเมื่อมี Request แบบPOST
(ซึ่งเหมาะกับการส่งข้อมูลเพื่อสร้างอะไรใหม่ๆ) วิ่งเข้ามาที่ Path/api/shorten
ตัวreq
จะเก็บข้อมูลที่ส่งมาทั้งหมดawait req.json()
: เราคาดว่าหน้าบ้านจะส่ง URL ยาวๆ มาในรูปแบบ JSON เลยใช้คำสั่งนี้เพื่ออ่านข้อมูลออกมาwhile (!isSlugUnique)
: วนลูปสร้าง Slug ใหม่ไปเรื่อยๆ จนกว่าprisma.url.findUnique
จะหา Slug นั้นไม่เจอในฐานข้อมูล (แสดงว่ายังไม่มีใครใช้)prisma.url.create()
: ใช้คำสั่งของ Prisma เพื่อสร้าง "แถว" ข้อมูลใหม่ลงในตารางurl
ของเราครับNextResponse.json()
: ใช้ส่งคำตอบกลับไปให้หน้าบ้านในรูปแบบ JSONtry...catch
: เป็นการดักจับข้อผิดพลาดครับ ถ้ามีอะไรพังในtry
โปรแกรมจะไม่แครช แต่จะไปทำงานในcatch
แทน ซึ่งเราจะให้มัน log error ไว้ดู และส่งข้อความบอกหน้าบ้านว่ามีปัญหา
2. API สำหรับ "เปลี่ยนเส้นทาง" (Redirect จาก Slug)
ส่วนนี้จะทำงานเมื่อมีคนคลิก หรือเข้า URL สั้นๆ ที่เราสร้างขึ้นครับ (เช่น http://yourdomain.com/aBc1Xy
) หน้าที่ของมันคือ:
- รับ Slug: API จะดึงเอาส่วนที่เป็น Slug (เช่น
aBc1Xy
) ออกมาจาก URL ที่คนคลิกเข้ามา - ค้นหา URL จริง: เอา Slug ที่ได้ไปค้นหาในฐานข้อมูลว่ามันตรงกับ URL ยาวๆ อันไหน
- ส่งต่อไป: ถ้าเจอ URL ยาวๆ ที่คู่กัน ก็สั่งให้เบราว์เซอร์ของผู้ใช้ "เปลี่ยนเส้นทาง" (Redirect) ไปยัง URL ยาวๆ นั้นทันที / ถ้าไม่เจอ ก็บอกไปว่า "ไม่พบ URL นี้"
มาสร้างไฟล์ API กัน:
คราวนี้เราต้องการให้ API ทำงานเมื่อมีคนเข้า URL ที่ต่อท้ายด้วย Slug อะไรก็ได้ เช่น /aBc1Xy
, /zYx9Wv
ฯลฯ ใน Next.js เราจะใช้สิ่งที่เรียกว่า Dynamic Routes ครับ
ให้คุณสร้างโฟลเดอร์ใหม่ใน app
โดยใช้ชื่อเป็น [slug]
(มีวงเล็บเหลี่ยมครอบ) จากนั้นสร้างไฟล์ route.ts
ไว้ข้างในครับ (เซียนเจ้าเดิมก็จะเรียกว่า app/[slug]/route.ts
ครับ 😄 ชื่อโฟลเดอร์ [slug]
นี่แหละคือตัวบอก Next.js ว่า "ตรงนี้รับค่าอะไรก็ได้นะ แล้วเก็บไว้ในตัวแปรชื่อ slug
")

ใส่โค้ดนี้ลงไปในไฟล์ app/[slug]/route.ts
ครับ:
import { prisma } from "@/lib/prisma"; // Import ตัวช่วยเชื่อมต่อ DB
import { NextRequest, NextResponse } from "next/server"; // Import เครื่องมือ Request/Response
// ฟังก์ชันหลักที่จะทำงานเมื่อมี Request แบบ GET ส่งมาที่ Path ที่มี Slug ต่อท้าย (เช่น /aBc1Xy)
export async function GET(
_req: NextRequest, // ตัว Request object (ในกรณีนี้เราไม่ได้ใช้ข้อมูลจาก req)
{ params }: { params: { slug: string } } // ดึงค่า slug จาก URL ที่เข้ามา
) {
// ดึงค่า slug จริงๆ ออกมาจาก params
// ชื่อ 'slug' ตรงนี้ ต้องตรงกับชื่อโฟลเดอร์ [slug] ที่เราตั้งไว้นะครับ
const slug = params.slug;
try {
// 2. ค้นหาข้อมูลในตาราง url โดยใช้ slug ที่ได้มา
const findData = await prisma.url.findUnique({
where: {
slug: slug, // ค้นหาแถวที่คอลัมน์ slug ตรงกับค่าที่ได้จาก URL
},
});
// 3. ตรวจสอบผลการค้นหา
if (!findData) {
// ถ้าหาไม่เจอ (findData เป็น null)
return NextResponse.json(
{ error: "โอ๊ะ! ไม่เจอ URL ปลายทางสำหรับรหัสนี้" },
{ status: 404 } // 404 Not Found
);
}
// ถ้าเจอข้อมูล (findData มีค่า)
// สั่งให้ Redirect ไปยัง URL ยาวๆ ที่เก็บไว้ใน findData.url
return NextResponse.redirect(findData.url);
} catch (error) {
// ดักจับ Error ที่อาจเกิดขึ้นระหว่างค้นหาใน DB
console.error(`เกิดข้อผิดพลาดในการค้นหา Slug '${slug}':`, error);
return NextResponse.json(
{ error: "เกิดข้อผิดพลาดบางอย่างที่ Server" },
{ status: 500 } // Internal Server Error
);
}
}
อธิบายโค้ดเพิ่มเติม:
export async function GET(...)
: ฟังก์ชันนี้จะทำงานเมื่อมี Request แบบGET
(ซึ่งเหมาะกับการดึงข้อมูลหรือการเข้าถึงหน้าเว็บ) วิ่งเข้ามาที่ Path ที่ตรงกับรูปแบบ/[อะไรก็ได้]
{ params }: { params: { slug: string } }
: นี่คือวิธีที่ Next.js ส่งค่าที่ได้จาก Dynamic Route ([slug]
) มาให้เราครับ ค่าslug
จริงๆ จะอยู่ในparams.slug
prisma.url.findUnique()
: ใช้ค้นหาข้อมูลเพียงแถวเดียวโดยใช้เงื่อนไขที่ unique (ซึ่งslug
ของเราเป็น@unique
อยู่แล้ว)if (!findData)
: ถ้าfindUnique
ค้นหาไม่เจอ มันจะคืนค่าnull
เราเลยเช็คตรงนี้ครับNextResponse.redirect(findData.url)
: คำสั่งสำคัญ! ถ้าเจอข้อมูล มันจะส่ง Response แบบ Redirect กลับไปบอกเบราว์เซอร์ของผู้ใช้ให้เปลี่ยนหน้าไปยัง URL ยาวๆ (findData.url
) ทันที
โอเค! ตอนนี้ "สมอง" หรือส่วนหลังบ้าน (API) ของเราก็พร้อมทำงานทั้ง 2 ส่วนแล้วนะครับ ทั้งส่วนรับย่อลิงก์ และส่วนพาไปยังลิงก์ปลายทาง เดี๋ยวเราไปสร้าง "หน้าตา" หรือส่วนหน้าบ้าน (Frontend) ให้ผู้ใช้เข้ามาใช้งานกันต่อเลยครับ! 😊
สร้างหน้าตาให้โปรเจค: เขียนโค้ดหน้าบ้าน (Frontend) 🎨
ตอนนี้เรามี "สมอง" (API) กับ "ที่เก็บของ" (Database) แล้ว ก็ถึงเวลาสร้าง "หน้าตา" หรือ Frontend ที่ให้ผู้ใช้งานอย่างเราๆ เข้ามาโต้ตอบกับโปรเจค URL Shortener ของเราได้แล้วล่ะครับ!
เราจะไปแก้ไขไฟล์หลักที่แสดงหน้าแรกของเว็บเรา ซึ่งก็คือไฟล์ page.tsx
ที่อยู่ในโฟลเดอร์ app
นั่นเองครับ

1. เตรียมไฟล์:
- เปิดไฟล์
app/page.tsx
ขึ้นมาครับ - ลบโค้ดเดิมที่ Next.js สร้างมาให้ตอนแรกออกให้หมดเลยครับ เราจะมาเขียนใหม่กันทั้งหมด
2. ใส่โค้ดหน้าบ้าน:
คัดลอกโค้ดทั้งหมดนี้ ไปวางในไฟล์ app/page.tsx
ที่ว่างเปล่าเมื่อกี้ได้เลยครับ:
"use client";
import { useState, useEffect } from "react";
export default function Home() {
const [inputUrl, setInputUrl] = useState("");
const [shortUrl, setShortUrl] = useState("");
const [loading, setLoading] = useState(false);
const [showResult, setShowResult] = useState(false);
const [currentURL, setCurrentURL] = useState("");
useEffect(() => {
setCurrentURL(window.location.origin);
}, []);
const handleShorten = async () => {
if (!inputUrl || !currentURL) return;
setLoading(true);
try {
const response = await fetch(`${currentURL}/api/shorten`, {
method: "POST",
body: JSON.stringify({ url: inputUrl }),
});
if (response.ok) {
const json = await response.json();
setShortUrl(`${currentURL}/${json.slug}`);
setShowResult(true);
} else {
alert("Failed to shorten URL");
}
} catch (error) {
console.error("Error shortening URL:", error);
alert("An error occurred");
} finally {
setLoading(false);
}
};
const handleCopy = () => {
navigator.clipboard.writeText(shortUrl);
};
const handleShortenAnother = () => {
setInputUrl("");
setShortUrl("");
setShowResult(false);
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="w-full max-w-md p-6 bg-white border border-gray-300 rounded shadow">
<h1 className="text-center text-2xl font-bold mb-6">URL Shorter</h1>
{!showResult ? (
<div className="flex mb-4">
<input
type="text"
value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)}
placeholder="https://your-long-url.com/abc"
className="flex-grow p-2 border border-gray-300 rounded-l focus:outline-none"
style={{ color: inputUrl ? "red" : "black" }}
/>
<button
onClick={handleShorten}
disabled={loading}
className={`px-4 py-2 bg-white border border-gray-300 rounded-r hover:bg-gray-100 ${
loading ? "opacity-50" : ""
}`}
style={{ backgroundColor: loading ? "#ffcccb" : "white" }}
>
Shorten
</button>
</div>
) : (
<div className="mb-4">
<div className="mb-2 text-center">Result:</div>
<div className="flex mb-4">
<input
type="text"
value={shortUrl}
readOnly
className="flex-grow p-2 border border-gray-300 rounded-l focus:outline-none"
style={{ color: "red" }}
/>
<button
onClick={handleCopy}
className="px-4 py-2 bg-pink-100 border border-gray-300 rounded-r hover:bg-pink-200"
>
Copy
</button>
</div>
<div className="flex justify-center">
<button
onClick={handleShortenAnother}
className="px-4 py-2 bg-white border border-gray-300 rounded hover:bg-gray-100"
>
Shorten Another
</button>
</div>
</div>
)}
</div>
</div>
);
}
อธิบายโค้ดหน้าบ้านแบบง่ายๆ:
"use client";
: บรรทัดนี้สำคัญมากครับ สำหรับ Next.js App Router มันเป็นการบอกว่า Component นี้ต้องทำงานฝั่ง Client (ในเบราว์เซอร์ของผู้ใช้) เพราะเราต้องใช้ Hooks อย่างuseState
,useEffect
และโต้ตอบกับผู้ใช้โดยตรงuseState(...)
: เราใช้ Hook นี้สร้าง "กล่องเก็บข้อมูล" หรือ State หลายๆ อัน เพื่อจำสถานะต่างๆ ของหน้าเว็บ เช่น URL ที่ผู้ใช้กรอก (inputUrl
), URL สั้นๆ ที่ได้ผลลัพธ์ (shortUrl
), สถานะว่ากำลังโหลดอยู่ไหม (loading
), และสถานะว่าจะแสดงฟอร์มหรือแสดงผลลัพธ์ (showResult
)useEffect(...)
: Hook นี้เอาไว้จัดการ "ผลข้างเคียง" ครับ ในที่นี้เราใช้มันเพื่อดึง URL หลักของเว็บเรา (window.location.origin
) มาเก็บไว้ใน StatecurrentURL
แค่ครั้งเดียวตอนหน้าเว็บโหลดเสร็จใหม่ๆ เราต้องใช้ URL นี้เพื่อสร้างลิงก์ย่อที่สมบูรณ์ และเพื่อบอกfetch
ว่าต้องยิง API ไปที่ไหนhandleShorten
: ฟังก์ชันหัวใจหลักของการย่อลิงก์ครับ มันจะทำงานเมื่อกดปุ่ม "ย่อลิงก์" โดยจะ:- เช็คว่ากรอก URL หรือยัง
- ตั้งค่า
loading
เป็นtrue
- ใช้
fetch
ยิง Request แบบPOST
ไปที่/api/shorten
พร้อมส่ง URL ยาวๆ ไปในbody
(แปลงเป็น JSON ก่อน) - รอ
response
จาก API ถ้าสำเร็จ (response.ok
) ก็อ่านslug
ที่ได้กลับมา สร้างเป็นshortUrl
เต็มๆ แล้วเปลี่ยนหน้าไปแสดงผล (setShowResult(true)
) - ถ้าไม่สำเร็จ หรือเกิด Error ก็แสดง
alert
บอกผู้ใช้ - สุดท้าย ไม่ว่าจะสำเร็จหรือพลาด ก็ตั้ง
loading
กลับเป็นfalse
ในfinally
handleCopy
: ใช้navigator.clipboard.writeText()
ซึ่งเป็นวิธีมาตรฐานของเบราว์เซอร์ในการคัดลอกข้อความ (URL สั้นๆ) ไปยัง Clipboard ของผู้ใช้handleShortenAnother
: แค่รีเซ็ต State ต่างๆ กลับไปเป็นค่าเริ่มต้น เพื่อให้ผู้ใช้สามารถย่อลิงก์อันต่อไปได้- ส่วน JSX (ที่อยู่ใน
return (...)
): นี่คือส่วนที่กำหนดว่าหน้าตาเว็บ (HTML) จะเป็นอย่างไร เราใช้ Tag ต่างๆ ผสมกับ Tailwind CSS Classes (เช่นflex
,items-center
,p-6
,bg-white
,rounded
,shadow
) เพื่อจัดวางและตกแต่งหน้าเว็บให้สวยงามครับ- มีการใช้เงื่อนไข
{!showResult ? ... : ...}
เพื่อสลับการแสดงผลระหว่าง "ฟอร์มกรอก URL" กับ "หน้าแสดงผลลัพธ์" - มีการใช้
disabled={loading}
หรือdisabled={!inputUrl.trim()}
เพื่อปิดการใช้งานปุ่มเมื่อกำลังโหลด หรือเมื่อยังไม่ได้กรอกข้อมูล ป้องกันการกดซ้ำซ้อนครับ
- มีการใช้เงื่อนไข
เรียบร้อยครับ! ตอนนี้หน้าบ้านของเราก็พร้อมเชื่อมต่อกับ API ที่เราทำไว้แล้ว
ได้เวลาทดสอบ 🚀
มาลองดูกันครับว่าสิ่งที่เราสร้างมาทั้งหมด มันทำงานได้จริงไหม!
- รันโปรเจค: เปิด Terminal ขึ้นมา แล้วรันคำสั่งนี้เพื่อเปิด Development Server:
npm run dev
- เปิดเบราว์เซอร์: รอสักครู่จน Terminal บอกว่า Server พร้อมทำงานแล้ว (มักจะขึ้นว่า
ready started server on 0.0.0.0:3000, url: http://localhost:3000
) จากนั้นเปิดเบราว์เซอร์ที่คุณใช้ประจำ แล้วเข้าไปที่:http://localhost:3000
- ทดลองย่อลิงก์: คุณควรจะเห็นหน้าเว็บ URL Shortener ที่เราเพิ่งสร้างกันไป ลองหา URL ยาวๆ จากเว็บไหนก็ได้ มาวางในช่องกรอก แล้วกดปุ่ม "ย่อลิงก์"
- ดูผลลัพธ์: ถ้าทุกอย่างถูกต้อง หน้าเว็บควรจะเปลี่ยนไปแสดง URL สั้นๆ ที่ขึ้นต้นด้วย
http://localhost:3000/
ตามด้วย Slug สุ่มๆ (เช่นhttp://localhost:3000/xY7zPq
) - ทดลองคัดลอกและเปิด: ลองกดปุ่ม "คัดลอก" แล้วไปเปิด Tab ใหม่ในเบราว์เซอร์ วาง URL สั้นๆ ที่คัดลอกมาลงไป แล้วกด Enter
- ตรวจสอบการ Redirect: ถ้า API ส่วน
[slug]/route.ts
ของเราทำงานถูกต้อง เบราว์เซอร์ของคุณควรจะถูก Redirect พาไปยัง URL ยาวๆ ตัวเดิมที่คุณเอามาย่อในตอนแรกครับ!
เย้! 🎉 ถ้าคุณทำตามมาถึงตรงนี้แล้วทุกอย่างทำงานได้ถูกต้อง ก็ขอแสดงความยินดีด้วยนะครับ! คุณได้สร้างโปรเจค Next.js URL Shortener ตัวแรกสำเร็จแล้ว! สุดยอดไปเลยครับเหม่ง! 👍
ใน EP ต่อไป เราค่อยมาดูวิธีปรับปรุงโค้ดให้ดีขึ้น (Refactor) เพิ่มความปลอดภัยเบื้องต้น และลองเชื่อมต่อกับฐานข้อมูลจริงๆ พร้อมกับเอาขึ้น Vercel ให้คนอื่นเข้ามาใช้งานได้กันนะครับ! (เดะผมขอเวลาเขียนก่อนนะ ฮ่าๆ)