# RESTful Routing

# [Lecture] Intro to REST

# What is REST/RESTful?

表現層狀態轉換(REST, Representational State Transfer) 是一種網路架構的風格,具有此種風格的系統可以稱為是 RESTful 的,或者更直白地說:「將 URL 定位資源,以 HTTP 協議所定義的 GETPOSTDELETE 等請求來描述操作」。在這樣的基礎之下,可以直觀地從 URL 名稱、發送的請求以及請求所得到的狀態碼就知道做了什麼操作?結果如何?

# URL Design

RESTful 的核心思想就是讓用戶端發送的請求操作都具備有「動詞 + 受詞」的結構,其中「動詞」的部分透過常用的 HTTP 方法來實踐,對應 CRUD 操作:

  • GET:讀取(Read)
  • POST:創建(Create)
  • PUT:更新(Update)
  • PATCH:更新(Update),通常是部分更新
  • DELETE:刪除(Delete)

而「受詞」的部分就是 API 的 URL,這部份透過設置路由來完成。

關於 RESTful 的解釋與說明,很建議查看這篇問題下的回答 知乎 | 怎样用通俗的语言解释REST,以及RESTful?阮一峰的网络日志 | RESTful API 最佳实践

# A Table of all 7 RESTful routes

承上所述,所謂的 REST 就是將 HTTP 路由與 CRUD 操作進行對應,

Name Path HTTP Verb. Purpose
INDEX /dogs/ GET List all dogs.
NEW /dogs/new GET Show new dog form.
CREATE /dogs/ POST Create a new dog, then redirect somewhere.
SHOW /dogs/:id GET Show info about one specific dog.
EDIT /dogs/:id/edit GET Shoe edit form for one dog.
UPDATE /dogs/:id PUT Update a particular dog, then redirect somewhere.
DESTORY /dogs/:id DELETE Delete a particular dog, then redirect somewhere.

# [Lecture] RESTful Blog App: INDEX

# Create Project

首先創建專案資料夾並安裝套件:

# Create Project Folder
$ mkdir BlogApp

# npm init and install packages
$ cd BlogApp
$ npm init
$ npm install express mongoose body-parser --save
$ npm install ejs --save

# Initiate Views File

創建頁面:

$ mkdir views
$ touch views/index.ejs

# Initiate app.js

// Import dependencies
const bodyParser = require('body-parser'),
      mongoose   = require('mongoose'),
      express    = require('express'),
      app        = express();

// Application Config
mongoose.connect('mongodb://localhost:27017/blog_app', {useNewUrlParser: true});
app.set("view engine", "ejs");
app.use(express.static("public"));
app.use(bodyParser.urlencoded({extended: true}));

// Database Schema Config
const blogSchema = new mongoose.Schema({
  title: String,
  image: String,
  body: String,
  created: {type: Date, default: Date.now}
});

const Blog = mongoose.model("Blog", blogSchema);

// // Add content to DATABASE
// Blog.create({
//   title: "Test Blog",
//   image: "https://images.unsplash.com/photo-1554365228-f051bbfbcab0?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80",
//   body: "HELLO, THIS IS A BLOG POST!"
// });

// Index Routes
app.get('/', function (req, res) {
  res.redirect('/blogs');
});

app.get("/blogs", function(req, res) {
  Blog.find({}, function(err, blogs) {
    if (err) {
      console.log("Error");
    } else {
      res.render("index", {blogs: blogs});
    }
  });
});

app.listen(process.env.PORT, process.env.IP, function() {
  console.log("SERVER IS RUNNING!");
});

# [Lecture] Blog App: Layout

# Create Folders and Files

$ mkdir views/partials
$ touch views/partials/header.ejs
$ touch views/partials/footer.ejs

# header.ejs

<html>
  <head>
    <title>Blog App</title>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
    <link rel="stylesheet" type="text/css" href="/stylesheets/app.css">
  </head>
  <body>
    <div class="ui fixed inverted menu">
      <div class="ui container">
        <div class="header item"><i class="code icon"></i>Blog Site</div>
        <a href="/" class="item">Home</a>
        <a href="/blogs/new" class="item">New Post</a>
      </div>
    </div>

# index.ejs

<% include ./partials/header %>

