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.
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().
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:
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
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 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:
next()
para continuar con la cadena de Middlewares.
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'.
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.
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.
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.
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.
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:
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:
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
.
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.
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.
Ejemplo de cómo se muestran los datos tras realizar la consulta desde Postman:
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.
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í.