Paso 7: Persistencia con el stack MEAN


Recuerdas la app ToDo que realizé con AngularJS?

Se implementó en esa entrada como forma de persistencia el almacenamiento local en el navegador del usuario, utilizando LocalStorage.

En próximas entradas se abordarán otros sistemas de persistencia modernos, como Firebase, pero antes, le toca el turno al stack MEAN.

Crearé en este espacio un backend en forma de API Rest que funcione de forma independente y pueda ser usado como base para otras aplicaciones más complejas.

Para desarrollar esta API Rest usaré NodeJS con las últimas bondades de express 4.X y MongoDB con mongoose, para juntos frontend y backend completar las tecnologías que forman dicho stack MEAN.

Si eres principiante en estos temas, te introduzco a continuación algunos conceptos que debes dominar:

REST

REST es el principio de arquitectura subyacente de la web. Lo sorprendente de la web es el hecho de que los clientes (navegadores) y servidores pueden interactuar de maneras complejas sin que el cliente saber nada de antemano sobre el servidor y los recursos que alberga.

La restricción de clave es que el servidor y el cliente deben estar de acuerdo sobre los medios utilizados, que en el caso de la web es HTML.

Una API que se adhiere a los principios de REST no requiere que el cliente conozca nada de la estructura de la API.

Más bien, el servidor tiene que proporcionar la información que el cliente necesita para interactuar con el servicio.

Entonces, ¿cómo se aplica esto a HTTP, y cómo puede aplicarse en la práctica? HTTP se orienta en torno a los verbos y los recursos.

Los dos verbos en el uso de la corriente principal son GET y POST, que creo que todo el mundo reconocerá, mas el estándar HTTP define varias otras como PUT y DELETE.

Estos verbos se aplican entonces a los recursos, de acuerdo con las instrucciones proporcionadas por el servidor.

MEAN

La omnipresencia de JavaScript en el desarrollo de aplicaciones web se plasma en el stack conocido con el acrónimo MEAN (MongoDB – Express – AngularJS – Node.JS).

Desde el cliente al servidor, pasando por la base de datos, todas con el mismo punto en común: Desarrollo end-to-end usando JavaScript tanto en el frontend, backend y la base de datos.

El auge de estas tecnologías y su perfecta integración entre ellas ha dado pie a que existan distintas soluciones tipo boilerplates MEAN que nos permitan montar todo lo necesario para empezar a crear nuestra aplicación sobre esta infraestructura JavaScript.

