在 AWS 以 Aurora Serverless 搭建無伺服器 Web 應用程式 part5 — 實作 REST API 及測試

Luyo
verybuy-dev
Published in
13 min readJun 1, 2019

上一篇中,我們完成了 serverless 專案的初始化並佈署上 AWS,成功讓一支 API 回傳了訊息。接下來就可以開始實作能夠連接上 Aurora Serverless cluster 的 serverless 應用程式了。

本文的範例會以 nodejs 來建立幾個簡單的 REST API endpoints,並且在各環境確認是否能正常運作。

本文參考:

註:我發現這個參考來源在 sequelize 套件的使用上並沒有顧及軟體架構設計的延展性,但我並沒有太多時間針對這點做重構,所以本文跟 sequelize ORM 有關的程式碼是有改進空間的。有興趣可參考此文章

建立資料庫連線

請在專案目錄底下新增一個 db.js 檔案,內容如下:

const Sequelize = require('sequelize')
const NoteModel = require('./models/Note')
const config = require(__dirname + '/config/' + process.env.NODE_ENV + '/db.json');
const sequelize = new Sequelize(
config.database,
config.username,
config.password,
{
dialect: config.dialect,
host: config.host
}
)
const Note = NoteModel(sequelize, Sequelize)
const Models = { Note }
const connection = {}
module.exports = async () => {
if (connection.isConnected) {
console.log('=> Using existing connection.')
return Models
}
await sequelize.sync()
await sequelize.authenticate()
connection.isConnected = true
console.log('=> Created a new connection.')
return Models
}

這邊我們將 db.json 的值設帶入,並初始化用來連線的 sequelize 物件。

建立 model

請先新增一個資料夾:

$ mkdir models

接著新增一個 models/Note.js 檔案,內容如下:

module.exports = (sequelize, type) => {
return sequelize.define('note', {
id: {
type: type.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: type.STRING,
description: type.STRING
})
}

這邊是利用 sequelize ORM 建立一個 model,資料表為 note ,有 id , title , descrition 這幾個欄位。

修改 handle.js

專案底下的 handle.js這個檔案是 Serverless Framework 自動產生的檔案,用來處理 http requests。

一開始檔案內有一個簡單的範例,就是我們在上一篇最後回應的 /hello 這個 endpoint 的程式碼。

現在請將檔案內容全部清空,並貼上以下程式碼:

const connectToDatabase = require('./db') // initialize connection// simple Error constructor for handling HTTP error codes
function HTTPError (statusCode, message) {
const error = new Error(message)
error.statusCode = statusCode
return error
}
module.exports.hello = async (event) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}, null, 2),
};
};
module.exports.healthCheck = async () => {
await connectToDatabase()
console.log('Connection successful.')
return {
statusCode: 200,
body: JSON.stringify({ message: 'Connection successful.' })
}
}
module.exports.create = async (event) => {
try {
const { Note } = await connectToDatabase()
const note = await Note.create(JSON.parse(event.body))
return {
statusCode: 200,
body: JSON.stringify(note)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: 'Could not create the note.'
}
}
}
module.exports.getOne = async (event) => {
try {
const { Note } = await connectToDatabase()
const note = await Note.findByPk(event.pathParameters.id)
if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
return {
statusCode: 200,
body: JSON.stringify(note)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: err.message || 'Could not fetch the Note.'
}
}
}
module.exports.getAll = async () => {
try {
const { Note } = await connectToDatabase()
const notes = await Note.findAll()
return {
statusCode: 200,
body: JSON.stringify(notes)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: 'Could not fetch the notes.'
}
}
}
module.exports.update = async (event) => {
try {
const input = JSON.parse(event.body)
const { Note } = await connectToDatabase()
const note = await Note.findByPk(event.pathParameters.id)
if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
if (input.title) note.title = input.title
if (input.description) note.description = input.description
await note.save()
return {
statusCode: 200,
body: JSON.stringify(note)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: err.message || 'Could not update the Note.'
}
}
}
module.exports.destroy = async (event) => {
try {
const { Note } = await connectToDatabase()
const note = await Note.findByPk(event.pathParameters.id)
if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
await note.destroy()
return {
statusCode: 200,
body: JSON.stringify(note)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: err.message || 'Could destroy fetch the Note.'
}
}
}

除了原本就有的 /hello 之外的 6 個 method 都是會跟資料庫做溝通的 handlers,對應的 endpoints 如下:

healthCheckGET /

createPOST /notes

getOneGET /notes/{id}

getAllGET /notes

updatePUT /notes/{id}

destroyDELETE /notes/{id}

在 local 端測試

到這邊,程式碼算是都準備好了,接下來我們要在 local 跑看看 API 是否能正常運作。請回到 commend line 下端令:

$ sls offline start

應該會出現類似下圖的結果:

這樣 http server 就準備好了,來用瀏覽器開看看:

沒有問題!那接下來我們要送 POST request,會稍微麻煩一點,你可以安裝瀏覽器的外掛,或使用 ReqBin 之類的 web 服務。本文參考的出處是使用 Insomnia 這套桌面軟體,因為介面看起來蠻順眼的所以我就下載來玩玩看了。

下載並安裝完後,就可以來試著送 request 到 POST /notes 這個 endpoint。將左上角的 method 下拉選單選擇 POST ,接著在網址列填入 http://localhost:3000/notes

再來是 request body,因為 Note 這個 model 要給 title, description 這兩個欄位,所以 JSON 的內容就會長這樣:

{
"title": "買菜",
"description": "筊白筍空心菜地瓜葉"
}

輸入完後就可以送出 request 了

200 OK,新增資料成功了,回到 commend line 會看到以下資訊:

若 request 有錯誤也會顯示在這邊,方便做 debug。

接著再來試一下另一個 endpoint GET /notes ,這個 endpoint 會撈出現資料庫裡 note 資料表的所有資料:

也是 200 OK ,確認 local 端沒有問題,可以來 deploy 上 AWS 來測試囉!

佈署至 AWS

佈署的動作我們已經在上一篇試做過了,這次就一樣用相同的指令來佈署到 dev 環境:

$ sls deploy --stage dev

跑完後的畫面會長這樣:

接著送 request 至 POST /notes 這個 endpoint 對應的 uri:

回傳 200 OK ,順利新增一筆資料,接著送 request 至 GET /notes

一樣 200 OK ,沒什麼問題,接下來就剩 deploy 到 prod 環境了:

$ sls deploy --stage prod

照理應該就跟 dev 環境差不多,然後會得到另一組 endpoints。

到這邊就算是大功告成啦!雖然真的要讓它變成一個能夠面對客戶的 application 還有不少東西要處理,例如怎麼換成自己的 domain name、怎麼設定 https 憑證之類的,但礙於篇幅,這邊就不多做討論囉。

移除專案

如果想要移除專案也非常簡單,只要下指令 :

$ sls remove --stage dev

這樣就可以囉。

在移除專案之前,如果你去 AWS console 查看,可以發現 Serverless Framework 實際上幫你產生了以下這些資源:

(1) IAM role

(2) CouldFormation stack

(3) API Gateway APIs

(4) S3 bucket

(5) Lambda functions

在下了 sls remove 指令之後,這些資源都會自動被移除。

註:在版本 1.42.2 的時候, IAM role 不會被移除,應該是 bug,後來升上 1.44.1 之後就正常了。

結語

廢話就不多說了,把時間留給 serverless application 的開發吧,結束。

上一篇:在 AWS 以 Aurora Serverless 搭建無伺服器 Web 應用程式 part4 — 建立及佈署 Serverless Framework 專案

--

--