您的位置:首页 > Web前端 > Node.js

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

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 in
bin/server
and 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
-t
argument 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 Implemented
and the corresponding action so that we can be sure we've hit the correct route.

We use the
map.path
syntax 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
/bookmarks
and
/bookmarks/foobar
respectively 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/json
header 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.setup
is 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
,
update
and
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 Error
with 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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: