Manuel Torres - Departamento de Informática. UAL

di.png

Resumen

Veremos los pasos básicos para el desarrollo de una API REST para la pila MEAN.

Objetivos
  • Configurar el entorno

  • Crear la estructura organizada de carpetas y archivos de un proyecto MEAN

  • Conocer el funcionamiento básico de rutas, controladores y modelos

  • Realizar las operaciones CRUD básicas

  • Aprender a consumir de una API REST MEAN

  • Usar JSON Web Tokens (JWT) como control de acceso a la API

  • Usar Swagger para la documentación de la API

1. Configuración del entorno para Ubuntu Linux

Para el desarrollo de este tutorial necesitaremos configurar un entorno con MongoDB (un gestor de bases de datos de documentos), Node.js (un entorno de ejecución JavaScript) y Express (un framework de Node.js para desarrollo web muy útil en la creación de APIs REST).

También usaremos nodemon, una utilidad que recarga el servidor Node.js ante cualquier cambio en el código del proyecto.

# Installing MongoDB
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4
$ sudo echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list
$ sudo apt-get update
$ sudo apt-get install -y mongodb-org
$ sudo service mongod start

# Installing Node
$ sudo curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
$ apt-get install -y nodejs

# Installing Express
$ npm install -g express-generator

# Installing nodemon
$ npm install -g nodemon

Comprobaremos el funcionamiento correcto desde una terminal:

$ node --version
v10.15.3
$ npm --version
6.4.1
$ express --version
4.16.0

2. Preparación del proyecto

Nuestro proyecto tratará sobre la creación de una API REST sencilla para la gestión de libros. Implementaremos los métodos CRUD básicos. Cada libro tendrá un título, género, descripción, autor, editorial, número de páginas, y URL de la imagen del libro.

2.1. Creación del proyecto

Para el desarrollo del proyecto usaremos Mongoose, un ODM que facilita el desarrollo de aplicaciones Node.js para MongoDB. Entre otras características, Mongoose introduce esquemas para las colecciones MongoDB permitiendo el modelado de datos de la aplicación, así como la validación de tipos.

Comenzaremos creando un directorio para nuestro proyecto, inicializando un proyecto Express, instalando todas las dependencias del proyecto e instalando Mongoose en nuestro proyecto para la interacción con MongoDB.

$ mkdir bookstore
$ cd bookstore
$ express
$ npm install
$ npm install mongoose --save

2.2. Estructura predeterminada del proyecto

Tras inicializar el proyecto con Express, la estructura del proyecto es similar a la que se describe a continuación.

├── app.js (1)
├── bin
│   └── www
├── node_modules (2)
│   ├── ...
├── package.json (3)
├── public (4)
│   ├── images
│   ├── javascripts
│   └── stylesheets
├── routes (5)
│   ├── index.js
│   └── users.js
└── views (6)
    ├── error.jade
    ├── index.jade
    └── layout.jade

Destacamos lo siguiente:

1 En el archivo app.js se definen, entre otros, los archivos de rutas (p.e. archivos de rutas de la aplicación y de la API), el motor de plantilla usado (p.e. Jade) y la ubicación de la carpeta de vistas.
2 El directorio node_modules contiene los módulos instalados de la aplicación.
3 El archivo package.json contiene información descriptiva de la aplicación, punto de inicio (p.e. bin/www) y dependencias (p.e. Express, Jade, Mongoose, …​)
4 En el directorio public colocaremos las imágenes, hojas de estilo y scripts que no queremos que bloqueen al servidor mientras son servidos a los clientes.
5 El directorio routes contiene archivos de rutas que indican los controladores que dan respuesta a cada petición
6 El directorio views contiene cada una de las vistas de presentación de datos de la aplicación.

2.3. Configuración de carpetas

Express crea de forma predeterminada la estructura anterior. Sin embargo, de cara a desarollar la API es conveniente crear una carpeta aparte que incluya los modelos, rutas y controladores asociados. Esta es la organzación propuesta:

api_server/
├── controllers
├── models
└── routes

Para crearla, ejecutaríamos estos comandos desde la carpeta del proyecto

$ mkdir -p api_server/models
$ mkdir -p api_server/controllers
$ mkdir -p api_server/routes

2.4. Inicializar la base de datos

De cara a poder trabajar en la API, desde la shell de MongoDB inicializaremos una base de datos de ejemplo que incluya una colección con al menos un documento para poder hacer las pruebas con operaciones GET. La base de datos se denomina bookstore y la colección books.

mongo> create database bookstore;
mongo> use bookstore;
mongo> db.books.insert(
{
	"_id" : ObjectId("5abe944733599b27439db885"),
	"title" : "Harry Potter y la piedra filosofal",
	"genre" : "Acción y aventura",
	"description" : "Harry vive con sus horribles tíos y el insoportable primo Dudley, hasta que su ingreso en el Colegio Hogwarts de Magia y Hechicería cambia su vida para siempre. Allí aprenderá trucos y encantamientos fabulosos, y hará un puñado de buenos amigos... aunque también algunos temibles enemigos.",
	"author" : "J.K. Rowling",
	"publisher" : "Salamandra",
	"pages" : 256,
	"image_url" : "https://images-na.ssl-images-amazon.com/images/I/51lEw8wGCPL._SX312_BO1,204,203,200_.jpg"
}
);

2.5. Conexión y lógica de conexión/desconexión

Archivo api_server/models/db.js
var mongoose = require('mongoose'); (1)

var dbURI = 'mongodb://localhost/bookstore'; (2)
mongoose.connect(dbURI); (3)

// CONNECTION EVENTS
mongoose.connection.on('connected', function() {
    console.log('Mongoose connected to ' + dbURI);
});
mongoose.connection.on('error', function(err) {
    console.log('Mongoose connection error: ' + err);
});
mongoose.connection.on('disconnected', function() {
    console.log('Mongoose disconnected');
});

// CAPTURE APP TERMINATION / RESTART EVENTS
// To be called when process is restarted or terminated
gracefulShutdown = function(msg, callback) {
    mongoose.connection.close(function() {
        console.log('Mongoose disconnected through ' + msg);
        callback();
    });
};
// For nodemon restarts
process.once('SIGUSR2', function() {
    gracefulShutdown('nodemon restart', function() {
        process.kill(process.pid, 'SIGUSR2');
    });
});
// For app termination
process.on('SIGINT', function() {
    gracefulShutdown('app termination', function() {
        process.exit(0);
    });
});

