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中文网其他相关文章!