Sin entrar en definir complementamente cada una de las tecnologías que lo componen nos encontramos con:
* MongoDB como base de datos que almacena documentos JSON.
* Express como web framework basado en Node.js que nos permite crear APIs REST.
* Angularjs como framework para crear la parte cliente de la aplicación en formato SPA (Single Page Application, de sus siglas en inglés.
* Node.js como framework JavaScript basado en V8 que proporciona funcionalidades core para nuestra aplicación bajo un modelo asíncrono de eventos.

Contruyendo la aplicación REST

Los siguientes pasos asumen que tienes instalado y configurado tu sistema para utilizar MongoDB y NodeJS.

El fichero app.js

Este fichero en una aplicación node es donde se cargan las dependencias, se establece la lógica a seguir por la app y es el encargado de ejecutar el servidor. A continuación te explico por partes su implementación.

var express = require("express"),
  app = express(),
  bodyParser = require("body-parser"),
  methodOverride = require("method-override")

// Middlewares
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
app.use(methodOverride());
app.set('port', process.env.PORT || 3020);

En este caso al usar express v.4 con respecto a la versión 3, varios Middlewares han sido eliminados y son tratados como dependencias aparte, como es el caso de bodyParser, compress, logger, session, favicon, methodOverride, etc.

No se encuentran dentro del paquete Express y hay que importarlos si se van a usar en el fichero package.json. De esta manera Express queda más ligero y tenemos más flexibilidad en cómo podemos definir las rutas para nuestras aplicaciones.

Con el snippet anterior importé el módulo express, lo doté con más funcionalidades a través de los Middlewares y configuré el puerto de escucha del servidor a 3020.

mongoose = require('mongoose');
// Connexón a la Base de Datos
mongoose.connect('mongodb://localhost/todoApi', function(err, res) {
if (err)
    throw err;
console.log('Connected to Database');
});

Importamos mongoose y nos conectamos a la base de datos en localhost de nombre todoApi.

// Importar Modelos y controladores
var models = require('./models/todo')(app, mongoose);
var TodoController = require('./controllers/todo')(mongoose);

Importamos los modelos y controladores que se explicarán a continuación.

// Router de express
var router = express.Router();

// Ruta principal
router.get('/', function(req, res) {
    res.send("Server API Rest para la app ToDo");
});
app.use(router);

Configuramos un router general de la aplicación para que en la en la ruta raíz muestre el mensaje especificado.

app.all('*', function(req, res, next) {

  res.header("Access-Control-Allow-Origin", "*");

  res.header('Access-Control-Allow-Methods', 'OPTIONS,GET,POST,PUT,DELETE');

  res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
  if ('OPTIONS' == req.method) {
    return res.send(200);
  }
  next();
});

Le indicamos al servidor que en todas las rutas acepte peticiones cross-domain por GET, POST, PUT y DELETE, necesarias para implementar el CRUD.

var todos = express.Router();

todos.route('/todos')
  .get(TodoController.getTodos)
  .post(TodoController.insertTodo);

  .get(TodoController.findById)
  .put(TodoController.updateTodo)
  .delete(TodoController.deleteTodo);

app.use('/api_todos', todos);

Creamos y configuramos otra instancia de ruteo y establecemos las rutas para nuestro CRUD.

// Iniciar el servidor
app.listen(app.get('port'), function() {
  console.log("Node server running on " + app.get('port'));
});

Finalmente iniciamos el servidor por el puerto especificado y mostramos un mensaje en consola.

Modelos

Dentro de la carpeta modelo vamos a ir registrando cada modelo nuevo que vaya apareciendo en la aplicación. Por el momento solo creamos el de todo.js.

exports = module.exports = function(app, mongoose) {

  var todoSchema = new mongoose.Schema({
    name: {type: String},
    done: {type: Boolean},
  });

  mongoose.model('todo', todoSchema);

};

Cabe destacar que mongoose.Schema tiene otras potencialidades que normalmente en este punto no son explotadas.

Controladores

Definimos igualmente cuantos controladores necesite nuestra aplicación y estos los ubicamos en la carpeta controllers.

A continuación te muestro el código comentariado:

// define variable controladora donde se registrarán las funciones
var todo_controller = {};

// usa module.exports pasando por parámetro mongoose
exports = module.exports = function(mongoose) {

    // crea una instancia del modelo Todo
    var Todo = mongoose.model('todo');

    // obtiene todos los ToDo's con el método find()
    todo_controller.getTodos = function(req, res) {

        Todo.find(function(err, todos) {

            if (err)
                res.send(500, err.message);
            console.log('GET /todos')
            res.status(200).jsonp(todos);

        });

    };

    // inserta el documento en la colección Todo
    todo_controller.insertTodo = function(req, res) {

        // se crea una nueva instancia del schema
        var todo_new = new Todo({
            name: req.body.name,
            done: false,
        });

        todo_new.save(function(err, todo) {
            if (err)
                return res.send(500, err.message);
            res.status(200).jsonp(todo);
        });

    };

    // obtiene un documento ToDo dado un ID
    todo_controller.findById = function(req, res) {

        Todo.findById(req.body.id, function(error, todo) {
            if (error) {
                return res.send(500, error.message);
            } else {
                res.status(200).jsonp(todo);
            }
        });

    }

    // actualiza el documento por ID
    todo_controller.updateTodo = function(req, res) {

        Todo.update(
                {_id: req.body._id},
        {$set: {
                name: req.body.name,
                done: req.body.done
            }}, function(err) {
            if (err)
                return res.send(500, err.message);
            else
                res.status(200).jsonp('ok');
        });

    }

    // elimina un documento de la colección por ID
    todo_controller.deleteTodo = function(req, res) {

        Todo.remove({_id: req.params.entryId}, function(error) {
            if (error) {
                return res.send(500, error.message);
            } else {
                res.status(200).jsonp('ok');
            }
        });

    }
    return todo_controller;
};

Ejecuta el servidor

Para realizar esta tarea debemos asegurarnos primero que tenemos resueltas todas las dependencias con npm, para ello nos movemos al directorio del proyecto y ejecutamos:

npm install, luego node app.js.

Modificando el servicio ToDo en Angular

Con esto ya tenemos practicamente implementado el servidor REST, ahora hay que hacer modificaciones al servicio que hará uso de este. Para ello vamos al directorioapp/scripts/services y cambiamos el archivotodo.js` con lo siguiente.

angular.module('todosApp')
.factory('Todo', function($http, $resource) {

    var todos = [];

    // URL del servidor rest
    var url_server_api_rest = 'http://127.0.0.1:3020/api_todos/';

    /*
    Configuramos el servicio $resource de AngularJS para interactuar con la API Rest.
    */

    var api_todo = $resource(url_server_api_rest + 'todos/:entryId', {}, {

        query  : {method: 'GET', params: {entryId: ''}, isArray: true},
        post   : {method: 'POST'},
        update : {method: 'PUT', params: {entryId: '@entryId'}},
        remove : {method: 'DELETE', params: {entryId: '@entryId'}}

    });

    var result = {

        getTodos: function() {
            todos = api_todo.query();
            return todos;
        },

        insertTodo: function(todo) {
            // ejemplo usando $http para variar
            $http.post(url_server_api_rest + "todos", todo)
                    .success(function(respuesta) {
                todos.push(respuesta);
            }).
                    error(function() {
                alert('Error');
            });
        },

        updateTodo: function(todo) {
            var position = todos.indexOf(todo);
            if (position >= 0) {
                todo.done = !todo.done;
                todos[position] = todo;
                api_todo.update({entryId: todo._id}, todo)
                return true;
            }
            return false;
        },

        deleteTodo: function(todo) {
            var position = todos.indexOf(todo);
            if (position >= 0) {

                todos.splice(position, 1);
                api_todo.remove({entryId: todo._id})
                return true;

            }

            return false;
        }

    };
    return result;
});

Este archivo no cambió mucho, solo declaramos la dirección donde se está ejecutando el servidor REST e inyectamos $resource, un servicio de Angular especializado en APIs REST.

Conclusiones

Haciendo uso de yeoman podemos usar su generador para cuando se usen estas tecnologías, no tener que estar escribiendo el mismo código una y otra vez.

Si no te gustó MongoDB, puedes cambiar de base de datos o usar un ORM.

Hasta la próxima.