首頁  >  文章  >  web前端  >  使用NodeJS和Restify設計RESTful API

使用NodeJS和Restify設計RESTful API

WBOY
WBOY原創
2023-09-03 10:37:011591瀏覽

使用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