RESTful API Design With NodeJS & Restify

http://code.tutsplus.com/tutorials/restful-api-design-with-nodejs-restify--cms-22637

The RESTful API consists of two main concepts: Resource, and Representation. Resource can be any object associated with data, or identified with a URI (more than one URI can refer to the same resource), and can be operated using HTTP methods. Representation is the way you display the resource. In this tutorial we will cover some theoretical information about RESTful API design, and implement an example blogging application API by using NodeJS.

Resource

Choosing the correct resources for a RESTful API is an important section of designing. First of all, you need to analyze your business domain and then decide how many and what kind of resources will be used that are relevant to your business need. If you are designing a blogging API, you will probably use ArticleUser, and Comment. Those are the resource names, and the data associated with that is the resource itself:


01

02

03

04

05

06

07

08

09

10

11

{

    "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"

}

Resource Verbs

You can proceed with a resource operation after you have decided on the required resources. Operation here refers to HTTP methods. For example, in order to create an article, you can make the following request:


01

02

03

04

05

06

07

08

09

10

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"

}

In the same way, you can view an existing article by issuing the following request:


1

2

3

GET /articles/123456789012 HTTP/1.1

Host: localhost:3000

Content-Type: application/json

What about updating an existing article? I can hear that you are saying:

I can make another POST request to /articles/update/123456789012 with the payload.

Maybe preferable, but the URI is becoming more complex. As we said earlier, operations can refer to HTTP methods. This means, state the update operation in the HTTP method instead of putting that in the URI. For example:


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

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"

}

By the way, in this example you see tags and category fields. Those don‘t need to be mandatory fields. You can leave them blank and set them in future.

Sometimes, you need to delete an article when it is outdated. In that case you can use a DELETE HTTP request to /articles/123456789012.

HTTP methods are standard concepts. If you use them as an operation, you will have simple URIs, and this kind of simple API will help you gain happy consumers.

What if you want to insert a comment to an article? You can select the article and add a new comment to the selected article. By using this statement, you can use the following request:


1

2

3

4

5

6

7

POST /articles/123456789012/comments HTTP/1.1

Host: localhost:3000

Content-Type: application/json

{

    "text": "Wow! this is a good tutorial",

    "author": "john doe"

}

The above form of resource is called as a sub-resource. Comment is a sub-resource of Article.The Comment payload above will be inserted in the database as a child of Article. Sometimes, a different URI refers to the same resource. For example, to view a specific comment, you can use either:


1

2

3

GET /articles/123456789012/comments/123 HTTP/1.1

Host: localhost:3000

Content-Type: application/json

or:


1

2

3

GET /comments/123456789012 HTTP/1.1

Host: localhost:3000

Content-Type: application/json

Versioning

In general, API features change frequently in order to provide new features to consumers. In that case, two versions of the same API can exist at the same time. In order to separate those two features, you can use versioning. There are two forms of versioning

  1. Version in URI: You can provide the version number in the URI. For example, /v1.1/articles/123456789012. 
  2. Version in Header: Provide the version number in the header, and never change the URI. For example:

1

2

3

GET /articles/123456789012 HTTP/1.1

Host: localhost:3000

Accept-Version: 1.0

Actually, the version changes only the representation of the resource, not the concept of the resource. So, you do not need to change the URI structure. In v1.1, maybe a new field was added to Article. However, it still returns an article. In the second option, the URI is still simple and consumers do not need to change their URI in client-side implementations.

It is important to design a strategy for situations where the consumer does not provide a version number. You can raise an error when version is not provided, or you can return a response by using the first version. If you use the latest stable version as a default, consumers can get many errors for their client-side implementations.

Representation

Representation is the way that an API displays the resource. When you call an API endpoint, you will get returned a resource. This resource can be in any format like XML, JSON, etc. JSON is preferable if you are designing a new API. However, if you are updating an existing API that used to return an XML response, you can provide another version for a JSON response.

That‘s enough theoretical information about RESTful API design. Let‘s have a look at real life usage by designing and implementing a Blogging API using Restify.

Blogging REST API

Design

In order to design a RESTful API, we need to analyze the business domain. Then we can define our resources. In a Blogging API, we need:

  • Create, Update, Delete, View Article
  • Create a comment for a specific Article, Update, Delete, View, Comment
  • Create, Update, Delete, View User

In this API, I will not cover how to authenticate a user in order to create an article or comment. For the authentication part, you can refer to the Token-Based Authentication with AngularJS & NodeJS tutorial.

