Ejemplo API REST en express.js y mongodb

21 de mayo 2019ComentariosjavascriptDavid Poza SuárezComentarios

Seguimos avanzando en completar nuestro arsenal full-stack con javascript, y hoy toca una breve intro de qué es express.js y mongoDB.

Express

Express.js es un popular framework dentro del ecosistema nodejs, y está pensado para escribir servidores web, que realizarán habitualmente las funciones de API, aunque en realidad esa es solo una de las muchas posibilidades que ofrece.

Ofrece tanta simplicidad en su uso que crear un servidor web es tan sencillo como el siguiente código:

const express = require('express');
const app = express();

app.get('/', function (req, res) {
  res.send('Hello World!');
});

app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});

El código anterior crea un servidor http a la escucha en el puerto 3000 y responde a las peticiones de tipo GET con la ruta apuntando a la raíz mediante una pagina con el texto "Hello World!.

Programar el backend usando javascript tiene el beneficio de aprovechar el mismo conocimiento del lenguaje que usas para el frontend, y no solo eso, sino poder usar las miles de librerías que ya existen en npm tanto en frontend como en backend. Por si fuera poco es un lenguaje que posee un rendimiento muy superior a otros como PHP, actualmente el más usado debido principalmente a la cantidad de herramientas existentes (wordpress o prestashop por ejemplo), y a que ya viene instalado en todos los hosting.

Podemos ver una comparativa de rendimiento de express.js contra los frameworks symfony(php), silex(php) y django(python) en la conocida web https://www.techempower.com/benchmarks/ , que realiza benchmarks varias veces al año. Algunas capturas interesantes:

Los resultados son **peticiones de texto plano en un segundo**, Con un hardware Dell R440 Xeon Gold + 10 GbE. bajo Linux. Fecha 30/10/2018

Los resultados son peticiones de texto plano en un segundo, Con un hardware Dell R440 Xeon Gold + 10 GbE. bajo Linux. Fecha 30/10/2018

Los resultados son **queries a base de datos por segundo**, Con un hardware Dell R440 Xeon Gold + 10 GbE. bajo Linux. Fecha 30/10/2018

Los resultados son queries a base de datos por segundo, Con un hardware Dell R440 Xeon Gold + 10 GbE. bajo Linux. Fecha 30/10/2018

Hay que puntualizar que estamos comparando un micro-framework (express) contra un framework full-stack (symfony), es decir, que symfony tiene muchas más utilidades, por ejemplo para el frontend, que express no tiene. Por ello es ideal para crear microservicios basados en API REST. Para nada estoy diciendo que django y symfony sean malos framework, he usado ambos y son geniales. Sin embargo PHP es un lastre aún en su versión 7.

En las pruebas contra bases de datos, todos los resultados usan mysql menos express-mongo, que me sorprende que quede por debajo de express-mysql, pero supongo que donde mongo destaca es en las operaciones de inserción, varias veces más rápido que mysql.

A continuación la diferencia entre ambas clasificaciones de framework:

micro vs full stack frameworks medium 1024x909

MongoDB

Ya que lo hemos mencionado arriba, vamos a explicar qué es mongoDB: Se trata de un motor de bases de datos de tipo no-sql o no relacional, como sí lo es Mysql por ejemplo. Esto implica que en lugar de almacenar los datos en tablas, lo hace en colecciones de documentos, que son objetos JSON en binario. Es muy cómodo de usar junto a Javascript pues las consultas se realizan usando objetos json. Además es opensource.

Esta claro que las bases de datos no-sql tienen su demanda y así lo demuestran los servicios que han sacado GoogleCloud (DataStore), AWS (DynamoDB) y Azure (Table Storage).

Además está ganando mucha popularidad, como vemos en el gráfico de db-engines:

db engines

La ventaja de MongoDB es su velocidad de inserción, que es varias veces más rápido que Mysql por ejemplo. Además el año pasado incorporaron la posibilidad de realizar transacciones multidocumento (varias operaciones de forma atómica). Sin embargo, tiene como punto negativo, entre otros, que no dispone de JOINS, por lo que es posible que necesites normalizar los datos en tu aplicación, con el coste que ello supone. Por todo ello está especialmente indicado para almacenar registros, estadísticas, juegos, apps, etc.

En Javascript disponemos del cliente Mongoose, que permite realizar consultas de forma tremendamente simple mediante objetos JSON, por ejemplo obtener un documento buscado por una propiedad de tipo String:

PersonModel.findOne({first\_name:"Pedro"})
.then ( data=>{
  console.log(data.first\_name + " " + data.last\_name);
})
.catch ( err=>{
  console.log(err.message);
})

