Convierte Express 4 en un framework MVC

  • #express
  • #nodejs

Express es un framework archiconocido en el mundo entorno a Node.js, y hoy te vamos a enseñar a convertirlo en un completo framework con estructura MVC.

Express es sin duda uno de los mejores frameworks disponibles para Node.js, ya que es minimalista, ligero y robusto a la vez. El minimalismo es una gran ventaja, ya que se convierte en flexibilidad a la hora de decidir cómo queremos estructurar nuestra aplicación, pero es también un problema si no tenemos experiencia suficiente estructurando el framework. Por defecto, en Express construiremos nuestra aplicación en un único fichero, generalmente app.js.

Patrón Modelo-Vista-Controlador

Es por ello que dependiendo del proyecto necesitemos un patrón de arquitectura. En este artículo vamos a implementar un patrón MVC (Modelo-Vista-Controlador) junto con su estructuración de directorios. El flujo de una petición se resolverá de la siguiente manera:

  1. La peticion se envía al enrutador para que la procese y cargue el controlador necesario.
  2. El controlador se encargará de la lógica de la aplicación y pedirá datos (si fuera necesario) al modelo. Después, con o sin datos, llamará a la vista.
  3. La vista, mediante un motor de plantillas, generará el resultado que verá el usuario.

Por otro lado, la estructura de ficheros y carpetas será la siguiente:

Ruta Descripción
package.json Contiene la lista de paquetes necesarios por la aplicación (entre otras cosas).
app.js Punto de entrada a la aplicación. Carga y gestiona todo lo necesario.
/app/config.js Fichero de configuración.
/app/routes.js Fichero que gestiona el enrutamiento y llama a los controladores.
/app/controllers Controladores para gestionar la lógica de la aplicación.
/app/models Modelos para gestionar la lógica de negocio.
/app/views Plantillas.
/app/libs Librerías básicas. En este artículo será un directorio vacío.
/public Carpeta pública. En este ejemplo solo contiene favicon.ico.

Puedes encontrar el código en nuestro repositorio de GitHub: https://github.com/felixsanz/express4-mvc-example.

Patrón MVC en Express

package.json

No voy a hablar de este fichero aquí porque su utilidad escapa al propósito de este artículo. Más información: package.json. Un fichero bastante básico para nuestro cometido:

package.json
JSON
{
  "name": "Express",
  "version": "0.0.0",
  "engines": {
    "node": "*"
  },
  "dependencies": {
    "express": ">=4.0",
    "morgan": "*",
    "body-parser": "*",
    "errorhandler": "*",
    "method-override": "*",
    "cookie-parser": "*",
    "serve-favicon": "*",
    "glob": "*",
    "mongoose": "*",
    "jade": "*"
  }
}

app/config.js

Exportamos un objeto JSON con información sobre nuestra aplicación.

app/config.js
JavaScript
module.exports = {
  name: 'Example',
  description: 'This domain is established to be used for illustrative examples in documents.',
  domain: 'example.com',
  url: 'http://www.example.com',
  env: 'development',
  port: 3000,

  database: {
    domain: 'localhost',
    name: 'example',
  },
}

Las claves name, description, domain y url no son utilizadas en este artículo, su propósito es utilizar sus valores en las plantillas: link(rel="stylesheet", href="#{config.url}/css/main.css").

A nosotros nos gusta añadir en este fichero valores como el código UA de Google Analytics, app_id de Facebook y todo ese tipo de cosas que utilizamos en plantillas y es buena idea tenerlas organizadas en un único fichero.

app.js

Lo primero es trabajar el fichero app.js que cargará los módulos necesarios para el arranque de nuestra aplicación.

Cargamos e instanciamos Express:

app.js
JavaScript
var express = require('express');
var path = require('path');

var app = express();

Después, como ya podemos trabajar con path y app, cargamos la configuración de nuestra aplicación y la guardamos mediante app.set para acceder a ella desde cualquier parte:

