PJCHENder
4/12/2017 - 1:50 PM

Build a REST API With Express @ Treehouse

Build a REST API With Express @ Treehouse

// 1-1.載入需要的模組
const express = require('express')
const router = express.Router()
const Question = require('./models').Question

// 2. 設定 endpoints
// 當 URL 中的 params :qID 存在時,執行 callback
router.param('qID', function (req, res, next, id) {
  // 這裡的 id 會是 qID 的值
  Question.findById(req.params.qID, function (err, doc) {
    if (err) return next(err)
    if (!doc) {
      // 如果找不到
      err = new Error('Not Found')
      err.status(404)
      return next(err)
    }
    req.question = doc  //  讓它可以在其他 middleware 中被使用
    return next()
  })
})

// 當 URL 中帶有 params :aID 時,執行 callback
router.param('aID', function (req, res, next, id) {
  // id 這個方法會回傳符合該 id 的 Document
  req.answer = req.question.answers.id(id) // req.question 來自 router.param('qID', callback)
  if (!req.answer) {
    // 如果找不到該答案
    err = new Error('Not Found')
    err.status = 404
    return next(err)
  }
  next()
})

// 2-1. GET '/questions',顯示所有問題(R)
router.get('/', (req, res, next) => {
  Question.find({})
          .sort({createdAt: -1})
          .exec(function (err, questions) {
            if (err) return next(err)
            res.json(questions)
          })
})

// 2-2. POST '/questions',建立問題(C)
router.post('/', (req, res, next) => {
  let question = new Question(req.body)       //  新增 document
  question.save(function (err, question) {    //  儲存 document
    if (err) return next(err)
    res.status(201)
    res.json(question)
  })
})

// 2-3. GET '/questions/:qID',顯示特定問題(R)
router.get('/:qID', (req, res, next) => {
  // req.question 是從 router.param('qID', callback) 這個 middleware 傳來
  // 指的是符合 :qID 值的 questions document
  res.json(req.question)   
})

// 2-4. POST '/questions/:qID/answers,建立答案(C)
router.post('/:qID/answers', (req, res, next) => {
  req.question.answers.push(req.body)           //  把答案推進去
  req.question.save(function (err, question) {  //  儲存該 document
    if (err) return next(err)
    res.status(201)
    res.json(question)
  })
})

// 2-5. PUT '/questions/:qID/answers/:aID',修改答案(U)
router.put('/:qID/answers/:aID', (req, res, next) => {
  req.answer.update(req.body, function (err, result) {    // update 這個是寫在 Model 的 instance method
    if (err) return next(err)
    res.json(result)
  })
})

// 2-6. DELETE '/questions/:qID/answers/:aID',刪除特定答案(D)
router.delete('/:qID/answers/:aID', (req, res, next) => {
  req.answer.remove(function (err) {
    // 把 answer 移除
    if (err) return next(err)
    // 接著儲存 question
    req.question.save(function (err, question) {
      if (err) return next(err)
      res.json(question)
    })
  })
})


// 2-7-1. POST '/questions/:qID/answers/:aID/vote-up',加一票
// 2-7-2. POST '/questions/:qID/answers/:aID/vote-down',減一票
// router.post('<path>', <middleware1>, <middleware2>, ...)
router.post('/:qID/answers/:aID/vote-:dir',
(req, res, next) => {
  // First Middleware
  if (req.params.dir.search(/^(up|down)$/) === -1) {
    // 如果 :dir 不是 up 或 down
    let err = new Error('Not Found(404)')
    err.status = 404
    next(err)
  } else {
    req.vote = req.params.dir   //  將 :dir 代到 req.vote 給下一個 middleware 用
    next()
  }
}, (req, res, next) => {
  // Second Middleware
  req.answer.vote(req.vote, function (err, question) {    // vote 這個是寫在 Model 的 instance method
    if (err) return next(err)
    res.json(question)
  })
})

// 1-2. 模組匯出
module.exports = router
{
  "name": "RESTAPI",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.17.1",
    "express": "^4.15.2",
    "mongoose": "^4.9.4",
    "morgan": "^1.8.1",
    "nodemon": "^1.11.0"
  }
}
// 1-1. 載入模組
const mongoose = require('mongoose')

