使用 Hapi 開發 RESTful APIs

fairyly發表於2018-07-29

搜了國內的文章 使用 Hapi 開發 RESTful API 的很少很少,一些 社群介紹的部分在新版本中已經不在適用,新版本中有很多變化

什麼是 RESTful API

RESTful API: REST -- REpresentational State Transfer,英語的直譯就是“表現層狀態轉移”

可以參考下面理解:

Hapi

Hapi 是一個 Node.js 的 web 框架,即一個構建應用程式和服務豐富的框架。 目前 Hapi v17 僅支援 node v8.9.0 及以上版本

Hapi 特性

  • 認證和授權:內建的驗證和授權方案
  • 快取:提供客戶端和服務端快取,catbox
  • 路由: 內建路由
  • 驗證:使用 Joi
  • Cookies:提供了配置選項處理 cookie
  • 日誌: 內建的日誌記錄
  • 錯誤處理:boom
  • 程式監控:hapi 外掛 good

使用 Hapi 的必要條件

  • 安裝 Node.js v8.9.0 及以上
  • 安裝 MongoDB
  • 熟悉資料庫概念和 JavaScript 的應用知識

Hapi v17 新的變化

  • server.connection 方法移除,現在使用

    const server = new Hapi.Server({
        host: 'localhost',
        port: 3000
    })
    複製程式碼
  • 開啟關閉服務方法 完全非同步,沒有回撥

    try {
        await server.start()
    }
    catch (err) {
        console.log(err)
    }
      
    try {
        await server.stop()
    }
    catch (err) {
        console.log(err)
    }
    複製程式碼
  • reply() 回撥方法移除,response.hold() 和 response.resume() 方法不在可用

    // 以前
    const handler = function (request, reply) {
      return reply('ok');
    };
    
    // 現在
    
    const handler = function (request, h) {
      return 'ok';
    };
    複製程式碼

    更多使用 h 的示例如下:

    const handler = (request, h) => {
      // return a string
      return 'ok'
    
      // return an object and hapi creates JSON out of it
      return { name: 'Authentication Library', library: true }
    
      // redirect to 404
      return h.redirect('/404')
    
      // return a view
      return h.view('index', { name: 'Authentication Library' })
    
      // use the "h" response toolkit to create a response
      return h
       .response(thisHTML)
        .type('text/html')
        .header('X-Custom', 'my-value')
        .code(201)
    }
    複製程式碼
  • 三個請求事件型別 request, request-interval, request-error 合併成一個單一的 request 事件

  • 觸發的方法 像 server.on, request.on, response.on 被替換,使用server.events.on(), request.events.on(), response.events.on() 替代

  • 新的請求擴充套件:

    server.ext('onPreAuth', (request, h) => { … })
    server.ext('onCredentials', (request, h) => { … })
    server.ext('onPostAuth', (request, h) => { … }) ]
    複製程式碼
  • 在路由定義的時候 替換 config 為 options

    server.route({
      method: 'POST',
      path: '/',
      options: { … }
    })
    複製程式碼
  • 外掛 目前使用

    exports.plugin = { register, name, version, multiple, dependencies, once, pkg }
    複製程式碼

更多變化詳見GitHub 完整的更新日誌

正題:使用 Hapi 構建一個簡單的 api

在這個示例中提供 CURD 操作

  • Get all demos - GET /demos
  • Get one demo - GET /demo/11
  • Create a demo - POST /demos
  • Edit a demo - PUT /demos/11
  • Delete a demo - DELETE /demos/11

A demo 有下面的屬性:

  • name
  • age

1 建立專案目錄 demo,最後結構如下:

demo
├── package.json
├── server.js 
├── .gitignore (可選)
└── src
    ├
    ├── controller
    |   └── test.js
    └─── models
        └── test.js
   
複製程式碼

2 編寫 model

src/models/test.js
'use strict';

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const demoModel = new Schema({
  name: { type: String, required: true, index: { unique: true } },
  age: { type: Number, required: true }
});

module.exports = mongoose.model('Demo', demoModel, 'demos'); 
複製程式碼

編寫 controllers

src/controllers/test.js

var Demo =  require('../models/test');

/**
 * List Demos
 */
exports.list = (req, h) => {
  return Demo.find({}).exec().then((demo) => {

    return { demos: demo };

  }).catch((err) => {

    return { err: err };

  });
}

/**
 * Get Demo by ID
 */
exports.get = (req, h) => {

  return Demo.findById(req.params.id).exec().then((demo) => {

    if(!demo) return { message: 'Demo not Found' };

    return { demos: demo };

  }).catch((err) => {

    return { err: err };

  });
}


/**
 * POST a Demo
 */
exports.create = (req, h) => {

  const demoData = {
    name: req.payload.name,
    age: req.payload.age
  };

  return Demo.create(demoData).then((demo) => {

     return { message: "Demo created successfully", demo: demo };

  }).catch((err) => {

    return { err: err };

  });
}

/**
 * PUT | Update Demo by ID
 */
exports.update = (req, h) => {

  return Demo.findById(req.params.id).exec().then((demo) => {

    if (!demo) return { err: 'Demo not found' };

    demo.name = req.payload.name;
    demo.breed = req.payload.breed;
    demo.age = req.payload.age;
    demo.image = req.payload.image;

    demo.save(dogData);

  }).then((data) => {

      return { message: "Demo data updated successfully" };

  }).catch((err) => {

      return { err: err };

  });
}

/**
 * Delete Demo by ID
 */
exports.remove = (req, h) => {

  return Demo.findById(req.params.id).exec(function (err, demo) {

    if (err) return { dberror: err };
    if (!demo) return { message: 'Demo not found' };

    demo.remove(function (err) {
      if (err) return { dberror: err };

      return { success: true };
    });
  });
}
複製程式碼

編寫 server.js

'use strict';

const Hapi = require('hapi');
const mongoose = require('mongoose');
const DemoController =  require('./src/controllers/test');
const MongoDBUrl = 'mongodb://localhost:27017/demoapi';

const server = new Hapi.Server({
  port: 3000,
  host: 'localhost'
});

server.route({
  method: 'GET',
  path: '/demos',
  handler: DemoController.list
});

server.route({
  method: 'GET',
  path: '/demos/{id}',
  handler: DemoController.get
});
server.route({
  method: 'POST',
  path: '/demos',
  handler: DemoController.create
});

server.route({
  method: 'PUT',
  path: '/demos/{id}',
  handler: DemoController.update
});

server.route({
  method: 'DELETE',
  path: '/demos/{id}',
  handler: DemoController.remove
});

(async () => {
  try {  
    await server.start();
    // Once started, connect to Mongo through Mongoose
    mongoose.connect(MongoDBUrl, {}).then(() => { console.log(`Connected to Mongo server`) }, err => { console.log(err) });
    console.log(`Server running at: ${server.info.uri}`);
  }
  catch (err) {  
    console.log(err)
  }
})();
複製程式碼

測試 API

使用 postman 測試 http://localhost:3000/demos

增加授權驗證

目前步驟有點多,還需要註冊一個平臺,暫不做詳細說明了

參考

僅供學習交流

相關文章