接下來這一篇我們將會來嘗試用純 Node.js 建立 HTTP API,基本上會使用到前面的知識點,所以前面的許多知識點都非常的重要唷~
開始之前你可以會想說什麼是 API(Application Programming Interface,應用程式介面),API 就是...
由於這個解釋下去可能會花太多時間,所以我想提供我之前寫的文章讓你自行去閱讀:
因為這一篇我比較想要偏實作方面,因此就不花太多時間解釋了,接下來就讓我們準備一步步來建立 HTTP API 吧!
首先請你打開終端機並輸入以下指令建立一個新專案
mkdir http-api-example
cd http-api-example
接下來請記住一個流程,只要你開始一個新的專案的時候,起手式必定會有以下流程:
那麼接下來就讓我們來執行這兩個流程吧!
git init
npm init -y
接下來我們就可以準備開始撰寫我們的程式碼囉。
Note
git init
的資料夾.git
預設是隱藏的,如果你想要看到的話可以在終端機輸入ls -a
,這個指令可以顯示所有的檔案,包含隱藏的檔案。
那麼由於我這是一個示範範例,所以不會針對 http-api-example 資料夾做 git init
初始化,因此你在練習的時候要多加注意一下,如果你有使用 git init
初始化的話,那麼請記得建立 .gitignore
檔案,並且將 node_modules
加入忽略清單,這樣才不會將 node_modules
加入到 git 版本控制中。
Note
別忘了在package.json
中的scripts
屬性增加"start": "node src/main.js"
指令,這樣才能夠透過npm start
來執行程式碼;運作npm start
請記得務必執行npm install
安裝相關套件。
接下來請你輸入以下指令建立相關資料夾
mkdir src
touch src/main.js
接下來呢?很簡單,我們將前面我們所撰寫的範例程式碼貼到 src/main.js
中:
const http = require('node:http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
接下來我們要利用這一份範例程式碼來建立我們的 HTTP API,我們主要會需要 Get、Post、Put、Delete 四種方法,因此我們就來一個一個實作吧!
首先我們先來實作 Get 方法,我們先來看一下 Get 方法的範例程式碼:
const http = require('node:http');
// ...省略其他程式碼
const server = http.createServer((req, res) => {
if (req.method === 'GET') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'Hello, World!' }));
}
});
// ...省略其他程式碼
我想你應該已經很熟悉了,所以我就不多說明了,接下來你就可以使用 Postman 來測試一下了,你可以使用以下網址來測試
Note
如果你不熟悉 Postman 的話,可以參考我先前寫的「跟著我一起快速入門 Postman 吧!」,這篇文章會教你如何使用 Postman 來測試 API,實戰上也會很常使用到 Postman。
http://localhost:3000
不出意外的話,你會得到以下結果
{
"message": "Hello, World!"
}
這樣子你就成功了。
那麼我們做了什麼事情呢?我們前面有解釋到 req
與 res
個別代表著「請求(Request)」與「回應(Response)」,而 req.method
就是代表著使用者請求的 HTTP 方法,因此這一段的程式碼白話文就是...
「使用者發出請求,如果請求的方法是 Get 的話,就回應一個狀態碼 200,並且回應一個 JSON 物件,裡面有一個 message 屬性,值為 Hello, World!
」
接著這裡有幾個核心重點,你會看到原本的 res.setHeader
被改成了 res.setHeader('Content-Type', 'application/json')
,這是因為我們要回應的是 JSON 物件,因此我們要告訴瀏覽器這是一個 JSON 物件,這樣瀏覽器才會正確的解析。
接著你會看到 res.end(JSON.stringify({ message: 'Hello, World!' }))
,雖然我們前面有告知瀏覽器我們即將回傳一個 application/json
的格式,但也不代表我們可以直接將 JavaScript 物件直接傳遞給 res.end
,JavaScript 的物件並不是一個字串,因此我們必須透過 JSON.stringify
來將 JavaScript 物件轉換成字串,這樣才能夠正確的回傳給瀏覽器。
基本上 Post 就是 Get 的延伸,因此我們只需要將 Get 的程式碼稍微修改一下就可以了,但通常 Post 我們會需要接收資料,通常可能會這樣傳送...
{
"data": "Ray"
}
因此範例程式碼會稍微有一點不一樣
const http = require('node:http');
// ...省略其他程式碼
const server = http.createServer((req, res) => {
if (req.method === 'GET') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'Hello, World!' }));
}
if (req.method === 'POST') {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: `新增成功, ${JSON.parse(body).data}!` }));
});
}
});
// ...省略其他程式碼
接下來,你一樣可以透過 Postman 嘗試戳看看,我這邊就不特別示範並截圖了,基本上如果你傳送的資料是以下的話
{
"data": "Ray"
}
那麼你就會得到以下結果:
{
"message": "Hello, Ray!"
}
req.method
這邊基本上就不用花太多時間解釋了,我相信聰明伶俐的你一定已經懂了!
所以我只會針對 req.on('data', (chunk) => {...})
與 req.on('end', () => {...})
這兩個部分來做解釋。
首先 req.on
是什麼呢?簡單來講就是監聽事件,這邊我們監聽了 data
與 end
兩個事件,當 data
事件被觸發時,就會執行 req.on('data', (chunk) => {...})
中的程式碼,而 end
事件被觸發時,就會執行 req.on('end', () => {...})
中的程式碼。
你可以把它看成前端開發上的 DOM 事件監聽
const button = document.querySelector('button');
button.addEventListener('click', () => {
// ...
});
當使用者點擊按鈕時,就會執行 button.addEventListener('click', () => {...})
中的程式碼,這樣的概念。
那麼 req.on('data', (chunk) => {...})
是在幹嘛呢?由於我們的使用者會傳送資料過來,因此我們要去監聽「請求數據流」的事件。當有資料傳送過來時,就會執行 req.on('data', (chunk) => {...})
中的程式碼,而 chunk
就是傳送過來的資料片段,但是這邊有一個問題,因為資料可能會很大,因此可能會分成好幾個 chunk
傳送過來,因此我們要將這些 chunk
串接起來,這樣才能夠正確的解析資料,因此我們使用了 body += chunk.toString()
來串接資料。
Note
如果你嘗試直接將chunk
輸出的話,你會發現它是一個 Buffer,可能會長這樣子<Buffer 7b 0a 20 20 20 20 22 64 61 74 61 22 3a 20 22 48 65 6c 6c 6f 22 0a 7d>
,因此我們才會需要透過chunk.toString()
將它轉換成字串。
最後當所有數據處理完畢時,就會觸發結束事件(end
),這時候我們就可以將結果回傳給使用者了,就是這麼的簡單(?)
Note
Buffer
是 Node.js 用來處理二進位(0
跟1
)的資料類別,在瀏覽器則是ArrayBuffer
。
基本上你掌握了 Post 的話,Put 與 Delete 就不會覺得很難了,因為是大同小異的,所以這邊我就直接提供 Put 與 Delete 的範例程式碼讓你參考
const http = require('node:http');
// ...省略其他程式碼
const server = http.createServer((req, res) => {
if (req.method === 'GET') {
// ...省略其他程式碼
}
if (req.method === 'POST') {
// ...省略其他程式碼
}
if (req.method === 'PUT') {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: `更新成功, ${JSON.parse(body).data}!` }));
});
}
if (req.method === 'DELETE') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: '刪除成功' }));
}
});
// ...省略其他程式碼
恭喜,基本上你已經完成了一個簡單的 HTTP API 了,雖然只是一個簡單的示範範例,但也足夠讓你了解到 HTTP API 的運作方式了。
Note
請記得,修改程式碼後都要中斷程式碼,並重新執行npm start
才能夠看到修改後的結果。
那麼你可能會想說...
「實戰呢?難道後端開發 API 都是這樣子嗎?」
當然不是,這邊我只是想要讓你了解到 HTTP API 的運作方式,因此才會使用純 Node.js 來實作,但實際上我們在開發後端 API 時,通常都會使用框架來開發,例如:Express、Koa、Fastify 等等,這些框架都會幫我們處理很多事情,例如:路由、錯誤處理、資料驗證等等,因此我們可以專注在開發業務邏輯上,而不用花太多時間在處理這些事情上。
前面我們可以發現,我們在打 Postman 的時候都是打 http://localhost:3000
,但我們實際在開發時,往往其實不會只有這樣子,而是會有很多不同的路由,例如:http://localhost:3000/users
、http://localhost:3000/users/1
、http://localhost:3000/users/1/posts
等等,這些都是不同的路由,因此接下來我們就要來實作路由囉~
首先這邊的範例我們將會以 TodoList 作為示範,因此我們會有以下幾個路由
GET /todos
:取得所有的 TodoPOST /todos
:新增一個 TodoPUT /todos/:id
:更新特定的 TodoDELETE /todos/:id
:刪除特定的 Todo那麼我並不會全部都介紹完畢,因為部分的觀念其實都是一樣的,因此我只會針對 GET /todos
與 POST /todos
這兩個路由做介紹,其他的路由你可以嘗試自己實作看看,也算是我留給你的小功課。
而這邊的範例將會把資料暫存在記憶體中,因此當你關閉伺服器時,資料就會消失,這邊我們只是想要讓你了解到路由的運作方式,因此就不會使用資料庫了。
Note
什麼是記憶體呢?你可以把記憶體想像成你把東西寫在紙上,當你把電腦關閉時,你就等於把紙丟進垃圾桶內,因此你的資料就會消失,如果你把紙收藏到抽屜內,那麼你的資料就會永久保存,這就是記憶體與資料庫的簡單比喻。
在前面的時候我們都是使用 req.method
來判斷使用者的 HTTP 方法,但如果要做到路由判斷的話,那麼我們就必須要搭配 req.url
來使用。
那麼我們就先來看一下如果我們今天打 Get http://localhost:3000/todos
的話,會發生什麼事情
const http = require('node:http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
console.log(req.method, req.url);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
不出意外的話,你應該是可以得到 POST /todo
這樣子的訊息
因此我們可以知道 req.url
會回傳 /todos
,因此我們就可以透過 req.url
來判斷路由了。
首先我們要先宣告一個 data
變數作為一個暫存區,當使用者新增一個 Todo 時,我們就會將資料存入 data
變數中,當使用者取得所有 Todo 時,我們就會將 data
變數中的資料回傳給使用者,這樣就可以達到暫存資料的目的了。
const http = require('node:http');
const hostname = '127.0.0.1';
const port = 3000;
let data = [];
// ...省略其他程式碼
接下來就稍微特別一點了,我們會宣告一個 routers
物件,這個物件會與我們定義的路由相對應,當使用者發出請求時,我們就會去 routers
物件中尋找對應的路由,如果有找到的話,就會執行對應的程式碼,如果沒有找到的話,就會回傳 404 狀態碼給使用者。
const http = require('node:http');
const hostname = '127.0.0.1';
const port = 3000;
let data = [];
const routers = {};
接下來該怎麼寫呢?其實很簡單,就只需要 routers
+ [HTTP 方法]
+ [路由]
就可以了,例如:routers['GET/todos']
,這樣就可以了,這邊我先示範寫一個簡單的 GET /todos
路由
const http = require('node:http');
// ...省略其他程式碼
routers['GET/todos'] = (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data));
};
接下來 http.createServer
的部分就會變成這樣子
const http = require('node:http');
// ...省略其他程式碼
routers['GET/todos'] = (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data));
};
const server = http.createServer((req, res) => {
const handler = routers[`${req.method}${req.url}`] || ((req, res) => {
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'Not Found' }));
});
handler(req, res);
});
// ...省略其他程式碼
我們可以看到我們宣告了一個 handler
函式,這個函式會去 routers
物件中尋找對應的路由,如果有找到的話,就會執行對應的程式碼,如果沒有找到的話,就會回傳 404 狀態碼給使用者。
如果你嘗試戳 http://localhost:3000/todos
的話,你應該會得到以下結果
[]
如果戳 http://localhost:3000/todo
的話,你應該會得到以下結果
{
"message": "Not Found"
}
超簡單的對吧?那麼 Post 也是依樣畫葫蘆,我們可以拿前面所寫的範本直接改一下就可以了
const http = require('node:http');
// ...省略其他程式碼
routers['GET/todos'] = (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data));
};
routers['POST/todos'] = (req, res) => {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
const cacheBody = JSON.parse(body);
data.push({
id: new Date().getTime(),
title: cacheBody.title,
completed: cacheBody.completed,
});
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: '新增成功' }));
});
};
// ...省略其他程式碼
這樣子就完成了,你可以嘗試戳 Post http://localhost:3000/todos
新增之後,接著你就可以戳 Get http://localhost:3000/todos
來取得所有的 Todo 了。
而後面的 Put 與 Delete 也是類似的,我這邊就不額外示範了,就當作留給你的一個小功課吧!
這一篇也差不多了,我們下一篇見囉~
Note
如果你修改過程都要一直重新啟動伺服器太麻煩的話,可以考慮使用nodemon
來幫你自動重新啟動伺服器,使用後請記得將package.json
中的scripts
屬性改成"start": "nodemon src/main.js"
。
本來碎碎念想寫點什麼,但突然想想還是不要寫好了,可是當我要寫的時候,又覺得還是算了,所以今天就先這樣吧。