// BRING IN YOUR SCHEMAS & MODELS
// require('./yourmodel'); (4)
1 Uso de Mongoose
2 Inicialización de la URI de la base de datos bookstore
3 Conexión a la base de datos
4 Más adelante incluiremos aquí los modelos conforme los vayamos creando

2.6. Incluir el archivo db.js en app.js

Fragmento del archivo app.js
...
var createError = require('http-errors');
var express = require('express');
...
require('./api_server/models/db'); (1)
...
1 Conectar a la base de datos y cargar los modelos

Si lanzamos la aplicación desde la terminal con nodemon sobre la carpeta del proyecto obtenderemos

Mongoose connected to mongodb://localhost/bookstore

3. Creación del primer endpoint

3.1. Creación del modelo

En Mongoose todo comienza con un esquema. De acuerdo con la documentación de Mongoose, cada esquema se corresponde con una colección MongoDB y define la estructura de los documentos en la colección. En cada esquema definimos los campos, con sus tipos y restricciones.

Una vez creada la definición del esquema, se convierte a un modelo, que es con el que se trabajará desde la aplicación. Los modelos se crean pasando el nombre que tendrá el modelo y el nombre del esquema a partir del que se crean.

mongoose.model(modelName, schema)

A continuación se muestra el modelo para los libros de la aplicación de ejemplo.

El archivo api_server/models/book.js
var mongoose = require('mongoose');

var bookSchema = mongoose.Schema({ (1)
    title: {
        type: String,
        required: true
    },
    genre: {
        type: String,
        required: true
    },
    description: {
        type: String
    },
    author: {
        type: String,
        required: true
    },
    publisher: {
        type: String
    },
    pages: {
        type: Number
    },
    image_url: {
        type: String
    }
});

mongoose.model('Book', bookSchema); (2)
1 Creación del esquema
2 Creación del modelo Book a partir del esquema bookSchema

Una vez definido el modelo, lo incluiremos al final del archivo db.js

Carga del modelo en el archivo api_server/models/db.js
...
require('./book');

3.2. Creación de un controlador básico

Nuestra API deberá ofrecer una serie de endpoints con cada una de las operaciones permitidas. Cada endpoint será resuelto por su propio controlador.

Para ver cómo funciona esto, comenzaremos creando un controlador para una operación sencilla de recuperación de un libro cualquiera sin entrar todavía en el paso de parámetros.

Primer controlador en el archivo api_server/controllers/book.js
var mongoose = require('mongoose'); (1)
var Book = mongoose.model('Book'); (2)

module.exports.bookFindOne = function(req, res) { (3)
    Book (4)
    .findOne() (5)
    .exec( (6)
        function(err, book) { (7)
            return res (8)
            .status(200)
            .send(book);
        });
    };
1 Objeto Mongoose para interactuar con MongoDB
2 Modelo que se corresponde con la colección books de MongoDB
3 Controlador implementado mediante la función asíncrona bookFindOne. El controlador recibe la petición en req y devolverá el resultado en res
4 Uso del modelo
5 Llamada a la función findOne de Mongoose, que se corresponde con la función findOne de MongoDB
6 Ejecución de la consulta y paso del resultado a una función asíncrona
7 Función asíncrona que se ejecuta tras la consulta y que devuelve los resultados. El objeto err será el objeto en el que se deuelva el error en caso de que se produzca. Si todo funciona correctamente, el resultado se pasa a book
8 Se devuelve el resultado book con el estado 200 en el objeto res del controlador

Un controlador más elaborado contendría un control de errores mínimo como el que se muestra a continuación

Añadiendo control de errores al controlador en el archivo api_server/controllers/book.js
var mongoose = require('mongoose');
var Book = mongoose.model('Book');

var sendJSONresponse = function(res, status, content) {
  res.status(status);
  res.json(content);
};

module.exports.bookFindOne = function(req, res) {
  console.log('Finding book details', req.params);
  Book
  .findOne()
  .exec(function(err, book) {
    if (!book) {
      sendJSONresponse(res, 404, {
        "message": "book not found"
      });
      return;
    } else if (err) {
      console.log(err);
      sendJSONresponse(res, 404, err);
      return;
    }
    console.log(book);
    sendJSONresponse(res, 200, book);
  });
};

3.3. Creación de la ruta

Tras crear el controlador procedemos a conectarlo a una ruta. De esta forma al usar esa ruta con un método HTTP concreto se desencadenará la ejecución del controlador.

El archivo api_server/routes/index.js
var express = require('express');
var router = express.Router();
var ctrlBook = require('../controllers/book'); (1)

router.get('/', ctrlBook.bookFindOne); (2)

module.exports = router;
1 Archivo con el código del controlador
2 Asociar la ejecución del controlador bookFindOne a una llamada GET a la raíz

3.4. Incoporación del archivo de rutas en app.js

Una vez creado el archivo de rutas para la API, lo cargaremos en app.js, ya que el archivo de rutas predeterminado es para la aplicación Jade que crea al inicializarse el proyecto Express.

Incoporación del archivo de rutas a app.js
...
var apiRouter = require('./api_server/routes/index'); (1)
...
app.use('/api', apiRouter); (2)
...
1 Archivo que contiene las rutas a atender y las funciones que las gestionarán
2 Ruta en la que se atenderán las llamadas a la API

El endpoint se puede probar en

localhost:3000/api

y devolverá un libro almacenado.

Una vez creado el primer endpoint, los siguientes se crean de forma más sencilla debido a que ya está creada la infraestrucutra que soporta la API (estructura de directorios, archivo db.js con la lógica de conexión/desconexion a MongoDB, creación de los archivos de rutas y controlador, creación de los esquemas y modelos Mongoose y adaptación del archivo app.js para tratar con la carpeta de la API).

El procedimiento a seguir para crear nuevos endpoints será:

  1. Añadir la nueva nueva función del controlador al archivo del controlador

  2. Añadir la nueva ruta al archivo de rutas para asociar el nuevo enpoint con la función creada en el controlador

4. Creación de otros endpoints de consulta (GET)

Los parámetros se pasan en la ruta precedidos de dos puntos y se reciben en el controlador con el nombre del parámetro sin los dos puntos en req.param.nombre-del-parametro.

4.1. Obtener libros por ID

4.1.1. Creación de la función en el controlador

