從頭建立一套 web-based 服務架構 (8) API Server
到昨天為止,我們介紹了身為主角的 frontend server 和 web app 的大致結構,接下來幾天就先改開支線任務,進行 API Server 和 database 的開發。 如同先前的預告,本次專案在 API Server 會著墨較少,以快速開發完備功能供前端使用為目標,因此開發方式會和 Frontend Server 較為不同。
在這系列文章提到的 API Server 是指連接 database、提供 RESTful HTTP endpoints 的 Node.js server。 由這台 Server 提供的 RESTful API 除了會被 Frontend 的 server/client 呼叫外,也能讓 mobile apps 呼叫,因此在架構上 API Server 和 Frontend Server 是獨立運作、佈署的。 這樣的職責分割所帶來的 server 結構和以往全部寫成同一台 server 的 Monolithic 結構相比,有著更容易分工開發、易於分別彈性擴展(elastic scaling)等優點,不過也會帶來額外的 communication overhead。
快速打造 API Server
綜合以上所述,API Server 的職責主要有:
- 連接 database 進行操作並管理 schema,包括初始的 table creation 和後續的 schema migration
- 提供 HTTP API 讓其他人(統稱為 client)可藉由 HTTP 進行資料操作
在 Node.js ecosystem 有山積一般的 module 能幫忙達成這些目標,不管是對應 MongoDB 的 ODM (註1)套件 Mongoose、自帶優秀 routing 和 middleware 能力的 Express.js 都是常用的 module。
這些 module 適合以 bottom-up 的角度逐步打造出自己需要的 API Server,不過這樣的開發速度還不夠快,我們想要更加偷懶快速的解決方案,能在 schema 宣告完成後直接連接 database 並根據 schema 定義的 model 自行生成標準的 RESTful routes。
這樣的作法是以 top-down 的角度,一次自動生成出許多 RESTful API,再根據前端需求自行決定是否增減。
以下介紹一些作者曾經使用過的相關套件:
Keystone.js
嚴格來說 Keystone.js 不算是快速自動生成 RESTful API 的選擇之一,但它對於快速開發基於 Content Management System (CMS) 的系統而言非常方便,只要做好 model definition 和初始設定,就能自動生成基於 Mongoose 的 ODM 和完整的後台編輯 web app。 由於 Keystone 的結構完全基於 Node.js ecosystem 常見的套件(Express.js、Mongoose),開發者可以輕易的將 Keystone app 嵌入(embed)到更大的 Express.js app 內,然後藉由 Keystone API 快速建造自己需要的 API(註2):
import express from 'express';
import keystoneServer from './keystone';
import apiServer from './api';
const app = express();
app.use('/keystone', keystoneServer);
app.use('/api/v1', apiServer);
app.listen(3000);
Loopback
在 Loopback 官網的標語開宗明義將自己介紹為「Node.js API Framework」; 它能夠以 JSON 定義好 server 環境、database 設定、Schema (model) definition 後自動生成整個 server runtime、ORM/ODM、RESTful APIs 甚至 access control list (ACL) 等機制。 除了 server side 的 API 建置,Loopback 也提供 Objective-C、Android、Angular.js 等 client side SDK 方便使用者整合 Loopback 的 API,當然我們也能自行撰寫對口串接。
儘管 Loopback 違背了在第二天提到的以 library aggregation 為主的概念,基於本次專案對快速建造 API 的需求來說,Loopback 是相當合適的選擇。 接下來會以 Loopback 作為後端開發的主要架構,逐步為專案需求打造 API Server。
專案架構
開發 Loopback app 最簡單的方式是安裝 Strongloop CLI tool 然後利用 slc
指令生成不同的 Loopback 元件,不過在這次的專案中我們改採取手動設定的作法來靠近檢視 Loopback 專案內的不同元件組成。
以下是本次 API Server 的專案架構:
src
|- common
|- config
|- boot
|- index.js
package.json
server.js
server.js
單純啟用 babel-register
來轉換 ES6 程式碼。
src/common
在 src/common
資料夾內主要擺放 model definition 和 ACL roles 的程式碼。
Loopback 會根據 model 的設定檔更新 database 並生成 RESTful API endpoints;
ACL roles 則會被當成存取 API 時用來作身分授權(authorization)的 middleware。
src/config
顧名思義,在這個資料夾擺放的是設定 Loopback 用的各種 JSON 設定檔,包括 datasource、middleware 等。
在命名檔案時,可以加入和環境變數相同的 postfix 進行設定覆寫,例如 config.json
會被所有環境載入,但當 NODE_ENV
為 production
時,config.production.json
檔案中的設定值會覆寫 config.json
中宣告的設定。
除了設定檔以外, src/config/boot
資料夾則是擺放給 loopback-boot
使用的 boostrapping scripts。
src/index.js
Loopback app 本體。
由於 Loopback 的 app instance 同樣是基於 Express.js app,這裡可以自行加入需要的 Express middleware。
有別於直接呼叫 app.listen()
,先使用 loopback-boot
做 Loopback bootstrapping 讀取 src/config/boot
資料夾內的 bootstrapping scripts:
import path from 'path';
import { promisify, promisifyAll } from 'bluebird';
import bodyParser from 'body-parser';
import loopback from 'loopback';
import boot from 'loopback-boot';
const bootAsync = promisify(boot);
const app = loopback();
promisifyAll(app, { filter: name => name === 'listen' });
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
bootAsync(app, path.join(__dirname, 'config'))
.then(app.listenAsync)
.then(() => {
app.emit('started');
const baseUrl = app.get('url').replace(/\/$/, '');
console.info('API server listening at', baseUrl);
const componentExplorer = app.get('loopback-component-explorer');
if (componentExplorer) {
console.info('REST API Explorer at', componentExplorer.mountPath);
}
});
以上先大致帶完 API Server 的專案架構,細部的 config、modeling、ACL、boot 等部分將於接下來幾天逐一介紹。
註1:相對於對接 SQL database 用的 Object-Relation Mapper (ORM),在 document based 的 noSQL 部分則是稱為 Object-Document Mapper (ODM)
註2:每個 Express app(由 express()
建立的物件)均為 middleware 的子類別實體,因此可以直接被 app.use
作為 middleware 使用。