# 使用 ChatGPT 製作 todolist API & 基本寫法 ###### tags: Node 完整程式碼: https://github.com/Stevetanus/To-do-list/tree/master/chatGPT_todolist ## 詠唱 Q: 我想用 NodeJS 寫一個 HTTP Server A: ![](https://i.imgur.com/0L9G8ye.png) Q: 我想在 127.0.0.1 的伺服器端建立 /todos 的路徑,在 get 指令時會回傳 todos 物件的資料,post 指令時會將資料加入 todos 物件,也需要 delete 指令去刪除特定 id 的資料,關於 id 的部分,我想使用 uuid 模組,最後,我會需要 patch 指令去修改特定 id 的資料 A: ![](https://i.imgur.com/z9wJqwr.png) ```javascript const http = require('http'); const { v4: uuidv4 } = require('uuid'); const hostname = '127.0.0.1'; const port = 3000; const todos = {}; const server = http.createServer((req, res) => { const { method, url } = req; if (url === '/todos') { switch (method) { case 'GET': res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(todos)); break; case 'POST': let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { const id = uuidv4(); const todo = JSON.parse(body); todos[id] = todo; res.statusCode = 201; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ id, ...todo })); }); break; case 'DELETE': const deleteId = url.split('/')[2]; delete todos[deleteId]; res.statusCode = 204; res.end(); break; case 'PATCH': let patchBody = ''; req.on('data', chunk => { patchBody += chunk.toString(); }); req.on('end', () => { const patchId = url.split('/')[2]; const patchData = JSON.parse(patchBody); todos[patchId] = { ...todos[patchId], ...patchData }; res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(todos[patchId])); }); break; default: res.statusCode = 405; res.end(); break; } } else { res.statusCode = 404; res.end(); } }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); ``` ![](https://i.imgur.com/KS1s3Vy.png) Q: 請幫我加上 handleError.js 來去處理常見的錯誤狀況與輸入檢查 A: ![](https://i.imgur.com/LclRhwA.png) [handleError.js](https://github.com/Stevetanus/To-do-list/blob/master/chatGPT_todolist/handleError.js) ## 基本寫法 這是一個包含 **GET** 、 **POST** 、 **DELETE** 、 **PATCH** 請求,以及有**錯誤處理**和 **CORS 處理 headers** 的 API 伺服器。 完整程式碼: https://github.com/Stevetanus/To-do-list/tree/master/to-do-list%20(Node) ### 建立 server 透過 http 模組,建立在 localhost://3005 的 server,`requestListener` 函式去處理 `req` 、`res` 的回應,最基本的伺服器要帶入 `headers` ,裡面的 `Content-Type` 表示傳遞資料的類型 ``` javascript const http = require("http"); const todos = []; // todos 儲存之 array const requestListener = (req, res) => { // 不同的 domain 會先發一個 preflight 預檢請求 (OPTIONS API 檢查機制) const headers = { "Access-Control-Allow-Headers": "Content-Type, Authorization, Content-Length, X-Requested-With", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "PATCH, POST, GET, OPTIONS, DELETE", "Content-Type": "application/json", }; // routes... }; const server = http.createServer(requestListener); // 帶入處理 req 和 res 的函式 server.listen(3005); ``` ### GET 取得 在 client 發送 `/todos` 的 `GET` 請求,`JSON.stringify` 將 javascript value 轉成 JSON 格式,`res.write()` 傳回 JSON 檔案,最後要加上 `res.end()` 表示結束。 ``` javascript if (req.url == "/todos" && req.method == "GET") { res.writeHead(200, headers); res.write( JSON.stringify({ status: "success", data: todos, }) ); res.end(); ``` ### POST 新增 由於 `req` 夾帶的資料可能會很多,我們建立 `body` 去儲存全部的 `chunk`, todo 物件會有 `title` 和 `id` 屬性 (uuid)。`try catch` 處理 `body` 的資料不當或是空的情況,交給 `errorHandle.js`。 `JSON.parse()` 解析 JSON 為 javascript value。新增成功的話,`res.write()` 回傳新增成功的 JSON。 ``` javascript const errorHandle = require("./errorHandle"); const { v4: uuidv4 } = require("uuid"); // ... let body = ""; req.on("data", (chunk) => { body += chunk; }); else if (req.url == "/todos" && req.method == "POST") { req.on("end", () => { try { const title = JSON.parse(body).title; if (title !== undefined) { const todo = { title: title, id: uuidv4(), }; todos.push(todo); res.writeHead(200, headers); res.write( JSON.stringify({ status: "success", data: todo, }) ); res.end(); } else { errorHandle(res); } } catch (error) { errorHandle(res); } }); ``` ### errorHandle 錯誤處理 回傳代號 400,與錯誤訊息的 JSON,透過 `module.exports` 匯出給 `server.js` 使用。 ``` javascript function errorHandle(res) { const headers = { "Access-Control-Allow-Headers": "Content-Type, Authorization, Content-Length, X-Requested-With", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "PATCH, POST, GET, OPTIONS, DELETE", "Content-Type": "application/json", }; res.writeHead(400, headers); res.write( JSON.stringify({ status: "failure", message: "欄位未填寫正確,或無此 todo id", }) ); res.end(); // 關門 } module.exports = errorHandle; ``` ### DELETE 刪除 1. 刪除全部 todos `todos.length = 0` 將一個 array 的長度歸零,會使該 array 變成空的。 ``` javascript } else if (req.url == "/todos" && req.method == "DELETE") { todos.length = 0; res.writeHead(200, headers); res.write( JSON.stringify({ status: "success", data: todos, }) ); res.end(); ``` 3. 刪除特定 todos `req.url.startsWith("/todos/")` 確定請求網址可能帶有 id ,`.split("/").pop` 取得以 `/` 分開的最後一個字串 (id),透過 `findIndex` 去找到在 todos 裡面相同 id 的索引,若是回傳 `-1` ,就進錯誤處理;找到的話,透過 `todos.splice(index, 1)` 刪除該筆資料。 ``` javascript } else if (req.url.startsWith("/todos/") && req.method == "DELETE") { const id = req.url.split("/").pop(); const index = todos.findIndex((element) => element.id == id); if (index !== -1) { todos.splice(index, 1); res.writeHead(200, headers); res.write( JSON.stringify({ status: "success", data: todos, }) ); res.end(); } else { errorHandle(res); } ``` ### PATCH 修改 與上面**刪除特定 todos**的做法類似,只是將該筆資料用新的 todo 去做替代,若是沒有 `title` 、 `title` 為空、 找不到該`id`,進錯誤處理。 ``` javascript } else if (req.url.startsWith("/todos/") && req.method == "PATCH") { req.on("end", () => { try { const todo = JSON.parse(body).title; // 取得新資料的 title const id = req.url.split("/").pop(); const index = todos.findIndex((element) => element.id == id); if (todo !== undefined && index !== -1) { todos[index].title = todo; // 更換 res.writeHead(200, headers); res.write( JSON.stringify({ status: "success", data: todos, }) ); res.end(); } else { errorHandle(res); } } catch (error) { errorHandle(res); } }); ``` ## OPTIONS 預檢 > 不同於「簡單請求」的例子,「預檢(preflighted)」請求會先以 HTTP 的 OPTIONS 方法送出請求到另一個網域,確認後續實際(actual)請求是否可安全送出,由於跨站請求可能會攜帶使用者資料,所以要先進行預檢請求。 在資料會做修改的跨站請求之中,會先發送一個 OPTIONS 的預檢請求,預檢一次來確定該 server 有符合條件的 headers ,也就是為什麼同一個 API 會請求兩次的原因,**PATCH** 和 **DELETE** 都會觸發。 ``` javascript } else if (req.method == "OPTIONS") { // preflight res.writeHead(200, headers); res.end(); ``` > [不只是簡單的跨域請求 Preflight Request](https://vii120.coderbridge.io/2020/11/16/preflight-request/) > [MDN 預檢請求](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORS#%E9%A0%90%E6%AA%A2%E8%AB%8B%E6%B1%82)