Fragmento del archivo api_server/controllers/book.js
....
module.exports.bookFindById = function(req, res) {
    if (req.params && req.params.id) { (1)
        Book
        .findById(req.params.id) (2)
        .exec(
            function(err, book) {
                if (!book) { (3)
                    return res
                    .status(404)
                    .send({"message": "book not found"});
                } else if (err) {
                    return res
                    .status(404)
                    .send(err);
                }
                return res (4)
                .status(200)
                .send(book);
            }
        );
    } else {
        return res
        .status(404)
        .send({"message": "No book in the request"});
    }
};
1 Accederemos a req.params para saber si se han pasado parámetros y a req.params.id para acceder al parámetro id
2 Llamada a la función findById de Mongoose para recuperar un documento por su Id
3 Comprobamos en la función de callback si se ha devuelto un libro
4 Se devuelve el resultado book con el estado 200 en el objeto res del controlador

4.1.2. Creación de la ruta

Ahora sólo faltaría añadir la ruta del endpoint en el archivo de rutas asociando la ruta y el método HTTP a la función definida en el archivos del controlador.

Fragmento del archivo api_server/routes/index.js
...
router.get('/id/:id', ctrlBook.bookFindById); (1)
...
1 Los parámetros se pasan precedidos de dos puntos (:)

El endpoint se puede probar en

localhost:3000/api/id/5abe944733599b27439db885

y devolverá el libro solicitado.

4.2. Obtener libros por género

En este ejemplo veremos la implementación de un endpoint que devuelve una lista de libros. El endpoint tomará el género como parámetro.

4.2.1. Creación del controlador

Fragmento del archivo api_server/models/book.js
...
module.exports.bookFindByGenre = function(req, res) {
    if (req.params && req.params.genre) { (1)
        Book
        .find({genre: req.params.genre}) (2)
        .exec(
            function(err, books) {
                if (!books) { (3)
                    return res
                    .status(404)
                    .send({"message": "genre not found"});
                } else if (err) {
                    return res
                    .status(404)
                    .send(err);
                }
                return res (4)
                .status(200)
                .send(books);
            }
        );
    } else {
        return res
        .status(404)
        .send({"message": "No `genre` in request"});
    }
};
...
1 Accederemos a req.params para saber si se han pasado parámetros y a req.params.genre para acceder al parámetro genre
2 Llamada a la función find de Mongoose, que se corresponde con la función find de Mongo, y se le pasarán las condiciones de la consulta en forma de documento JSON, al igual que en MongoDB
3 Comprobamos en la función de callback si se han devuelto libros
4 Se devuelve el resultado books con el estado 200 en el objeto res del controlador

4.2.2. Creación de la ruta

Ahora sólo faltaría añadir la ruta del endpoint en el archivo de rutas asociando la ruta y el método HTTP a la función definida en el archivos del controlador.

Fragmento del archivo api_server/routes/index.js
...
router.get('/genre/:genre', ctrlBook.bookFindByGenre);
...

El endpoint se puede probar en

localhost:3000/api/genre/Historia

y devolverá los libros del género solicitado.

5. Las otras operaciones CRUD

Una vez visto cómo realizar operaciones de recuperación (GET), veremos cómo realizar el resto de operaciones CRUD.

Seguiremos el mismo procedimiento anterior, creando primero la función que resuelve el endpoint en el controlador y añadiendo después la ruta del endpoint al archivo de rutas.

5.1. Creación de documentos (POST)

5.1.1. Creación del controlador

Los documentos se crean en Mongoose con el método create. Los parámetros se recogen en req.body.nombre-parametro.

Para el envío de parámetros del POST desde Postman añadiremos parejas clave-valor en x-www-form-urlencoded tal y como se ilustra a continuación.

post book.png
Fragmento del archivo api_server/controllers/book.js
....
module.exports.bookCreate = function(req, res) {
    Book
    .create({ (1)
        title: req.body.title, (2)
        genre: req.body.genre,
        description: req.body.description,
        author: req.body.author,
        publisher: req.body.publisher,
        pages: req.body.pages,
        image_url: req.body.image_url
    },function(err, book) {
        if (err) { (3)
            return res
            .status(400)
            .send(err);
        }
        return res (4)
        .status(201)
        .send(book);
    });
};
...
1 Llamada a la función create de Mongoose, que creará un documento en MongoDB de acuerdo al esquema definido para la colección
2 Los valores a insertar son recogidos en req.body.nombreDelParametro (p.e. req.body.title, req.body.genre, …​)
3 Comprobamos en la función de callback si se ha producido un error al insertar
4 Se devuelve el código de estado 200 y el libro creado como resultado

5.1.2. Creación de la ruta

Ahora sólo faltaría añadir la ruta del endpoint en el archivo de rutas asociando la ruta y el método POST a la función definida en el archivos del controlador.

Fragmento del archivo api_server/routes/index.js
...
router.post('/book', ctrlBook.bookCreate);
...

5.2. Eliminación de documentos (DELETE)

La eliminación se realizará pasando el id del documento a eliminar

5.2.1. Creación del controlador

Fragmento del archivo api_server/controllers/book.js
...
module.exports.bookDelete = function(req, res) {
    if (req.params && req.params.id) { (1)
        Book
        .findByIdAndDelete(req.params.id) (2)
        .exec(
            function(err, book) {
                if (err) { (3)
                    return res
                    .status(400)
                    .send(err);
                }
                return res (4)
                .status(204)
                .send(null);
            }
        );
    } else {
        return res
        .status(404)
        .send({"message": "No id in the request"});
    }
};
...
1 Accederemos a req.params para saber si se han pasado parámetros y a req.params.id para acceder al parámetro id
2 Llamada a la función findByIdAndDelete de Mongoose, inspirada en la función findOneAndDelete de MongoDB, y se le pasará como parámetro el id del libro a borrar
3 Comprobamos en la función de callback si se ha producido un error
4 Se devuelve el código de estado 204 y null que es el convenio para eliminaciones satisfactorias

5.2.2. Creación de la ruta

Ahora sólo faltaría añadir la ruta del endpoint en el archivo de rutas asociando la ruta y el método DELETE a la función definida en el archivos del controlador.

Fragmento del archivo api_server/routes/index.js
....
router.delete('/book/:id', ctrlBook.bookDelete);
....

5.3. Actualización de documentos (PUT)

La actualización se realizará pasando el id del documento a modificar y los campos a actualizar. Se actualizarán sólo los campos pasados en la petición dejando el resto intactos.

5.3.1. Creación del controlador

Fragmento del archivo api_server/controllers/book.js
...
module.exports.bookUpdate = function(req, res) {
    if (req.params && req.params.id) { (1)
        Book
        .findById(req.params.id) (2)
        .exec(
            function(err, book) {
                if (!book) { (3)
                    return res
                    .status(404)
                    .send({"message": "no book found"});
                } else {
                    if (req.body.title) { (4)
                        book.title = req.body.title;
                    }
                    if (req.body.genre) {
                        book.genre = req.body.genre;
                    }
                    if (req.body.description) {
                        book.description = req.body.description;
                    }
                    if (req.body.author) {
                        book.author = req.body.author;
                    }
                    if (req.body.publisher) {
                        book.publisher = req.body.publisher;
                    }
                    if (req.body.pages) {
                        book.pages = req.body.pages;
                    }
                    if (req.body.image_url) {
                        book.image_url = req.body.image_url;
                    }
                    book.save(function (err, book) { (5)
                        if (err) { (6)
                            return res
                            .status(404)
                            .send(err);
                        }
                        else {
                            return res (7)
                            .status(200)
                            .send(book);
                        }
                    });
                }
            }
        );
    } else {
        return res
        .status(404)
        .send({"message": "No id in the request"});
    }
};
...
1 Accederemos a req.params para saber si se han pasado parámetros y a req.params.id para acceder al parámetro id
2 Llamada a la función findById de Mongoose pasándole el id como argumento
3 Comprobamos en la función de callback si se ha encontrado en libro
4 Se comprueba si se han pasado valores para cada campo del documento comprobando los parámetros pasados
5 Llamada a la función save de Mongoose para almacenar las modificaciones
6 Se comprueba si se ha producido algún error
7 Se devuelve el estado 200 y el libro modificado, que es el convenio en operaciones de modificación

5.3.2. Creación de la ruta

Ahora sólo faltaría añadir la ruta del endpoint en el archivo de rutas asociando la ruta y el método PUT a la función definida en el archivos del controlador.

Fragmento del archivo api_server/routes/index.js
...
router.put('/book/:id', ctrlBook.bookUpdate);
...

6. Consumir de la API REST

Para ilustrar cómo usar la API REST desarrollada anteriormente desarrollaremos un pequeño ejemplo que muestre la lista de libros devueltos por el endpoint localhost:3000/api/books

De forma predeterminada, la aplicación Express tiene las rutas y las vistas en directorios justo debajo del directorio de la aplicación. Para una mejor organización crearemos un directorio app_server para incluir los directorios de las rutas, controladores y vistas, tal y como se muestra a continuacion.

app_server/
├── controllers
├── routes
└── views

Podemos crear esa estructura con los comandos siguientes

$ mkdir -p app_server/views
$ mkdir -p app_server/controllers
$ mkdir -p app_server/routes

6.1. Creación del controlador

Para hacer uso de la API REST desarrollada anteriormente realizaremos peticiones HTTP a usando un objeto request disponible en el paquete request. Lo instalaremos en nuesro proyecto con

$ npm install request --save

Crearemos un controlador denominado books.js para mostrar el listado de libros y estará en la ruta creada app_server/controllers

El archivo app_server/controllers/books.js
var request = require('request'); (1)
var apiOptions = { (2)
  server: 'http://localhost:3000/api'
};

var renderBooksPage = function(req, res, responseBody) { (3)
  res.render('index', {
    title: 'Express',
    books: responseBody (4)
  });

};

module.exports.bookList = function(req, res, next) { (5)
  var path = '/';
  var requestOptions = { (6)
    url: apiOptions.server + path,
    method: 'GET',
    json: {},
    qs: {}
  };

  request(requestOptions, function(err, response, responseBody) { (7)
    renderBooksPage(req, res, responseBody); (8)
  });
};
1 Paquete que ofrece una forma sencilla de realizar operaciones HTTP
2 Variable para almacenar la ruta base
3 Función de carga de la vista. Se le inyectan los datos que tiene que presentar (título y lista de libros)
4 Listado de libros a mostrar en la vista
5 Controlador para mostrar el listado de libros
6 Opciones configuradas que necesita el objeto request
7 Llamada a la API y creación de la función asíncrona
8 Función que resuelve la presentación de la vista tras recuperar los datos de la API

6.2. Creación de la ruta

Crearemos un archivo de rutas denominado index.js que contendrá todas las rutas que atienda la aplicación y estará en el directorio creado app_server/routes

El archivo app_server/routes/index.js
var express = require('express');
var router = express.Router();
var ctrlBooks = require('../controllers/books'); (1)

/* GET home page. */
router.get('/', ctrlBooks.bookList); (2)

module.exports = router;
1 Archivo de controladores
2 Asociación de ruta a controlador

6.3. Creación de la vista

Crearemos un archivo para la vista raíz denominado index.js que presentará el listado de libros y estará en el directorio creado app_server/views. Los datos a mostrar en la vista son inyectados por el controlador.

El archivo app_server/views/index.jade
extends layout

block content
  h1= title
  p Welcome to #{title} (1)
  each book in books (2)
    p= book.title (3)
1 Título capturando el título proporcionado por el controlador
2 Bucle para recorrer la lista de libros inyectados por el controlador
3 Titulo del libro

6.4. Modificación del archivo app.js

Dado que las vistas y los controladores ahora se encuentran dentro de la carpeta app_server, es necesario indicar este cambio en el archivo app.js

Fragmento del archivo app.js
...
var indexRouter = require('./app_server/routes/index'); (1)
...
app.set('views', path.join(__dirname, 'app_server', 'views')); (2)
...
1 Incluir app_server en el patch de las rutas
2 Incluir app_server en la ruta de las vistas

7. Acceso a la API mediante JSON Web Tokens (JWT)

Node.js y Express no mantienen información sobre la sesión de cada usuario en el servidor. Además, en aplicaciones SPA el código es entregado al cliente al iniciar la aplicación y después no hay posibilidad de interactuar con el servidor para manejar los datos de las sesiones. Por tanto, el enfoque tradicional para la autenticación no es válido en aplicaciones MEAN. La solución a este problema pasa por almacenar cierta información sobre la sesión en el navegador de forma que sea la propia aplicación la que decide lo que se puede mostrar o no a cada usuario. Una forma de guardar estos datos en el cliente es mediante JSON Web Token (JWT)

