Skip to content

Instantly share code, notes, and snippets.

@pjchender
Last active April 13, 2017 01:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pjchender/cf8e4c1efdfb0c2d178867df7136273f to your computer and use it in GitHub Desktop.
Save pjchender/cf8e4c1efdfb0c2d178867df7136273f to your computer and use it in GitHub Desktop.
Build a REST API With Express @ Treehouse
// 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 + ' ...')
})
// 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
{
"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 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
@pjchender
Copy link
Author

pjchender commented Apr 12, 2017

app.js

[mongoDB 連線]

  • 透過 mongoose.connect('<mongoDBPath>') 來與 mongoDB 連線

mongoose.connect('mongodb://localhost:27017/bookworm')

  • mongoDB 預設的 port 是 27017
  • bookworm 則是 database

const db = mongoose.connection 這個物件會發出相關的事件讓我們可以監聽或處理

  • 處理錯誤連線情況 db.on('error', callback<err>)
  • 成功連線時 db.once('open', callback)
// -- 1. 和 mongoDB 連線 --
mongoose.connect('mongodb://localhost:27017/bookworm')
var db = mongoose.connection         
// --  2. 處理連線錯誤的情況 --
db.on('error', (err) => {
  console.error('connection error:', err)
})
// --  3. 成功連線要執行的動作 --
db.once('open', () => {
  console.log('db connection successful')
})

[錯誤處理]

  • 在 Express 中,當遇到錯誤時,它會停止整個程式,然後將錯誤找到最適合的 error handler 來處理。
  • 我們通常會把 Error Hanlder 寫在最後一個 middleware ,透過它來補捉任何沒有被 routes 所處理的 requests。
  • 如果沒有自訂 Error Handler 則會用預設的 error handler 來呈現(就是醜醜的樣子)
  • 當我們在 callback 中多代入 err 這個參數時,它會知道這個 error handler 而不是 middlerware

捕捉 routes 沒處理到的錯誤

// catch 404 and forward to error handler
app.use((req, res, next) => {
  let err = new Error('Not found (404)')
  err.status = 404
  next(err)   //  將錯誤訊息傳送給 error handler
})

Error Handler

// Error Handler
app.use((err, req, res, next) => {
  res.status(err.status || 500)    //  如果有給 err.status 則顯示,否則顯示 500(internal server error)
  res.json({
    error: {
      message: err.message
    }
  })
})

[監聽伺服器]

const port = process.env.PORT || 3000
app.listen(port, () => {
  console.log('Express is listening on port ' + port + ' ...')
})

[response header 設定]

// Some Header to Set
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()
})

@pjchender
Copy link
Author

pjchender commented Apr 12, 2017

models.js

[collection 的名稱]

當我們使用 const Question = mongoose.model('Question', QuestionSchema) 時,會在 db 中建立一個名為 questions 的 collection。

[Middleware]

利用 Middleware (pre, post hook) 來達到在儲存資料前做某些行為

var schema = new Schema(..);
schema.pre('save', function(next) {
  // do stuff
  next();
});

[嵌套模型]

  • 利用 createdAt:{type: Data, default: Date.now} 可以在新增資料時存入時間
  • 利用 answers: [AnswerSchema] 可以讓某個 document 嵌套在另一個 document 中
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}
})

const QuestionSchema = new Schema({
  text: String,
  createdAt: {type: Date, default: Date.now},
  answers: [AnswerSchema]           //  告訴 mongoose answers 會 nested in AnswerSchema
})

[instance method]

  • 利用 instance method 可以讓 document 使用某個方法
  • 利用 this.parent().save(callback) 可以執行儲存 document 的動作
AnswerSchema.method('vote', function (vote, callback) {
  if (vote === 'up') {
    this.votes += 1
  } else {
    this.votes -= 1
  }
  this.parent().save(callback)
})

@pjchender
Copy link
Author

pjchender commented Apr 12, 2017

routes.js

[根目錄]

  • 因為在 app.js 中是套用 app.use('/qustions', routes),所以這個 router 的根目錄為 '/questions'
  • 在 Express 的 router 中間可以加入自己的 callback function,它會依序執行
  • router.get('/', <callback1>, <callback2>, (req, res, next) => {})

[針對特定 params]

  • 如果針對特定的 params 需要進行前處理,可以使用 router.param(':para', callback<req, res, next, id>),其中 callback 的 id 指的是 URL params 的值,這個方法會在當 URL 中帶有該 params 時就執行
  • 透過 req.anyThing 可以讓這個變數在其他 middleware 被使用
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()
  })
})

[middleware]

routes 中可以接不只一個 middleware,例如,router.post('<path>', <middleware1>, <middleware2>, ...)

[不解]

QuestionSchema 要寫在 AnswerSchema.method(...) 後面,否則會出現錯誤 "The #update method is not available on EmbeddedDocuments"

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
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment