ข้ามไปยังเนื้อหา

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 →