使用 node.js 寫出串接 API 的程式


Posted by 小小碼農 on 2021-05-08

廢話不多說,這邊先上文件說明

API 文件

Base URL : https://lidemy-book-store.herokuapp.com

說明 Method path 參數 範例
獲取所有書籍 GET /books _limit:限制回傳資料數量 /books?_limit=5
獲取單一書籍 GET /books/:id /books/10
新增書籍 POST /books name: 書名
刪除書籍 DELETE /books/:id
更改書籍資訊 PATCH /books/:id name: 書名

Q1 : 來自秋秋鞋的任務

有一天,住你隔壁的鄰居秋秋鞋跑來按門鈴,說有事想要找你討論,他最近在做一個知識型的 YouTube 頻道,可是快要沒有靈感了。

這時,他想到一個好主意!他只要能夠看到書店提供的書籍相關資訊,就可以從中汲取靈感,之後就可以發想相關題材,頻道就不會一直不更新了。

身為秋秋鞋的好朋友,這個重責大任當然就交給你了!

請閱讀開頭給的 API 文件並串接,用 node.js 寫出一個程式,執行後會在 console 列出前十本書籍的 id 以及書名。

順帶一提,叫做秋秋鞋是因為他很喜歡秋天。

範例:

node hw1.js

1 克雷的橋
2 當我想你時,全世界都救不了我
3 我殺的人與殺我的人
....

我當然會想扁這種朋友,又沒有付錢,還給我取奇怪的名稱 ... 但還是先做再說 ...

Q1 解法

const request = require("request");
const URL = "https://lidemy-book-store.herokuapp.com";

request(`${URL}/books?_limit=10`, (err, res, body) => {
  // 發生錯誤提早結束
  if (err) return console.error(err);

  let data;
  // 正常情況
  if (res.statusCode >= 200 && res.statusCode < 300) {
    try {
      data = JSON.parse(body);
      for (let i of data) {
        console.log(`${i.id} ${i.name}`);
      }
    } catch (err) {
      console.error(err);
      return;
    }
    // 4xx, 5xx 錯誤情況
  } else {
    console.log(res.statusCode);
    console.log("擷取錯誤 ...");
  }
});

Q1 心得

這邊要注意的就是 Base URL 跟開頭文件說明中不同的 path,例如說你在看「獲取所有書籍」這隻 API 的時候,path 是 /books,跟 Base URL 結合起來之後就會變成:https://lidemy-book-store.herokuapp.com/books,這就是完整的 API 位置


Q2 : 最後的考驗

原本以為上次就已經是最後一次幫忙,沒想到秋秋鞋還是又跑來找你了。他說他想要更多功能,他想把這整個書籍資料庫當作自己的來用,必須能夠顯示前 20 本書的資料、刪除、新增以及修改書本,這樣他就可以管理自己的書籍了。

(跟 Q1 不同,之前是 10 本,這次要顯示 20 本)

雖然你很想問他說為什麼不用 Excel 就好,但你問不出口,再加上你最近剛學程式需要練習的機會,於是你就答應了。

請閱讀開頭給的 API 文件並串接,用 node.js 寫出一個程式並接受參數,輸出相對應的結果,範例如下:

node hw2.js list // 印出前二十本書的 id 與書名
node hw2.js read 1 // 輸出 id 為 1 的書籍
node hw2.js delete 1 // 刪除 id 為 1 的書籍
node hw2.js create "I love coding" // 新增一本名為 I love coding 的書
node hw2.js update 1 "new name" // 更新 id 為 1 的書名為 new name

OK 這朋友很棒,毛病很多 ... 🤷‍

Q2 解法

const request = require("request");
const process = require("process");
const URL = "https://lidemy-book-store.herokuapp.com";

const action = process.argv[2];
const bookId = process.argv[3];
const bookName = process.argv[4] || process.argv[3];

switch (action) {
  case "list":
    listBooks();
    break;
  case "read":
    readOneBook(bookId);
    break;
  case "delete":
    deleteOneBook(bookId);
    break;
  case "create":
    createOneBook(bookName);
    break;
  case "update":
    updateOneBook(bookId, bookName);
    break;
  default:
    console.log("Available commands: list, read, delete, create, update 😉");
}

