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 Article, User, 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
- Version in URI: You can provide the version number in the URI. For example,
/v1.1/articles/123456789012
. - 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 |
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 likevar 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.
RESTful API Design With NodeJS & Restify的更多相关文章
- Principles of good RESTful API Design 好的 RESTful API 设计
UPDATE: This post has been expanded upon and converted into an eBook. Good API design is hard! An AP ...
- RESTful API Design: 13 Best Practices to Make Your Users Happy
RESTful API Design: 13 Best Practices to Make Your Users Happy First step to the RESTful way: make s ...
- RESTful API Design
https://restful-api-design.readthedocs.org/en/latest/scope.html http://blog.mashape.com/30-ways-to-m ...
- RESTful API 设计指南【转】
网络应用程序,分为前端和后端两个部分.当前的发展趋势,就是前端设备层出不穷(手机.平板.桌面电脑.其他专用设备......). 因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信.这导致AP ...
- 好RESTful API的设计原则
说在前面,这篇文章是无意中发现的,因为感觉写的很好,所以翻译了一下.由于英文水平有限,难免有出错的地方,请看官理解一下.翻译和校正文章花了我大约2周的业余时间,如有人愿意转载请注明出处,谢谢^_^ P ...
- Django RESTful API 设计指南
网络应用程序,分为前端和后端两个部分.当前的发展趋势,就是前端设备层出不穷(手机.平板.桌面电脑.其他专用设备......). 因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信.这导致AP ...
- RESTful API的设计原则
好RESTful API的设计原则 说在前面,这篇文章是无意中发现的,因为感觉写的很好,所以翻译了一下.由于英文水平有限,难免有出错的地方,请看官理解一下.翻译和校正文章花了我大约2周的业余时间, ...
- 好的RESTful API的设计原则
转载自一位大佬 英文原版 Principles of good RESTful API Design Good API design is hard! An API represents a cont ...
- 【转载】RESTful API 设计指南
作者: 阮一峰 日期: 2014年5月22日 网络应用程序,分为前端和后端两个部分.当前的发展趋势,就是前端设备层出不穷(手机.平板.桌面电脑.其他专用设备......). 因此,必须有一种统一的机制 ...
随机推荐
- Linux中利用crontab创建计划任务
在linux中启动crontab服务: /etc/init.d/crond start crontab的命令格式 crontab -l 显示当前的crontab 文件(默认编写的crontab文 ...
- 使用CSVDE批量导入命令/出口AD用户
使用CSVDE批量导入命令/出口AD用户 CSVDE命令行工具可以与真实的用户信息CSV文件.批量导入/出口AD在. 导入的基本的语法命令是这种: csvde -i -f c:\fil ...
- cocos2d-x2.2.5 + cocos2d-x3.2鸟跳便宜源代码“开源”
尊重开发人员的劳动成果,转载请注明From郝萌主 游戏简单介绍: 贱鸟跳跳,贱贱的小鸟这次遇上大问题了.被它整蛊过的同类都在找它的麻烦,如今我们赶紧到游戏中帮帮它吧!左右手互撸,合理操控.获得高分,打 ...
- poj2411(状压dp)
题目链接:http://poj.org/problem?id=2411 题意:由1*2 的矩形通过组合拼成大矩形,求拼成指定的大矩形有几种拼法. 分析:如果是横着的就定义11,如果竖着的定义为竖着的0 ...
- Restlet+Fastjson 高速构建轻量级 Java RESTful Webservice
自己入门Java时做过一个小型RESTful Web Service的项目,这里总结一下. 服务的数据交换格式主要採用JSON,服务为REST风格.连接採用Http协议,数据库使用MySQL,OR M ...
- 关于Platinum库的MediaRender具体C++代码实现探讨
接上篇博文 NDK下 将Platinum SDK 编译成so库 (android - upnp) 讲述了如何利用该代码库编译给android程序调用的so库,其中也提到了,在使用sample-upnp ...
- TMG 2010 VPN配置
微软的ISA 到2006以后就叫TMG了,上周在公司的服务器上安装测试了下,虽然增加了很多功能,但是主要功能上和ISA 2004差不多,最近在部署L2TP VPN,由于防火墙带的远程访问VPN为纯的L ...
- hdu4553(线段树)
题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=4553 线段树功能:update:区间替换 query:询问满足条件的最左断点 分析:poj3667的加 ...
- Oracle单表的复杂查询
Oracle单表的复杂查询 select avg(sal),max(sal),deptnofrom empgroupby deptno; orderby deptno; 查询工资高于500或者是岗位为 ...
- 自己定义 ViewGroup 支持无限循环翻页之三(响应回调事件)
大家假设喜欢我的博客,请关注一下我的微博,请点击这里(http://weibo.com/kifile),谢谢 转载请标明出处,再次感谢 ################################ ...