# 火箭隊|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)