A Simple Web Service In Node.Js + CouchDB
2011-04-12 00:21
441 查看
作者基于例子来讲解如何用nodejs开发web程序,特别是存储部分使用了CouchDB,通过阅读可以熟练使用Journey, Cradle, Winston和Optimist。
文章来源:http://blog.nodejitsu.com/a-simple-webservice-in-nodejs
I recently gave a talk at ADICU Devfest 2011 on node.js. The talk was aimed at Computer Science students who did not know anything about node.js and more importantly, how to get started building simple and elegant web services from scratch. The slides (and code) from my talk are available onGitHub.
This article will walk through the code that I presented to build a simple RESTful bookmarking API using node.js and:
Journey: A liberal JSON-only HTTP request router for node.js
Cradle: A high-level, caching, CouchDB library for Node.js
Winston: A multi-transport async logging library for node.js
Optimist: Light-weight option parsing for node.js
getting-started.js
The above example is simple (almost trivial): it exports a server that reads the request body, logs the request using winston, and responds with the appropriate HTTP response code (501 - Not Implemented). Not very exciting but enough to get us started.
00getting-started: The full source code from 'Getting Started'
01routing: The full source code from 'Adding some Routes'
02couchdb: The full source code from 'Interacting with CouchDB'
03authentication: The full source code from 'Adding HTTP Basic Auth'
List: GET to /bookmarks should respond with a list of bookmarks
Create: POST to /bookmarks should create a new bookmark
Show: GET to /bookmarks/:id should respond with a specific bookmark
Update: PUT to /bookmarks/:id should update a specific bookmark
Destroy: DELETE to /bookmarks/:id should delete a specific bookmark
Pretty simple right? Absolutely. To accomplish this routing tasks we are going to use Journey. Journey is a great library with a lot of features. 0.3.1 was just released, which we will be taking advantage of in this post.
add-routes.js
The above code generates a Journey router that matches the routes we outlined using regular expressions. In each route, we respond with
We use the
add-routes-server.js
You can view the entire file on GitHub here.
The alternative was to use http-console: a simple, intuitive HTTP REPL. Getting http-console is easy to install using npm:
So lets fire-up http-console for an interactive session a couple of our newly minted routes:
We made two request to
database.js
The above code requires cradle, configures it with the remote host, port and authentication (if required). If
Now that we have a connection to CouchDB for our application, we can go ahead and create a Bookmark resource that consumes the connection. Within this resource we want to define functions for each of the CRUD operations we've outlined:
bookmark.js
I won't go into the details of how each of these methods work, but rest assured that they do. It's all very basic usage for cradle, so if you're interested in the specifics I invite you to read the documentation on the cradle GitHub page.
So now that we've configured CouchDB and we have a Bookmark resource, we need to connect our resource to the Journey router that we defined earlier.
resource-routes.js
In each of the new routes, we send the appropriate request data to the Bookmark resource, and asynchronously respond with the appropriate HTTP response code when complete. In the event of an error, we always send
helpers.js
We can set this method on the Journey router by passing it in the options hash. Any routes that we wish to be behind the authentication filter need to be wrapped in a call to
router-auth.js
I hope this has been helpful for those of you looking to get started with node.js. Check out the rest of our blog for more advanced libraries and tutorials. Come back soon for more on the Art of Nodejitsu.
文章来源:http://blog.nodejitsu.com/a-simple-webservice-in-nodejs
I recently gave a talk at ADICU Devfest 2011 on node.js. The talk was aimed at Computer Science students who did not know anything about node.js and more importantly, how to get started building simple and elegant web services from scratch. The slides (and code) from my talk are available onGitHub.
This article will walk through the code that I presented to build a simple RESTful bookmarking API using node.js and:
Journey: A liberal JSON-only HTTP request router for node.js
Cradle: A high-level, caching, CouchDB library for Node.js
Winston: A multi-transport async logging library for node.js
Optimist: Light-weight option parsing for node.js
Getting Started
The first step to doing anything with node.js is creating a server to accept incoming HTTP requests. You can also create a TCP server or work with UDP, but I won't be going into that here. So what does such a server look like?getting-started.js
var http = require('http'), winston = require('winston'); /** * Creates the server for the pinpoint web service * @param {int} port: Port for the server to run on */ exports.createServer = function (port) { var server = http.createServer(function (request, response) { var data = ''; winston.info('Incoming Request', { url: request.url }); request.on('data', function (chunk) { data += chunk; }); response.writeHead(501, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ message: 'not implemented' })); }); if (port) { server.listen(port); } return server; };
The above example is simple (almost trivial): it exports a server that reads the request body, logs the request using winston, and responds with the appropriate HTTP response code (501 - Not Implemented). Not very exciting but enough to get us started.
Running the development server
The development server lives inbin/serverand has been configured so that we can run the development server at the various stages of development outlined in this tutorial. Running it is simple, just remember to pass the
-targument which specifies which directory under /lib to use for the server:
00getting-started: The full source code from 'Getting Started'
01routing: The full source code from 'Adding some Routes'
02couchdb: The full source code from 'Interacting with CouchDB'
03authentication: The full source code from 'Adding HTTP Basic Auth'
$ bin/server -t 00getting-started Pinpoint demo server listening for 00getting-started on http://127.0.0.1:8000 4 Feb 17:59:22 - info: Incoming Request url=/
Adding some Routes
Now that we have a server that tells the world we haven't done anything it's time to think about what our application does:List: GET to /bookmarks should respond with a list of bookmarks
Create: POST to /bookmarks should create a new bookmark
Show: GET to /bookmarks/:id should respond with a specific bookmark
Update: PUT to /bookmarks/:id should update a specific bookmark
Destroy: DELETE to /bookmarks/:id should delete a specific bookmark
Pretty simple right? Absolutely. To accomplish this routing tasks we are going to use Journey. Journey is a great library with a lot of features. 0.3.1 was just released, which we will be taking advantage of in this post.
add-routes.js
exports.createRouter = function () { return new (journey.Router)(function (map) { map.path(///bookmarks/, function () { // // LIST: GET to /bookmarks lists all bookmarks // this.get().bind(function (res) { res.send(501, {}, { action: 'list' }); }); // // SHOW: GET to /bookmarks/:id shows the details of a specific bookmark // this.get(///([/w|/d|/-|/_]+)/).bind(function (res, id) { res.send(501, {}, { action: 'show' }); }); // // CREATE: POST to /bookmarks creates a new bookmark // this.post().bind(function (res, bookmark) { res.send(501, {}, { action: 'create' }); }); // // UPDATE: PUT to /bookmarks updates an existing bookmark // this.put(///([/w|/d|/-|/_]+)/).bind(function (res, bookmark) { res.send(501, {}, { action: 'update' }); }); // // DELETE: DELETE to /bookmarks/:id deletes a specific bookmark // this.del(///([/w|/d|/-|/_]+)/).bind(function (res, id) { res.send(501, {}, { action: 'delete' }); }); }); }, { strict: false }); };
The above code generates a Journey router that matches the routes we outlined using regular expressions. In each route, we respond with
501 Not Implementedand the corresponding action so that we can be sure we've hit the correct route.
We use the
map.pathsyntax to scope the subsequent routes behind
/bookmarks. The changes that need to be made to our development server are minimal. We just need to add code to create our router and later it to route our request with the associated request body within our HTTP server:
add-routes-server.js
request.on('end', function () { // // Dispatch the request to the router // router.route(request, body, function (route) { response.writeHead(route.status, route.headers); response.end(route.body); }); });
You can view the entire file on GitHub here.
Testing Routes using http-console
One of the things I did differently in this demo than I do in my own projects is that there are no vowsjs tests. I choose not to include tests in this demo because the additional overhead of understanding how a particular test framework works seemed a little high for the complete beginner.The alternative was to use http-console: a simple, intuitive HTTP REPL. Getting http-console is easy to install using npm:
[sudo] npm install http-console[/code]
So lets fire-up http-console for an interactive session a couple of our newly minted routes:
$ http-console http://127.0.0.1:8000 > http-console 0.5.1 > Welcome, enter /help if you're lost. > Connecting to 127.0.0.1 on port 8000. http://127.0.0.1:8000/> GET /bookmarks HTTP/1.1 501 Not Implemented Date: Sat, 05 Feb 2011 02:57:46 GMT Server: journey/0.3.0 Content-Type: application/json Content-Length: 17 Connection: close { action: 'list' } http://127.0.0.1:8000/> GET /bookmarks/foobar HTTP/1.1 501 Not Implemented Date: Sat, 05 Feb 2011 02:57:52 GMT Server: journey/0.3.0 Content-Type: application/json Content-Length: 17 Connection: close { action: 'show' } http://127.0.0.1:8000/>[/code]
We made two request to
/bookmarksand
/bookmarks/foobarrespectively and got back 501 in both cases with valid JSON representing the specified action that is not yet implemented. We also got back the
application/jsonheader which was automatically set for us by Journey.
Interacting with CouchDB
Interacting with a persistent data store is a must have for any webservice or web application. At Nodejitsu we use CouchDB and our library of choice iscradle. We will define a Bookmark resource with a couple of methods for performing basic CRUD on our bookmark object. Before we get to that we need to configure our Couch with a Design Document for Bookmark resources. If you want to learn more about Design Doucments see CouchDB: The Definitive Guide.database.js
var cradle = require('cradle'); var setup = exports.setup = function (options, callback) { // Set connection configuration cradle.setup({ host: options.host || '127.0.0.1', port: 5984, options: options.options, }); // Connect to cradle var conn = new (cradle.Connection)({ auth: options.auth }), db = conn.database(options.database || 'pinpoint-dev'); if (options.setup) { initViews(db, callback); } else { callback(null, db); } }; var initViews = exports.initViews = function (db, callback) { var designs = [ { '_id': '_design/Bookmark', views: { all: { map: function (doc) { if (doc.resource === 'Bookmark') emit(doc._id, doc) } }, byUrl: { map: function (doc) { if (doc.resource === 'Bookmark') { emit(doc.url, doc); } } }, byDate: { map: function (doc) { if (doc.resource === 'Bookmark') { emit(doc.date, doc); } } } } } ]; db.save(designs, function (err) { if (err) return callback(err); callback(null, db); }); };[/code]
The above code requires cradle, configures it with the remote host, port and authentication (if required). If
options.setupis set then it asynchronously creates the Design Document in CouchDB and later responds with the database connection
db.
Now that we have a connection to CouchDB for our application, we can go ahead and create a Bookmark resource that consumes the connection. Within this resource we want to define functions for each of the CRUD operations we've outlined:
create,
show,
list,
updateand
destroy.
bookmark.js
/** * Constructor function for the Bookmark object.. * @constructor * @param {connection} database: Connection to CouchDB */ var Bookmark = exports.Bookmark = function (database) { this.database = database; }; /** * Lists all Bookmarks in the database * @param {function} callback: Callback function */ Bookmark.prototype.list = function (callback) { this.database.view('Bookmark/all', function (err, result) { if (err) { return callback(err); } callback(null, result.rows.map(function (row) { return row.value })); }) }; /** * Shows details of a particular bookmark * @param {string} id: ID of the bookmark * @param {function} callback: Callback function */ Bookmark.prototype.show = function (id, callback) { this.database.get(id, function (err, doc) { if (err) { return callback(err); } callback(null, doc); }); }; /** * Creates a new bookmark with the specified properties * @param {object} bookmark: Properties to use for the bookmark * @param {function} callback: Callback function */ Bookmark.prototype.create = function (bookmark, callback) { bookmark._id = helpers.randomString(32); bookmark.resource = "Bookmark"; this.database.save(bookmark._id, bookmark, function (err, res) { if (err) { return callback(err); } callback(null, bookmark); }) }; /** * Updates a new bookmark with the specified id and properties * @param {object} bookmark: Properties to update the bookmark with * @param {function} callback: Callback function */ Bookmark.prototype.update = function (id, bookmark, callback) { this.database.merge(id, bookmark, function (err, res) { if (err) { return callback(err); } callback(null, true); }); }; /** * Destroys a bookmark with the specified ID * @param {string} id: ID of the bookmark to destroy * @param {function} callback: Callback function */ Bookmark.prototype.destroy = function (id, callback) { var self = this; this.show(id, function (err, doc) { if (err) { return callback(err); } self.database.remove(id, doc._rev, function (err, res) { if (err) { return callback(err); } callback(null, true); }); }); };[/code]
I won't go into the details of how each of these methods work, but rest assured that they do. It's all very basic usage for cradle, so if you're interested in the specifics I invite you to read the documentation on the cradle GitHub page.
So now that we've configured CouchDB and we have a Bookmark resource, we need to connect our resource to the Journey router that we defined earlier.
resource-routes.js
exports.createRouter = function (resource) { return new (journey.Router)(function (map) { map.path(///bookmarks/, function () { // // LIST: GET to /bookmarks lists all bookmarks // this.get().bind(function (res) { resource.list(function (err, bookmarks) { if (err) { return res.send(500, {}, { error: err.error }); } res.send(200, {}, { bookmarks: bookmarks }); }); }); // // SHOW: GET to /bookmarks/:id shows the details of a specific bookmark // this.get(///([/w|/d|/-|/_]+)/).bind(function (res, id) { resource.show(id, function (err, bookmark) { if (err) { return res.send(500, {}, { error: err.error }); } res.send(200, {}, { bookmark: bookmark }); }); }); // // CREATE: POST to /bookmarks creates a new bookmark // this.post().bind(function (res, bookmark) { resource.create(bookmark, function (err, result) { if (err) { return res.send(500, {}, { error: err.error }); } res.send(200, {}, { bookmark: result }); }); }); // // UPDATE: PUT to /bookmarks updates an existing bookmark // this.put(///([/w|/d|/-|/_]+)/).bind(function (res, id, bookmark) { resource.update(id, bookmark, function (err, updated) { if (err) { return res.send(500, {}, { error: err.error }); } res.send(200, {}, { updated: updated }); }); }); // // DELETE: DELETE to /bookmarks/:id deletes a specific bookmark // this.del(///([/w|/d|/-|/_]+)/).bind(function (res, id) { resource.destroy(id, function (err, destroyed) { if (err) { return res.send(500, {}, { error: err.error }); } res.send(200, {}, { destroyed: destroyed }); }); }); }); }, { strict: false }); };[/code]
In each of the new routes, we send the appropriate request data to the Bookmark resource, and asynchronously respond with the appropriate HTTP response code when complete. In the event of an error, we always send
500 Internal Server Errorwith the error message.
Adding HTTP Basic Auth
The main focus of Journey 0.3.0 was to add a feature where the programmer could specify a filter function that takes the request and body. This filter function will intercept requests before they are passed to any route handler. If the filter function returns a pre-defined Journey error, the router will short-circuit and respond with the status code. We will use this feature to define a filter function that performs HTTP Basic Auth.helpers.js
var auth = exports.auth = { username: 'admin', password: 'password', basicAuth: function (request, body, callback) { var realm = "Authorization Required", authorization = request.headers.authorization; if (!authorization) { return callback(new journey.NotAuthorized("Authorization header is required.")); } var parts = authorization.split(" "), // Basic salkd787&u34n= scheme = parts[0], // Basic credentials = base64.decode(parts[1]).split(":"); // admin:password if (scheme !== "Basic") { return callback(new journey.NotAuthorized("Authorization scheme must be 'Basic'")); } else if(!credentials[0] && !credentials[1]){ return callback(new journey.NotAuthorized("Both username and password are required")); } else if(credentials[0] !== auth.username || credentials[1] !== auth.password) { return callback(new journey.NotAuthorized("Invalid username or password")); } // Respond with no error if username and password match callback(null); } };[/code]
We can set this method on the Journey router by passing it in the options hash. Any routes that we wish to be behind the authentication filter need to be wrapped in a call to
map.filter(function () { ... })
router-auth.js
exports.createRouter = function (resource) { return new (journey.Router)(function (map) { // // Resource: Bookmarks // map.path(///bookmarks/, function () { // // Authentication: Add a filter() method to perform HTTP Basic Auth // map.filter(function () { // // All of the previous routes we had go in here, but they // are now behind HTTP Basic Auth // }); }); }, { strict: false, filter: helpers.auth.basicAuth }); };[/code]
Wrapping up
Now that we have completed our web service, lets fire up http-console for an interactive session with our Bookmark resource.$ http-console http://127.0.0.1:8000 > http-console 0.5.1 > Welcome, enter /help if you're lost. > Connecting to 127.0.0.1 on port 8000. http://127.0.0.1:8000/> Authorization: Basic YWRtaW46cGFzc3dvcmQ http://127.0.0.1:8000/> /json http://127.0.0.1:8000/> /headers Accept: */* Authorization: Basic YWRtaW46cGFzc3dvcmQ Content-Type: application/json http://127.0.0.1:8000/> POST /bookmarks ... { "url": "http://nodejs.org" } HTTP/1.1 200 OK Date: Sat, 05 Feb 2011 05:38:47 GMT Server: journey/0.3.0 Content-Type: application/json Content-Length: 77 Connection: close { bookmark: { url: 'http://nodejs.org', _id: 'xnIgT8', resource: 'Bookmark' } } http://127.0.0.1:8000/> GET /bookmarks/xnIgT8 HTTP/1.1 200 OK Date: Sat, 05 Feb 2011 05:39:01 GMT Server: journey/0.3.0 Content-Type: application/json Content-Length: 121 Connection: close { bookmark: { url: 'http://nodejs.org', _id: 'xnIgT8', resource: 'Bookmark', _rev: '1-cfced13a45a068e95daa04beff562360' } } http://127.0.0.1:8000/> GET /bookmarks HTTP/1.1 200 OK Date: Sat, 05 Feb 2011 05:39:05 GMT Server: journey/0.3.0 Content-Type: application/json Content-Length: 369 Connection: close { bookmarks: [ { _id: 'xnIgT8', _rev: '1-cfced13a45a068e95daa04beff562360', url: 'http://nodejs.org', resource: 'Bookmark' } ] }[/code]
I hope this has been helpful for those of you looking to get started with node.js. Check out the rest of our blog for more advanced libraries and tutorials. Come back soon for more on the Art of Nodejitsu.
相关文章推荐
- [Docker] Build a Simple Node.js Web Server with Docker
- A Simple MVC Setup In Node.JS
- Build Node.Js web server in Docker containers: nodejs+pm2+mongodb+redis
- [Node.js] Creating JWTs (JSON Web Tokens) in Node
- [转] Creating a Simple RESTful Web App with Node.js, Express, and MongoDB
- How to create simple web service in VS2010, NOT WCF service
- Run ionic web app in nodejs
- Part 17 Consuming ASP NET Web Service in AngularJS using $http
- [转]Work With Odata in Web API: Create Your First Odata Service
- 十大 Node.js 的 Web 框架_快速提升工作效率
- Use command line arguments in Node.js
- MDT 2010 - Setting the Computer Description in AD without a webservice
- Node.js的原型继承函数 util.inherits
- Express 4.x Node.js的Web框架
- Create an ASP.NET web app in Azure App Service
- Using the Web Service Callbacks in the .NET Application
- sap.ui.require in SAP UI5 and require in nodejs
- react-webpack-express实现多页面 — node.js开发
- Node.js Web框架之Express