---
# System prepended metadata

title: 使用 ChatGPT 製作 todolist API & 基本寫法

---

# 使用 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)