<div class="ui main text container">
  <div class="ui huge header">RESTful Blog App</div>
  <div class="ui top attached segment">
    <div class="ui divided items">
      <% blogs.forEach(function(blog){ %>
        <div class="item">
          <div class="image">
           <img src="<%= blog.image %>">
          </div>
          <div class="content">
            <a class="header" href="/blogs/<%= blog._id %>"><%= blog.title %></a>
            <div class="meta">
              <span><%= blog.created.toDateString() %></span>
            </div>
            <div class="description">
              <p><%- blog.body.substring(0, 100) %>...</p>
            </div>
            <div class="extra">
              <a class="ui floated basic violet button" href="/blogs/<%= blog._id %>">
                Read More<i class="right chevron icon"></i>
              </a>
            </div>
          </div>
        </div>
      <% }) %>
    </div>
  </div>
</div>

<% include ./partials/footer %>

# app.css

i.icon {
    font-size: 2em;
}

.container.main {
    margin-top: 7.0em;
}

#delete {
    display: inline;
}

# [Lecture] Note about RESTful Blog App: New and Create

在下一個課程中,講師將會介紹一種新的方式來透過 <form> 表單提交資料到伺服器端,在以往我們會使用 name 屬性:

<input type="text" name="title">

新的使用方法會是:

<input type="text" name="blog[title]"> 

前一種方法要取值時使用 req.body.title,而後者則為 req.body.blog.title

# [Lecture] RESTful Blog App: NEW and CREATE

# Add Routers

// NEW ROUTE
app.get("/blogs/new", function(req, res) {
  res.render("new");
});

// CREATE ROUTE
app.post("/blogs", function(req, res) {
  Blog.create(req.body.blog, function(err, newBlog) {
    if (err) {
      res.render("new");
    } else {
      res.redirect("/blogs");
    }
  });
});

# new.ejs

<% include ./partials/header %>

<div class="ui main text container segment">
  <div class="ui huge header">New Blog</div>
  <form class="ui form" action="/blogs" method="POST">
    <div class="field">
      <label>Title</label>
      <input type="text" name="blog[title]" placeholder="Title">
    </div>
    <div class="field">
      <label>Image</label>
      <input type="text" name="blog[image]" placeholder="Image">
    </div>
    <div class="field">
      <label>Blog Content</label>
      <textarea name="blog[body]"></textarea>
    </div>
    <input class="ui violet big basic button" type="submit">
  </form>
</div>

<% include ./partials/footer %>

# [Lecture] Note about RESTful Blog App: SHOW

在下一個課程中,講師將會介紹使用字串的 substring() 方法來將文字進行略縮,如果採用此種方法可能會返回 Cannot read property 'substring' of undefined 錯誤,這將使一筆不含 body 的資料存入資料庫中,只需要將此筆資料進行刪除即可。

除此之外,關於文件層級:

  • / 表示根目錄
  • ./ 表示當前目錄
  • ../ 表示上一層目錄

# [Lecture] RESTful Blog App: SHOW

# Add Router

// SHOW ROUTE
app.get("/blogs/:id", function(req, res) {
    Blog.findById(req.params.id, function(err, foundBlog) {
        if (err) {
            res.redirect("/blogs");
        } else {
            res.render("show", {blog: foundBlog});
        }
    });
});

# show.ejs

<% include ./partials/header %>

<div class="ui main text container segment">
  <div class="ui huge header"><%= blog.title %></div>
  <div class="ui top attached">
    <div class="item">
      <img class="ui centered rounded image" src="<%= blog.image %>">
      <div class="content">
        <span><%= blog.created.toDateString() %></span>
      </div>
      <div class="description">
        <p><%- blog.body %></p> 
      </div>
      <a class="ui orange basic button" href="/blogs/<%= blog._id %>/edit">Edit</a>
      <form id="delete" action="/blogs/<%= blog._id %>?_method=DELETE" method="POST">
        <button class="ui red basic button">Delete</button>
      </form>
    </div>
  </div>
</div>

<% include ./partials/footer %>

# [Lecture] RESTful Blog App: EDIT AND UPDATE

# Add Routers


# Method Override

在 HTML 中並沒有提供 PUT 方法和 DELETE 方法,詳細內容可以參考 StackExchange | Why are there are no PUT and DELETE methods on HTML forms? 這篇回答的內容。因此我們必須使用 method-override 來替我們將 GETPOST 請求改成其他 HTTP 動詞,在此之前必須安裝 method-override 套件:

$ npm install method-override --save

然後就可以在表單中加入 _method 的隱藏輸入控件(也可以使用其他名稱)來承載實際需要的 HTTP 動詞,透過伺服器端對這個控件的動詞進行改寫。

# edit.ejs

<% include ./partials/header %>

<div class="ui main text container segment">
  <div class="ui huge header">Edit <%= blog.title %></div>
  <form class="ui form" action="/blogs/<%= blog._id %>?_method=PUT" method="POST">
    <div class="field">
      <label>Title</label>
      <input type="text" name="blog[title]" value= "<%= blog.title %>">
    </div>
    <div class="field">
      <label>Image</label>
      <input type="text" name="blog[image]" value= "<%= blog.image %>">
    </div>
    <div class="field">
      <label>Blog Content</label>
      <textarea name="blog[body]"><%= blog.body %></textarea>
    </div>
    <input class="ui violet big basic button" type="submit">
  </form>
</div>

<% include ./partials/footer %>

# [Lecture] RESTful Blog App: DESTROY

// DESTROY ROUTE
app.delete("/blogs/:id", function(req, res) {
  Blog.findByIdAndRemove(req.params.id, function(err) {
    if (err) {
      res.redirect("/blogs");
    } else {
      res.redirect("/blogs");
    }
  });
});

# [Lecture] Note about RESTful Blog App: Final Touches

# [Lecture] RESTful Blog App: Final Touches

# Sanitizer

為了確保安全性及避免 XSS 注入攻擊,使用 express-sanitizer 套件來去除含有惡意 JavaScript 代碼的內容:

# Install express-sanitizer with npm
$ npm install express-sanitizer --save

# Final app.js

// Import dependencies
const bodyParser       = require('body-parser'),
      methodOverride   = require("method-override"),
      expressSanitizer = require("express-sanitizer"),
      mongoose         = require('mongoose'),
      express          = require('express'),
      app              = express();

// Application Config
mongoose.connect('mongodb://localhost:27017/blog_app', {useNewUrlParser: true});
app.set("view engine", "ejs");
app.use(express.static("public"));
app.use(bodyParser.urlencoded({extended: true}));
app.use(methodOverride("_method"));
app.use(expressSanitizer());

// Database Schema Config
const blogSchema = new mongoose.Schema({
  title: String,
  image: String,
  body: String,
  created: {type: Date, default: Date.now}
});

const Blog = mongoose.model("Blog", blogSchema);

// // Add content to DATABASE
// Blog.create({
//   title: "Test Blog",
//   image: "https://images.unsplash.com/photo-1554365228-f051bbfbcab0?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80",
//   body: "HELLO, THIS IS A BLOG POST!"
// });

// INDEX ROUTER
app.get('/', function (req, res) {
  res.redirect('/blogs');
});

app.get("/blogs", function(req, res) {
  Blog.find({}, function(err, blogs) {
    if (err) {
      console.log("Error");
    } else {
      res.render("index", {blogs: blogs});
    }
  });
});

// NEW ROUTE
app.get("/blogs/new", function(req, res) {
  res.render("new");
});

// CREATE ROUTE
app.post("/blogs", function(req, res) {
  req.body.blog.body = req.sanitize(req.body.blog.body);
  Blog.create(req.body.blog, function(err, newBlog) {
    if (err) {
      res.render("new");
    } else {
      res.redirect("/blogs");
    }
  });
});

// SHOW ROUTE
app.get("/blogs/:id", function(req, res) {
  Blog.findById(req.params.id, function(err, foundBlog) {
    if (err) {
      res.redirect("/blogs");
    } else {
      res.render("show", {blog: foundBlog});
    }
  });
});

// EDIT ROUTE
app.get("/blogs/:id/edit", function(req, res) {
  Blog.findById(req.params.id, function(err, foundBlog) {
    if (err) {
      res.redirect("/blogs");
    } else {
      res.render("edit", {blog: foundBlog});  
    }
  });
});

// UPDATE ROUTE
app.put("/blogs/:id", function(req, res) {
  req.body.blog.body = req.sanitize(req.body.blog.body);
  Blog.findByIdAndUpdate(req.params.id, req.body.blog, function(err, updatedBlog) {
    if (err) {
      res.redirect("/blogs");
    } else {
      res.redirect("/blogs/" + req.params.id);
    }
  });
});

// DESTROY ROUTE
app.delete("/blogs/:id", function(req, res) {
  Blog.findByIdAndRemove(req.params.id, function(err) {
    if (err) {
      res.redirect("/blogs");
    } else {
      res.redirect("/blogs");
    }
  });
});

app.listen(process.env.PORT, process.env.IP, function() {
  console.log("SERVER IS RUNNING!");
});
Last Updated: 12/15/2020, 10:27:30 PM