首页 >web前端 >js教程 >使用NodeJS和Restify设计RESTful API

使用NodeJS和Restify设计RESTful API

WBOY
WBOY原创
2023-09-03 10:37:011616浏览

使用NodeJS和Restify设计RESTful API

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 的两个版本可以同时存在。为了分离这两个功能,您可以使用版本控制。版本控制有两种形式

  1. URI 中的版本:您可以在 URI 中提供版本号。例如,/v1.1/articles/123456789012 
  2. 标头中的版本:在标头中提供版本号,并且切勿更改 URI。 例如:
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 来看看现实生活中的用法。

博客 REST 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

项目设置

在此项目中,我们将使用 NodeJSRestify。资源将保存在 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_v2viewArticleviewArticle_v2 都执行相同的工作,显示资源,但它们以不同的格式显示文章资源,正如您在 中看到的那样title 字段如下。最后,服务器在特定端口上启动,并应用一些错误报告检查。我们可以继续使用控制器方法对资源进行 HTTP 操作。

article.js

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 操作的说明:

  • createArticle:这是对从请求正文发送的 articleModel 的简单保存操作。可以通过将请求正文作为构造函数传递给模型来创建新模型,例如 vararticleModel = new Article(req.body)
  • viewArticle:为了查看文章详细信息,URL 参数中需要提供文章 ID。 findOne 带有 ID 参数足以返回文章详细信息。
  • updateArticle:文章更新是一个简单的查找查询并对返回的文章进行一些数据操作。最后,需要通过发出 save 命令将更新后的模型保存到数据库中。
  • deleteArticle:findByIdAndRemove 是通过提供文章 ID 来删除文章的最佳方法。

上面提到的 Mongoose 命令只是通过 Article 对象进行静态方法,该对象也是 Mongoose 模式的引用。

comment.js

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 发出请求时,控制器中声明的相关函数将被执行。控制器文件中的每个函数都可以使用 reqres 对象。这里的评论资源是文章的子资源。 所有的查询操作都是通过Article模型进行的,以便找到子文档并进行必要的更新。但是,每当您尝试查看 Comment 资源时,即使 MongoDB 中没有集合,您也会看到一个 Comment 资源。

其他设计建议

  • 选择易于理解的资源,以便消费者轻松使用。
  • 让业务逻辑由消费者实现。例如,文章资源有一个名为 slug 的字段。 消费者不需要将此详细信息发送到 REST API。这种 slug 策略应该在 REST API 端进行管理,以减少 API 和消费者之间的耦合。消费者只需要发送标题详细信息,您就可以在REST API端根据您的业务需求生成slug。
  • 为您的 API 端点实施授权层。未经授权的消费者可以访问属于其他用户的受限数据。在本教程中,我们没有介绍用户资源,但您可以参阅使用 AngularJS 和 NodeJS 进行基于令牌的身份验证,以获取有关 API 身份验证的更多信息。
  • 用户 URI 而不是查询字符串。 /articles/123  (好),/articles?id=123(差)。
  • 不保留状态;始终使用即时输入/输出。
  • 使用名词来表示您的资源。您可以使用 HTTP 方法来操作资源。

最后,如果您按照这些基本规则设计 RESTful API,您将始终拥有一个灵活、可维护、易于理解的系统。

以上是使用NodeJS和Restify设计RESTful API的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn