Aruitectura MVC

03/11/2016 Modelo, Vista, Controlador

Introducción

En este capítulo construiremos una arquitectura MVC en nuestra Aplicación, para ello, deberemos crear algunos directorios en la carpeta server, de modo que el scaffold de dicha arquitectura se implementará desde el Back-end. Además de los Modelos, las Vistas y los Controladores, también hablaremos de las Rutas y los Middlewares.

Middlewares

En Express todo son Middlewares debido a que Express está construido sobre Connect, un Framework de bajo nivel para NodeJS. Este framework ofrece el mecanismo de ensamblación de módulos que ExpressJS utiliza y pone a disposición de un desarrollador mediante el método use(). La naturaleza de este mecanismo consiste en montar Middlewares uno detrás de otro según los requisitos de la Aplicación.

Por ejemplo, el módulo BodyParser es un Middleware que se monta en Express con la finalidad de otorgar a la aplicación de la capacidad de recibir datos de un formulario de cliente. Todos los Middlewares se montan con el método app.use().

El método use()

Este método es la clave de cómo se construye una aplicación con Express. El método se puede invocar de dos formas:

  1. app.use(callback): Recibiendo como parámetro una función. Esta función recibirá como argumentos una req y una res. Cualquier petición que se haga al servidor ejecutará el cuerpo de dicho callback; se dice entonces que ese callback es un Middleware.
                            
                                app.use(bodyParser.json());
                                app.use(bodyParser.urlencoded({ extended: false }));
                            
                        
  2. app.use(path, callback): En este caso recibe dos parámetros, donde el primero es una ruta donde montar el Middleware, es decir, dicho callback sólo será invocado ante aquellas peticiones realizadas contra el path especificado.
                            
                                var index = require('./server/routes/index');
                                ...
                                app.use('/', index);
                            
                        

Lo más interesante, es que los Middlewares, a su vez, pueden montar otros Middlewares!

Si nos fijamos en el caso de app.use('/', index); vemos que como callback recibe un Router de Express, el cual monta a su vez, en la ruta / otro Middleware con router.get('/', indexCtrl.getIndex);. De este modo se consigue una gran flexibilidad a la hora de modularizar el código.

La función de callback

La función de callback puede ser una función de 'respuesta o final' o un 'middleware' que dejará paso a otros middlewares. Esto se determina según el número de parámetros:

  • callback(req, res): Dos parámetros indican que este middleware ejecutará el callback y nada más.
  • callback(req, res, next): El tercer parámetro es una función de callback. Esto significa que, en el cuerpo de la función llamaremos a next() para continuar con la cadena de Middlewares.

Midllewares propios

Como hemos visto, todo son Middlewares, sin embargo, deberemos hacer distinciones entre Middlewares según su función. Distinguiremos Rutas, Controladores, Módulos y 'Middlewares'.

  • Rutas: Mapean Url's.
  • Controladores: Realizan el control de datos entre Vista y Modelo.
  • Módulos: Extiende la funcionalidad de Express (BodyParser o Mongoose por ejemplo).
  • 'Middlewares': Serán Middlewares de desarrollo propio.

Los 'Middlewares' propios los ubicaremos dentro de un directorio middlewares ubicado en server. Ahí depositaremos los distintos Middlewares propios que vayamos desarrollando. Su razón de ser será ejecutarse siempre, por ejemplo, para mantener una cookie o una variable global de toda la aplicación.

Rutas

Las rutas son el mecanismo por el cual se mapean las url's de la aplicación con funciones de un Controlador. Las rutas las tendremos dentro de server/routes. Podemos ver que Express Generator nos ha facilitado dos archivos, index.js y users.js, sin embargo, aunque definen url's no usan ningún controlador.

Modificaremos el archivo index.js para que tenga el siguiente contenido:

                
                    var express = require('express');
                    var router = express.Router();

                    var indexCtrl = require('../controllers/IndexCtrl');

                    /* GET home page. */
                    router.get('/', indexCtrl.getIndex);

                    module.exports = router;
                
            

Hemos modificado la línea con el router.get() para indicar que, cuando un usuario acceda a la url base / el sistema haga una llamada al método getIndex() del controlador IndexCtrl. En el siguiente apartado crearemos dicho Controlador.

Controladores

Los controladores se encargan de controlar los datos que fluyen entre la Vista y los Modelos.

