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 + ' ...')
})