Hay que anotar que Mongoose admite su uso mediante callbacks, aunque yo prefiero la sintaxis "thenable", puedes ver sus ventajas aquí.

Ejemplo: Acortador de enlaces

Ahora vamos a crear un ejemplo de API muy básico que almacenará los datos en mongoDB: Un acortador de enlaces. Subiré el proyecto a github, y lo desplegaré en glitch y mongoDB Atlas: este último un servicio por parte de la empresa desarrolladora del mismo, que permite desplegar clusters mongo en AWS, Google Cloud y Azure, y que dispone de una capa gratuita (RAM y CPU compartidos) con 512MB de almacenamiento.

Lo primero como siempre es crear el proyecto con npm init. En este ejemplo tendremos las siguientes dependencias (recuerda que indicamos la versión usando semantic versioning, permitiendo cambios menores y parches con el prefijo ^):

{
  "name": "dps-url-shortener",
  "version": "1.0.0",
  "description": "simple url shortener based on express.js REST API.",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.16.4",
    "dotenv": "^8.0.0",
    "mongodb": "^3.2.4",
    "mongoose": "^5.5.8",
    "cors": "^2.8.5",
    "body-parser": "^1.19.0",
    "shortid": "^2.2.14"
  },
  "engines": {
    "node": "8.x"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/davidpoza/dps-url-shortener.git"
  },
  "author": "David Poza Suárez",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/davidpoza/dps-url-shortener/issues"
  },
  "homepage": "https://github.com/davidpoza/dps-url-shortener#readme"
}

Nuestra aplicación servidor va a estar contenida prácticamente en un solo fichero, y comenzaremos incluyendo las siguientes librerías mediante la sintaxis CommonJS , en lugar de import, que usábamos en el frontend gracias a Babel.

require('dotenv').config(); //Por si usas Windows. Realiza la lectura y parseo de las variables del fichero .env, disponibles a través de process.env.VAR.
const path = require('path'); //Para componer las rutas abstrayendonos del sistema operativo
const express = require('express'); //framework para manejar las peticiones y respuestas http, parámetros etc.
const mongo = require('mongodb'), //el driver para poder conectar a mongodb
const mongoose = require('mongoose'); //el cliente para poder realizar consultas.
const Schema = mongoose.Schema; //la clase que permite definir "esquemas" de documento
const bodyParser = require('body-parser'); //middleware para poder codificar datos enviados por POST en formato urlencoded, y para poder enviar el payload como json
const cors = require('cors'); //middleware para configurar el CORS (compartición de recursos de origen cruzado) para permitir acceso al API desde dominios externos.
const shortid = require('shortid'); //librería para generar ids únicos de corta longitud a partir de un conjunto de caracteres dado.
const utils = require('./utils'); //modulo nuestro para separar el código de algunas funciones

Lo siguiente será conectar a la base de datos, crear el esquema para los documentos y el modelo:

Podemos ver los tipos que ofrece Mongoose aquí: https://mongoosejs.com/docs/schematypes.html

mongoose.connect(process.env.MONGO\_URI, { useNewUrlParser: true })
.catch((err)=>{console.log(err); process.exit(1)});

let UrlSchema = new Schema({
  \_id: {type:String, required:true},
  originalUrl: {type:String, required:true},
});

let UrlModel = mongoose.model('Url', UrlSchema, 'urls'); //el nombre del modelo es "Url" y el de su colección "urls"

A continuación vamos a aplicar todos los middleware. Un middleware en expressJS es una función que se ejecuta en mitad de cada petición http y tiene las capacidades de:

  • Ejecutar cualquier código.
  • Realizar cambios en la solicitud y los objetos de respuesta.
  • Finalizar el ciclo de solicitud/respuestas.
  • Invocar la siguiente función de middleware en la pila.
app.use(cors());
app.use(bodyParser.urlencoded({extended:false}));
app.use(bodyParser.json());
app.use('/public', express.static(process.cwd() + '/public'));

Ahora viene la lógica de la aplicación: definir cada endpoint del API:

/*
endpoint de página index
*/
app.get('/', function(req, res){
  res.sendFile(process.cwd() + '/views/index.html'); //devolvemos un html estático
});

/*
endpoint de redirección
Redirige a la url original
*/
app.get("/api/shorturl/:id", (req,res)=>{
  UrlModel.findById(req.params.id)
  .then((data)=>{
    res.redirect(data.originalUrl);
  })
});

