透過 Next.js API Routes 新增後端程式碼,實現 Fullstack React
其實專案使用 Next.js 的話,可以考慮直接在 Next 專案中的 API Routes 進行後端的開發,只是這次開發公司專案是第一次用,當初並沒有規劃這一塊,所以目前專案完成上線後,沒有安排進行這方面的重構,不過我們還是可以學習一下怎麼在 Next 這個框架中完成全端開發。
什麼是 API Routes
Next.js API Routes 是 Next.js 框架中用來處理 API 請求的一個特殊目錄,透過 API Routes,我們可以用非常簡單的方式創建 API。
首先。這些 API Routes 的檔案必須位於專案的 pages/api
目錄下,Next.js 會將它們視為 API Endpoints 而非 Pages。
此外,這些檔案與程式碼都只會執行在 Server Side,並不會打包到 Client Side 的 Bundle 裡面。就如同 “getStaticProps” 與 “getServerSideProps” 裡面的程式碼一樣,都不會外洩給客戶端的使用者看見內容。
1// <root>/pages/api/feedback.js 2 3function handler(req, res) { 4 res.status(200).json({ message: 'Our First API Route' }); 5} 6 7export default handler; 8
實際上這邊就是在撰寫 Node.js 程式碼,Next.js 把 API Routes 設計得跟 Express.js 很像。
req
:Request Objectres
:Response Object
接著當你造訪 http://localhost:3000/api/feedback
時,你就可以看到我們的 JSON Response。
將 SPA 連接到資料庫的問題
在 Web 應用程式中,我們也會需要在資料庫中保存和提取資料,否則我們只能使用瀏覽器存取資料,但是將資料庫連接到 Web 應用程式會有一個嚴重的問題,那就是「安全」。
正所謂在前端的世界豪無隱私,你沒辦法隱藏任何 Client Side 的程式碼,不管你怎麼藏,只要開啟開發工具仔細翻找 Source,你儲存在 Client-Side Code 的機密資訊都是會被找到的。因此,像這種資料庫的登入憑證是不能放在前端的,通常我們會在後端像是 Node.js 來存放與編寫。
有些專案則是使用 Firebase 等服務提供的 SDK,但是這也不是直接與資料庫溝通,而是透過他們提供的 Web API 再去存取 Firebase 資料庫。
不過在 Next.js 的世界,我們可以透過 API Routes 完美地解決以上問題。
表單提交範例
這是一個提交表單的範例,透過 React 的 “useRef” 取得表單 Input 與 Email 欄位的值之後,禁止默認的提交行為,改用 JavaScript 自定義後續的表單送出方式。
這邊串接的 API Routes 不用寫 Domain,相對路徑會直接帶上我們目前的 Domain。
我們這裡預期會有兩種 HTTP Method,POST
提交表單並返回所有資料,與 GET
取得所有資料,所使用的 URL 是同一個,符合 CRUD。
1import { useRef, useState } from 'react'; 2 3function HomePage() { 4 const [feedbackItems, setFeedbackItems] = useState([]); 5 6 const emailInputRef = useRef(); 7 const feedbackInputRef = useRef(); 8 9 // 送出 JSON 資料提交表單,並返回最新 JSON 資料 10 function submitFormHandler(event) { 11 event.preventDefault(); 12 13 const enteredEmail = emailInputRef.current.value; 14 const enteredFeedback = feedbackInputRef.current.value; 15 16 const reqBody = { email: enteredEmail, text: enteredFeedback }; 17 18 fetch('/api/feedback', { 19 method: 'POST', 20 body: JSON.stringify(reqBody), 21 headers: { 22 'Content-Type': 'application/json', 23 }, 24 }) 25 .then((response) => response.json()) 26 .then((data) => console.log(data)); 27 } 28 29 // 取得最新 JSON 資料 30 function loadFeedbackHandler() { 31 fetch('/api/feedback') 32 .then((response) => response.json()) 33 .then((data) => { 34 setFeedbackItems(data.feedback); 35 }); 36 } 37 38 return ( 39 <div> 40 <h1>The Home Page</h1> 41 <form onSubmit={submitFormHandler}> 42 <div> 43 <label htmlFor="email">Your Email Address</label> 44 <input type="email" id="email" ref={emailInputRef} /> 45 </div> 46 <div> 47 <label htmlFor="feedback">Your Feedback</label> 48 <textarea id="feedback" rows="5" ref={feedbackInputRef}></textarea> 49 </div> 50 <button>Send Feedback</button> 51 </form> 52 <hr /> 53 <button onClick={loadFeedbackHandler}>Load Feedback</button> 54 <ul> 55 {feedbackItems.map((item) => ( 56 <li key={item.id}>{item.text}</li> 57 ))} 58 </ul> 59 </div> 60 ); 61} 62 63export default HomePage; 64
這是我們對應的 API Routes 的 Code,我們會判斷 req.method
如果是 POST
就將新的 Feedback 寫入檔案,然後將最新的所有資料返回給前端顯示。
1// <root>/pages/api/feedback.js 2 3import fs from 'fs'; // import file system Node.js module 4import path from 'path'; // import path Node.js module 5 6export function buildFeedbackPath() { 7 // process.cwd() returns the current working directory of the Node.js process 8 return path.join(process.cwd(), 'data', 'feedback.json'); 9} 10 11export function extractFeedback(filePath) { 12 // readFileSync() reads the entire contents of a file synchronously 13 const fileData = fs.readFileSync(filePath); 14 // JSON.parse() parses a JSON string, constructing the JavaScript value or object described by the string 15 const data = JSON.parse(fileData); 16 return data; 17} 18 19function handler(req, res) { 20 if (req.method === 'POST') { 21 const { email } = req.body; 22 const feedbackText = req.body.text; 23 24 const newFeedback = { 25 id: new Date().toISOString(), 26 email, 27 text: feedbackText, 28 }; 29 30 // store that in a database or in a file 31 const filePath = buildFeedbackPath(); 32 const data = extractFeedback(filePath); 33 data.push(newFeedback); 34 // writeFileSync() writes data to a file, replacing the file if it already exists 35 // JSON.stringify() converts a JavaScript object or value to a JSON string 36 fs.writeFileSync(filePath, JSON.stringify(data)); 37 res.status(201).json({ message: 'Success!', feedback: newFeedback }); 38 } else { 39 const filePath = buildFeedbackPath(); 40 const data = extractFeedback(filePath); 41 res.status(200).json({ feedback: data }); 42 } 43} 44 45export default handler; 46
當然這些 Node.js 的 Code 也能使用在同為 Server Side 的 Pre-Rendering Page 上面,舉例來說,在 “getStaticProps” 或 “getServerSideProps” 中取得 Feedback 資料。
在 Server Side 不能使用 fetch
取得資料,但是我們可以直接使用從 API Routes 使用 buildFeedbackPath
與 extractFeedback
來取得 Feedback 資料。
1export async function getStaticProps() { 2 const filePath = buildFeedbackPath(); 3 const data = extractFeedback(filePath); 4 return { 5 props: { 6 feedbackItems: data, 7 }, 8 }; 9} 10
動態 ID 的 API Routes
在 Next.js 我們可以使用 [slug]
的方式創建資料夾或檔案,完成前端動態 ID 的路由配置。
同樣地,我們想要取得特定 Feedback ID 的資訊,希望 API 路徑是 /api/feedback/[feedbackId]
,這也是可以做到的。
我們是命名為 [feedbackId].js
,就可以透過 req.query.feedbackId
取得動態的 ID,再進一步篩選出該 ID 的資料並返回。
1// <root>/pages/api/feedback/[feedbackId].js 2 3import { buildFeedbackPath, extractFeedback } from '.'; 4 5function handler(req, res) { 6 const feedbackId = req.query.feedbackId; 7 const filePath = buildFeedbackPath(); 8 const feedbackData = extractFeedback(filePath); 9 const selectedFeedback = feedbackData.find( 10 (feedback) => feedback.id === feedbackId, 11 ); 12 res.status(200).json({ feedback: selectedFeedback }); 13} 14 15export default handler; 16
至於在 Feedback 詳細頁等元件中的使用方式,大概就像是以下這樣 👇
1function loadFeedbackHandler(id) { 2 fetch(`/api/feedback/${id}`) 3 .then((response) => response.json()) 4 .then((data) => { 5 setFeedbackData(data.feedback); 6 }); 7} 8
💡 Catch-All Routes 也能在這裡使用,總之只要記得,不管是 Pages Router 或是 API Routes 都是愈具體的路由順位愈高(Concrete Routes > Dynamic Routes > Catch-All Routes)
最後,Next.js 的 API Routes 完全可以取代 Node.js Server,你可以使用 Node.js 建構一個完整的 MERN App。