Ejemplo de REST con Node.js + express y MongoDB

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.

by Pello Altadill 08/20/2013 23:15:49 - 430 hits

Ejemplo de Web Workers, los hilos javascript

Desde que los navegadores tenían soporte para Javascript, su código siempre se ha ejecutado en un único hilo. Los scripts que se desarrollan para el navegador están orientados a eventos y estos se van ejecutando en una especie de cola. Es decir, conforme se van generando los eventos estos van a una cola de tareas y Javascript los va procesando de uno en uno en un bucle. Web Workers, hilos en tu navegador HTML5 trae como novedad los Web Workers, que son procesos de javascript separado...

by Pello Altadill 08/20/2013 12:51:28 - 535 hits

More »

Services en Android

En Android no solamente hay aplicaciones de ventanas o activities. Hay muchos otros tipos de elementos como por ejemplo los Services. Los servicios nos permiten dejar un programa residente en la memoria y disponible para cualquiera que lo necesite. Los servicios no tienen interfaz gráfico ni nada, son programas pensados para ser utilizados desde las Activities o incluso desde otros servicios. Un service puede crearse para dar servicio -privado- a una sola aplicación o puede hacerse público (a través ...

by Pello Altadill 08/19/2013 22:29:37 - 557 hits

More »

Creando módulos en Node.js

Lo del servidor web con pocas líneas o refactorizado para que tenga forma de clase va quedando más o menos apañado. Pero en cualquier proyecto que vaya creciendo vamos a necesitar bastantes más cosas y claro, ir metiendo todo el programa en un único fichero, aunque algún programador bajeril pueda decir que es más simple, pues está muy feo. Como en cualquier otro lenguaje y sea el tipo de programación que se...

by Pello Altadill 08/18/2013 22:45:09 - 270 hits

More »

AsyncTask en Android

Los AsyncTask están recomendados para tareas concretas y finitas, como por ejemplo descargar el contenido de una URL. Si lo que necesitas es una especie de Hilo que esté ejecutándose contínuamente tendrías que usar los Thread convencionales (no se detienen ni al pulsar home ni al pulsar Back) o directamente crear un Service de Android. Para crear un AsyncTask debes extender la clase AsyncTask indicándole tres tipos de clase en la declaración, como por ejemplo AsyncTask<String, String, V...

by Pello Altadill 08/18/2013 11:06:08 - 571 hits

More »

Perfiles de desarrollo y producción con Maven

Algo muy común y recomendable en cualquier proyecto medianamente serio es contar con un servidor de desarrollo y otro de producción, además del propio equipo del programador donde se hacen las primeras pruebas. Los programas se prueban en local, se prueban en desarrollo y finalmente, cuando se ha verificado que todo está correcto, se puede pasar a producción donde se explotará una BD con datos reales y habrá usuarios reales. Hay lugares en los que incluso existe una servidor de pre-producción....

by Pello Altadill 08/17/2013 22:16:00 - 451 hits

More »

Hilos en Android con Threads java

Toda aplicación de Android tiene al menos un hilo o thread de ejecución. Ese thread es el encargado de la pantalla que tienes ante ti y es imprescindible por la manera en la que Android gestiona los cambios en el interfaz: a través de una cola de mensajes. Es decir, cada vez que aplicamos algún cambio en algún View no se aplica directamente sino que se deja la petición en una cola. Y ese hilo, el UI thread o el hilo principal de la aplicación es quien precisamente se encarga de procesar esa cola...

by Pello Altadill 08/17/2013 10:50:52 - 593 hits

More »

Servidor web de Node.js refactorizado

Sí, seguro que el primer código que has visto de Node.js es uno de esos milagrosos servidores web hechos con pocas líneas. Olvídese de aparatosos sockets, de punteros o de BufferedStreamsWriters, un módulo, unos callbacks, eliges un puerto y a dar servicio. Bueno, uno trata de ser un good Node.js citizen y este código se entiende y mola, pero a la larga, si lo queremos ir mejorando y añadiendo otras funcionalidades cada vez va a ser más complicado mantenerlo. Lo que propongo en ...

by Pello Altadill 08/16/2013 23:18:31 - 249 hits

More »

Hibernate self-join con anotaciones

Vamos a dejar por unos instantes tanto Javascript sin tipos y sin estructura y pongámonos otra vez la corbata para volver al mainstream de aplicaciones empresariales J2EE. Quedaba pendiente ver cómo montar un self-join pero con anotaciones, es decir una clase que se hace referencia a sí misma. En su momento ya puse aquí la versión con el mapeo por XML, vamos a ver cómo quedaría la cosa pero usando las anotaciones en la ...

by Pello Altadill 08/16/2013 17:06:58 - 234 hits

More »