Last active
April 13, 2017 01:13
-
-
Save pjchender/cf8e4c1efdfb0c2d178867df7136273f to your computer and use it in GitHub Desktop.
Build a REST API With Express @ Treehouse
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 + ' ...') | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
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)
})
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
app.js
[mongoDB 連線]
mongoose.connect('<mongoDBPath>')
來與 mongoDB 連線在
mongoose.connect('mongodb://localhost:27017/bookworm')
中const db = mongoose.connection
這個物件會發出相關的事件讓我們可以監聽或處理db.on('error', callback<err>)
db.once('open', callback)
[錯誤處理]
err
這個參數時,它會知道這個 error handler 而不是 middlerware捕捉 routes 沒處理到的錯誤
Error Handler
[監聽伺服器]
[response header 設定]