JWT ofrece una forma de asegurar el acceso en una aplicación. Se trata de un objeto JSON cifrado en una cadena que puede ser decodificado por la aplicación y el servidor.

Para el proceso de login, el usuario envía sus credenciales al servidor en las llamadas a la API REST. El servidor valida las credenciales (p.e. usando una base de datos) y devuelve un token al navegador. El navegador almacenará este token para reutilizarlo después. Con este enfoque los datos de las sesiones no se guardan en el servidor; se guardan en el navegador.

Las API REST no guardan estado y no saben quién está realizando la llamada. En cada llamada se enviará el token al endpoint a través de un middleware. El middleware decodificará el token y determinará si el usuario está autorizado a realizar esa operación. En caso de estar autorizado se continuará con la llamada a la función que resuelve el endpoint.

7.1. JWT

Un JWT consta de tres cadenas separadas por puntos:

  • Cabecera: Objeto JSON con el tipo algoritmo de hashing usado codificado en base64url.

  • Payload: Objeto JSON codificado en base64url con los datos o privilegios, es decir, el cuerpo en sí del token.

  • Firma: Hash codificado en base64url de la cabecera y el payload usando un secreto que sólo conoce el servidor que ha creado el token. La firma permite determinar si el token ha sido creado usando el secreto establecido. Si no se ha creado usando dicho secreto, concluiremos que el token es falso y se rechazará la petición.

Procedimiento de creación de un JWT
key           = 'secretkey'
unsignedToken = encodeBase64Url(header) + '.' + encodeBase64Url(payload)
signature     = HMAC-SHA256(key, unsignedToken)

token = encodeBase64Url(header) + '.' + encodeBase64Url(payload) + '.' + encodeBase64Url(signature)

Un token de ejemplo con sus tres partes separadas por puntos:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtdG9ycmVzIiwiaWF0IjoxNTU3MjExNzM5LCJleHAiOjE1NTcyMTE4NTl9.SySZ9rd8iJHUKgsia0pY7YvLTmAkVwJdK-wkQkTJiB8

7.2. Incorporación de JWT a la API REST

A continuación se describen los pasos a seguir para crear una API REST con control de acceso basado en tokens.

  1. Creación de los controladores, si no están creados previamente

  2. Instalación de las dependencias (jsonwebtoken)

  3. Creación de la clave secreta para firmar los tokens

  4. Creación de la función de creación de tokens

  5. Creación de las funciones de autenticación (registro y login)

  6. Creación del middleware

  7. Creación de las rutas incluyendo el middleware

7.2.1. Creación de los controladores

Los controladores incluyen las funciones que atienden a las peticiones de la API REST. Estas funciones no tienen en cuenta la autenticación. Las API REST no tienen estado. Del control de acceso se encarga el middleware. Las funciones de los controladores se ejecutarán en función de lo que indique el middleware.

La creación de los controladores ya estaba finalizada en las secciones Creación del primer endpoint, Creación de otros endpoints de consulta (GET) y Las otras operaciones CRUD

7.2.2. Instalación de los módulos necesarios

Existen módulos Node.js para generar JWT, como es jsonwebtoken. Lo instalaremos en nuestro proyecto con

$ npm install jsonwebtoken --save

También instalaremos el módulo moment para las operaciones con los tiempos para establecer la caducidad de los tokens.

$ npm install moment --save

7.2.3. Creación de la clave secreta

A continuación crearemos un archivo que contiene la clave secreta de firma de los tokens. Por motivos de seguridad, este archivo será excluido del control de versiones. Primero intentará tomarse el valor para el secreto desde la variable de entorno. Si la variable de entorno no está configurada, se le asignará como valor predeterminado el que se indique en el archivo config.js.

El archivo api_server/config.js
module.exports = {
    TOKEN_SECRET: process.env.TOKEN_SECRET || "password"
  };

7.2.4. Creación del token

La creación del token se realizará a través de una función que denominaremos createToken. La función construye el payload tomando los valores que queramos incluir en el token. Estos valores se conocen como claims. Existen claims registrados o reservados (p.e. sub para el nombre de usuarios, iat para la fecha de expedición y exp para la fecha de caducidad). También se pueden crear claims personalizados o privados para intercambiar información a través del token. La función createToken devolverá el token firmado con el secreto configurado anteriormente.

El archivo api_server/controllers/service.js
var jwt = require('jsonwebtoken'); (1)
var moment = require('moment'); (2)
var config = require('../config'); (3)

exports.createToken = function(user) { (4)
  var payload = {
    sub: user, (5)
    iat: moment().unix(), (6)
    exp: moment().add(2, "minutes").unix(), (7)
  };
  return jwt.sign(payload, config.TOKEN_SECRET); (8)
};
1 Uso del módulo jsonwebtoken
2 Uso del módulo moment para manipulación de fechas y horas.
3 Carga del secreto
4 La función de creación del token toma al usuario como argumento en este ejemplo
5 Inclusión del usuario en el payload
6 Inclusión de la fecha actual
7 Configuración de la caducidad del token como 2 minutos despúes de la fecha actual
8 Creación del token añadiéndole el secreto

7.2.5. Módulo de autenticación

El módulo de autenticación se encarga de registrar usuarios y comprobar si pueden iniciar sesión (p.e. comprobando si existen en la base de datos de usuarios registrados). Si todo va bien, se devolverá un token que permitirá el acceso a los endpoints privados de la API.

El archivo api_server/controllers/auth.js
var mongoose = require('mongoose');
var User = mongoose.model('User');
var service = require('../service'); (1)

module.exports.signup = function(req, res) { (2)
  User
  .create({username: req.body.username, password: req.body.password}, function(err, user) { (3)
    if (err) {
      return res.status(400).send(err); (4)
    }
    return res
    .status(200)
    .send({token: service.createToken(req.body.username)}); (5)
  });
};

module.exports.login = function(req, res) { (6)
  if (req.body.username && req.body.password) {
    User
    .count({username: req.body.username, password: req.body.password}) (7)
    .exec(function(err, user) {
      if (!user) {
        return res.status(401).send({"message": "Invalid user and/or password"}); (8)
      } else if (err) {
        return res.status(404).send(err); (9)
      }
      return res
      .status(200)
      .send({token: service.createToken(req.body.username)}); (10)
    });
  } else {
    return res.status(401).send({"message": "Invalid user and/or password"}); (11)
  }
};
1 Carga del servicio para poder usar la función createToken
2 Función de registro de usuarios. Tras un registro satisfactorio devuelve un token de acceso a la API
3 Creación del usuario en la base de datos
4 Devolver un error si no se ha creado con éxito el usuario
5 Devolver un token si el usuario se ha creado con éxito
6 Función de inicio de sesión.
7 Consulta que devuelve el número de usuarios con el login y pass proporcionados
8 Si no existe nada con esas creadenciales se devuelve un error
9 Devolver un error si se produce un error al consultar
10 Si existen las credenciales se devuelve un token de acceso a la API
11 Devolver error si falta algún parámetro

