RESTful API 包含兩個主要概念:資源和表示。資源可以是與資料關聯的任何對象,或以 URI 標識(多個 URI 可以引用相同資源),並且可以使用 HTTP 方法進行操作。表示是顯示資源的方式。在本教程中,我們將介紹有關 RESTful API 設計的一些理論信息,並使用 NodeJS 實作範例部落格應用程式 API。
為 RESTful API 選擇正確的資源是設計的一個重要部分。首先,您需要分析您的業務領域,然後決定使用與您的業務需求相關的資源數量和類型。如果您正在設計部落格 API,您可能會使用文章、使用者和評論。這些是資源名稱,與之關聯的資料是資源本身:
{ "title": "How to Design RESTful API", "content": "RESTful API design is a very important case in the software development world.", "author": "huseyinbabal", "tags": [ "technology", "nodejs", "node-restify" ] "category": "NodeJS" }
確定所需資源後,您可以繼續進行資源操作。這裡的操作指的是HTTP方法。例如,為了建立一篇文章,您可以提出以下請求:
POST /articles HTTP/1.1 Host: localhost:3000 Content-Type: application/json { "title": "RESTful API Design with Restify", "slug": "restful-api-design-with-restify", "content": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", "author": "huseyinbabal" }
以相同的方式,您可以透過發出以下請求來查看現有文章:
GET /articles/123456789012 HTTP/1.1 Host: localhost:3000 Content-Type: application/json
更新現有文章怎麼樣?我聽到你在說:
我可以使用有效負載向 /articles/update/123456789012 發出另一個 POST 請求。
也許更好,但是 URI 變得越來越複雜。正如我們前面所說,操作可以引用HTTP方法。這意味著,在 HTTP 方法中宣告更新操作,而不是將其放入 URI 中。例如:
PUT /articles/123456789012 HTTP/1.1 Host: localhost:3000 Content-Type: application/json { "title": "Updated How to Design RESTful API", "content": "Updated RESTful API design is a very important case in the software development world.", "author": "huseyinbabal", "tags": [ "technology", "nodejs", "restify", "one more tag" ] "category": "NodeJS" }
順便說一下,在此範例中您會看到標籤和類別欄位。這些不需要是必填欄位。您可以將它們留空並在將來設置它們。
有時,您需要在文章過時時將其刪除。在這種情況下,您可以對 /articles/123456789012 使用DELETEHTTP 請求。
HTTP 方法是標準概念。如果您將它們用作操作,您將擁有簡單的 URI,而這種簡單的 API 將幫助您贏得滿意的消費者。
#如果您想在文章中插入評論怎麼辦?您可以選擇文章並為所選文章新增評論。透過使用此語句,您可以使用以下請求:
POST /articles/123456789012/comments HTTP/1.1 Host: localhost:3000 Content-Type: application/json { "text": "Wow! this is a good tutorial", "author": "john doe" }
以上形式的資源稱為子資源。 評論是文章的子資源。 上面的評論有效負載將作為文章的子項目插入到資料庫中。有時,不同的 URI 會引用相同的資源。例如,要查看特定評論,您可以使用:
GET /articles/123456789012/comments/123 HTTP/1.1 Host: localhost:3000 Content-Type: application/json
或:
GET /comments/123456789012 HTTP/1.1 Host: localhost:3000 Content-Type: application/json
一般來說,API 功能會經常更改,以便為消費者提供新功能。在這種情況下,同一 API 的兩個版本可以同時存在。為了分離這兩個功能,您可以使用版本控制。版本控制有兩種形式
/v1.1/articles/123456789012
。
GET /articles/123456789012 HTTP/1.1 Host: localhost:3000 Accept-Version: 1.0
實際上,版本改變的只是資源的表示形式,而不是資源的概念。因此,您不需要變更 URI 結構。在 v1.1 中,可能在 Article 中新增了一個新欄位。但是,它仍然返回一篇文章。在第二個選項中,URI 仍然很簡單,消費者不需要在客戶端實作中更改其 URI。
針對消費者不提供版本號碼的情況設計策略非常重要。當未提供版本時,您可以引發錯誤,也可以使用第一個版本回傳回應。如果您使用最新的穩定版本作為預設版本,消費者的用戶端實作可能會出現許多錯誤。
表示是 API 顯示資源的方式。當您呼叫 API 端點時,您將傳回一個資源。此資源可以是任何格式,例如 XML、JSON 等。如果您正在設計新的 API,則最好使用 JSON。但是,如果您要更新用於傳回 XML 回應的現有 API,則可以為 JSON 回應提供另一個版本。
有關 RESTful API 設計的理論資訊已經足夠了。讓我們透過使用 Restify 設計和實作部落格 API 來看看現實生活中的用法。
為了設計 RESTful API,我們需要分析業務領域。然後我們就可以定義我們的資源。在部落格 API 中,我們需要:
在此 API 中,我不會介紹如何驗證使用者身分以建立文章或評論。對於身份驗證部分,您可以參考 AngularJS & NodeJS 的基於令牌的身份驗證教學。
#我們的資源名稱已準備就緒。資源操作就是簡單的CRUD。您可以參考下表來了解 API 的整體展示。
資源名稱 | HTTP 動詞 | HTTP 方法 |
---|---|---|
文章 | 建立文章 更新文章 刪除文章 檢視文章 |
POST /articles with Payload PUT /articles/123 with Payload DELETE /articles/123 #GET /article/123 |
評論 | 建立評論 更新評論 刪除評論 查看評論 |
#帶負載的POST /articles/123/comments 帶負載的PUT /comments/123 刪除/comments/123 GET /comments/123 |
用戶 | 建立使用者 更新使用者 刪除使用者 檢視使用者 |
POST /users with Payload PUT /users/123 with Payload DELETE /users/123 GET /users/123 |
在此项目中,我们将使用 NodeJS 和 Restify。资源将保存在 MongoDB 数据库中。首先,我们可以在Restify中将资源定义为模型。
var mongoose = require("mongoose"); var Schema = mongoose.Schema; var ArticleSchema = new Schema({ title: String, slug: String, content: String, author: { type: String, ref: "User" } }); mongoose.model('Article', ArticleSchema);
var mongoose = require("mongoose"); var Schema = mongoose.Schema; var CommentSchema = new Schema({ text: String, article: { type: String, ref: "Article" }, author: { type: String, ref: "User" } }); mongoose.model('Comment', CommentSchema);
不会对用户资源进行任何操作。我们假设我们已经知道能够对文章或评论进行操作的当前用户。
您可能会问这个猫鼬模块来自哪里。它是作为 NodeJS 模块编写的最流行的 MongoDB ORM 框架。该模块包含在项目的另一个配置文件中。
现在我们可以为上述资源定义 HTTP 动词。您可以看到以下内容:
var restify = require('restify') , fs = require('fs') var controllers = {} , controllers_path = process.cwd() + '/app/controllers' fs.readdirSync(controllers_path).forEach(function (file) { if (file.indexOf('.js') != -1) { controllers[file.split('.')[0]] = require(controllers_path + '/' + file) } }) var server = restify.createServer(); server .use(restify.fullResponse()) .use(restify.bodyParser()) // Article Start server.post("/articles", controllers.article.createArticle) server.put("/articles/:id", controllers.article.updateArticle) server.del("/articles/:id", controllers.article.deleteArticle) server.get({path: "/articles/:id", version: "1.0.0"}, controllers.article.viewArticle) server.get({path: "/articles/:id", version: "2.0.0"}, controllers.article.viewArticle_v2) // Article End // Comment Start server.post("/comments", controllers.comment.createComment) server.put("/comments/:id", controllers.comment.viewComment) server.del("/comments/:id", controllers.comment.deleteComment) server.get("/comments/:id", controllers.comment.viewComment) // Comment End var port = process.env.PORT || 3000; server.listen(port, function (err) { if (err) console.error(err) else console.log('App is ready at : ' + port) }) if (process.env.environment == 'production') process.on('uncaughtException', function (err) { console.error(JSON.parse(JSON.stringify(err, ['stack', 'message', 'inner'], 2))) })
在此代码片段中,首先迭代包含控制器方法的所有控制器文件,并初始化所有控制器,以便执行对 URI 的特定请求。之后,为基本的CRUD操作定义了具体操作的URI。 Article 上的其中一项操作也有版本控制。
例如,如果您在 Accept-Version 标头中将版本声明为 2
,则将执行 viewArticle_v2
。 viewArticle
和 viewArticle_v2
都执行相同的工作,显示资源,但它们以不同的格式显示文章资源,正如您在 中看到的那样title
字段如下。最后,服务器在特定端口上启动,并应用一些错误报告检查。我们可以继续使用控制器方法对资源进行 HTTP 操作。
var mongoose = require('mongoose'), Article = mongoose.model("Article"), ObjectId = mongoose.Types.ObjectId exports.createArticle = function(req, res, next) { var articleModel = new Article(req.body); articleModel.save(function(err, article) { if (err) { res.status(500); res.json({ type: false, data: "Error occured: " + err }) } else { res.json({ type: true, data: article }) } }) } exports.viewArticle = function(req, res, next) { Article.findById(new ObjectId(req.params.id), function(err, article) { if (err) { res.status(500); res.json({ type: false, data: "Error occured: " + err }) } else { if (article) { res.json({ type: true, data: article }) } else { res.json({ type: false, data: "Article: " + req.params.id + " not found" }) } } }) } exports.viewArticle_v2 = function(req, res, next) { Article.findById(new ObjectId(req.params.id), function(err, article) { if (err) { res.status(500); res.json({ type: false, data: "Error occured: " + err }) } else { if (article) { article.title = article.title + " v2" res.json({ type: true, data: article }) } else { res.json({ type: false, data: "Article: " + req.params.id + " not found" }) } } }) } exports.updateArticle = function(req, res, next) { var updatedArticleModel = new Article(req.body); Article.findByIdAndUpdate(new ObjectId(req.params.id), updatedArticleModel, function(err, article) { if (err) { res.status(500); res.json({ type: false, data: "Error occured: " + err }) } else { if (article) { res.json({ type: true, data: article }) } else { res.json({ type: false, data: "Article: " + req.params.id + " not found" }) } } }) } exports.deleteArticle = function(req, res, next) { Article.findByIdAndRemove(new Object(req.params.id), function(err, article) { if (err) { res.status(500); res.json({ type: false, data: "Error occured: " + err }) } else { res.json({ type: true, data: "Article: " + req.params.id + " deleted successfully" }) } }) }
您可以在下面找到 Mongoose 端基本 CRUD 操作的说明:
articleModel
的简单保存操作。可以通过将请求正文作为构造函数传递给模型来创建新模型,例如 vararticleModel = new Article(req.body)
。 findOne
带有 ID 参数足以返回文章详细信息。save
命令将更新后的模型保存到数据库中。findByIdAndRemove
是通过提供文章 ID 来删除文章的最佳方法。上面提到的 Mongoose 命令只是通过 Article 对象进行静态方法,该对象也是 Mongoose 模式的引用。
var mongoose = require('mongoose'), Comment = mongoose.model("Comment"), Article = mongoose.model("Article"), ObjectId = mongoose.Types.ObjectId exports.viewComment = function(req, res) { Article.findOne({"comments._id": new ObjectId(req.params.id)}, {"comments.$": 1}, function(err, comment) { if (err) { res.status(500); res.json({ type: false, data: "Error occured: " + err }) } else { if (comment) { res.json({ type: true, data: new Comment(comment.comments[0]) }) } else { res.json({ type: false, data: "Comment: " + req.params.id + " not found" }) } } }) } exports.updateComment = function(req, res, next) { var updatedCommentModel = new Comment(req.body); console.log(updatedCommentModel) Article.update( {"comments._id": new ObjectId(req.params.id)}, {"$set": {"comments.$.text": updatedCommentModel.text, "comments.$.author": updatedCommentModel.author}}, function(err) { if (err) { res.status(500); res.json({ type: false, data: "Error occured: " + err }) } else { res.json({ type: true, data: "Comment: " + req.params.id + " updated" }) } }) } exports.deleteComment = function(req, res, next) { Article.findOneAndUpdate({"comments._id": new ObjectId(req.params.id)}, {"$pull": {"comments": {"_id": new ObjectId(req.params.id)}}}, function(err, article) { if (err) { res.status(500); res.json({ type: false, data: "Error occured: " + err }) } else { if (article) { res.json({ type: true, data: article }) } else { res.json({ type: false, data: "Comment: " + req.params.id + " not found" }) } } }) }
当您向某个资源 URI 发出请求时,控制器中声明的相关函数将被执行。控制器文件中的每个函数都可以使用 req 和 res 对象。这里的评论资源是文章的子资源。 所有的查询操作都是通过Article模型进行的,以便找到子文档并进行必要的更新。但是,每当您尝试查看 Comment 资源时,即使 MongoDB 中没有集合,您也会看到一个 Comment 资源。
/articles/123
(好),/articles?id=123
(差)。最后,如果您按照这些基本规则设计 RESTful API,您将始终拥有一个灵活、可维护、易于理解的系统。
以上是使用NodeJS和Restify設計RESTful API的詳細內容。更多資訊請關注PHP中文網其他相關文章!