Node.js で小規模なアプリケーションを開発する方法 その12

# はじめに
この記事ではAPIの仮組みについて解説します。

# 仮組みするAPI
仮組みするAPIを下記に示します。

- プロジェクト
  - 一覧 (GET /projects)
  - 追加 (POST /projects)
  - 詳細 (GET /projects/:id)
  - 更新 (PUT /projects/:id)
  - 削除 (DELETE /projects/:id)
- レポート
  - 一覧 (GET /reports)
  - 追加 (POST /reports)
  - 詳細 (GET /reports/:id)
  - 更新 (PUT /reports/:id)
  - 削除 (DELETE /reports/:id)

# scaffoldクラスの作成
Scaffoldクラスを記述するファイルと、そのファイルを格納するディレクトリを作成します。

```
mkdir app/utils
touch app/utils/scaffold.js
```

scaffold.jsの内容を下記に示します。

```js:app/utils/scaffold.js
'use strict';

module.exports = factory
module.exports.Scaffold = Scaffold

function factory(model) {
  return new Scaffold({ model: model })
}

function Scaffold(options) {
  this.model = options.model
}

Scaffold.prototype.findById = function () {
  var self = this

  return function (req, res, next) {
    self.model.findOne({ where: { id: req.params.id } })
      .then(function (resource) {
        if (resource === null) {
          var err = new Error('Not Found')
          err.status = 404
          return next(err)
        }

        req.resource = resource
        res.locals.resource = resource

        next()
      })
  }
}

Scaffold.prototype.list = function () {
  var self = this

  return function (req, res, next) {
    return self.model.findAll()
      .then(function (resources) {
        res.send(resources)
      })
  }
}

Scaffold.prototype.post = function () {
  var self = this

  return function (req, res, next) {
    return self.model.create(req.body)
      .then(function () {
        res.status(201).end()
      })
  }
}

Scaffold.prototype.get = function () {
  return function (req, res, next) {
    res.send(req.resource)
  }
}

Scaffold.prototype.put = function () {
  return function (req, res, next) {
    Object.keys(req.body).forEach(function (key) {
      req.resource[key] = req.body[key]
    })

    req.resource.save()
      .then(function () {
        res.status(204).end()
      })
  }
}

Scaffold.prototype.delete = function () {
  return function (req, res, next) {
    req.resource.destroy()
      .then(function () {
        res.status(204).end()
      })
  }
}
```

# ルータの編集

## app/routes/projects.jsの編集
変更後の内容を下記に示します。

```js:app/routes/projects.js
'use strict';

var express = require('express')
var models = require('../models')
var scaffold = require('../utils/scaffold')(models.Project)

var adminHtml = express.Router({ strict: true })
var adminApi = express.Router({ strict: true })

module.exports = {
  admin: {
    html: adminHtml,
    api: adminApi,
  },
}

adminHtml.get('/', adminHtmlList())
adminHtml.get('/add/', adminHtmlAdd())
adminHtml.get('/:id([0-9]+)/', adminFindById(), adminHtmlDetail())
adminHtml.get('/:id([0-9]+)/edit/', adminFindById(), adminHtmlEdit())
adminHtml.get('/:id([0-9]+)/delete/', adminFindById(), adminHtmlDelete())

adminApi.get('/', adminApiList())
adminApi.post('/', adminApiPost())
adminApi.get('/:id([0-9]+)', adminFindById(), adminApiGet())
adminApi.put('/:id([0-9]+)', adminFindById(), adminApiPut())
adminApi.delete('/:id([0-9]+)', adminFindById(), adminApiDelete())

function adminHtmlList() {
  return function (req, res) {
    res.render('projects/admin-list')
  }
}

function adminHtmlAdd() {
  return function (req, res) {
    res.render('projects/admin-add')
  }
}

function adminHtmlDetail() {
  return function (req, res) {
    res.render('projects/admin-detail')
  }
}

function adminHtmlEdit() {
  return function (req, res) {
    res.render('projects/admin-edit')
  }
}

function adminHtmlDelete() {
  return function (req, res) {
    res.render('projects/admin-delete')
  }
}

function adminApiList() {
  return scaffold.list()
}

function adminApiPost() {
  return scaffold.post()
}

function adminApiGet() {
  return scaffold.get()
}

function adminApiPut() {
  return scaffold.put()
}

function adminApiDelete() {
  return scaffold.delete()
}

function adminFindById() {
  return scaffold.findById()
}
```

変更点は下記の通りです。

