Alrededor de Node.js existen infinidad de proyectos en plena ebullición, y prueba de ello es que solamente para desarrollar webs existen varios frameworks, algunos basados en el clásico MVC y otros más orientados a servir recursos REST. Ese es el caso de Express, uno de los frameworks más populares disponibles para Node.js. En este post veremos cómo crear un servidor que ofrezca recursos REST a un frontend. Si no te gusta javascript, amijo, jejej, no sé: deja la web y vuelve al Cobol, porque esto va a ser una fiesta.

Una de las cosas que son encantadoras es que podemos instalar una especie de ejecutable de express como un comando global en el sistema:

linux:~/nodejs/express# npm install -g express
...
(installing zillions of js stuff)
...
linux:~/nodejs/express# express -h

  Usage: express [options] [dir]

  Options:

    -h, --help          output usage information
    -V, --version       output the version number
    -s, --sessions      add session support
    -e, --ejs           add ejs engine support (defaults to jade)
    -J, --jshtml        add jshtml engine support (defaults to jade)
    -H, --hogan         add hogan.js engine support
    -c, --css   add stylesheet  support (less|stylus) (defaults to plain css)
    -f, --force         force on non-empty directory


Y ahora iniciar nuestro desarrollo partiendo de un comando que nos genera el site con el contenido necesario para el server y el contenido para la parte pública de la web. Esto, como dice un colega, es la crema:

linux:~/nodejs/express# express music_chart

   create : music_chart
   create : music_chart/package.json
   create : music_chart/app.js
   create : music_chart/public
   create : music_chart/public/javascripts
   create : music_chart/public/images
   create : music_chart/public/stylesheets
   create : music_chart/public/stylesheets/style.css
   create : music_chart/routes
   create : music_chart/routes/index.js
   create : music_chart/routes/user.js
   create : music_chart/views
   create : music_chart/views/layout.jade
   create : music_chart/views/index.jade

   install dependencies:
     $ cd music_chart && npm install

   run the app:
     $ node app

linux:~/nodejs/express#

Sí, como ya habrás adivinado la aplicación nos servirá para mantener una lista de canciones, como la de los cuarenta aborrecibles pero con música algo mejor. Si te lo instalas todo puedes probarlo con una página web de pruebas llamada rest.html que realiza las 4 operaciones básicas con recursos REST. Para el PUT y el DELETE se utiliza un POST con un parámetro oculto llamado _method. Node.js los interpretará como peticiones PUT y DELETE y se sentirá super RESTful y tú un RESTafari.

contenidos del proyecto

Puedes descargarte el proyecto aquí Aparte de las carpetas generadas por express he creado alguna más y algún que otro fichero. Esto sería lo más reseñable:

  • models: el modelo, los datos del dominio, una especie de clase que representa la tabla de canciones.
  • routes: esta carpeta la genera express y sirve para guardar todas las rutas, es decir: qué tiene que responder el servidor ante determinadas peticiones. Desde el servidor se crea el mapeo de peticiones-a-rutas y en esa carpeta es donde se atiende cada petición, donde se accede a la BD y se da la respuesta.
  • lib: carpeta en la que he metido alguna utilidad como el logger de colores.
  • config.js: un fichero que contiene un array con parámetros de configuración básicos como la URL de la BD, password, etc...
  • songs.json: un volcado de la BD que se puede importar siguiendo las instrucciones del README.txt

music_chart.js: el servidor

Este es un servidor Web que por un lado puede servir contenido estático que se encuentre dentro de la carpeta public y por otro tiene definidas una serie de rutas para poder ofrecer recursos REST. Por no volver a explicar que era REST echa un ojo a esto. Bueno, el servidor está comentado. Lo crucial está al final, cuando se definen las rutas.


/**
 * music_chart.js server
 * Based on http and express modules.
 * The purpose is to manage a list of songs located in a MongoDB database,
 * providing REST resources.
 * To test it try with http://localhost:3000/rest.html
 *
 * @author Pello Xabier Altadill Izura
 * @greetz For those interested in localStorage and such...
 */

