# 火箭隊|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)
## 環境講解


- 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

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