```
--- a/app/routes/projects.js
+++ b/app/routes/projects.js
@@ -1,6 +1,8 @@
 'use strict';
 
 var express = require('express')
+var models = require('../models')
+var scaffold = require('../utils/scaffold')(models.Project)
 
 var adminHtml = express.Router({ strict: true })
 var adminApi = express.Router({ strict: true })
@@ -14,15 +16,15 @@ module.exports = {
 
 adminHtml.get('/', adminHtmlList())
 adminHtml.get('/add/', adminHtmlAdd())
-adminHtml.get('/:id([0-9]+)/', adminHtmlDetail())
-adminHtml.get('/:id([0-9]+)/edit/', adminHtmlEdit())
-adminHtml.get('/:id([0-9]+)/delete/', adminHtmlDelete())
+adminHtml.get('/:id([0-9]+)/', adminFindById(), adminHtmlDetail())
+adminHtml.get('/:id([0-9]+)/edit/', adminFindById(), adminHtmlEdit())
+adminHtml.get('/:id([0-9]+)/delete/', adminFindById(), adminHtmlDelete())
 
 adminApi.get('/', adminApiList())
 adminApi.post('/', adminApiPost())
-adminApi.get('/:id([0-9]+)', adminApiGet())
-adminApi.put('/:id([0-9]+)', adminApiPut())
-adminApi.delete('/:id([0-9]+)', adminApiDelete())
+adminApi.get('/:id([0-9]+)', adminFindById(), adminApiGet())
+adminApi.put('/:id([0-9]+)', adminFindById(), adminApiPut())
+adminApi.delete('/:id([0-9]+)', adminFindById(), adminApiDelete())
 
 function adminHtmlList() {
   return function (req, res) {
@@ -55,41 +57,25 @@ function adminHtmlDelete() {
 }
 
 function adminApiList() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.list()
 }
 
 function adminApiPost() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.post()
 }
 
 function adminApiGet() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.get()
 }
 
 function adminApiPut() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.put()
 }
 
 function adminApiDelete() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.delete()
+}
+
+function adminFindById() {
+  return scaffold.findById()
 }
```

## app/routes/reports.jsの編集
変更後の内容を下記に示します。

```js:app/routes/reports.js
'use strict';

var express = require('express')
var models = require('../models')
var scaffold = require('../utils/scaffold')(models.Report)

var adminHtml = express.Router({ strict: true })
var adminApi = express.Router({ strict: true })

module.exports = {
  admin: {
    html: adminHtml,
    api: adminApi,
  },
}

adminHtml.get('/', adminHtmlList())
adminHtml.get('/add/', adminHtmlAdd())
adminHtml.get('/:id([0-9]+)/', adminFindById(), adminHtmlDetail())
adminHtml.get('/:id([0-9]+)/edit/', adminFindById(), adminHtmlEdit())
adminHtml.get('/:id([0-9]+)/delete/', adminFindById(), adminHtmlDelete())

adminApi.get('/', adminApiList())
adminApi.post('/', adminApiPost())
adminApi.get('/:id([0-9]+)', adminFindById(), adminApiGet())
adminApi.put('/:id([0-9]+)', adminFindById(), adminApiPut())
adminApi.delete('/:id([0-9]+)', adminFindById(), adminApiDelete())

function adminHtmlList() {
  return function (req, res) {
    res.render('reports/admin-list')
  }
}

function adminHtmlAdd() {
  return function (req, res) {
    res.render('reports/admin-add')
  }
}

function adminHtmlDetail() {
  return function (req, res) {
    res.render('reports/admin-detail')
  }
}

function adminHtmlEdit() {
  return function (req, res) {
    res.render('reports/admin-edit')
  }
}

function adminHtmlDelete() {
  return function (req, res) {
    res.render('reports/admin-delete')
  }
}

function adminApiList() {
  return scaffold.list()
}

function adminApiPost() {
  return scaffold.post()
}

function adminApiGet() {
  return scaffold.get()
}

function adminApiPut() {
  return scaffold.put()
}

function adminApiDelete() {
  return scaffold.delete()
}

function adminFindById() {
  return scaffold.findById()
}
```

変更点は下記の通りです。