/*
endpoint de acortado de url.
1. Comprueba si la url está correctamente formada y el dominio resuelve correctamente.
2. Comprueba si ya existe un shortcut para esa url en la base de datos, si es así devuelve ese su id.
   Si no existe nada, se crea con un id alteatorio usando la librería shortid.
3. Si algo falla devuelve un json con el error "Invalid URL".
*/
app.post("/api/shorturl/new", (req,res)=>{
  utils.urlValid(req.body.url) //gracias a bodyParser podemos recoger los parametros via POST en req.body
  .then((data)=>{
    return UrlModel.findOne({originalUrl: data})
  })
  .then((data)=>{
    if(!data){ //no existe la url en la db
      let document = new UrlModel({
        _id: shortid.generate(),
        originalUrl: req.body.url,
      });
      return document.save();
    }
    else //ya existe, se devuelve el resultado del find
      return data;
  })
  .then((data)=>{
    res.json({
      original_url: data.originalUrl,
      short_url: data._id
    })
  })
  .catch((err)=>{
    console.log(err);
    res.json({error: "invalid URL"})
  })
});

Según vayamos creando shortcuts se irán creando, sin repetirse, los documentos en la colección "urls" dentro de la db "shorturl", tal y como vemos en la siguiente captura:

MongoDB Compass es una herramienta GUI para gestionar bases de datos mongoDB.

MongoDB Compass es una herramienta GUI para gestionar bases de datos mongoDB.

Los ficheros adicionales son el utils.js y el index.html. En el index haremos una petición asíncrona al api via fetch.

<!DOCTYPE html>
<html>
   <head>
      <title>URL Shortener</title>
      <link rel="shortcut icon" href="https://cdn.hyperdev.com/us-east-1%3A52a203ff-088b-420f-81be-45bf559d01b1%2Ffavicon.ico" type="image/x-icon"/>
      <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" type="text/css">
      <link href="/public/style.css" rel="stylesheet" type="text/css">
      <script type="text/javascript">

        function onSubmit(url){
          fetch(window.location + "api/shorturl/new", {
            method: "POST",
            headers: {
              "Accept": "application/json",
              "Content-Type": "application/json"
            },
            body: JSON.stringify({
              url: url,
            })
          })
          .then(
            (response)=>response.json()
          )
          .then((data)=>{
            if(data.error){
              document.getElementById("result").innerHTML = data.error;
            }
            else{
              document.getElementById("result").innerHTML =
              \`
              The shorten url is:
              <a href='${window.location}api/shorturl/${data.short\_url}'>
                ${window.location}api/shorturl/${data.short\_url}
              </a>
              \`
            }

          })
        }

      </script>
   </head>

   <body>
      <div class="container">
        <h2>API Project: URL Shortener Microservice</h2>
        <div>
          This example works with an express.js API and mongoDB database.
        </div>
        <h3>Short URL Creation </h3>
        <p>
          This page makes a POST request to: <code>/api/shorturl/new</code> with a key with name "url".
        </p>
        <label for="url\_input">URL to be shortened: </label>
        <input id="url\_input" type="text" name="url" value="https://google.es">
        <button onclick="onSubmit(document.getElementById('url\_input').value)">Shorten url</button>

        <div id="result">

        </div>
      </div>
   </body>
</html>

En el utils definimos un módulo de utilidades, como se puede ver por el momento solo tiene una función que básicamente es un Promise que ejecuta un test con una expresión regular y un dns.lookup sobre el dominio para ver si resuelve correctamente a una ip.

const dns = require('dns'); //forma parte del core de nodejs por lo que no hace falta añadir nada en el packages.json

const utils = {
  /\*
  Valida que la url sea del tipo: http/https://subdominio.dominio.com/ruta/subruta?param=1&param=2.
  Y además trata de resolver el dominio para ver si existe
  \*/
  urlValid: (url)=>{
    let p = new Promise((resolve,reject)=>{
      let regex = /https?:\\/\\/(\[a-zA-Z0-9-.\]\*\[a-z\]{2,6})\[a-zA-Z0-9\_\\-.\\/?=&%+\]\*/;
      if(regex.test(url)){
        let url\_no\_protocol = url.match(regex)\[1\];
        dns.lookup(url\_no\_protocol, (err, address) => {
          if(err!=null){
            reject(err);
          }
          else
            resolve(url);
        });
      }
    });
    return p;
  }
};

module.exports = utils;

Lo único que nos queda es crear y arrancar el servidor http. Para lanzar el servidor basta con ejecutar el comando npm start como hemos definido en el package.json.

let app = express();
let port = process.env.PORT || 3000;
app.listen(port, function () {
  console.log('Node.js listening ...');
});

Código y demo

express microservice demo

Puedes visitar el ejemplo corriendo en la url: https://fcc-block5-shorturl.glitch.me

Para ver todo el código aquí tienes el repositorio: https://github.com/davidpoza/dps-url-shortener

Documentación