Diseño de API RESTful con NodeJS y Restify
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)



La API RESTful consta de dos conceptos principales: Recurso y
Representación. El
recurso puede ser cualquier objeto asociado con datos, o identificado
con un URI (más de un URI puede referirse al mismo recurso), y puede ser
operado usando métodos HTTP. La representación es la forma en que
muestra el recurso. En
este tutorial, cubriremos algunos datos teóricos sobre el diseño
RESTful API e implementaremos un ejemplo de API de aplicación de blog
utilizando NodeJS.
Recurso
Elegir los recursos correctos para una API RESTful es una sección importante del diseño. En primer lugar, debe analizar el dominio de su negocio y luego decidir cuántos y qué tipo de recursos se utilizarán que sean relevantes para su negocio. Si está diseñando una API de blogging, probablemente use Article, User y Comment. Esos son los nombres de los recursos, y los datos asociados con eso son los recursos mismos:
1 |
{
|
2 |
"title": "How to Design RESTful API", |
3 |
"content": "RESTful API design is a very important case in the software development world.", |
4 |
"author": "huseyinbabal", |
5 |
"tags": [ |
6 |
"technology", |
7 |
"nodejs", |
8 |
"node-restify" |
9 |
]
|
10 |
"category": "NodeJS" |
11 |
}
|
Verbos de recursos
Puede continuar con una operación de recursos después de haber decidido los recursos necesarios. La operación aquí se refiere a los métodos HTTP. Por ejemplo, para crear un artículo, puede hacer la siguiente solicitud:
1 |
POST /articles HTTP/1.1 |
2 |
Host: localhost:3000 |
3 |
Content-Type: application/json |
4 |
|
5 |
{
|
6 |
"title": "RESTful API Design with Restify", |
7 |
"slug": "restful-api-design-with-restify", |
8 |
"content": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", |
9 |
"author": "huseyinbabal" |
10 |
} |
De la misma manera, puede ver un artículo existente emitiendo la siguiente solicitud:
1 |
GET /articles/123456789012 HTTP/1.1 |
2 |
Host: localhost:3000 |
3 |
Content-Type: application/json |
¿Qué hay de actualizar un artículo existente? Puedo escuchar que estás diciendo:
Puedo hacer otra solicitud de POST a / articles / update / 123456789012 con la carga.
Quizás sea preferible, pero el URI se está volviendo más complejo. Como dijimos anteriormente, las operaciones pueden referirse a métodos HTTP. Esto significa, indicar la operación de actualización en el método HTTP en lugar de poner eso en el URI. Por ejemplo:
1 |
PUT /articles/123456789012 HTTP/1.1 |
2 |
Host: localhost:3000 |
3 |
Content-Type: application/json |
4 |
{
|
5 |
"title": "Updated How to Design RESTful API", |
6 |
"content": "Updated RESTful API design is a very important case in the software development world.", |
7 |
"author": "huseyinbabal", |
8 |
"tags": [ |
9 |
"technology", |
10 |
"nodejs", |
11 |
"restify", |
12 |
"one more tag" |
13 |
] |
14 |
"category": "NodeJS" |
15 |
} |
Por cierto, en este ejemplo, verá etiquetas y campos de categoría. Esos no necesitan ser campos obligatorios. Puede dejarlos en blanco y configurarlos en el futuro.
En ocasiones, debe eliminar un artículo cuando esté desactualizado. En ese caso, puede usar una solicitud HTTP DELETE a /articles/123456789012.
Los métodos HTTP son conceptos estándar. Si los usa como
una operación, tendrá URI simples, y este tipo de API simple lo ayudará a
obtener consumidores satisfechos.
¿Qué sucede si quiere insertar un comentario en un artículo? Puede seleccionar el artículo y agregar un nuevo comentario al artículo seleccionado. Al usar esta declaración, puede usar la siguiente solicitud:
1 |
POST /articles/123456789012/comments HTTP/1.1 |
2 |
Host: localhost:3000 |
3 |
Content-Type: application/json |
4 |
{
|
5 |
"text": "Wow! this is a good tutorial", |
6 |
"author": "john doe" |
7 |
} |
La forma de recurso anterior se llama como un sub-recurso. Comment es un sub recurso de Article. La carga de Comment anterior se insertará en la base de datos como elemento secundario de Article. A veces, un URI diferente se refiere al mismo recurso. Por ejemplo, para ver un comentario específico, puede usar:
1 |
GET /articles/123456789012/comments/123 HTTP/1.1 |
2 |
Host: localhost:3000 |
3 |
Content-Type: application/json |
o:
1 |
GET /comments/123456789012 HTTP/1.1 |
2 |
Host: localhost:3000 |
3 |
Content-Type: application/json |
Versiones
En general, las características de API cambian frecuentemente para proporcionar nuevas características a los consumidores. En ese caso, pueden existir dos versiones de la misma API al mismo tiempo. Para separar esas dos características, puede usar el control de versiones. Hay dos formas de control de versiones
- Versión en URI: puede
proporcionar el número de versión en el URI. Por ejemplo,
/v1.1/articles/123456789012. - Versión en encabezado: proporcione el número de versión en el encabezado y nunca cambie el URI. Por ejemplo:
1 |
GET /articles/123456789012 HTTP/1.1 |
2 |
Host: localhost:3000 |
3 |
Accept-Version: 1.0 |
En realidad, la versión solo cambia la representación del recurso, no el concepto del recurso. Por lo tanto, no necesita cambiar la estructura de URI. En v1.1, tal vez se agregó un nuevo campo a Article. Sin embargo, todavía devuelve un artículo. En la segunda opción, el URI sigue siendo simple y los consumidores no necesitan cambiar su URI en las implementaciones del lado del cliente.
Es importante diseñar una estrategia para situaciones en las que el consumidor no proporciona un número de versión. Puede generar un error cuando no se proporciona la versión, o puede devolver una respuesta utilizando la primera versión. Si usa la última versión estable como predeterminada, los consumidores pueden obtener muchos errores para sus implementaciones del lado del cliente.
Representación
La representación es la forma en que una API muestra el recurso. Cuando llamas a un punto final API, recibirás un recurso. Este recurso puede estar en cualquier formato como XML, JSON, etc. JSON es preferible si está diseñando una nueva API. Sin embargo, si está actualizando una API existente que solía devolver una respuesta XML, puede proporcionar otra versión para una respuesta JSON.
Esa es suficiente información teórica sobre el diseño RESTful API. Echemos un vistazo al uso de la vida real diseñando e implementando una API de blogs utilizando Restify.
Blogging REST API
Diseño
Para diseñar una API RESTful, tenemos que analizar el dominio comercial. Entonces podemos definir nuestros recursos. En una API de blogs, necesitamos:
- Crear, actualizar, eliminar, ver Article
- Crear un comentario para un Article específico, actualizar,
eliminar, ver, Comment
- Crear, actualizar, eliminar, ver User
En
esta API, no cubriré cómo autenticar a un usuario para crear un artículo
o comentario. Para la parte de autenticación, puede consultar la
Autenticación basada en tokens con el tutorial AngularJS &
NodeJS.
Nuestros nombres de recursos están listos. Las operaciones de recursos son simplemente CRUD. Puede consultar la siguiente tabla para obtener una muestra general de API.
| Nombre del recurso | verbos HTTP | Metodos HTTP |
|---|---|---|
| Article | crear artículo actualizar artículo Eliminar artículo ver Artículo | POST /articles con
carga PUT /articles/123 con carga ELIMINAR /articles/123 GET /article/123 |
| Comment | crear comentario actualización Comentario Eliminar comentario ver comentario | POST /articles/123/comments con carga PUT /comments/123 con carga DELETE /comments/123 GET /comments/123 |
| User | crear usuario actualizar usuario borrar usuario ver usuario | POST /users con carga
útil PUT /users/123 con carga útil ELIMINAR /users/123 GET /users/123 |
Configuración del proyecto
En este proyecto usaremos NodeJS con Restify. Los recursos se guardarán en la base de datos MongoDB. Antes que nada, podemos definir recursos como modelos en Restify.
Article
1 |
var mongoose = require("mongoose"); |
2 |
var Schema = mongoose.Schema; |
3 |
|
4 |
var ArticleSchema = new Schema({ |
5 |
title: String, |
6 |
slug: String, |
7 |
content: String, |
8 |
author: { |
9 |
type: String, |
10 |
ref: "User" |
11 |
}
|
12 |
});
|
13 |
mongoose.model('Article', ArticleSchema); |
Comment
1 |
var mongoose = require("mongoose"); |
2 |
var Schema = mongoose.Schema; |
3 |
|
4 |
var CommentSchema = new Schema({ |
5 |
text: String, |
6 |
article: { |
7 |
type: String, |
8 |
ref: "Article" |
9 |
},
|
10 |
author: { |
11 |
type: String, |
12 |
ref: "User" |
13 |
}
|
14 |
});
|
15 |
mongoose.model('Comment', CommentSchema); |
User
No habrá ninguna operación para el recurso de usuario. Supondremos que ya conocemos al usuario actual que podrá operar artículos o comentarios.
Puede preguntar de dónde viene este módulo mongoose. Es el marco ORM más popular para MongoDB escrito como un módulo NodeJS. Este módulo está incluido en el proyecto dentro de otro archivo de configuración.
Ahora podemos definir nuestros verbos HTTP para los recursos anteriores. Puedes ver lo siguiente:
1 |
var restify = require('restify') |
2 |
, fs = require('fs') |
3 |
|
4 |
|
5 |
var controllers = {} |
6 |
, controllers_path = process.cwd() + '/app/controllers' |
7 |
fs.readdirSync(controllers_path).forEach(function (file) { |
8 |
if (file.indexOf('.js') != -1) { |
9 |
controllers[file.split('.')[0]] = require(controllers_path + '/' + file) |
10 |
}
|
11 |
})
|
12 |
|
13 |
var server = restify.createServer(); |
14 |
|
15 |
server
|
16 |
.use(restify.fullResponse()) |
17 |
.use(restify.bodyParser()) |
18 |
|
19 |
// Article Start
|
20 |
server.post("/articles", controllers.article.createArticle) |
21 |
server.put("/articles/:id", controllers.article.updateArticle) |
22 |
server.del("/articles/:id", controllers.article.deleteArticle) |
23 |
server.get({path: "/articles/:id", version: "1.0.0"}, controllers.article.viewArticle) |
24 |
server.get({path: "/articles/:id", version: "2.0.0"}, controllers.article.viewArticle_v2) |
25 |
// Article End
|
26 |
|
27 |
// Comment Start
|
28 |
server.post("/comments", controllers.comment.createComment) |
29 |
server.put("/comments/:id", controllers.comment.viewComment) |
30 |
server.del("/comments/:id", controllers.comment.deleteComment) |
31 |
server.get("/comments/:id", controllers.comment.viewComment) |
32 |
// Comment End
|
33 |
|
34 |
var port = process.env.PORT || 3000; |
35 |
server.listen(port, function (err) { |
36 |
if (err) |
37 |
console.error(err) |
38 |
else
|
39 |
console.log('App is ready at : ' + port) |
40 |
})
|
41 |
|
42 |
if (process.env.environment == 'production') |
43 |
process.on('uncaughtException', function (err) { |
44 |
console.error(JSON.parse(JSON.stringify(err, ['stack', 'message', 'inner'], 2))) |
45 |
})
|
En este fragmento de código, primero se iteran los archivos del controlador que contienen los métodos del controlador y todos los controladores se inicializan para ejecutar una solicitud específica al URI. Después de eso, los URI para operaciones específicas se definen para las operaciones CRUD básicas. También hay control de versiones para una de las operaciones en Article.
Por ejemplo, si declara la versión como 2 en
el encabezado Accept-Version, se ejecutará viewArticle_v2. viewArticle y viewArticle_v2 hacen el mismo trabajo, mostrando el recurso, pero
muestran el recurso Artículo en un formato diferente, como se puede ver
en el campo title a continuación. Finalmente, el servidor se inicia
en un puerto específico y se aplican algunas comprobaciones de informes
de errores. Podemos proceder con los métodos de controlador para las
operaciones HTTP en los recursos.
article.js
1 |
var mongoose = require('mongoose'), |
2 |
Article = mongoose.model("Article"), |
3 |
ObjectId = mongoose.Types.ObjectId |
4 |
|
5 |
exports.createArticle = function(req, res, next) { |
6 |
var articleModel = new Article(req.body); |
7 |
articleModel.save(function(err, article) { |
8 |
if (err) { |
9 |
res.status(500); |
10 |
res.json({ |
11 |
type: false, |
12 |
data: "Error occured: " + err |
13 |
})
|
14 |
} else { |
15 |
res.json({ |
16 |
type: true, |
17 |
data: article |
18 |
})
|
19 |
}
|
20 |
})
|
21 |
}
|
22 |
|
23 |
exports.viewArticle = function(req, res, next) { |
24 |
Article.findById(new ObjectId(req.params.id), function(err, article) { |
25 |
if (err) { |
26 |
res.status(500); |
27 |
res.json({ |
28 |
type: false, |
29 |
data: "Error occured: " + err |
30 |
})
|
31 |
} else { |
32 |
if (article) { |
33 |
res.json({ |
34 |
type: true, |
35 |
data: article |
36 |
})
|
37 |
} else { |
38 |
res.json({ |
39 |
type: false, |
40 |
data: "Article: " + req.params.id + " not found" |
41 |
})
|
42 |
}
|
43 |
}
|
44 |
})
|
45 |
}
|
46 |
|
47 |
exports.viewArticle_v2 = function(req, res, next) { |
48 |
Article.findById(new ObjectId(req.params.id), function(err, article) { |
49 |
if (err) { |
50 |
res.status(500); |
51 |
res.json({ |
52 |
type: false, |
53 |
data: "Error occured: " + err |
54 |
})
|
55 |
} else { |
56 |
if (article) { |
57 |
article.title = article.title + " v2" |
58 |
res.json({ |
59 |
type: true, |
60 |
data: article |
61 |
})
|
62 |
} else { |
63 |
res.json({ |
64 |
type: false, |
65 |
data: "Article: " + req.params.id + " not found" |
66 |
})
|
67 |
}
|
68 |
}
|
69 |
})
|
70 |
}
|
71 |
|
72 |
exports.updateArticle = function(req, res, next) { |
73 |
var updatedArticleModel = new Article(req.body); |
74 |
Article.findByIdAndUpdate(new ObjectId(req.params.id), updatedArticleModel, function(err, article) { |
75 |
if (err) { |
76 |
res.status(500); |
77 |
res.json({ |
78 |
type: false, |
79 |
data: "Error occured: " + err |
80 |
})
|
81 |
} else { |
82 |
if (article) { |
83 |
res.json({ |
84 |
type: true, |
85 |
data: article |
86 |
})
|
87 |
} else { |
88 |
res.json({ |
89 |
type: false, |
90 |
data: "Article: " + req.params.id + " not found" |
91 |
})
|
92 |
}
|
93 |
}
|
94 |
})
|
95 |
}
|
96 |
|
97 |
exports.deleteArticle = function(req, res, next) { |
98 |
Article.findByIdAndRemove(new Object(req.params.id), function(err, article) { |
99 |
if (err) { |
100 |
res.status(500); |
101 |
res.json({ |
102 |
type: false, |
103 |
data: "Error occured: " + err |
104 |
})
|
105 |
} else { |
106 |
res.json({ |
107 |
type: true, |
108 |
data: "Article: " + req.params.id + " deleted successfully" |
109 |
})
|
110 |
}
|
111 |
})
|
112 |
}
|
Puede encontrar una explicación de las operaciones CRUD básicas en el lado Mongoose a continuación:
- createArticle: esta es una operación simple save en
articleModelenviada desde el cuerpo de la solicitud. Se puede crear un nuevo modelo pasando el cuerpo de la solicitud como un constructor a un modelo comovar articleModel = new Article(req.body). - viewArticle: para ver los detalles del artículo, se
necesita una ID de artículo en el parámetro de URL.
findOnecon un parámetro de ID es suficiente para devolver los detalles del artículo. - updateArticle:
la actualización de artículo es una consulta de búsqueda simple y algo
de manipulación de datos en el artículo devuelto. Finalmente, el modelo
actualizado debe guardarse en la base de datos emitiendo un comando
save. - deleteArticle:
findByIdAndRemovees la mejor manera de eliminar un artículo proporcionando la ID del artículo.
Los comandos de Mangosta mencionados anteriormente son simplemente estáticos como método a través del objeto Artículo que también es una referencia del esquema de Mongoose.
comment.js
1 |
var mongoose = require('mongoose'), |
2 |
Comment = mongoose.model("Comment"), |
3 |
Article = mongoose.model("Article"), |
4 |
ObjectId = mongoose.Types.ObjectId |
5 |
|
6 |
exports.viewComment = function(req, res) { |
7 |
Article.findOne({"comments._id": new ObjectId(req.params.id)}, {"comments.$": 1}, function(err, comment) { |
8 |
if (err) { |
9 |
res.status(500); |
10 |
res.json({ |
11 |
type: false, |
12 |
data: "Error occured: " + err |
13 |
})
|
14 |
} else { |
15 |
if (comment) { |
16 |
res.json({ |
17 |
type: true, |
18 |
data: new Comment(comment.comments[0]) |
19 |
})
|
20 |
} else { |
21 |
res.json({ |
22 |
type: false, |
23 |
data: "Comment: " + req.params.id + " not found" |
24 |
})
|
25 |
}
|
26 |
}
|
27 |
})
|
28 |
}
|
29 |
|
30 |
exports.updateComment = function(req, res, next) { |
31 |
var updatedCommentModel = new Comment(req.body); |
32 |
console.log(updatedCommentModel) |
33 |
Article.update( |
34 |
{"comments._id": new ObjectId(req.params.id)}, |
35 |
{"$set": {"comments.$.text": updatedCommentModel.text, "comments.$.author": updatedCommentModel.author}}, |
36 |
function(err) { |
37 |
if (err) { |
38 |
res.status(500); |
39 |
res.json({ |
40 |
type: false, |
41 |
data: "Error occured: " + err |
42 |
})
|
43 |
} else { |
44 |
res.json({ |
45 |
type: true, |
46 |
data: "Comment: " + req.params.id + " updated" |
47 |
})
|
48 |
}
|
49 |
})
|
50 |
}
|
51 |
|
52 |
exports.deleteComment = function(req, res, next) { |
53 |
Article.findOneAndUpdate({"comments._id": new ObjectId(req.params.id)}, |
54 |
{"$pull": {"comments": {"_id": new ObjectId(req.params.id)}}}, |
55 |
function(err, article) { |
56 |
if (err) { |
57 |
res.status(500); |
58 |
res.json({ |
59 |
type: false, |
60 |
data: "Error occured: " + err |
61 |
})
|
62 |
} else { |
63 |
if (article) { |
64 |
res.json({ |
65 |
type: true, |
66 |
data: article |
67 |
})
|
68 |
} else { |
69 |
res.json({ |
70 |
type: false, |
71 |
data: "Comment: " + req.params.id + " not found" |
72 |
})
|
73 |
}
|
74 |
}
|
75 |
})
|
76 |
}
|
Cuando realiza una solicitud a uno de los URI de recursos, se ejecutará la función relacionada indicada en el controlador. Cada función dentro de los archivos del controlador puede usar los objetos req y res. El recurso comment aquí es un sub recurso de Article. Todas las operaciones de consulta se realizan a través del modelo Article para encontrar un subdocumento y realizar la actualización necesaria. Sin embargo, cada vez que intente ver un recurso Comment, verá uno incluso si no hay una colección en MongoDB.
Otras sugerencias de diseño
- Seleccione recursos fáciles de entender para facilitar el uso a los consumidores.
- Deje que los consumidores implementen la lógica de negocios. Por ejemplo, el recurso Article tiene un campo llamado slug. Los consumidores no necesitan enviar este detalle a la API REST. Esta estrategia de babosas debe administrarse en el lado REST API para reducir el acoplamiento entre la API y los consumidores. Los consumidores solo necesitan enviar los detalles del título, y usted puede generar el slug de acuerdo con las necesidades de su negocio en el lado REST API.
- Implemente una capa de autorización para sus puntos finales API. Los consumidores no autorizados pueden acceder a datos restringidos que pertenecen a otro usuario. En este tutorial, no cubrimos el recurso de usuario, pero puede consultar Autenticación basada en tokens con AngularJS y NodeJS para obtener más información sobre las autenticaciones API.
- URI de usuario
en lugar de cadena de consulta.
/articles/123(Bueno),/articles?id=123(Malo). - No guardes el estado; siempre use entrada / salida instantánea.
- Usa el sustantivo para tus recursos. Puede usar métodos HTTP para operar en recursos.
Finalmente, si diseña una API RESTful siguiendo estas reglas fundamentales, siempre tendrá un sistema flexible, fácil de mantener y fácil de entender.



