Architecture
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”DevBlog มีสี่เลเยอร์ที่ขอบเขตชัดเจน: browser, frontend Next.js App Router, NestJS GraphQL API ที่ Apollo ให้บริการ และฐานข้อมูล MongoDB ที่เข้าถึงผ่าน Mongoose ไดอะแกรมด้านล่างแสดงว่า request ไหลผ่านเลเยอร์เหล่านี้อย่างไร และเส้นทางอ่าน (ผู้อ่านเปิดโพสต์) ต่างจากเส้นทางเขียน (ผู้เขียนเผยแพร่หรือตรวจสอบจาก admin) ตรงไหน
flowchart TD Browser["Browser"]
subgraph Next["Next.js — App Router"] RSC["Server Components<br/>(public blog · SSR/ISR)"] Admin["Client Components<br/>(admin dashboard)"] end
subgraph API["NestJS GraphQL API (Apollo)"] Resolvers["Resolvers"] Guards["JWT Auth Guards"] end
DB[("MongoDB<br/>via Mongoose")]
Browser -->|"read: request a page"| RSC Browser -->|"admin: authed UI"| Admin RSC -->|"GraphQL query (server-side)"| Resolvers Admin -->|"GraphQL mutation + JWT"| Guards Guards --> Resolvers Resolvers -->|"Mongoose models"| DB DB -->|"documents"| Resolvers Resolvers -->|"typed response"| RSC RSC -->|"rendered HTML"| Browserทราฟฟิกสองแบบที่ต่างกันมากวิ่งเข้ามาที่ระบบนี้ และการออกแบบตอบสนองแต่ละแบบไม่เหมือนกัน
เส้นทางอ่าน (บล็อกสาธารณะ, ISR) เมื่อผู้อ่านเปิดโพสต์ request จะไปตกที่ server component ใน Next.js App Router คอมโพเนนต์นั้นดึงโพสต์จาก GraphQL API บนเซิร์ฟเวอร์ แล้วเรนเดอร์ HTML ด้วย Incremental Static Regeneration (ISR) ตัว Next.js ส่งหน้าที่แคชและเรนเดอร์ล่วงหน้าไว้กลับไปทันที แล้วค่อยสร้างใหม่เงียบ ๆ อยู่เบื้องหลังเมื่อพ้นช่วง revalidation ผู้อ่านจึงได้หน้าที่เร็วเท่า static แต่การแก้ไขที่เผยแพร่แล้วก็ยังขึ้นให้เห็นโดยไม่ต้อง build ใหม่ทั้งหมด และไม่มี credential เข้ามาเกี่ยวเลยเพราะการอ่านเป็นสาธารณะ
เส้นทางเขียน/admin (mutation ที่ยืนยันด้วย JWT) เมื่อผู้เขียนทำงานใน admin dashboard เขาอยู่ใน client component ที่ถือ session ไว้ ทุกการเผยแพร่ แก้ไข ใส่แท็ก หรือตรวจสอบ คือ GraphQL mutation ที่พก JWT ไว้ใน header ฝั่ง API มี guard แบบ Passport-JWT ตรวจ token ก่อน resolver จะทำงาน เฉพาะผู้เขียนที่ยืนยันตัวตนแล้วเท่านั้นที่ mutate ข้อมูลได้ จากนั้น resolver จึงบันทึกการเปลี่ยนแปลงผ่านโมเดลของ Mongoose และเพราะเส้นทางอ่านใช้ ISR คอมเมนต์ที่ตรวจสอบแล้วหรือโพสต์ที่เพิ่งเผยแพร่จะไปโผล่ให้ผู้อ่านเห็นในรอบ revalidation ถัดไป
ทำไมต้องสแตกนี้
หัวข้อที่มีชื่อว่า “ทำไมต้องสแตกนี้”ทุกตัวเลือกด้านล่างเป็นการแลกเปลี่ยน (trade-off) ไม่ใช่กฎตายตัว นี่คือเหตุผลตรงไปตรงมาของแต่ละตัว
NestJS (เทียบกับ Express ล้วน) Nest ให้ module, dependency injection และแนวคิดเรื่องโครงสร้างที่ชัดเจนมาให้ทันที ช่วยให้ API ที่โตขึ้นยังไล่โค้ดง่ายและเทสต์ได้ ต้นทุนคือพิธีการที่มากกว่าและเส้นโค้งการเรียนรู้ที่ชันกว่าแอป Express เปล่า ๆ สำหรับเซอร์วิสสาม endpoint ค่าโสหุ้ยนั้นไม่คุ้ม แต่สำหรับ CMS ที่มีทั้ง auth เนื้อหา และคอมเมนต์ คุ้มแน่นอน
MongoDB / document model (เทียบกับ relational) โพสต์บล็อกเป็นเอกสารโดยธรรมชาติ: ชื่อเรื่อง เนื้อหา Markdown อาร์เรย์ของแท็ก การอ้างอิงผู้เขียน timestamp การเก็บเป็นเอกสารเดียวแมปเข้ากับวิธีอ่านและเขียนอย่างสะอาด และ schema ปรับเปลี่ยนได้โดยไม่ต้องทำ migration สิ่งที่แลกคือการรองรับ join ข้ามเอนทิตีที่ซับซ้อนและความสมบูรณ์เชิงสัมพันธ์ที่อ่อนกว่า คอมเมนต์ต่อโพสต์และการกรองแท็กทำได้ง่าย แต่ถ้าภายหลัง DevBlog ต้องการรายงานเชิงสัมพันธ์หนัก ๆ ที่เก็บแบบ SQL จะเหมาะกว่า
GraphQL (เทียบกับ REST) schema เดียวที่มี type ทำให้ frontend แต่ละตัวขอเฉพาะ field ที่ต้องการได้ หน้าโพสต์สาธารณะกับรายการใน admin ดึงรูปร่างข้อมูลคนละแบบจากกราฟเดียวกันได้โดยไม่ต้องมี endpoint เฉพาะ ต้นทุนคือกลไกที่เพิ่มขึ้น (schema, resolver, client) และการแคชที่ไม่ตรงไปตรงมาเท่าการแคช HTTP บน URL แบบ REST แต่สำหรับ client สองตัวที่ใช้โมเดลเดียวกันซึ่งยังปรับเปลี่ยนอยู่ตลอด contract ที่มี type คุ้มค่า
Next.js App Router + ISR (เทียบกับ SPA หรือ SSG ล้วน) SPA ล้วนเรนเดอร์ทุกอย่างฝั่ง client — แย่ต่อ SEO ของบล็อกและ first paint ช้ากว่า ส่วน SSG ล้วนเร็วก็จริงแต่ต้อง build ใหม่ทุกครั้งที่เนื้อหาเปลี่ยน App Router พร้อม ISR อยู่ตรงกลางพอดี: หน้าเรนเดอร์บนเซิร์ฟเวอร์ แคชได้ เป็นมิตรกับ SEO และอัปเดตเองในช่วง revalidation โดยไม่ต้อง redeploy ต้นทุนคือโมเดลการเรนเดอร์ที่ซับซ้อนกว่า SPA ธรรมดา — ต้องแยกให้ออกระหว่าง server กับ client component และเข้าใจความหมายของ revalidation
เนื้อหา Markdown (เทียบกับ WYSIWYG / rich-text store) การเก็บ Markdown ดิบทำให้เนื้อหาพกพาได้ diff ได้ และเรนเดอร์ง่าย และทำให้ผู้เขียนอยู่ใกล้ข้อความธรรมดา สิ่งที่แลกคือประสบการณ์แก้ไขที่ไม่เห็นภาพเท่า rich-text WYSIWYG และคุณต้องดูแลการเรนเดอร์และการ sanitise เอง สำหรับบล็อกที่กลุ่มเป้าหมายเป็นนักพัฒนา Markdown เป็นค่าเริ่มต้นที่ถูกต้อง
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”ลองไล่ทั้งสองเส้นทางในหัวก่อนไปต่อ:
- อ่าน: Browser → server component → GraphQL query → resolver → Mongoose → MongoDB แล้วส่ง HTML ที่เรนเดอร์กลับไปที่ browser เร็วเท่า static ผ่าน ISR
- เขียน: Browser (admin client) → GraphQL mutation + JWT → auth guard → resolver → Mongoose → MongoDB แล้วปรากฏต่อผู้อ่านในรอบ revalidation ถัดไป
ถ้าคุณบอกได้ว่าเส้นทางไหนต้องใช้ JWT (เส้นทางเขียน) และเส้นทางไหนแคชได้ (เส้นทางอ่าน) แสดงว่าคุณเข้าใจสถาปัตยกรรมแล้ว
DevBlog วางเลเยอร์เป็น browser, frontend Next.js App Router, NestJS + Apollo GraphQL API และ MongoDB ผ่าน Mongoose การอ่านไหลผ่าน server component พร้อม ISR เพื่อให้หน้าสาธารณะเร็วเท่า static และเป็นมิตรกับ SEO ส่วนการเขียนไหลผ่าน GraphQL mutation ที่ยืนยันด้วย JWT และมี guard คุมอยู่ฝั่ง API สแตกชุดนี้ — NestJS, MongoDB, GraphQL, App Router + ISR, Markdown — เลือกมาเพื่อ CMS ที่อัปเดตต่อเนื่องและมี client หลายตัว ทุกตัวเลือกคือการแลกความเรียบง่ายกับโครงสร้าง ในจุดที่ทีมเนื้อหาได้ประโยชน์จริง
ถัดไป: Prerequisites →