app.js
JavaScript
app.set('config', require(path.join(process.cwd(), 'app', 'config')));

Conectamos a la base de datos si lo necesitamos:

app.js
JavaScript
var mongoose = require('mongoose');
mongoose.connect('mongodb://' + app.get('settings').database.domain + '/' + app.get('settings').database.name);

Le indicamos a Express dónde están nuestras vistas y qué motor de plantillas debe utilizar (en este caso, Jade):

app.js
JavaScript
app.set('views', path.join(process.cwd(), 'app', 'views'));
app.set('view engine', 'jade');

Indicamos dónde está nuestra carpeta pública para servir ficheros estáticos:

app.js
JavaScript
app.use(express.static(path.join(process.cwd(), 'public')));

Cargamos módulos necesarios y/o recomendados en Express:

app.js
JavaScript
app.use(require('serve-favicon')(path.join(process.cwd(), 'public', 'favicon.ico')));
app.use(require('morgan')('combined'));
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(require('method-override')());
app.use(require('cookie-parser')());

Si utilizamos config.js para guardar datos que utilizaremos en las plantillas, debemos exportar la variable para hacerla visible en las plantillas:

app.js
JavaScript
app.locals.config = app.get('config');

De este modo después podemos utilizar variables como #{config.title} dentro de nuestras plantillas.

Mediante la variable env que se encuentra en config.js, podemos gestionar nuestro entorno para, por ejemplo, añadir lo siguiente:

app.js
JavaScript
if (app.get('config').env == 'development') {
  app.use(require('errorhandler')());
  app.locals.pretty = true; // No minificar código HTML
}

Si queremos utilizar nuestra instancia de app en cualquier parte (queremos), debemos exportarla primero:

app.js
JavaScript
module.exports = app;

Cargamos las rutas para que nuestra aplicación siga su curso:

app.js
JavaScript
require(path.join(process.cwd(), 'app', 'routes'))();

Y por último arrancamos la aplicación:

app.js
JavaScript
app.listen(app.get('config').port);

app/routes.js

Nuestra aplicación necesita, antes de gestionar las rutas, cargar los controladores. Primero cargamos los módulos necesarios:

app/routes.js
JavaScript
var path = require('path');
var app = require(path.join(process.cwd(), 'app'));
var fs = require('fs');
var glob = require('glob');

Al requerir app.js no se genera una instancia nueva, sino que se devuelve la existente gracias a que previamente hemos exportado.

Lo siguiente es crear un objeto vacío llamado controllers. Dentro de éste, guardaremos los controladores con la siguiente estructura:

  • controllers/users.js: controllers.users.
  • controllers/admin/users.js: controllers.admin.users.
  • etc.

Como habrás adivinado, no puede haber una carpeta en el mismo nivel que un fichero con el mismo nombre, algo que generalmente no necesitaremos.

El proceso que se encarga de requerir los controladores y asignarlos a controllers es el siguiente:

app/routes.js
JavaScript
var controllers = {};
var files = glob.sync(path.join(process.cwd(), 'app', 'controllers', '**', '*.js'));
files.forEach(function(file) {
  var temp = controllers;
  var parts = path.relative(path.join(process.cwd(), 'app', 'controllers'), file).slice(0, -3).split(path.sep);

  while (parts.length) {
    if (parts.length === 1) {
      temp[parts[0]] = require(file);
    } else {
      temp[parts[0]] = temp[parts[0]] || {};
    }
    temp = temp[parts.shift()];
  }
});

Hemos utilizado la librería glob para obtener una lista de los ficheros .js que hay dentro de la carpeta controllers. Esto lo podríamos haber hecho a mano pero entonces... ¡no tendríais un reto!

Utilizamos la versión síncrona de glob (en vez de asíncrona), porque necesitamos que los controladores se carguen antes de seguir con el enrutamiento. Es bloqueante pero solo se realiza cuando arranca la aplicación.