Our resource names are ready. Resource operations are simply CRUD. You can refer to the following table for a general showcase of API.

Resource Name HTTP Verbs HTTP Methods
Article create Article
update Article
delete Article
view Article
POST /articles with Payload
PUT /articles/123 with Payload
DELETE /articles/123
GET /article/123
Comment create Comment
update Coment
delete Comment
view Comment
POST /articles/123/comments with Payload
PUT /comments/123 with Payload
DELETE /comments/123
GET /comments/123
User create User
update User
delete User
view User
POST /users with Payload
PUT /users/123 with Payload
DELETE /users/123
GET /users/123

Advertisement

Project Setup

In this project we will use NodeJS with Restify. The resources will be saved in the MongoDB database. First of all, we can define resources as models in Restify.

Article


01

02

03

04

05

06

07

08

09

10

11

12

13

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);

Comment


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

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);

User

There won‘t be any operation for the User resource. We will assume that we already know the current user who will be able to operate on articles or comments.

You may ask where this mongoose module comes from. It is the most popular ORM framework for MongoDB written as a NodeJS module. This module is included in the project within another config file.

Now we can define our HTTP verbs for the above resources. You can see the following:


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

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)))

    })

In this code snippet, first of all the controller files that contain controller methods are iterated and all the controllers are initialized in order to execute a specific request to the URI. After that, URIs for specific operations are defined for basic CRUD operations. There is also versioning for one of the operations on Article.

For example, if you state version as 2 in Accept-Version header, viewArticle_v2 will be executed.viewArticle and viewArticle_v2 both do the same job, showing the resource, but they show Article resource in a different format, as you can see in the title field below. Finally, the server is started on a specific port, and some error reporting checks are applied. We can proceed with controller methods for HTTP operations on resources.

article.js


001

002

003

004

005

006

007

008

009

010

011

012

013

014

015

016

017

018

019

020

021

022

023

024

025

026

027

028

029

030

031

032

033

034

035

036

037

038

039

040

041

042

043

044

045

046

047

048

049

050

051

052

053

054

055

056

057

058

059

060

061

062

063

064

065

066

067

068

069

070

071

072

073

074

075

076

077

078

079

080

081

082

083

084

085

086

087

088

089

090

091

092

093

094

095

096

097

098

099

100

101

102

103

104

105

106

107

108

109

110

111

112

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"

            })

        }

    })

}

You can find an explanation of basic CRUD operations on the Mongoose side below:

  • createArticle: This is a simple save operation on articleModel sent from the request body. A new model can be created by passing the request body as a constructor to a model like var articleModel = new Article(req.body).
  • viewArticle: In order to view article detail, an article ID is needed in the URL parameter. findOne with an ID parameter is enough to return article detail.
  • updateArticle: Article update is a simple find query and some data manipulation on the returned article. Finally, the updated model needs to be saved to the database by issuing a save command.
  • deleteArticle: findByIdAndRemove is the best way to delete an article by providing the article ID.

The Mongoose commands mentioned above are simply static like method through Article object that is also a reference of the Mongoose schema.

comment.js


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

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"

                })

            }

        }

    })

}

When you make a request to one of the resource URIs, the related function stated in the controller will be executed. Every function inside the controller files can use the req and res objects. The comment resource here is a sub-resource of Article. All the query operations are made through the Article model in order to find a sub-document and make the necessary update. However, whenever you try to view a Comment resource, you will see one even if there is no collection in MongoDB.

Other Design Suggestions

  • Select easy-to-understand resources in order to provide easy usage to consumers.
  • Let business logic be implemented by consumers. For example, the Article resource has a field calledslug. Consumers do not need to send this detail to the REST API. This slug strategy should manage on the REST API side to reduce coupling between API and consumers. Consumers only need to send title detail, and you can generate the slug according to your business needs on the REST API side.
  • Implement an authorization layer for your API endpoints. Unauthorized consumers can access restricted data that belongs to another user. In this tutorial, we did not cover the User resource, but you can refer to Token Based Authentication with AngularJS & NodeJS for more information about API authentications.
  • User URI instead of query string. /articles/123  (Good), /articles?id=123 (Bad).
  • Do not keep the state; always use instant input/output.
  • Use noun for your resources. You can use HTTP methods in order to operate on resources.

Finally, if you design a RESTful API by following these fundamental rules, you will always have a flexible, maintainable, easily understandable system.

时间: 2024-10-12 15:19:58

RESTful API Design With NodeJS & Restify的相关文章

【转载】RESTful API 设计指南

作者: 阮一峰 日期: 2014年5月22日 网络应用程序,分为前端和后端两个部分.当前的发展趋势,就是前端设备层出不穷(手机.平板.桌面电脑.其他专用设备......). 因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信.这导致API构架的流行,甚至出现"API First"的设计思想.RESTful API是目前比较成熟的一套互联网应用程序的API设计理论.我以前写过一篇<理解RESTful架构>,探讨如何理解这个概念. 今天,我将介绍RESTful API

RESTful API的设计原则

最近一直在做公司的一个API平台的项目,前后大约有半年多了,中间穿插了好多其他的项目一起做的.上周经理要求写文档,我就重新打开项目开始检阅之前的代码,发现好多地方当初设计的并不合理,忽然就想到,一个好的API平台,应该怎么来设计呢?有哪些规范要遵守呢?面对自己的项目,感觉好多地方都要改,但是已经有人在用了,怎么办?全都要改动吗?所以就上网找解决方案,然后就发现一精品贴,现转载过来,以备不时查阅. 原文地址:http://www.cnblogs.com/moonz-wu/p/4211626.htm

好RESTful API的设计原则

说在前面,这篇文章是无意中发现的,因为感觉写的很好,所以翻译了一下.由于英文水平有限,难免有出错的地方,请看官理解一下.翻译和校正文章花了我大约2周的业余时间,如有人愿意转载请注明出处,谢谢^_^ Principles of good RESTful API Design 好RESTful API的设计原则 Good API design is hard! An API represents a contract between you and those who Consume your da

RESTful API 设计指南【转】

网络应用程序,分为前端和后端两个部分.当前的发展趋势,就是前端设备层出不穷(手机.平板.桌面电脑.其他专用设备......). 因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信.这导致API构架的流行,甚至出现"API First"的设计思想.RESTful API是目前比较成熟的一套互联网应用程序的API设计理论.我以前写过一篇<理解RESTful架构>,探讨如何理解这个概念. 今天,我将介绍RESTful API的设计细节,探讨如何设计一套合理.好用的API

RESTful API 最佳实践(转)

原文:http://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html 阮一峰老师的文章,他的文章把难懂的东西讲的易懂 RESTful 是目前最流行的 API 设计规范,用于 Web 数据接口的设计. 它的大原则容易把握,但是细节不容易做对.本文总结 RESTful 的设计细节,介绍如何设计出易于理解和使用的 API. 一.URL 设计 1.1 动词 + 宾语 RESTful 的核心思想就是,客户端发出的数据操作指令都是

拿nodejs快速搭建简单Oauth认证和restful API server攻略

拿nodejs快速搭建简单Oauth认证和restful API server攻略:http://blog.csdn.net/zhaoweitco/article/details/21708955 最近一直在鼓捣这个东西,拿出来分享下一下经验吧,其实很简单,一点也不难. 首先需求是这样,给自己的网站要增加API服务,API分为两种,公共的和私有授权的,授权的使用Oauth方法认证身份,API格式均为JOSN和JSONP. 嗯,别的语言我也没怎么学过,首先是找合适的框架进行实现吧.本身网站使用的e

使用Node.js + MongoDB 构建restful API

很多天前已经翻译了一大半了,今天收收尾~ RESTful API With Node.js + MongoDB Translated By 林凌灵 翻译目的:练练手,同时了解别人的思维方式 原文地址:RESTful API With Node.js + MongoDB 12 Sep 2013 我是一名移动应用开发者,我需要某种后端服务用来频繁地处理用户数据到数据库中.当然,我可以使用后端即服务类的网站(Parse, Backendless, 等等-),(译者:国内比较出名的有Bmob).但自己解

HTTP methods 与 RESTful API

目录: RESTful 是什么 JSON-server (提供 RESTful API 接口 + JSON 返回数据) 如何选择 REST 方法 HTTP verbs / method (安全 | 幂等) HTTP POST V.S. PUT REST POST | PUT | PATCH RESTful 是什么 阮一峰:理解RESTful架构 Representational State Transfer 表征状态转移 核心:resource.representation 指的是 resour

用expressjs写RESTful API

expressjs expressjs是一个基于nodejs的web开发框架:http://expressjs.com/,这篇博客目的就是用expressjs写一个关于products的最简单的RESTful API 一个最简单express的例子 package.json { "name": "hello-world", "description": "hello world of express js", "ve