// 2.建立 Schema
const Schema = mongoose.Schema

const AnswerSchema = new Schema({
  text: String,
  createdAt: {type: Date, default: Date.now},
  updatedAt: {type: Date, default: Date.now},
  votes: {type: Number, default: 0}
})

//  建立 instance method,這是用來 update Answer document
AnswerSchema.method('update', function (updates, callback) {
  // 這裡面的 this 指 document
  Object.assign(this, updates, {updatedAt: new Date()})
  this.parent().save(callback)
})

//  建立 instance method,這是 update vote 的值
AnswerSchema.method('vote', function (vote, callback) {
  if (vote === 'up') {
    this.votes += 1
  } else {
    this.votes -= 1
  }
  this.parent().save(callback)
})

// QuestionSchema 要寫在 AnswerSchema 後面,否則會出現錯誤
// "The #update method is not available on EmbeddedDocuments"
const QuestionSchema = new Schema({
  text: String,
  createdAt: {type: Date, default: Date.now},
  answers: [AnswerSchema]           //  告訴 mongoose AnswerSchema 會 nested in answers
})

// 建立排序資料的邏輯
const sortAnswers = function (a, b) {
  // 先根據投票數來排序(分數大的排上面)
  if (a.votes === b.votes) {
    // 如果投票數一樣,則根據更新時間排序(時間長的排上面)
    return b.updatedAt - a.updatedAt  // 由大排到小(大代表最近更新)
  }
  return b.votes - a.votes   //  由大排到小
}

// 使用 hook 讓每次儲存資料前都會排序資料
QuestionSchema.pre('save', function (next) {
  this.answers.sort(sortAnswers)  //  每次儲存資料前都會將資料排序
  next()
})

// 3.Compile 成 Model
const Question = mongoose.model('Question', QuestionSchema)

// 4. 匯出模組
module.exports.Question = Question
// 1-1.載入需要的模組
const express = require('express')
const routes = require('./routes')
const jsonParser = require('body-parser').json
const logger = require('morgan')
const mongoose = require('mongoose')

const app = express()

// 2.使用和 Parser 有關的 middleware

app.use(jsonParser())
app.use(logger('common'))  // 可以讓我們 Terminal 的 logger 變好看

// 3.和 MongoDB 連線

// -- 和 mongoDB 連線 --
mongoose.connect('mongodb://localhost:27017/bookworm')
const db = mongoose.connection               //  將連線的物件儲存,並可監聽事件

// -- 處理連線錯誤的情況 --
db.on('error', (err) => {
  console.error('connection error:', err)
})

// -- 成功連線要執行的動作 --
db.once('open', () => {
  console.log('db connection successful')
})

// 5.設定相關的 header 
app.use(function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*')    //  可以接受從任何 domain 過來的 request
  res.header('Access-Control-Allow-Header', 'Origin, X-Requested-With, Content-Type, Accept')
  if (req.method === 'OPTIONS') {
    // 如果是以 OPTIONS 的方式傳送 request(我們的路由並沒有處理這種 method)
    res.header('Access-Control-Allow-Methods', 'PUT, POST, DELETE')
    return res.stauts(200).json({})
  }
  next()
})


// 4. 連結路由
app.use('/questions', routes)   //  這個 middleware 只處理來自 '/questions' 的 URL

// 1-2. 錯誤處理

// 補捉 routes 沒處理到的錯誤
app.use((req, res, next) => {
  let err = new Error('Not found (404)')
  err.status = 404
  next(err)   //  將錯誤訊息傳送給 error handler
})


// Error Handler
// 當我們在 callback 中多代入 err 這個參數時,它會知道這個 error handler 而不是 middlerware
app.use((err, req, res, next) => {
  res.status(err.status || 500)    //  如果有給 err.status 則顯示,否則顯示 500(internal server error)
  res.json({
    error: {
      message: err.message
    }
  })
})


// 1-3. 監聽伺服器
const port = process.env.PORT || 3000
app.listen(port, () => {
  console.log('Express is listening on port ' + port + ' ...')
})