# 使用 ChatGPT 製作 todolist API & 基本寫法
###### tags: Node
完整程式碼: https://github.com/Stevetanus/To-do-list/tree/master/chatGPT_todolist
## 詠唱
Q: 我想用 NodeJS 寫一個 HTTP Server
A:

Q: 我想在 127.0.0.1 的伺服器端建立 /todos 的路徑,在 get 指令時會回傳 todos 物件的資料,post 指令時會將資料加入 todos 物件,也需要 delete 指令去刪除特定 id 的資料,關於 id 的部分,我想使用 uuid 模組,最後,我會需要 patch 指令去修改特定 id 的資料
A:

```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}/`);
});
```

Q: 請幫我加上 handleError.js 來去處理常見的錯誤狀況與輸入檢查
A:

[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)