// Requires modules
var express = require('express');
var http = require('http');
var path = require('path');

// my configuration
var config = require('./config');

var routes = require('./routes');

var songRoutes = require('./routes/song');

// We add some color to our logs...
var logger = require('./lib/coloredlog');
logger.setOpt('MusicChartSvr',true,false);

songRoutes.setLogger(logger);
// Just in case we don't like default colors
// logger.setColors('red','green','grey','rainbow');

// We set Model class for songs, passing our config
var songModel = require('./models/song');
mySongModel = new songModel.songModel(config.config);

songRoutes.setSongModel(mySongModel);


var app = express();


app.configure(function() {

	// We set express options
	// Server port
	app.set('port', process.env.PORT || 3000);
	// The views directory
	app.set('views', __dirname + '/views');
	// The template engine, jade based
	app.set('view engine', 'jade');

	// We will use some express facilities, for static content
	app.use(express.favicon());
	app.use(express.logger('dev'));
	app.use(express.bodyParser());
	// This allow to emulate RESTful queries through a hidden _method param
	// in forms (for PUT and DELETE).
	app.use(express.methodOverride());
	// This will enable our REST routes
	app.use(app.router);

	// Static content
	app.use(express.static(path.join(__dirname, 'public')));

	// Any other will be considered not found so:
	app.use(function(req, res, next){
		logger.logwarn(' 404 Resource not found: ' + req.url);
		res.send('Sorry ' + req.url + ' does not exist');
	});

});

/**
* By default node considers that it's in a development environment.
* To change environment set this var in shell:
* export NODE_ENV=production
*/
// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}



http.createServer(app).listen(app.get('port'), init);

function init (){
  logger.loginfo('Express server listening on port ' + app.get('port'));
};

app.get('/', routes.index);

// REST resources
app.get('/songs', songRoutes.list);
app.get('/song/:id',songRoutes.listOne);
app.post('/song/add',songRoutes.add);
app.put('/song/:id',songRoutes.update);
app.del('/song/:id',songRoutes.delete);
models/song.js: el modelo

A ver, en un principio tenía la idea de crear una especie de DAO por aquello de las viejas costumbres. Pero es que esto es algo distinto. Hay que retorcer mucho las librerías existentes e incluso hay quien sugiere módulos para forzar la ejecución en serie (ya hablé aquí), pero al final me queda la sensación de no estar haciendo las cosas al estilo Node.js. Así que le modelo básicamente proporciona (digamos) un Datasource y una instancia para poder mapear el schema de un array a la BD. Luego, cuando utilizo el modelo en el routes/song.js no queda mucho churro, por cada petición solamente hay una llamada con callback, relativamente corta y no sale un callback spaghetti de esos.

/**
* songModel
* defines schema for song table in mongodb database
* I'm not pretending to provide a kind of DAO, but
* 'Old habits are hard to break'.
* @Pello Xabier Altadill Izura
* @greetz GoF
*/


exports.songModel = function (config) {
	// self reference
	var self = this;

	var mongodb = require('mongoose');

	this.songs;
	this.songSchema;
	var db;

  	/**
  	* init
  	*/
  	this.init = function () {

  		mongodb.connect('mongodb://localhost/musicdb');
  		db = mongodb.connection;
		this.songSchema = new mongodb.Schema({ artist: {type: String},
										  song: {type: String},
										  album: {type: String},
										  rating: {type: Number}  });

		this.songs = mongodb.model('songs', this.songSchema);
  	};

};
routes/song.js: el controlador

Con el servidor en marcha, MongoDB con los datos cargados y el modelo de datos listo, ya solo nos quedan las rutas a las que atender. Es bastante fácil de seguir. Cada ruta va a una función, esta recoge los parámetros, llama a un método del modelo y según el resultado de una respuesta u otra.

