Building Node.js REST API Servers with Express.js and Hapi
Modern-day web developers use an architecture consisting of a thick client and a a thin back-end layer。如AngularJS,ReactJs, VueJS。用来建立厚厚的client。
另一方面,他们使用REST APIs建立thin back-end layer。
a representational state transfer (REST) web application programing interface (API) service。
这种结构,被称为厚客户端或者SPA单页面程序,变得越来越流行。因为它们有以下优势:
- SPA更快,因为它们渲染网页元素在浏览器内,无需总是从服务器取HTML.
- bandwidth更小,因为一旦它加载,大多数的页面布局位置相同,所以浏览器只需使用JSON格式的数据来改变网页的元素。
- 相同的后端REST API可以服务多客户apps/consumers, web app是其中之一。
- 不理解?:
- There is a separation of concerns, i.e., the clients can be replaced without compromising the integrity of the core business logic, and vice versa.
- UI/UX难以测试,尤其是事件驱动,单页面程序,这有一个增加的跨浏览器测试的复杂程度。但是,分离的业务逻辑进入back-end REST API, 这个逻辑变得容易测试:在unit和functional testing。
因此,大多数新程序接受使用REST API和Clients方法,即使开始只有一个客户。
使用Node创建REST API非常容易。
本章包括以下内容:
- REST API 基础
- Project 依赖
- 测试:Mocha (和superagent)
- REST API server implementation with Express and Mongoskin(替换为Mongoose)
- 使用Hapi.js重构。Hapi(1万✨)没有Express知名。
REST API server可以处理object的创建,object和collection的retrieval检索,改变object,删除object。
本章的所有代码https://github.com/azat-co/practicalnode
RESTful API Basics
REST API变得著名是因为在分布式系统中,每个事务都需要包含关于客户机状态的足够信息。这个标准是无状态的,因为服务器上不存储关于客户机状态的信息,这使得每个请求都可以由不同的系统提供服务。这使得向上或向下缩放系统变得轻而易举。
在某种意义上,无状态服务器就像编程中的松散耦合类。许多基础设施技术使用最好的编程实践;除了松耦合之外,版本控制、自动化和持续集成都可以应用于基础设施,从而获得巨大的好处。
RESTFUL api的独特特性:
- 更好的可伸缩性支持。 因为不同的组件可以独立部署到不同的服务器。
- 取代了 Simple Object Access Protocol (SOAP )。因为它更简单的动词和名词结构。
- 使用HTTP methods
- JSON不是唯一选择(尽管它是最著名的)。
下面是一个简单的CRUD REST api:
Method | URL | Meaning |
GET |
/messages.json | 用JSON格式返回list of messages. |
PUT |
/messages.json | 更新/替换所有的messages并返回JSON格式的status/error |
POST | /messages.json | 创建新的message,返回它的? |
GET | /messages/{id}.json | 返回message,包括?,JSON格式的 |
PUT | /messages/{id}.json | 更新id是{id}的message,如果{id}不存在,创建它。 |
DElETE | /messages/{id}.json | 删除id是{id}的message, 返回status/error , JSON格式 |
REST is not a protocol; it's an architecture in the sense that it's more flexible than SOAP, which we know is a protocol.
因此,REST AP可以像/messages/list.html或者/messages/list.xml。如果我们想要支持这些格式的话。
PUT and DELETE are idempotent methods. idempotent是一个专业术语,它的意思是server收到2个以上的类似请求,结果是相同的。
GET是安全的, nullipotent。
POST是不安全的, 非idempotent。它可能影响state和引起副作用。
相关文章
https://en.wikipedia.org/wiki/Representational_state_transfer
https://www.infoq.com/articles/rest-introduction
在我们的REST API server , 我们支持CRUD操作并使用app.use(), app.param()方法控制Express.js中间件概念。
因此,我们的app应该可以处理下面的命令,使用JSON格式
- POST /collections/{collectiionName}
- GET /collections/{collectionName}/{id}: 使用?来检索一个对象。
- GET /collections/{collectionName}/ : 请求从collection(items)来检索任意items, 本例子,我们有query选项: up to 10 items和使用Id排序。
- PUT /collections/{collectionName}/{id}: request with ID来更新一个对象。
- Delete /collections/{collectionName}/{id}
让我们通过声明依赖来开始我们的程序。
Project Dependencies
安装packages, 本章作者使用Mongoskin, 它比Mongoose更轻量化,它是无schema的。(作者个人喜欢,但许多开发者更喜欢安全和a schema的统一性)
第二个选择是framework。使用Express.js, 它是Node.js http模块的扩展。
Express.js框架有一大堆的modules插件,叫做middleware。可以供开发者选择使用,无需自己写了。
首先,创建文件夹。
$ mkdir rest-express $ cd rest-express $ npm init -y
npm/node.js提供多个方法安装依赖
- Manually, one by one, 使用npm install
... - As a part of
package.json (这个方法最简单, 复制这个文件,然后执行npm install)
- By downloading and copying modules
注意⚠️package.js内不要有多余的逗号“,”
//... ⚠️版本选择。 "dependencies": { "body-parser": "1.18.2", "express": "4.16.2", "mongodb": "2.2.33", "mongoskin": "2.1.0", "morgan": "1.9.0" }, "devDependencies": { "expect.js": "0.3.1", "mocha": "4.0.1", "standard": "10.0.3", "superagent": "3.8.0" } }
⚠️:
morgan是登陆用的中间件。
superagent可以用axios替代。
expect.js可以改用chai.js
npm i mocha chai standard axios --save-dev
Test Coverage with Mocha and Superagent
在执行app前,写功能测试。制造HTTP请求到不久后创建的REST API server。
在测试取代开发,我们使用这些测试来建立Node.js free JSON REST api server ,使用Express.js框架和Mongoose库forMongoDB(文章使用Mongoskin)
using the Mocha (http://visionmedia.github.io/mocha) and superagent
(http://visionmedia.github.io/superagent)[^5] libraries.
这些测试执行基本的CRUD, posting HTTP 请求到服务器。
具体安装mocha和使用见之前的博客:
https://www.cnblogs.com/chentianwei/p/10262044.html
现在创建test/index.js文件,并有6个程序组组:
- 创建一个object
- Retrieve an object with its ID
- Retrieve the whole collection
- Update an object by its ID
- Check an updated object by its ID
- Remove an object by its ID
const boot = require('../index.js').boot const shutdown = require('../index.js').shutdown const port = require('../index.js').port const superagent = require('superagent') const expect = require('expect.js') before(() => { boot() }) describe('express rest api server', () => { // ... }) after(() => { shutdown() })
然后,写第一个测试案例,在describe内,主要代码写在callback。
发出POST 请求到一个本地的server实例,并从测试文件boot()。
当发送请求后,我们传递数据,这创建一个对象。
我们期待没有❌。最后我们保存创建的对象的?到id变量,以便后面的测试案例使用。
describe('express rest api server', () => { let id //用于存储新创建的对象的_id, 然后使用它进行RUD的测试。 it('post object', (done) => { axios({ method: 'post', url: `http://localhost:${port}/collections/db2`, data: {name: 'John', email: '[email protected]'} }) .then((response) => { expect(response.data._id.length).to.equal(24) id = response.data._id }) .catch((error) => { if (error.response) { // console.log(error.response.data); } else if (error.request){ // error.request是http.ClientRequest的实例 console.log(error.request) } else { console.log("Error", error.message) } // console.log("config", error.config) }) .then(done) })
expect, 期待创建的document的_id有24个字母。
axios使用.then(done)告诉mocha结束测试。这个方法专门用于异步代码。
?测试案例response.data就是request.data, 因为在主文件index.js,app.post(url, callback)中的callback中使用res.send(userResponse), userResponse就是新增的记录document。
这里使用了很多自然语言的语法判断,具体见chai文档)
it('retrieves an object', (done) => { axios .get(`http://localhost:${port}/collections/db2/${id}`) .then((response) => { expect(typeof response.data).to.equal('object') expect(response.data._id.length).to.equal(24) expect(response.data._id).to.equal(id) }) .then(done) }) it('retrieves a collection', (done) => { axios .get(`http://localhost:${port}/collections/db2`) .then((response) => { expect(response.data.length).to.be.above(0) expect(response.data.map(function(item) { return item._id})).to.include(id) }) .then(done) }) it('updates an object', (done) => { axios.put(`http://localhost:${port}/collections/db2/${id}`, { name: 'Peter', email: '[email protected]' }) .then((response) => { expect(typeof response.data).to.equal('object') expect(response.data.msg).to.equal('success') }) .then(done) }) it('checks an updated object', (done) => { axios.get(`http://localhost:${port}/collections/db2/${id}`) .then((res) => { expect(typeof res.data).to.equal('object') expect(res.data._id.length).to.equal(24) expect(res.data._id).to.equal(id) expect(res.data.name).to.equal('Peter') }) .then(done) }) it('delete an object', (done) => { axios.delete(`http://localhost:${port}/collections/db2/${id}`) .then((res) => { // 数据库返回的数据 expect(res.data).to.equal("Deleted successfully!") }) .then(done) }) })
最后使用mocha test/index.js命令或者npm test运行测试。
⚠️,如果想要生成测试报告,使用-R
- $mocha test -R list,
- $mocha test -R nyan (有一个猫的字符图形)
⚠️: axios的Response结构是一个对象
{ data: {}, status: 200, statusText: 'ok', headers: {}, config: {}, request: {} //它是node.js内的最后一个ClientRequest实例。 }
如果返回error, 可以使用error.response
REST API Server Implementation with Express and Mongoskin
创建并打开一个index.js文件:这是我们的主文件。
⚠️这里使用了body-parser中间件,用于使用Express#req.body。
const express = require('express') const mongoose = require('mongoose') const bodyParser = require('body-parser') const logger = require('morgan') const http = require('http') const models = require('./model/index.js') const app = express() app.use(bodyParser.json()) app.use(logger()) app.set('port', process.env.PORT || 3000 ) const db = mongoose.connect('mongodb://localhost:27017/db2', { useNewUrlParser: true })
然后,添加route, (这些route方法的回调函数可以抽象出来。)
app.param('collectionName', (req, res, next, collectionName) => { if (!models.User) { return next(new Error('No models.')) } req.models = models return next() }) app.get('/', (req, res, next) => { // 提示使用/collection/messages. res.send('Select a collection, e.g., /collection/messages') }) // 得到所有的user信息 app.get('/collections/:collectionName', (req, res, next) => { req.models.User.find({}, null, {limit: 10, sort: {_id: -1}}, (error, users) => { if (error) return next(error) res.send(users) }) }) // 新增一个user app.post('/collections/:collectionName', (req, res, next) => { if (!req.body) { return next(new Error("No user payload!!!!!!")) } let user = req.body req.models.User.create(user, (error, userResponse) => { if (error) { return next(error) } res.send(userResponse) }) }) // 根据_id,查询 app.get('/collections/:collectionName/:id', (req, res, next) => { // req.params属性是一个对象,它包含属性映射route的参数,因此本例子req.params内有一个id属性 if (!req.params.id) { return next(new Error('No user id')) } req.models.User.findById(req.params.id, (error, user) => { if (error) return next(error) res.send(user) }) }) // 修改 app.put('/collections/:collectionName/:id', (req, res, next) => { if (!req.params.id) return next(new Error('No user id')) if (!req.body) return next(new Error('No user payload')) req.models.User.findById(req.params.id, (error, user) => { if (error) return next(error) user.set(req.body) user.save((error, savedUser) => { if (error) return next(error) res.send((savedUser._id == req.params.id) ? {msg: 'success'} : {msg: 'error'}) }) }) }) app.delete('/collections/:collectionName/:id', (req, res, next) => { if (!req.params.id) return next(new Error('No article ID.')) req.models.User.deleteOne({_id: req.params.id}, (error) => { if (error) { return next(error) } // 使用send方法,向客户端发送数据。 res.send("Deleted successfully!") }) })
最后建立创建一个http.Server的实例,当boot时监听端口,当shutdown时关闭服务器的连接。
const shutdown = () => { server.close(process.exit) } if (require.main === module) { boot() } else { console.info('Running app as a module') exports.boot = boot exports.shutdown = shutdown exports.port = app.get('port') }
完成后,使用$mocha test -R list测试
选择:
- 使用浏览器输入url,测试也可以。
- 使用postman程序
- 使用Linux的terminal的curl命令。
附加Express API
app.param([name], callback)
增加callback triggers回调触发点给route parameters。
param callbacks defined on app
will be triggered only by route parameters defined on app
routes.
这个方法是一个Express.js 中间件。
例如,当一个请求模式内包含一个字符串'collectionName', 并且前面有一个冒号“:”。那么这个中间件就会被触发,执行它的回调函数。
- 参数name可以是string或者数组。
- 回调函数的参数是请求对象,响应对象,下一个中间件next(), 参数的值和参数的名字,按照顺序排列。
在app.param中间件的回调函数执行后,才会执行路径的handler。
例子1,
:id存在于一个路径内时,会按照顺序执行下面的中间件。
app.param('id', function (req, res, next, id) { console.log('CALLED ONLY ONCE'); next(); }); app.get('/user/:id', function (req, res, next) { console.log('although this matches'); next(); }); app.get('/user/:id', function (req, res) { console.log('and this matches too'); res.end(); });
当Get /user/42时,会在控制台打印:
CALLED ONLY ONCE although this matches and this matches too
注意⚠️res.end()其实会调用Node.js的核心module--HTTP中的response.end()
表示所有的响应的头和体已经发出,服务器应该考虑这个信息完成。这个方法必须在每个response中调用。
例子2
如果参数name是一个数组,回调函数触发点是声明在name内的每一个元素,它们按照顺序被声明
(具体见http://expressjs.com/en/4x/api.html#app.param)