function listBooks() {
  request.get(`${URL}/books?_limit=20`, (err, res, body) => {
    if (res.statusCode >= 400 && res.statusCode < 600)
      return console.error("擷取失敗");
    let data;
    try {
      data = JSON.parse(body);
    } catch (err) {
      return console.error(err);
    }
    for (let i = 0; i < data.length; i++) {
      const { id, name } = data[i];
      console.log(`${id} ${name}`);
    }
  });
}

function readOneBook(id) {
  request.get(`${URL}/books/${id}`, (err, res, body) => {
    const data = JSON.parse(body);
    const { id, name } = data;
    if (res.statusCode >= 400 && res.statusCode < 600)
      return console.error("擷取失敗");
    return console.log(`${id} ${name}`);
  });
}

function deleteOneBook(id) {
  request.delete(`${URL}/books/${id}`, (err, res, body) => {
    const data = JSON.parse(body);
    const { id, name } = data;
    if (res.statusCode >= 400 && res.statusCode < 600)
      return console.error("刪除失敗");
    return console.log("刪除成功");
  });
}

function createOneBook(bookName) {
  request.post(
    {
      url: `${URL}/books`,
      form: { name: bookName },
    },
    (err, res, body) => {
      if (res.statusCode >= 400 && res.statusCode < 600)
        return console.error("新增失敗");
      return console.log("新增成功");
    }
  );
}

function updateOneBook(id, bookName) {
  request.patch(
    {
      url: `${URL}/books/${id}`,
      form: { name: bookName },
    },
    (err, res, body) => {
      if (res.statusCode >= 400 && res.statusCode < 600)
        return console.error("更新失敗");
      return console.log("更新成功");
    }
  );
}

Q2 心得

  • process.argv 這個陣列拿到相對應的參數,這個陣列可以知道要取輸入的第幾個參數。

  • 這邊命名的部分,特別注意const bookName = process.argv[4] || process.argv[3];,這樣子可以省掉很多另外註解的特殊狀況。

  • 使用 switch 寫更簡潔,下面也沒什麼好說的,就是寫各個 function 成功跟失敗的印出值,要注意的就是錯誤的範圍要定義的明確,不能只有 if(err)這樣,可以定義一下 statusCode 的範圍。


Q3 周遊列國

你的好麻吉小立是一個很愛到處旅遊的人,在前一陣子才靠著便宜的 bug 機票以及特價的商務艙玩遍了許多地方。不過小立一直有個困擾,那就是他希望了解更多跟國家有關的知識,因此他來請你幫忙寫一個搜尋國家資訊的小程式。

這個程式很簡單,只要輸入國家的英文名字,就能夠查詢符合的國家的資訊,會輸出以下幾項:

  1. 國家名稱
  2. 首都
  3. 使用的貨幣名稱
  4. 電話國碼

請參考以下範例:

node hw3.js tai

============
國家:Taiwan
首都:Taipei
貨幣:TWD
國碼:886
============
國家:United Kingdom of Great Britain and Northern Ireland
首都:London
貨幣:GBP
國碼:44
============
國家:Lao People's Democratic Republic
首都:Vientiane
貨幣:LAK
國碼:856

另外,如果沒有找到任何符合的國家,請輸出:「找不到國家資訊」。

相關的資訊都可以在這個佛心的 API 找到:https://restcountries.eu/#api-endpoints-name

奇怪 ... 這種朋友怎麼越來越多 ...

Q3 解法

const request = require("request");
const process = require("process");
const URL = "https://restcountries.eu/rest/v2";

const userInput = process.argv[2];
// 發生錯誤提早結束
if (!userInput) {
  return console.log("請輸入國家名稱");
}

request.get(`${URL}/name/${userInput}`, (err, res, body) => {
  const data = JSON.parse(body);
  // 正常情況
  if (res.statusCode >= 200 && res.statusCode < 300) {
    try {
      for (let i of data) {
        console.log(`============
國家:${i.name}
首都:${i.capital}
貨幣:${i.currencies[0].code}
國碼:${i.callingCodes[0]}`);
      }
    } catch (err) {
      console.error(err);
      return;
    }
    // 4xx, 5xx 錯誤情況
  } else {
    console.log(res.statusCode);
    console.log("擷取錯誤 ...");
  }
});