```
@@ -1,6 +1,8 @@
 'use strict';
 
 var express = require('express')
+var models = require('../models')
+var scaffold = require('../utils/scaffold')(models.Report)
 
 var adminHtml = express.Router({ strict: true })
 var adminApi = express.Router({ strict: true })
@@ -14,15 +16,15 @@ module.exports = {
 
 adminHtml.get('/', adminHtmlList())
 adminHtml.get('/add/', adminHtmlAdd())
-adminHtml.get('/:id([0-9]+)/', adminHtmlDetail())
-adminHtml.get('/:id([0-9]+)/edit/', adminHtmlEdit())
-adminHtml.get('/:id([0-9]+)/delete/', adminHtmlDelete())
+adminHtml.get('/:id([0-9]+)/', adminFindById(), adminHtmlDetail())
+adminHtml.get('/:id([0-9]+)/edit/', adminFindById(), adminHtmlEdit())
+adminHtml.get('/:id([0-9]+)/delete/', adminFindById(), adminHtmlDelete())
 
 adminApi.get('/', adminApiList())
 adminApi.post('/', adminApiPost())
-adminApi.get('/:id([0-9]+)', adminApiGet())
-adminApi.put('/:id([0-9]+)', adminApiPut())
-adminApi.delete('/:id([0-9]+)', adminApiDelete())
+adminApi.get('/:id([0-9]+)', adminFindById(), adminApiGet())
+adminApi.put('/:id([0-9]+)', adminFindById(), adminApiPut())
+adminApi.delete('/:id([0-9]+)', adminFindById(), adminApiDelete())
 
 function adminHtmlList() {
   return function (req, res) {
@@ -55,41 +57,25 @@ function adminHtmlDelete() {
 }
 
 function adminApiList() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.list()
 }
 
 function adminApiPost() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.post()
 }
 
 function adminApiGet() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.get()
 }
 
 function adminApiPut() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.put()
 }
 
 function adminApiDelete() {
-  return function (req, res, next) {
-    var err = new Error('not implemented')
-    err.status = 500
-    next(err)
-  }
+  return scaffold.delete()
+}
+
+function adminFindById() {
+  return scaffold.findById()
 }
```

# body-parserのインストール
コマンドを下記に示します。

```
npm install --save body-parser
```

# app/index.jsの編集
変更後の内容を下記に示します。

```js:app/index.js
'use strict';

var env = process.env.NODE_ENV || 'development'

var url = require('url')
var path = require('path')
var morgan = require('morgan')
var express = require('express')
var bodyParser = require('body-parser')
var serveStatic = require('serve-static')
var proxyMiddleware = require('proxy-middleware')

var pages = require('./routes/pages')
var account = require('./routes/account')
var reports = require('./routes/reports')
var projects = require('./routes/projects')

var siteConfig = require('./config/site')[env]

module.exports = factory

function factory(options) {
  options = options || {}

  var staticPath = options.staticPath
  var app = express()

  app.set('strict routing', true)
  app.set('views', path.join(__dirname, 'views'))
  app.set('view engine', 'jade')

  app.locals.baseUrl = siteConfig.baseUrl

  app.use(morgan('dev'))
  app.use(bodyParser.json())
  app.get('/', pages.anonymous.home())

  app.get('/admin/', pages.admin.dashboard())
  app.use('/admin/account', account.admin.html)
  app.use('/admin/reports', reports.admin.html)
  app.use('/admin/projects', projects.admin.html)
  app.use('/admin/api/v1/account', account.admin.api)
  app.use('/admin/api/v1/reports', reports.admin.api)
  app.use('/admin/api/v1/projects', projects.admin.api)

  if (env === 'development') {
    app.use('/static', proxyMiddleware(url.parse('http://127.0.0.1:8080')))
  } else {
    app.use('/static', serveStatic(staticPath))
  }

  return app
}
```

変更点は下記の通りです。

```
@@ -6,6 +6,7 @@ var url = require('url')
 var path = require('path')
 var morgan = require('morgan')
 var express = require('express')
+var bodyParser = require('body-parser')
 var serveStatic = require('serve-static')
 var proxyMiddleware = require('proxy-middleware')
 
@@ -31,6 +32,7 @@ function factory(options) {
   app.locals.baseUrl = siteConfig.baseUrl
 
   app.use(morgan('dev'))
+  app.use(bodyParser.json())
   app.get('/', pages.anonymous.home())
 
   app.get('/admin/', pages.admin.dashboard())
diff --git a/app/package.json b/app/package.json
index 764b260..3ee5be5 100644
```

# 動作確認
appとstaticを起動して下記のURLなどにアクセスします。

- http://127.0.0.1:3000/admin/api/v1/projects
- http://127.0.0.1:3000/admin/api/v1/projects/1
- http://127.0.0.1:3000/admin/api/v1/reports
- http://127.0.0.1:3000/admin/api/v1/reports/1

データがJSON形式で表示されれば正常に動作しています。

# おわりに
次回はstaticのモデル作成について解説します。