7.2.6. Creación del middleware

A continuación tenemos que crear el middleware. Su función es la de actuar como una fase intermedia entre la petición y su resolución. El objetivo del middleware es el de determinar si se trata de una petición autorizada.

La función ensureAuthenticated pasará la ejecución a la etapa siguiente (la resolución de la llamada al endpoint) si se cumplen todas estas condiciones:

  • La petición incluye una autorización en la cabecera

  • La signatura incluye el secreto concertado

  • El token no está caducado

El archivo api_server/controllers/middleware.js
var jwt = require('jsonwebtoken'); (1)
var moment = require('moment');
var config = require('../config');

exports.ensureAuthenticated = function(req, res, next) {
  if(!req.headers.authorization) { (2)
    return res
    .status(403)
    .send({message: "Petición sin cabecera de autorización"});
  }

  var token = req.headers.authorization.split(" ")[1]; (3)
  var payload = jwt.verify(token, config.TOKEN_SECRET, function(err, payload) {
    if (err) {
      switch (err.name) { (4)
        case 'JsonWebTokenError':
          return res.status(401).send({message: "Signatura incorrecta"});
        case 'TokenExpiredError':
          return res.status(401).send({message: "Token caducado"});
        default:
          return res.status(401).send(err);
      }
    }
    req.user = payload.sub; (5)
    next(); (6)
  });
}
1 Uso del módulo jsonwebtoken
2 Comprobación de la existencia de autorización en la cabecera
3 Obtención del token incluido en la cabecera
4 Comprobación de la existencia de errores
5 Carga de datos del payload desde el middleware para pasarlos a la etapa siguiente
6 Paso a la etapa siguiente

7.2.7. Creación de las rutas

El archivo de rutas indica cómo resolver cada una de las peticiones, tanto de login/registro, como de los endpoints en sí de la API. Para una mayor modularidad, el código de los controladores estará fuera del archivo de rutas.

El archivo routes/index.js
var express = require('express');
var router = express.Router();

var middleware = require('../controllers/middleware'); (1)
var ctrlAuth = require('../controllers/auth'); (2)
var ctrlBook = require('../controllers/book'); (3)

router.get('/', middleware.ensureAuthenticated, ctrlBook.bookList); (4)
router.get('/book/:id', middleware.ensureAuthenticated, ctrlBook.bookFindById);
router.get('/genre/:genre', middleware.ensureAuthenticated, ctrlBook.bookFindByGenre);

router.post('/book', middleware.ensureAuthenticated, ctrlBook.bookCreate);
router.delete('/book/:id', middleware.ensureAuthenticated, ctrlBook.bookDelete);
router.put('/book/:id', middleware.ensureAuthenticated, ctrlBook.bookUpdate);

// Rutas de registro y login
router.post('/auth/signup', ctrlAuth.emailSignup); (5)
router.post('/auth/login', ctrlAuth.emailLogin);

module.exports = router;
1 Carga del middleware para comprobar si se permite el acceso
2 Carga del controlador de registro y login
3 Carga del controlador de libros
4 Llamada a controladores condicionada al resultado de la evaluación (comprobación de token) del middleware
5 Rutas de registro y login

7.3. Uso de la API mediante autenticación

Veamos el funcionamiento de la API ante las diversas situaciones que se pueden presentar:

  • Si intentamos acceder sin cabecera al endpoint privado (localhost:3000/api) devuelve el código de error 403 Forbidden con el siguiente contenido:

    {
        "message": "Petición sin cabecera de autorización"
    }
  • Si nos registramos pasando el username y password (localhost:3000/api/auth/signup) devuelve el código de estado 200 OK con el token:

    {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtdG9ycmVzIiwiaWF0IjoxNTU3MjExNzM5LCJleHAiOjE1NTcyMTE4NTl9.SySZ9rd8iJHUKgsia0pY7YvLTmAkVwJdK-wkQkTJiB8"
    }
  • Si iniciamos sesión (localhost:3000/api/auth/login) pasando los datos de login (username y password), se devuelve el código de estado 200 OK con el token:

    {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtdG9ycmVzIiwiaWF0IjoxNTU3MjExODA1LCJleHAiOjE1NTcyMTE5MjV9.28a8e5y8uYFuo_t7pNuGVzP1qsl4iyAQ_v2503RYC-8"
    }
  • Si accedemos a un endpoint privado (p.e. localhost:3000/api) con el token antes de que caduque, se devuelve el código de estado 200 OK con el resultado de la petición

El token lo pasaremos como Bearer Token en el desplegable Tipo de la pestaña Autorización de Postman

  • Si intentamos acceder a un endpoint privado (p.e. localhost:3000/api) una vez caducado el token (el token del ejemplo caduca a los 2 minutos), se devuelve el código de error 401 Unauthorized con el siguiente contenido:

    {
        "message": "Token caducado"
    }

8. Documentación de la API mediante Swagger

Swagger es un framework que ofrece un conjunto de herramientas para la creación y documentación de APIs REST. Podemos optar por usar Swagger desde cero para crear la especificación de la API y Swagger nos asistirá en la generación de la documentación y del código de la API, así como de otras tareas de testing para comprobar, por ejemplo, si existe código asociado a cada tipo de estado devuelto por cada operación de la API. Otra opción es usar Swagger para documentar una API ya existente. En nuestro caso usaremos esta última opción para documentar la API desarrollada en este tutorial.

Para este tutorial usaremos el editor online de Swagger, aunque hay gran cantidad de herramientas Swagger online y on-premise que nos asisten en el proceso de creación y documentación de una API.

Al usar la live demo del editor online de Swagger ya tendremos un ejemplo de código fuente de la documentación de una API, así como su vista previa. A partir del ejemplo generado, podemos tomarlo como punto de partida y realizar las modificaciones necesarias para crear la documentación de nuestra propia API.

SwaggerEditor.png
Figure 1. Editor online de Swagger