Crearemos un directorio controllers dentro de server. En este directorio iremos guardando todos los controladores de la aplicación. Por ejemplo, crearemos un archivo llamado IndexCtrl.js. En él escribiremos:

                
                    'use strict';

                    module.exports.getIndex = (req, res) => {
                        res.render('index', { title: 'Express' });
                    };
                
            

De este modo, nuestro módulo IndexCtrl exportará una función llamada getIndex() que recibe una req (request del cliente) y una res (response del server). Esta es la función que se requiere para, al definir una ruta, realizar el enlace de una Url con una función.

La línea res.render() indica que se enviará una Respuesta al Cliente consistente en renderizar una vista llamada index. Esta vista deberá ubicarse dentro del directorio views.

Nota: Aunque el cambio en este caso no supone apenas nada, es recomendable mantener esta separación de responsabilidades, en un futuro la aplicación crecerá, de modo que si mantenemos un código pequeño y atómico nos será más fácil de escalar y modificar más adelante.

Modelos

El Modelo es el encargado de comunicarse con la Base de Datos; ya hemos creado algo en el capítulo anterior para establecer una conexión con el archivo db.js. Haremos algo similar con nuevos modelos con la intención de modelar entidades de nuestra BBDD.

MongoDB es una Base de Datos NoSQL, en este tipo de BBDD las tablas se llaman colecciones y representan una entidad que, al contrario que en una Base de Datos Relacional (como MySQL) puede tener una estructura de datos dinámica.

Los modelos los ubicaremos en server/models. Por ahora sólo tendremos el archivo db.js del capítulo anterior. Añdiremos más modelos más adelante.

Las distintas entidades de nuestra Base de Datos MongoDB serán modeladas haciendo uso de Mongoose, un excelente ORM que nos facilitará mucho la comunicación de los Modelos con la BBDD. Aquí podemos ver cómo se Modela con Mongoose. Ahora crearemos un Modelo User para explicar mejor cómo se implementa.

El Modelo User

En este proyecto crearemos un Modelo User para tratar con los Usuarios de nuestra app. El primer paso que haremos será modelar dicha entidad creando un archivo User.js en la carpeta models.

El archivo User.js contendrá un Modelo de Mongoose y para ello deberemos definir un Schema. Al definir un Schema deberemos tener claro qué propiedades conformarán la Entidad que estamos modelando.

Para todo Usuario de la aplicación definiremos:

  1. Email de tipo String y único.
  2. Name de tipo String y obligatorio.
  3. Password de tipo String y obligatorio.
  4. PasswordSalt de tipo String y obligatorio.
  5. CreatedAt de tipo fecha.
  6. Role de tipo String, por defecto 'user'.

Para el Password usaremos dos atributos, Password, que contendrá el hash del Password que introduzca un Usuario y PasswordSalt, que contendrá un String aleatorio con el fin de evitar que dos Passwords iguales puedan tener el mismo hash, esto es una pequeña medida de seguridad.

Para obtener un hash a partir de un String (el password que decida introducir un Usuario) deberemos usar un mecanismo de cifrado. En este caso haremos uso del paquete crypto, incluído en NodeJS.

Escribiremos en el archivo User.js lo siguiente:

                
                    'use strict';
                    const mongoose = require('mongoose');
                    const crypto = require('crypto');

                    const SALT_LEN = 16;
                    const ITERATIONS = 1000;
                    const KEY_LEN = 64;
                    const DIGEST = 'sha512';

                    let schema = new mongoose.Schema({
                        email:  {
                            type: String,
                            required: true,
                            unique: true
                        },
                        name: {
                            type: String
                        },
                        password: {
                            type: String,
                            required: true
                        },
                        passwordSalt: {
                            type: String,
                            required: true
                        },
                        createdAt: {
                            type: Date,
                            default: Date.now
                        },
                        role: {
                            type: String,
                            'default': 'user'
                        }
                    });

                    schema.methods.validPassword = function (password) {
                        let hash = this.constructor.hashPassword(password, this.passwordSalt);
                        return this.password === hash;
                    };

                    schema.statics.getSalt = function() {
                        return crypto.randomBytes(SALT_LEN).toString('hex');
                    };

                    schema.statics.hashPassword = function (password, salt) {
                        salt = salt || this.getSalt();
                        return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST).toString('hex');
                    };

                    mongoose.model('User', schema);
                
            