Q3 心得

其實有點不知所措的話,可以先觀察 API 的 response,決定怎麼取得你要的資訊,也是不太困難,說明幾點:

  • for (let i of data) {} 我直接這樣寫,不跑迴圈,對資料來說會讀比較快,也不需要冒著寫錯迴圈起始、終止條件的風險

  • 錯誤情況一樣要考慮周詳,有可能根本沒輸入


Q4 探索新世界

之前幫秋秋鞋做完那個小程式以後,你會寫程式的消息似乎就傳開了,有一位 Twitch 平台實況主果凍跑來聯繫你,想請你幫忙做個東西。

事情是這樣的,他原本是 LOL 的玩家,但因為某些原因帳號被 ban 掉了,為了維持實況的熱度,需要去找其他遊戲來玩,可是他又不知道哪些遊戲比較熱門,會有比較多人觀看。

因此,他寫請你寫一個小程式,能夠去撈取 Twitch 上面受歡迎的遊戲,他就能夠參考這個列表來決定要實況哪個遊戲。

由於你偶爾也會看他的實況,所以你欣然接受了這個挑戰,準備來串串看真實世界的 API。

請參考 Twitch API v5 的文件,寫一隻程式去呼叫 Twitch API,並拿到「最受歡迎的遊戲列表(Get Top Games)」,並依序印出目前觀看人數跟遊戲名稱。

在這個作業中,你必須自己看懂 Twitch API 的文件,知道怎麼去申請一個 Application 拿到 ClientID,並且在 API 文件當中找到對的那一個 API(Get Top Games),而且務必記得要在 request header 中帶上 ClientID 跟另一個參數 Accept,值是:application/vnd.twitchtv.v5+json

還有一件事情要提醒大家,Twitch API 有兩個版本,一個是最新版(New Twitch API,代號 Helix),一個是舊版的(Twitch API v5,代號 kraken),我們這次要串接的是舊版的,不要搞錯版本囉。

node hw4.js

259075 League of Legends
241160 Just Chatting
141901 Counter-Strike: Global Offensive
125571 Fortnite
120949 Dota 2
88466 Grand Theft Auto V
74198 Call of Duty: Modern Warfare
58553 World of Warcraft
56757 Escape From Tarkov
49213 Chess
....

Q4 解法

const request = require("request");

const options = {
  method: "GET",
  url: "https://api.twitch.tv/kraken/games/top",
  headers: {
    Accept: "application/vnd.twitchtv.v5+json",
    "Client-ID": "cf2rjdklgkbltczqmcirnljp09ykz5",
  },
};

request(options, (err, res, body) => {
  // 發生錯誤提早結束
  if (err) return console.error(err);

  let data = JSON.parse(body).top;

  if (res.statusCode >= 200 && res.statusCode < 300) {
    // 正常情況
    try {
      for (let i of data) {
        console.log(i.viewers, i.game.name);
      }
    } catch (err) {
      console.error(err);
    }
    // 4xx, 5xx 錯誤情況
  } else {
    console.log(res.statusCode);
    console.log("擷取錯誤 ...");
  }
});

Q4 心得

  • Using the Twitch API v5 的第一段「Getting a client ID」特別重要,你必須先申請 Twitch 帳號,然後前往 Twitch developer dashboard 註冊一個新的 Application,OAuth redirect URI 我們不會用到,隨便填就好,最後你會拿到一個 ClientID

  • 記得也要注意雙重驗證的問題

  • Headers 裡面的東西也要注意,仔細看 API 文件,看需要添加什麼、怎麼添加,也練習把 options 獨立出來寫,讓可用性更高


Q5 一些簡答

1. 以自己的話解釋 API 是什麼?

API (Application Programming Interface),應用程式介面

  • 狹義來說 :