8.1. Partes básicas de la documentación Swagger

La documentación de ejemplo generada está creada en YAML. En ella, podemos destacar las secciones siguientes:

  • swagger: Versión de Swagger que estamos usando. La versión actual es la 2, aunque ya existe una versión 3.

  • info: Información general de interés sobre la API (descripción, versión, titulo, términos de servicio, datos de contacto, …​)

  • host: Se usa para lanzar peticiones de prueba desde la propia documentación y se corresponde con el host donde reside la API.

  • tags: Permiten agrupar los endpoints (p.e. books, categories, users, …​)

  • schemes: Esquemas permitidos (http o https)

  • paths: Rutas de la API. Normalmente se agrupan por entidad (p.e. book, author). Dentro de cada entidad estarán las distintas operaciones permitidas (GET, PUT, POST, …​)

  • securityDefinitions: Permite definir modelos de autenticación para las pruebas de la API que se hagan desde la documentación Swagger de la API.

  • definitions: Definición de los modelos de la API

8.2. Documentación Swagger de la API (fragmento)

swagger: '2.0'
info: (1)
  description: |
    API REST de Bookstore
  version: 1.0.0
  title: Bookstore REST API
  termsOfService: http://swagger.io/terms/
  contact:
    email: mtorres@ual.es
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
tags: (2)
- name: book
  description: Libros de la tienda online
paths: (3)
  /book/{id}: (4)
    get: (5)
      tags: (6)
      - book
      summary: Obtener libro por id (7)
      operationId: bookFindById (8)
      produces: (9)
      - application/json
      parameters: (10)
      - name: id
        in: path
        description: El id del libro a recuperar. Use 5cac6351f4c126f6d91c6450 para pruebas.
        required: true
        type: string
      responses: (11)
        200:
          description: Operación correcta
          schema:
            $ref: '#/definitions/Book'
        400:
          description: id inválido
        404:
          description: Libro no encontrado
    put: (12)
      tags:
      - book
      summary: Libro actualizado
      description: Esta operación sólo la pueden realizar los usuarios que hayan iniciado sesión.
      operationId: bookUpdate
      produces:
      - application/json
      parameters:
      - name: id
        in: path
        description: id del libro a modificar
        required: true
        type: string
      responses:
        400:
          description: id inválido
        404:
          description: Libro no encontrado
    delete: (13)
      tags:
      - book
      summary: Eliminar usuario
      description: Esta operación sólo la pueden realizar los usuarios que hayan iniciado sesión.
      operationId: bookDelete
      produces:
      - application/json
      parameters:
      - name: id
        in: path
        description: El id del libro a eliminar
        required: true
        type: string
      responses:
        400:
          description: id inválido
        404:
          description: Libro no encontrado
definitions: (14)
  Book:
    type: object
    required:
    - title
    - genre
    - author
    properties:
      title:
        type: string
      genre:
        type: string
      description:
        type: string
      author:
        type: string
      publisher:
        type: string
      pages:
        type: integer
      image_url:
        type: boolean
    xml:
      name: Book
externalDocs: (15)
  description: Más información sobre Swagger
  url: http://swagger.io
host: localhost:3000 (16)
basePath: /api (17)
1 Información descriptiva
2 Tags para organizar los endpoints
3 Paths de la API
4 Ruta de acceso
5 Verbo HTTP del endpoint
6 Tag en el que agrupar el endpoint de cara a organizar los endpoints en la documentación
7 Descripción del endpoint
8 Nombre del método que se encarga de implementar la operación. Util cuando se usa la generáción de código y las funciones de testing
9 Tipo de respuesta devuelto
10 Parámetros de entrada del endpoint
11 Respuestas producidas por el método
12 Operación PUT
13 Operación DELETE
14 Definición de los modelos
15 Documentación complementaria de interés
16 Servidor donde está alojada la API
17 Path de acceso a la API

8.3. Incorporar la documentación de la API al proyecto

Una vez creada la documentación, para pasarla al proyecto usaremos el paquete Node.js swagger-ui-express, que sirve la documentación de Swagger mediante Swagger UI en la ruta que definamos en nuestro proyecto. Swagger UI presenta la documentación de la API y permite interactuar con los endpoints.

Después exportaremos a un archivo JSON local (normalmente swagger.json) la documentación creada en el editor online de Swagger.

Por último, añadiremos el archivo JSON con la documentación al proyecto y modificaremos app.js para incluir el acceso a la documentación.

A continuación se describen estos pasos.

8.3.1. Instalación de swagger-ui-express

$ npm install swagger-ui-express --save

8.3.2. Exportar la API a JSON

Swagger UI usa JSON como formato para la documentación de la API. Para guardar la documentación en formato JSON, en el editor online seleccionaremos File | Convert and save as JSON.

El resultado será algo similar a esto:

{
  "swagger" : "2.0",
  "info" : {
    "description" : "API REST de Bookstore\n",
    "version" : "1.0.0",
    "title" : "Bookstore REST API",
    "termsOfService" : "http://swagger.io/terms/",
    "contact" : {
      "email" : "mtorres@ual.es"
    },
    "license" : {
      "name" : "Apache 2.0",
      "url" : "http://www.apache.org/licenses/LICENSE-2.0.html"
    }
  },
...
  "schemes" : [ "https", "http" ],
  "paths" : {
    "/book/{id}" : {
      "get" : {
        "tags" : [ "book" ],
        "summary" : "Obtener libro por id",
        "operationId" : "bookFindById",
        "produces" : [ "application/json" ],
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "El id del libro a recuperar. Use xxx para pruebas.",
          "required" : true,
          "type" : "string"
        } ],
        "responses" : {
          "200" : {
            "description" : "Operación correcta",
            "schema" : {
              "$ref" : "#/definitions/Book"
            }
          },
          "400" : {
            "description" : "id inválido"
          },
          "404" : {
            "description" : "Libro no encontrado"
          }
        }
      },
...

Colocaremos este archivo como swagger.json en la carpeta raíz del código del proyecto.

8.4. Importación de la documentación como una ruta del proyecto

Swagger UI presentará la documentación de la API a través de una ruta de nuestra aplicación. Para ofrecer la documentación de la API a través de Swagger UI incluiremos el código siguiente en app.js

var swaggerUi = require('swagger-ui-express'); (1)
swaggerDocument = require('./swagger.json'); (2)
...
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); (3)
1 Uso de swagger-ui-express
2 Carga del documento JSON de la documentación
3 Creación de la ruta de acceso a la documentación