Vemos que hemos definido un Schema haciendo uso de mongoose.Schema(), de este modo definimos todos los atributos que tendrán nuestros Usuarios. Además, a este Schema también hemos añadido unos métodos:

  1. validPassword(password): Método encargado de determinar si un password introducido por un Usuario coincide con el password registrado previamente por dicho Usuario.
  2. getSalt(): Método estático. Sirve para obtener un salt, que no es más que un string aleatorio.
  3. hashPassword(password, salt): Método estático. Este método es el encargado de convertir el String introducido como password por un Usuario en un hash. Para ello hace uso de crypto y un poco de salt. Si la salt no es suministrada, calculará una.

Es muy importante la línea final mongoose.model('User', schema);, aquí es donde se registra nuestro modelo, bajo el nombre de 'User'. De este modo, más adelante, podremos llamar a var user = mongoose.model('User') para hacer uso de este Modelo y obtener instancias de Usuarios. Podemos pensar en esa línea final como un module.exports.

Nota: El método mongoose.model(model, schema, collection) puede recibir un tercer parámetro. Este parámetro 'collection' es el Nombre de la Colección. Si no se especifica nada, Mongoose lo que hará será seguir un convenio para averiguar cuál sería el nombre de la Colección a partir del nombre del Modelo. Este convenio es:
                    "El nombre de la Colección será el nombre del Modelo en minúsculas y en plural"
                
Esto quiere decir que, en este caso, nuestro Modelo User pertenecerá a una Colección llamada users.

Con esto damos por terminado el proceso de modelar un Usuario.

Lo único que faltaría es llamar a todo este módulo. Esto lo haremos desde nuestro db.js, núcleo de interacción con la BBDD. Editaremos el fichero añadiendo una línea al final del todo:

                
                    ...
                    // Register Models
                    require('../models/User');
                
            

Con esto ya tendremos la aplicación lista y configurada. Si lanzamos el servidor con gulp nodemon deberiamos ver que todo continúa funcionando como antes.

MVC en acción

Ahora lo juntaremos todo para hacer un pequeño ejemplo de cómo funcionaría esto. Usaremos el fichero que nos creó Express Generator ubicado en server/routes/users.js. Lo editaremos así:

                
                    var express = require('express');
                    var router = express.Router();

                    var usersCtrl = require('../controllers/UsersCtrl');

                    /* GET users listing. */
                    router.get('/', usersCtrl.getUsers);
                    
                    module.exports = router;
                
            

Ahora crearemos el Controlador UsersCtrl. Crearemos el archivo dentro de controllers y escribiremos en él lo siguiente:

Lo que hemos hecho es que, al accederse a la Url /users, enviaremos como respuesta al Cliente un json con todos los usuarios.

El Navegador de por sí nos mostrará el JSON en texto plano. Podemos darle un mejor formato usando alguna extensión. En mi caso utilizo Postman.

Ejemplo de cómo se muestran los datos tras realizar la consulta desde Postman:

Postman example

Como no tenemos ningún Usuario en BBDD, el array users aparece vacío ^^. En el siguiente capítulo veremos cómo meter datos en BBDD.

Si en lugar de usar el método res.json() usamos res.render(view) podríamos devolver una vista completa de nombre 'view' (y escrito en .pug) que se mostraría al cliente como un Html/CSS/JS. Esto lo veremos en el próximo capítulo.

Para finalizar, vamos a crear una ruta mas .

Ya hemos creado una ruta para listar a 'todos' los Usuarios, ahora crearemos una para mostrar a un Usuario en concreto. En el fichero routes/users.js añadimos:

                
                    ...
                    /* GET user by email */
                    router.get('/:email', usersCtrl.showUser);
                    ...
                
            

Esta ruta utiliza un parámetro, definido con la sintaxis de dos puntos :email; esto quiere decir que mediante la url podemos pasar como parámetro una variable 'email'. Esta variable la podremos usar desde un controlador.

En el controlador UsersCtrl.js añadiremos el método showUser():

                
                    ...
                    module.exports.showUser = (req, res) => {
                      var user = User.find({email: req.params.email}, function (err, user) {
                          if(err) console.log(err);
                          res.status(200);
                          res.json({user: user});
                      });
                    }
                
            

Consultamos el parámetro :email de la url mediante req.params.email. Seguidamente usamos dicha variable para realizar una consulta en BBDD usando Mongoose con el método find().

Con esto ya tenemos el scaffold más completo y adaptado a una aplicación Express más realista. El código lo podéis mirar aquí.

Si tienes alguna duda o sugerencia, no dudes en participar!