---
# System prepended metadata

title: Node.js Exercise - TodoList RESTful API Kata (WIP)
tags: [practice, nodejs]

---

# 火箭隊｜Node.js 練習：TodoList RESTful API Kata

# TodoList RESTful API Kata

- [todolist 流程圖](https://whimsical.com/todolist-restful-api-23MP3VDDa36quRCXUL4hEi)
- [todolist 線上架構](https://whimsical.com/todo-GDkQ2j9E6zwYsMUQbgdrv8)
- [codepen JS 小範例](https://codepen.io/hexschool/pen/OJWRqrN)

## 環境講解
![](https://hackmd.io/_uploads/By8iaMKsn.png)

![](https://hackmd.io/_uploads/BJTsazFs3.png)

- Postman
- [UUID](https://www.npmjs.com/package/uuid) - NPM package    
    ```bash
    $npm install uuid --save
    ```
    ```jsx
    const { v4: uuidv4 } = require('uuid');
    uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
    ```  
    [通用唯一辨識碼 - 維基百科，自由的百科全書 (wikipedia.org)](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81)
- [JSON VIEW](https://chrome.google.com/webstore/detail/json-viewer/gbmdgpbipfallnflgajpaliibnhdgobh) - Chrome extension
- codepen
- Heroku 雲端主機

## 建立環境

### 建立 server

```jsx
const http = require('http');

const requestListener = (req, res) => {
    res.writeHead(200,{"Content-Type":"text/HTML"});
    res.write("<h1>Hello</h1><p>yoyoyoyo</p>");
    res.end();   
}

const server = http.createServer(requestListener);
server.listen(8080);
```

### 建立首頁測試網址與 404 頁面

- 根據 `req.url` 判斷 user 造訪的頁面： `if(req.ur==”/”)`
    
    ```jsx
    const http = require('http');
    
    const requestListener = (req, res) => {
        if(**req.url** === "/"){
            res.writeHead(200,**{"Content-Type":"text/plain"}**);
            res.write("This is index page");
            res.end();   
        } else {
            res.writeHead(404,**{"Content-Type":"text/plain"}**);
            res.write("Ooops! Not found 404");
            res.end();   
        }
    }
    
    const server = http.createServer(requestListener);
    server.listen(8080);
    ```
    
- 將重複的 `{"Content-Type":"text/plain"}` 取出
    
    ```jsx
    const requestListener = (req, res) => {
        const headers = {
            "Content-Type":"text/plain"
        };
        if(req.url === "/"){
            res.writeHead(200,headers);
            res.write("This is index page");
            res.end();   
        } else {
            res.writeHead(404,headers);
            res.write("Ooops! Not found 404");
            res.end();   
        }
    }
    ```
    
- 根據 `req.method` 回傳資訊
    
    ```jsx
    const requestListener = (req, res) => {
        const headers = {
            "Content-Type":"text/plain"
        };
        if(req.url === "/" && **req.method === "GET"**){
            res.writeHead(200,headers);
            res.write("This is index page");
            res.end();   
        } else if (req.url === "/" && **req.method === "DELETE"**){
            res.writeHead(200,headers);
            res.write("This is index page");
            res.end();
        }else {
            res.writeHead(404,headers);
            res.write("Ooops! Not found 404");
            res.end();   
        }
    }
    ```
    

### 調整 headers 資訊，設置回傳 JSON & CORS 資訊

- 調整 headers 資訊
    - 將 `'Content-Type'` 設定為 `'application/json'`
    - 調整部署到 Heroku 時（跨網域）需做的設定
    
    ```jsx
    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 回傳 JSON 格式 `res.write(JSON.stringify( {JSON} ))`
    
    `JSON.stringify()`: 將字串以 JSON 方式回傳
    
    ```jsx
    if(req.url === "/" && req.method === "GET"){
    		res.writeHead(200,headers);
    		res.write(**JSON.stringify(**{
    		    "status":"success",
    		    "data":[]
    		}**)**);
    		res.end();   
    } else {
        res.writeHead(404,headers);
        res.write(**JSON.stringify(**{
            "status":"false",
            "message":"無此網站路由"
        }**)**);
        res.end();    
    }
    ```
    

### 建立 OPTIONS API 檢查機制

- **跨網域**申請網路請求時，先透過 preflight 發出請求 (Request Method: OPTIONS) → 待對方回傳 ok → 才會發第二次請求 (Request Method: DELETE) Ref: [Preflight request - MDN Web Docs Glossary: Definitions of Web-related terms | MDN (mozilla.org)](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request)   
- 設定 OPTIONS API
    ```jsx
    else if (req.method === "OPTIONS"){
            res.writeHead(200,headers);
            res.end();
    }
    ```  

## 設計 API
![RESTful API 列表](https://hackmd.io/_uploads/r11Q0zFo2.png)

### GET

使用 UUID

```bash
$npm install uuid --save
```

```jsx
// import uuid
const { v4: uuidv4 } = require('uuid');

//此次實作先將 todos 儲存在 node 的記憶體裡
const todos = [{
    "title":"今天吃菜",
    "id":uuidv4()
}];

```

### POST 新增 todo

- POST API router 環境建立： `req.url`
    
    ```jsx
    else if (req.url ==="/todos" && req.method === "POST")
    ```
    
- **💡 如何接收 POST API 的 body 資料**： `req.on(’data’, function)` / `req.on(’end’, function)`
    
    TCP 封包 - 檔案傳輸
    
    ```javascript
    let body = "";
    
    // request 裡的 on data 事件：有 data 時就會觸發；如果資料量大可能會跑很多次，每次都會 chunk 累加到 body
    req.on('data', chunk=>{
        body+=chunk;
    })
    
    // 當 on data 跑完之後，就會觸發 on end 一次
    req.on('end', () => {
        console.log(JSON.parse(body));
    })
    ```
    
    1. [Node.js 官網接收 buffer 教學](https://nodejs.org/api/stream.html#api-for-stream-consumers)
    2. [Node.js 開發者社群 - 各種原生與套件，接收 req.body 的方式](https://nodejs.dev/learn/get-http-request-body-data-using-nodejs)
    3. [TCP/IP Buffer 傳送示意圖](https://cacoo.com/diagrams/gSXxTWt8ystUlfIi/C5209)
- 等待 `req.body` 接收完成，透過 `on (’end’)` 觸發
    
    ```javascript
    req.on('end', () => {
        const title = JSON.parse(body).title; // 取得 body 的 title
        const todo = {
            "id": uuidv4(),
            "title":title
        };
        todos.push(todo); // 將 todo 加到 todos array
        res.writeHead(200,headers)
        res.write(JSON.stringify({
            "status":"success",
            "data":todos
        }))
        res.end();
    })
    ```
    
    - `JSON.parse()`
    - `array.push()`
- POST API 異常行為 - 判斷 `req.body` (`JSON.parse(body)`)是否為 JSON 格式：`try{} catch{}` ；
    
    ```javascript
    req.on('end', () => {
        try{
            const title = JSON.parse(body).title;
            const todo = {
                "id": uuidv4(),
                "title":title
            };
            todos.push(todo);
            res.writeHead(200,headers)
            res.write(JSON.stringify({
                "status":"success",
                "data":todos
            }))
            res.end();
        } catch (error){
            res.writeHead(404,headers);
            res.write(JSON.stringify({
                "status":"false",
                "message":"欄位未填寫正確，或無此 todo id"
            }));
            res.end();
        }
    });
    ```
    
- POST API 異常行為 - 判斷 `req.body` 是否有 title 屬性： `if( title !== undefined){} else{}`
    
    ```javascript
    if(title !== undefined){
        const todo = {
            "id": uuidv4(),
            "title":title
        };
        todos.push(todo);
        res.writeHead(200,headers)
        res.write(JSON.stringify({
            "status":"success",
            "data":todos
        }))
        res.end();
    } else {
        res.writeHead(400,headers);
        res.write(JSON.stringify({
            "status":"false",
            "message":"欄位未填寫正確，或無此 todo id"
        }));
        res.end();
    }
    ```
    
- 重構 POST API 異常行為：建立自己的 `errorHandle.js` module
    
    errorHandle.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":"false",
          "message":"欄位未填寫正確，或無此 todo id"
      }));
      res.end();
    }
    
    module.exports = errorHandle;
    ```
    
    server.js
    
    ```javascript
    const errorHandle = require('./errorHandle');
    
    req.on('end', () => {
        try{
            const title = JSON.parse(body).title;
            if(title !== undefined){
                const todo = {
                    "id": uuidv4(),
                    "title":title
                };
                todos.push(todo);
                res.writeHead(200,headers)
                res.write(JSON.stringify({
                    "status":"success",
                    "data":todos
                }))
                res.end();
            } else {
                errorHandle(res);
            }
        } catch {
            errorHandle(res);
        }
    });
    ```
    

### DELETE 刪除所有 todos: `todos.length = 0`

```javascript
else if (req.url === "/todos" && req.method === "DELETE"){
    todos.length = 0 // 將 todos array 清空
    res.writeHead(200,headers);
    res.write(JSON.stringify({
        "status":"success",
        "data":todos
    }));
    res.end();
}
```

### DELETE 刪除單筆 todo

- **💡 陣列與字串處理操作：startsWith、split、pop、findIndex**
    - `.startsWith`
        
        ```javascript
        "/todos/123333".startsWith("/todos/") // true
        ```
        
    - `.split` 切割
        
        ```javascript
        "/todos/123333".split("/") // ['','todos','123333']
        ```
        
    - `.pop` 刪除＆回傳
        
        ```javascript
        const id = "/todos/123333".split("/").pop() // '123333'
        ```
        
    - `.findIndex` 找出值＆回傳對應的 index
        
        ```javascript
        const ary = [0,1,2];
        ary.findeIndex( element => element == 2); // 2
        ary.findeIndex( element => element == 30); // -1 沒有值時一律回傳 -1
        ```
        
    - `.splice` 刪除 `splice(從 index 開始, 刪幾個)`
        
        ```javascript
        const ary = [3,4,5,6,7]
        ary.splice(2,1) // 刪除從 index 為 2 的 5 開始，刪 1個
        console.log(ary) // [3,4,6,7]
        ```
        
- 判斷 路由 是否為刪除單筆 todo
    
    ```javascript
    else if (req.url.startsWith("/todos/") && req.method === "DELETE")
    ```
    
- 判斷 todo id 是否存在 & 刪除單筆 todo
    
    ```javascript
    const id = req.url.split("/").pop(); //取得 id
    const index = todos.findIndex(element => element.id === id); // 檢查 id ＆取 index
    if (index !== -1){ // 不為 -1 代表有此 id
        todos.splice(index,1) // .splice() 刪除 array 裡的資料
        res.writeHead(200,headers);
        res.write(JSON.stringify({
            "status":"success",
            "data":todos
        }));
        res.end();
    } else {
        errorHandle(res);
    }
    ```
    

### PATCH 編輯單筆 todo

- 等待 `req.body` 接收完成，透過 `on (’end’)` 觸發
- 判斷 `req.body` 是否為 JSON 格式：`try{} catch{}`
- 判斷 `req.body` 是否有 title 屬性： `todos[index].title ≠undefined`
- 判斷 todo id 是否存在：`index≠1`
- 編輯單筆 todo （完整程式碼）
    
    ```javascript
    const id = req.url.split("/").pop();
    const index = todos.findIndex( element => element.id === id);
    if (todo !== undefined && index !== -1){ // 判斷 title 屬性＆判斷 id 是否存在
        todos[index].title = todo; //編輯 todo
        res.writeHead(200,headers);
        res.write(JSON.stringify({
            "status":"success",
            "data":todos[index]
        }));
        res.end();
    } else {
        errorHandle(res);
    ```
    

## Git 環境建置

- 啟用 git
    
    ```bash
    $git init
    ```
    
    (optional) 將 git default branch 從 master 改成 main
    
    ```bash
    $git config --global init.defaultBranch main
    ```
    
    也可以在之後再透過 `git branch -m newBranchName` 改名字
    
- 新增 `.gitignore` 檔案
    
    ```
    node_modules/
    ```
    

git add .

git commit -m “Initial setup”

## 部署 Heroku

### Heroku 環境設置

- 修改 server.js 檔案中 PORT 位置 `process.env.PORT` ↔ 環境變數
    
    ```javascript
    server.listen(process.env.PORT || 8080);
    ```
    
- 修改 package.json 的 scripts： `"start": "node server.js"`
    
    Heroku 只吃 `npm start` 時跑 `node server.js` 指令
    
    ```json
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node server.js"
      },
    ```
    
- 在 package.json 中加上 engines：希望 Heroku 用 node 哪個版本
    
    ```json
    "engines":{
    	"node":"16.x"
    }
    ```
    

### Heroku 部署流程

- 安裝 Heroku CLI: [The Heroku CLI | Heroku Dev Center](https://devcenter.heroku.com/articles/heroku-cli) `$npm install -g heroku`
    
    ```bash
    $npm install -g heroku
    ```
    
    查看版本
    
    ```bash
    heroku --version
    ```
    
- 登入 Heroku `$heroku login`
    
    ```bash
    $heroku login
    ```
    
- 新增雲端主機 `$heroku create`
    
    ```bash
    $heroku create
    ```
    
    當你用 `$heroku create` 時，git remote 已經設定好
    
- Deploy using Heroku Git: `$git push heroku main`
    
    
    ```bash
    $git push heroku main
    ```
    
- 開啟網站：`$heroku open`

## 作業成果

- Heroku:https://salty-woodland-72057.herokuapp.com/todos
- Github: https://github.com/yachuh/rocketCamp-Todolist-RESTful-API-kata

## References
- [Node.js 直播班](https://www.hexschool.com/courses/node-training.html)