Ahora la documentación de la API estará accesible desde http://localhost:3000/api-docs

BookstoreSwagger.png

9. Conclusiones

La mayoría de las aplicaciones de hoy día disponen de una API REST para recuperar o realizar operaciones sobre sus datos. Además, una API REST permite la creación de servicios complementarios a los de la aplicación desde la que ha nacido. Saber crear e interactuar con una API REST hoy día es una habilidad fundamental.

En este tutorial se estudia cómo desarollar una API REST con la pila MEAN (MongoDB, Express, Angular y Node.js) usando además Mongoose para la interacción con MongoDB. Hemos tratado un ejemplo práctico de creación de una API desde cero, comenzando con la instalación y preparación del entorno, para continuar con la creación de la estructura del proyecto, creación de modelos y creación de endpoints para operaciones CRUD. También hemos visto cómo consumir datos de la API, para terminar usando JWT como mecanismo de control de acceso y documentando la API con Swagger.

Apéndice A. Códigos de estado HTTP frecuentes

Table 1. Códigos de estado HTTP más frecuentes
Status code case

200

OK

A successful GET or PUT request

201

Created

A successful POST request

204

No content

A successful DELETE request

400

Bad request

An unsuccessful GET, POST, or PUT request, due to invalid content

401

Unauthorized

Requesting a restricted URL with incorrect credentials

403

Forbidden

Making a request that isn’t allowed

404

Not found

Unsuccessful request due to an incorrect parameter in the URL

500

Internal server error

Problem with your server or the database server

Apéndice B. Base de datos de ejemplo

mongo> use bookstore;
mongo> db.books.insertMany(
[
    {
        "_id": ObjectId("5cac6351f4c126f6d91c6450"),
        "title": "Una historia de España",
        "genre": "Historia",
        "description": "Un relato ameno, personal, a ratos irónico, pero siempre único, de nuestra accidentada historia a través de los siglos. Una obra concebida por el autor para, en palabras suyas, «divertirme, releer y disfrutar; un pretexto para mirar atrás desde los tiempos remotos hasta el presente, reflexionar un poco sobre ello y contarlo por escrito de una manera poco ortodoxa.",
        "author": "Arturo Pérez-Reverte",
        "publisher": "Alfaguara",
        "pages": 256,
        "image_url": "https://images-na.ssl-images-amazon.com/images/I/41%2B-e981m1L._SX311_BO1,204,203,200_.jpg"
    },
    {
        "_id": ObjectId("5cacf56222ee3f230a725895"),
        "title": "Historia de España contada para escépticos",
        "genre": "Historia",
        "description": "Como escribe el autor, no pretende ser veraz, justa y desapasionada, porque ninguna historia lo es. No está hecha para halagar a reyes y gobernantes, ni pretende halagar a los banqueros, ni a la Conferencia Episcopal, ni al colectivo gay.",
        "author": "Juan Eslava Galán",
        "publisher": "Booket",
        "pages": 592,
        "image_url": "https://images-na.ssl-images-amazon.com/images/I/51IyZ5Mq8YL._SX326_BO1,204,203,200_.jpg",
        "__v": 0
    }
]
);

Apéndice C. Aplicación base creada por Express

Al crear la aplicación con Express, se creó una estructura de archivos y directorios y una aplicacion web disponible en el puerto 3000

Express.png
Figure 2. Aplicación web inicial creada por Express

Esta aplicación web se carga porque en Express define una vista, una ruta y un controlador que se encarga de presentar la vista e inyectarle datos a la vista

De forma predeterminada, las vistas de la aplicación inicial se guardan en el directorio views y las rutas en routes. Más adelante veremos como organizar estos directorios en un directorio que contenga los controladores, rutas y vistas de la aplicación.

Por ahora basta con saber qué hay en el archivo app.js y cómo se carga la vista inicial de la aplicación.

El archivo app.js

Tras la generación del proyecto con Express se ha configurado la aplicación para que use Jade como motor de plantillas. Además, se indica que las vistas se almacenan en el directorio views del directorio de la aplicación.

....
// view engine setup
app.set('views', path.join(__dirname, 'views')); (1)
app.set('view engine', 'jade'); (2)
...
1 Se define como carpeta de vistas la carpeta views sobre el directorio de la aplicación (__dirname)
2 Jade como motor de plantillas

Además, en app.js se indica cómo responder a las peticiones que lleguen a la raíz (/). Para ello, se usará un archivo de rutas aparte que contendrá los endpoints relativos a la raíz junto con los controladores que resuelven las peticiones.

...
var indexRouter = require('./routes/index'); (1)
...
app.use('/', indexRouter); (2)
...
1 Ubicación del archivo de rutas
2 Uso de las rutas de indexRouter cuando lleguen peticiones a la raíz (/)

Las vistas iniciales

Inicialmente Express crea 3 vistas en la carpeta views del proyecto:

  • layout.jade: Página de base que contiene componentes reutilizados en otras páginas (p.e. la definición de la estructura de documento HTML, la hoja de estilos, y demás).

  • index.jade: Página de inicio de la aplicacion

  • error.jade:

El archivo layout.jade
doctype html
html
  head
    title= title (1)
    link(rel='stylesheet', href='/stylesheets/style.css') (2)
  body
    block content (3)
1 El segundo title es una variable cuyo valor es inyectado por el control y presentado al cargar la vista
2 Carga de la hoja de estilos
3 Define un marcador que será reemplazado posterioremente por otras vistas que extiendan este archivo

En Jade, el sangrado indica la creación de un subelemento

El archivo index.jade
extends layout (1)

block content (2)
  h1= title (3)
  p Welcome to #{title} (4)
1 Archivo del que se hereda
2 Definición del marcador que reemplazará el bloque en el archivo layout
3 Variable cuyo valor será inyectado por el controlador al carga la vista
4 Variable cuyo valor será inyectado por el controlador al carga la vista

La ruta y el controlador inicial

Inicialmente, Express crea una ruta en la raíz y un controlador asociado en el archivo routes/index.js con el código siguiente

Fragmento de routes/index.js con la ruta y controlador predeterminado para la raíz
...
/* GET home page. */
router.get('/', function(req, res, next) { (1)
  res.render('index', { title: 'Express' }); (2)
});
...
1 Ruta raíz y controlador asociado definido sobre la marcha
2 Mostrar la vista index pasándole un JSON con una variable title