Sean's Blog

An image showing avatar

Hi, I'm Sean

這裡記錄我學習網站開發的筆記
歡迎交流 (ゝ∀・)b

LinkedInGitHub

透過 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;

實際上這邊就是在撰寫 Node.js 程式碼,Next.js 把 API Routes 設計得跟 Express.js 很像。

  • req:Request Object
  • res: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;

這是我們對應的 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;

當然這些 Node.js 的 Code 也能使用在同為 Server Side 的 Pre-Rendering Page 上面,舉例來說,在 “getStaticProps” 或 “getServerSideProps” 中取得 Feedback 資料。

在 Server Side 不能使用 fetch 取得資料,但是我們可以直接使用從 API Routes 使用 buildFeedbackPathextractFeedback 來取得 Feedback 資料。

1export async function getStaticProps() {
2  const filePath = buildFeedbackPath();
3  const data = extractFeedback(filePath);
4  return {
5    props: {
6      feedbackItems: data,
7    },
8  };
9}

動態 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;

至於在 Feedback 詳細頁等元件中的使用方式,大概就像是以下這樣 👇

1function loadFeedbackHandler(id) {
2  fetch(`/api/feedback/${id}`)
3    .then((response) => response.json())
4    .then((data) => {
5      setFeedbackData(data.feedback);
6    });
7}

💡 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。