通常是指 Web API,如果今天雙方想要交換資料時,不可能完全把自己的資料庫給對方,這時候就需要限制彼此資料存取的權限,你開出你可以給的資料,我也這樣做,我們根據 API (介面) 讓對方可存取資料,故透過 API 能讓雙方交換資料

  • server 提供給你(client)的服務你才可以用,沒給的服務你都不能用
  • API 的目的是方便兩者之間的資料交換,交換的資料會透過純文字的形式來交換

  • 廣義來說 :

整個框架中任一個有用到函式 / 庫 / 命名都可適用,而不單只是後端公開的 API 接口,今日網路時代,API 絕不止一種,查天氣、查找關鍵字、PO 文、等很多動作,都有使用到 API

2. 找出三個課程沒教的 HTTP status code 並簡單介紹

1. 資訊回應 (Informational responses, 1xx) -> 再等等
2. 成功回應 (Successful responses, 2xx) -> 成功了
3. 重定向 (Redirects, 3xx) -> 去其他地方
4. 用戶端錯誤 (Client errors, 4xx) -> 你挫賽了(客戶端)
5. 伺服器端錯誤 (Server errors, 5xx) -> 我挫賽了(伺服器端)
  • 102 Processing
    此狀態碼表明伺服器收到並處理請求中,但目前未有回應

  • 403 Forbidden
    用戶端並無訪問權限,例如未被授權,所以伺服器拒絕給予應有的回應,但伺服器是知道用戶端的身份的,不同於 401

  • 201 Created
    請求成功且新的資源成功被創建,通常用於 POST 或一些 PUT 請求後的回應

  • 451 Unavailable For Legal Reasons:因法律問題而無法使用,通常是因為政治敏感因素導致伺服器無法提供該內容。451 典故出自反烏托邦小說《華氏 451 度》,而華氏 451 度為紙的燃點

3. 假設你現在是個餐廳平台,需要提供 API 給別人串接並提供基本的 CRUD 功能,包括:回傳所有餐廳資料、回傳單一餐廳資料、刪除餐廳、新增餐廳、更改餐廳,你的 API 會長什麼樣子?請提供一份 API 文件

CRUD (Create, Read, Update, Delete)

Base URL: https://lidemy-restaurant.herokuapp.com

說明 Method path 參數 範例
獲取所有餐廳 GET /restaurants _limit:限制回傳資料數量 /restaurants?_limit=5
獲取單一餐廳 GET /restaurants/:id /restaurants/10
新增餐廳 POST /restaurants name: 餐廳名稱
刪除餐廳 DELETE /restaurants/:id
更改餐廳資訊 PATCH /restaurants/:id name: 餐廳名稱

回傳所有餐廳資料

const request = require('request');

request({"https://lidemy-restaurant.herokuapp.com", (err, res, body) => {
    // 這裡是你要的內容
});

回傳單一餐廳資料

const request = require('request');

request({`https://lidemy-restaurant.herokuapp.com/${id}`, (err, res, body) => {
    // 這裡是你要的內容
});

刪除餐廳

const request = require('request');

request({`https://lidemy-restaurant.herokuapp.com/${id}`, (err, res, body) => {
    // 這裡可以加其他內容
});

新增餐廳

const request = require('request');

request.post(
    {
        url: "https://lidemy-restaurant.herokuapp.com",
        form: {
            name, // 新餐廳名稱
        },
    },
    (err,res,body) => {
        // 你想放的內容
    }
);

更改餐廳資訊

const request = require('request');

request.patch(
    {
        url: `https://lidemy-restaurant.herokuapp.com/${id}`,
        form: {
            name, // 新餐廳名稱
        },
    },
    (err, res,body) => {
        // 你想改的內容
    }
);

心得

對於串接 API,不外乎就是觀察文件格式,觀察 response 去取得想要的資訊,之後還會再有身份驗證跟加密處理,就之後再說,總之有上面這兩種朋友的話,我一定先扁


#API #node.js







Related Posts

[Git] git自動拉取(python + window工作排程器)

[Git] git自動拉取(python + window工作排程器)

開始紀錄

開始紀錄

[13] 物件導向 OOP - Class

[13] 物件導向 OOP - Class


Comments