El bucle lo que hace es ir generando un objeto vacío donde almacenar el siguiente objeto, así hasta que solo queda un elemento en el array parts y es cuando le asigna el controlador.

El último paso en nuestro fichero de enrutamiento es asociar las peticiones a los métodos del controlador:

app/routes.js
JavaScript
module.exports = function() {
  app.route('/').get(controllers.users.main);

  app.use(function(err, req, res, next) {
    console.error(err.stack);
    return res.status(500).render('500');
  });

  app.use(function(req, res) {
    return res.status(404).render('404');
  });
}

La ruta / ha sido asociada al controlador users y al método main, algo que veremos en breve.

Podemos asignar tantas rutas como queramos, incluyendo parámetros, middleware y todo lo que Express soporte. Algunos ejemplos:

JavaScript
app.route('/:id').get(controllers.users.show);
app.route('/admin').get(auth.check, controllers.admin.main);
app.route('/admin/edit/:id').get(auth.check, controllers.admin.edit);

app/controllers/

Nuestro enrutamiento ya se ha encargado de pasar la petición al controlador correspondiente. En el caso del ejemplo que estamos utilizando, controllers.users.main estará solicitando el método main dentro del fichero app/controllers/users.js. El fichero users.js primero tendrá que requerir las instancias de path y app. También cargamos en la variable User el modelo que se encuentra en app/models/users.js (lo veremos en el siguiente paso):

app/controllers/users.js
JavaScript
var path = require('path');
var app = require(path.join(process.cwd(), 'app'));
var User = require(path.join(process.cwd(), 'app', 'models', path.basename(__filename)));

Y por supuesto exportamos el controlador con todos sus métodos:

app/controllers/users.js
JavaScript
module.exports = {
  main: function(req, res) {
    return res.render('main');
  }
}

En este ejemplo, el método main simplemente devolverá la plantilla index.jade. Aquí es donde debemos realizar la lógica de la aplicación, pedir datos al modelo, enviarlos a la vista, etc.

Podemos tener tantos métodos como necesitemos dentro del controlador, siempre y cuando tenga sentido que estén en el mismo fichero.

app/models/

En nuestro ejemplo no utilizamos el modelo para nada, pero generalmente se encargará de la lógica de negocio. A modo de ejemplo si utilizamos MongoDB y mongoose, crearíamos un esquema de la colección y exportaríamos el modelo de mongoose, para utilizarlo en nuestro controlador:

app/models/users.js
JavaScript
var path = require('path');
var app = require(path.join(process.cwd(), 'app'));
var mongoose = require('mongoose');

var User = mongoose.Schema({
  name: { type: String, required: true, trim: true }
}, { collection: 'users' });

var model = mongoose.model('User', User);

module.exports = model;

De esta manera, en nuestro controlador podríamos utilizar simplemente User.find({ name: 'juan' }, function(err, data) { // etc });.

app/views/

Aquí irían las plantillas a las que el controlador llama. En nuestro ejemplo no hay nada que merezca la pena destacar, solo es una plantilla Jade:

doctype html
html
  head
  body
    | ¡Funciona!

app/libs/

Aunque este directorio está vacío, en futuros artículos lo utilizaremos y lo consideramos parte fundamental de la estructura. Aquí irían librerías como por ejemplo paginator o auth.

public/

Tampoco hay mucho que destacar aquí. La carpeta pública donde se encontrará nuestro favicon.ico, robots.txt, assets de CSS, JS, imágenes, fuentes, etc.

Resumen

A pesar de la longitud del artículo, el código resultante es pequeño y sigue manteniendo el minimalismo y ligereza que aporta Express al mundo del desarrollo web. Una estructura simple y concisa para realizar nuestros proyectos de manera ordenada aprovechando las muchas ventajas del patrón MVC.

Por supuesto, solo es una manera de implementar el patrón MVC en Express, cualquier aportación al código siempre es bienvenida mediante un pull request en GitHub al repositorio felixsanz/express4-mvc-example.

¡Esperamos que os haya resultado útil!

Compartir en