/*
* routes/song.js
* Here we define the routes for each REST request to /song/ url
* We define the handlers for every request mapped in express server.
*
* list:    mapped from GET /songs/  : read all from db.
* listOne: mapped from GET /song/id : read one record from db
* add:     mapped from POST /song/  : adds a new record
* update:  mapped from PUT /song/id : updates existing record
* delete:  mapped from DELETE /song/id : deletes existing record
*
* @author Pello Xabier Altadill Izura
* @greetz For those interested in Android Async Tasks
*/

var logger;

var mySongModel;

// Sets logger instance
exports.setLogger = function (otherLogger) {
  logger = otherLogger;
};

// Sets song Model instance
exports.setSongModel = function (songModel) {
	mySongModel = songModel;
  mySongModel.init();
};

/******************
* REST resources  *
*******************/

/**
* READ all songs
* very simple, we use the model to make a find without parameters.
*/
exports.list = function(req, res){
    mySongModel.songs.find({},{}, function (err, allSongs) {
      if (err) {
        logger.logwarn('Song not found.');
        res.send('Song not found.');
      } else {
        logger.loginfo('We\'ve got them: ' +allSongs);
        res.send(JSON.stringify(allSongs));
      }
    });
};

/**
* READ one song
* we expect the _id an we pass to our model to make a find with a parameter
*/
exports.listOne = function(req, res){
  logger.loginfo('GET Song requested: ' + req.params.id);
    mySongModel.songs.find({_id:req.params.id},{}, function (err, oneSong) {
      if (err) {
        logger.logwarn('Song not found.');
        res.send('Song not found.');
      } else {
        logger.loginfo('We\'ve got it: ' +oneSong);
        res.send(JSON.stringify(oneSong));
      }
    });
};

/**
* ADD a new song.
* First we must create an Object with the params received and map to our schema.
* Then we can save the object.
*/
exports.add = function(req, res){
  var newSong = {
    artist : req.body.artist,
    song : req.body.song,
    album : req.body.album,
    rating : parseInt(req.body.rating)
  };
  logger.loginfo('POST add Song: ' + newSong);

  var songObj = new mySongModel.songs(newSong);

  /**
  * and now our schema is ready to be saved, or not.
  */
  songObj.save(function(err, data) {
      if (err) {
        logger.logerr('Error saving data');
        res.send('Error saving data: ' +err);
      }  else {
        logger.loginfo(data);
        res.send('Ok, new song saved: ' + data);
      }
  });

};

/**
* UPDATES a song
* easier than create, we define an array with update data and
* we try to update using the _id as search criteria
*/
exports.update = function(req, res){
  logger.loginfo('PUT Song requested: ' + req.params._id);
  songId = req.body._id;

  var updatedSong = {
    artist : req.body.artist,
    song : req.body.song,
    album : req.body.album,
    rating : parseInt(req.body.rating)
  };
  logger.loginfo('Trying to update Song: ' + updatedSong);

  /**
  * and now we try to update data
  */
     mySongModel.songs.update({_id:songId},updatedSong, function (err) {
        if (err) {
          logger.error('Error updating data: ' +err);
          res.send('Error updating data: ' +err);
        }  else {
          logger.loginfo('Ok, data updated');
          res.send('Ok, song updated: ' + songId);
         }
    });
};

/**
* DELETES a song.
* It expects just the _id and then tries to remove It from database.
*/
exports.delete = function(req, res){
  logger.loginfo('DELETE Song requested: ' + req.params.id);
    songId = req.params.id;

     mySongModel.songs.remove({_id : songId}, function(err) {
      if (err) {
        logger.logerror('No song with id: ' + songId);
        res.send('No song with id: ' + songId);
      } else {
        logger.loginfo('deleted ' + songId);
        res.send('Song deleted ' + songId);
      }
    });

};
Probando nuestro server Node.js

Ahí tienes la página en acción solicitando recursos REST, con el server de fondo escupiendo logs informativos y vomitando JSONes. El server necesita miles y miles de mejoras: comprobación de campos, pruebas, internacionalización, etc... todo se andará. Bien, ahora sí que sí, falta la última pieza del puzzle: hacer que el cliente sea una página html con Backbone. Y entonces ya, el círculo se cerrará, la fiesta del Javascript será total y nuestra dicha completa.