Crear un servidor web HTTPs con NodeJs y Express

Crear un servidor web HTTPs con NodeJs y Express, la librería JavaScript. Aplica las mejores prácticas para su correcto desarrollo.

En esta publicación te voy a enseñar cómo crear un servidor web HTTPs con NodeJs y la librería Express.

Comunicación entre cliente servidor gracias a Express
Comunicación entre cliente servidor gracias a Express

Para simplificar al máximo las cosas, la guía que aquí encontrarás, se centra únicamente en estudiar la implementación de un servidor, y entender sus características.

En otro post aprovecharemos la base que aquí construiremos, para crear un sistema CRUD con autenticación de usuario. No te preocupes, tan pronto como esté público, actualizaré esta entrada con el enlace.

Pero, para llegar a eso, es necesario ir paso a paso. Así que hoy nos centraremos exclusivamente en la librería Express y en sus propiedades.

Por otra parte, para implementar proyectos como el que abordamos en ésta ocasión, es necesario conocer el entorno y la tecnología de desarrollo NodeJs.

Si no estás familiarizado con ello, te dejo el enlace a una publicación, donde se revisan las herramientas y recursos para crear proyectos basados en NodeJs. 

Utilizaremos ese punto de partida, para el objetivo de hoy.

Preparar un entorno de desarrollo NodeJs sólido.

No es necesario que lo leas para seguir los párrafos que siguen, pero hacerlo te dará un mayor entendimiento de los fundamentos sobre la que construiremos el servidor.

Los puntos por los que vamos a ir pasando son los siguientes, siéntete libre de saltar a cualquiera de ellos, si lo consideras necesario:

Antes de empezar a programar, es necesario presentar un poco de contexto sobre la librería Express.

Express, una capa de abstracción que simplifica el desarrollo de servidores web. 

Express existe desde hace más de doce años, su primera versión se lanzó el 22 de mayo de 2010 y fue ideado por TJ Holowaychuk.

A pesar de que se basa directamente en el módulo “http” integrado en el “core” de NodeJs, Express extiende sus funcionalidades y capacidades, convirtiéndose “de facto” en un framework de desarrollo “backend”.

Lo cual lo convierte en un candidato excelente para desarrollar APIs que conecten el «frontend» y el «backend» de nuestra solución web.

De entre las características principales de la herramienta, la más importante sin duda, es la facilidad que ofrece para incorporar capas de software adicionales en forma de “middleware”.

A grandes rasgos, los “middlewares” son instrucciones, que se ejecutan al procesar una petición del cliente, en un orden determinado

En conjunto, actúan como un embudo, donde la petición de entrada se somete a las acciones de cada “middleware” para, al final, entregar una respuesta acorde al conjunto.

Representación de middlewares
Representación de middlewares

Con un ejemplo, se entiende mucho mejor.

Imagina que has habilitado dos endpoints para un cliente. El primero permite obtener datos mediante una petición GET, y el segundo, agregar nuevos a través de otra llamada de tipo POST. 

Algo bastante común en el diseño de una API REST de hecho.

Más tarde, necesitas implementar un sistema de autenticación que solo permita a los usuarios validados, hacer uso de estos dos métodos.

Una solución sería comprobar si la petición va acompañada de un token válido antes de procesar la llamada. 

Pero, como imaginarás, hacerlo para cada endpoint sería repetitivo y muy poco escalable.

Por eso, si se añade la restricción en forma de “middleware”, justo en la entrada de la petición, se aplicaría de forma automática para todos los “endpoints” que existan, como si de un filtro se tratara.

Su popularidad no ha dejado de crecer desde sus inicios

A día de hoy, Express es una pieza fundamental en muchos «stacks» tecnológicos.

Algunas de las pilas más conocidas son MERN, MEAN o MEVN (MongoDB, Express, React / Angular / Vue, NodeJs).

En coordinación con el motor de bases de datos no relacionales MongoDB y un framework frontend, permite desarrollar aplicaciones «fullstack» íntegramente con JavaScript.

La popularidad de Express es indiscutible, su repositorio tiene una valoración de 58.8K estrellas y se descarga la friolera de 118.3M veces al mes.

Además, forma parte de la fundación OpenJs, que certifica la solidez de grandes proyectos realizados con NodeJs como Electron o Webpack.

Todo esto suena genial, pero ha llegado el momento de ponerlo en práctica.

Preparar el entorno de desarrollo.

Para poder seguir cómodamente los puntos que siguen, he subido el resultado en el repositorio de Github. Puedes utilizarlo como complemento, por si no se entiende del todo alguna parte.

https://github.com/Danivalldo/libreriasjs/tree/master/Express

Como dije al inicio, para empezar a programar, antes es preciso tener un entorno de desarrollo NodeJs preparado adecuadamente.

Puedes construir tu propia base, o aprovechar la que ya preparamos en éste otro artículo.

Sencillamente, descarga los archivos de este directorio del repositorio:

https://github.com/Danivalldo/libreriasjs/tree/master/NodeJsBackend

Puedes usar la herramienta online https://download-directory.github.io/ para descargar solo ese directorio dentro del repositorio.

Una vez lo tengas en tu ordenador, ejecuta el comando NPM install en su raíz:

npm install

Tras su instalación, vamos a realizar unas pequeñas modificaciones.

En esta ocasión no trabajaremos con TypeScript, de modo que puedes eliminar los archivos dentro del directorio “src/”:

  • ./src/index.ts
  • ./src/helloFoo.ts

En su lugar, añadiremos un nuevo documento llamado “index.js” dentro del directorio “src”. De momento solo va a contener la siguiente instrucción:

console.log("Hello World");

También tenemos que editar el código dentro de “nodemon.json”, para que reconozca este nuevo punto de entrada e ignore la configuración para TypeScript.

Edita la propiedad “exec” del JSON, por la siguiente línea:

{
  "watch": ["src"],
  "ext": ".ts,.js",
  "ignore": ["node_modules/*", "frontend/*"],
  "exec": "node --trace-warnings --inspect -r dotenv/config ./src/index.js dotenv_config_path=.env.local dotenv_config_debug=true"
}

Listo, con estos pocos pasos, podemos seguir con el tutorial de Express.

Instalar dependencias

Como suele ser habitual, incluiremos una serie de dependencias necesarias

Para ello, lanza el siguiente comando NPM en tu terminal:

npm i express body-parser helmet

Más adelante veremos en detalle qué hace cada uno de estos recursos, en cualquier caso, te hago un pequeño resumen aquí:

  • Express: Por supuesto la librería principal, que vamos a utilizar para levantar el servidor.
  • Body-Parser: Un middleware para procesar e interpretar los datos que llegan en el cuerpo de las peticiones.
  • Helmet: Otro middleware, para securizar las cabeceras de las respuestas emitidas desde el servidor Express.

Configurar y ejecutar un servidor Express.

En el archivo index.js, borra todo lo anterior, y escribe las líneas que siguen. 

import express from "express";
const app = express();
const PORT = 6006;
app.get("/", (req, res, next) => {
  res.status(200).send("<h1>Hello world!</h1>");
});
app.listen(PORT, () => {
  console.log(`Server running on: http://localhost:${PORT}/`);
});

Aunque parezca mentira, éstas pocas líneas de código son suficientes para levantar un servidor web con Express.

Veamos punto por punto qué hacen cada una de ellas:

Iniciamos el programa importando la librería “Express”, seguidamente declaramos una nueva variable “app” donde instanciar el servidor mediante la función “express()”.

import express from "express";
const app = express();

Gracias al método “.get”  mapeamos una función de “callback” a una ruta determinada. En este caso, una petición a “/”  devolverá una respuesta del servidor en forma de HTML.

app.get("/", (req, res, next) => {
  res.status(200).send("<h1>Hello world!</h1>");
});

El proceso de asignar URLs a funciones de respuesta, se conoce como “routing”.

Por supuesto, además de “GET” también podemos declarar los métodos “POST”, “PUT” o “DELETE”, propios de muchas API Rest. 

Pero para que estas, y otras capacidades del servidor funcionen, es necesario habilitar la escucha en un puerto determinado. En nuestro proyecto, será el 6006.

const PORT = 6006;
app.listen(PORT, () => {
  console.log(`Server running on: http://localhost:${PORT}/`);
});

Ejecuta el comando «npm run start:dev», verás que ahora tu terminal mantiene un proceso activo (el servidor en escucha permanente).

npm run start:dev

Prueba acceder con tu navegador a la URL http://localhost:6006. Si todo ha ido bien, deberías ver en pantalla un gran “Hello World!”

Es una buena práctica guardar el puerto en una variable de entorno dentro del archivo “.env”. Detén el servidor con “control + C” y genera un par de archivos nuevos en la raíz, llamados “.env” y “.env.local”.

En ellos declara la variable PORT tal que así:

PORT = 6006

Ahora, modifica el código del servidor para que lea la variable de entorno, en sustitución de una constante dentro del programa.

import express from "express";
const app = express();
app.get("/", (req, res, next) => {
  res.status(200).send("<h1>Hello world!</h1>");
});
app.listen(process.env.PORT, () => {
  console.log(`Server running on: http://localhost:${process.env.PORT}/`);
});

Inicializa el servidor de nuevo, y comprueba que todo sigue funcionando correctamente.

Definir un error 404 personalizado

Como cabe esperar, si tratamos de acceder a cualquier otra URL, el servidor no será capaz de devolver ninguna respuesta.

Por lo general Express ya prevé esta posibilidad, no obstante el mensaje que retorna es un tanto genérico.

Así que vamos a ampliar el código para personalizar esta página.

Justo por debajo de la ruta que tenemos declarada, agrega el siguente código:

app.use((req, res) => {
  res.status(404).send("<h1>404 - Page not found</h1>");
});

Para este tipo de modificaciones, no es necesario reiniciar el servidor, ya que Nodemon se encargará de eso por nosotros. De nuevo, si desconoces qué es Nodemon, te animo a leer el artículo de preparación del entorno.

A través del método “use”, añadimos un middleware capaz de capturar peticiones a cualquier otra ruta no especificada previamente. 

Prueba acceder a una dirección inventada con el navegador, debería aparecer este resultado.

página 404 Not found
página 404 Not found

Securizar las cabeceras y protocolo HTTPS

Seguimos aplicando buenas prácticas a nuestro ejercicio.

Express es genial, no obstante los parámetros de seguridad con los que viene predefinido son un tanto laxos.

La librería está pensada para ofrecer una experiencia de desarrollo ágil, de modo que deja la mayoría de cabeceras HTTP abiertas por defecto. 

Eso se traduce en problemas potenciales de seguridad, que usuarios malintencionados podrían explotar.

Por ese motivo, he incluido el paquete «helmet» entre los mínimos básicos.

Gracias a Helmet, aplicaremos las restricciones básicas a un conjunto de cabeceras. Incluye la librería al inicio de index.js, e inyecta el “middleware” en tu variable app.

import helmet from "helmet";
app.use(helmet());

Con este cambio, hemos corregido el valor de las siguientes cabeceras:

  • Content-Security-Policy: default-src ‘self’;base-uri ‘self’;font-src ‘self’ https: data:;form-action ‘self’;frame-ancestors ‘self’;img-src ‘self’ data:;object-src ‘none’;script-src ‘self’;script-src-attr ‘none’;style-src ‘self’ https: ‘unsafe-inline’;upgrade-insecure-requests
  • Cross-Origin-Embedder-Policy: require-corp
  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Resource-Policy: same-origin
  • Origin-Agent-Cluster: ?1
  • Referrer-Policy: no-referrer
  • Strict-Transport-Security: max-age=15552000; includeSubDomains
  • X-Content-Type-Options: nosniff
  • X-DNS-Prefetch-Control: off
  • X-Download-Options: noopen
  • X-Frame-Options: SAMEORIGIN
  • X-Permitted-Cross-Domain-Policies: none
  • X-XSS-Protection: 0

Para el caso que nos ocupa esto será suficiente, aunque evidentemente, te animo a que investigues un poco más acerca de las posibilidades de configuración que ofrece Helmet.

https://helmetjs.github.io/

Antes de dar por cerrado este punto, hay una consideración importante que también concierne a aspectos de seguridad.

Debes tener en cuenta que la comunicación con tu servidor siempre debería realizarse sobre un protocolo HTTPS

Habilitar la comunicación encriptada sobre HTTPS es fundamental, sobre todo cuando tu programa ya se encuentra en un entorno de producción. 

No disponer de esta capa de seguridad adicional, puede provocar que tu aplicación sea vulnerable a ataques de tipo “man in the middle”.

Activar este protocolo requiere de la creación de un certificado SSL validado. Puedes generar uno gratuito a través del servicio Let’s Encrypt.

https://letsencrypt.org/

Para un entorno de desarrollo local no es imprescindible (y de hecho Let’s Encrypt tampoco puede proveer certificados para localhost), así que por el momento no voy a cubrir más en detalle este aspecto.

Parsear los datos del cuerpo de una petición

Para que un servidor sea verdaderamente funcional debe ser capaz de reconocer los datos enviados desde el cliente y trabajar con estos.

A modo de ejemplo vamos a implementar una nueva ruta en la URL “/user”. Este endpoint podrá recibir datos de usuario mediante peticiones “POST”.

Para lograrlo, vamos a importar y configurar la librería “body-parser”. Se trata de un middleware para Express que interpreta y parsea la información adjunta en las peticiones del cliente.

Importamos la librería al inicio de index.js.

import bodyParser from "body-parser";

Configuramos el middleware antes de declarar las rutas.

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

Con este código vamos a dar cobertura tanto a peticiones con datos tipo JSON, como a datos codificados en la URL.

Seguidamente programamos la nueva ruta. 

En su función de respuesta seremos capaces de obtener los datos del cuerpo de la petición gracias a BodyParser

Si existen, devolvemos una respuesta en forma de listado HTML.

app.post("/user", (req, res, next) => {
  const { name, username, age } = req.body;
  if (!name || !username || !age) {
    return next();
  }
  res.status(200).send(`
    <div>
      <h1>Datos de usuario recibidos:</h1>
      <ul>
        <li>Nombre: ${name}</li>
        <li>Usuario: ${username}</li>
        <li>Edad: ${age}</li>
      </ul>
    </div>
  `);
});

Por contra, si alguno de los datos no está presente, llamamos al método “next()”.

Este método se encarga de pasar al siguiente “middleware”, que en nuestro proyecto será la respuesta 404.

Utilizar Postman

Para corroborar que este nuevo endpoint funciona correctamente vamos a utilizar Postman.

Postman es un programa gratuito para realizar peticiones a servicios web a través de sus APIs.

Puedes descargarlo a través de esta web:

https://www.postman.com/

Una vez instalado te encontrarás con una interfaz similar a esta:

Postman
Postman

Selecciona nueva petición HTTP, en la barra superior activa el método POST y escribe la URL “http://localhost:6006/user”.

En la pestaña body selecciona datos “raw” en formato JSON.

Escribe un objeto JSON con los datos que espera la petición, y haz clic en “Enviar”. Si todo ha ido bien deberías ver una respuesta como la de la imagen que sigue:

Petición POST con Postman
Petición POST con Postman

Responder con datos en formato JSON

Por supuesto, no es necesario responder siempre con contenido en formato HTML. 

De hecho, para construir una API Rest, es habitual obtener respuestas en forma de JSON. Así que vamos a generar un sencillo middleware, para ilustrar este escenario.

Crea un nuevo documento index.js en «./middleware/api/index.js»

En su interior declaramos dos endpoints nuevos para ser consultados a través de métodos GET y POST.

import { Router } from "express";
export const apiRouter = Router();
apiRouter.get("/", (req, res, next) => {
  res.status(200).json({
    status: "ok",
    timestamp: Date.now(),
  });
});
apiRouter.post("/", (req, res) => {
  res.status(200).json({
    timestamp: Date.now(),
    ...req.body,
  });
});

Haciendo uso de la función “Router”, podemos construir endpoints capaces de ser concatenados a otras rutas.

En esta ocasión, haremos que este middleware, “cuelgue” del “path” base “/api”.

import { apiRouter } from "./middleware/api/index.js";
app.use("/api", apiRouter);

¡Enhorabuena! Acabas de crear tu primera API Rest

De nuevo, puedes usar Postman para comprobar que estos nuevos endpoints funcionan como se espera.

Respuesta en formato JSON
Respuesta en formato JSON

Integrar la parte frontend y solventar el problema con la política de CORS

Ha llegado el momento de conectar el servidor a una aplicación frontend real.

Como bien sabrás, la parte frontend de un software web se compone principalmente de archivos HTML, CSS, JavaScript y otros ficheros como imágenes.

Sin embargo, durante el desarrollo, es común trabajar en un entorno aislado, y preparado, normalmente con la ayuda de «bundlers», como por ejemplo Vite. 

Tienes un tutorial entero dedicado a este tema, en otro post, por si no conoces qué es Vite, . 

Preparar un entorno de desarrollo Frontend con Vite

En consecuencia, vamos a realizar la integración con el servidor teniendo en cuenta este factor.

Tener dos servidores locales en marcha, y tratar de hacer peticiones asíncronas de uno a otro, provocará problemas de CORS

Si no sabes de qué te hablo, no te preocupes, pronto te explico de qué se trata, y te enseñaré una forma de solventarlo.

He preparado una aplicación frontend web sencilla, para ahorrarte el trabajo de tener que programarla.

Encontrarás su código en el directorio «frontend» dentro del repositorio.

https://github.com/Danivalldo/libreriasjs/tree/master/Express/frontend

Descarga este material, y ubícalo en la raíz de tu proyecto. 

En una nueva terminal, accede a su interior, e instala los paquetes indicados package.json, mediante el comando de siempre.

npm install

Acto seguido, ejecuta el comando NPM para ponerlo en marcha.

npm run dev

Accede con tu navegador a la URL localhost:5173, para ver la siguiente web en pantalla. 

Aplicación frontend
Aplicación frontend

Con ambos procesos (frontend y backend) activos en tu máquina, interactúa con la web para realizar peticiones asíncronas de un programa a otro. 

A pesar de que todo funciona correctamente, el navegador suele aplicar una política de seguridad, que limita la comunicación entre ambos

Esa restricción es conocida como CORS, y se da cuando un script intenta solicitar datos de un web service con el que no comparte orígen.

Si el servidor destino, no está preparado para ello, el navegador cortará la petición para evitar problemas potenciales de seguridad.

En realidad, ese comportamiento es bueno y necesario en entornos de producción, pero un verdadero incordio durante la fase de desarrollo.

Pero entonces, ¿Porqué no se está aplicando en este caso en concreto?. La respuesta se encuentra en el archivo “./frontend/vite.config.js”.

import { defineConfig } from "vite";
export default defineConfig({
  base: "./",
  server: {
    proxy: {
      "^(/api)": {
        target: "http://localhost:6006",
        changeOrigin: true,
      },
    },
  },
});

Con el parámetro “proxy”, realizamos una suerte de “bypass”, para las llamadas al endpoint “/api”. Con este sistema, logramos que las llamadas a dicho endpoint, pasen por un proxy intermedio. 

Engañando así al navegador para que no se apliquen restricciones CORS.

Bien, por lo pronto, ya podemos trabajar cómodamente con todo nuestro “stack” tecnológico.

Servir recursos estáticos

Antes de dar por finalizada esta guía, vamos a lograr que el servidor Express también sea capaz de servir recursos que se encuentren alojados en un directorio específico.

En concreto vamos a entregar una versión compilada del frontend y de todo el material que lo conforma.

Para ello, obviamente, primero es necesario disponer de la versión compilada para producción.

Así que accede al directorio “frontend” con tu terminal, y ejecuta el comando:

npm run build

Verás que Vite “transpila” y “minifica” todo el material que define nuestra interfaz web dentro de un nuevo directorio “dist”.

Todo lo que se encuentra alojado en esta carpeta, está preparado para ser correctamente manejado por el navegador.

Seguidamente, ampliamos el archivo “./src/index.js” de nuestro backend.

Importa la librería de NodeJs “path”

import path from "path";

Declara la ruta hasta el directorio “dist” dentro de “frontend”.

const publicFolder = path.join(".", "frontend", "dist");

Elimina la petición “GET” a la raíz “/”, y añade las siguientes líneas en su lugar.

app.use(express.static(publicFolder));

Si todo ha ido bien, deberías poder ver la interfaz de tu aplicación accediendo a http://localhost:6006 con tu navegador.

Este tipo de funcionalidades me recuerdan a las que ya ofrecen de serie otros softwares como Nginx o Apache.

Conclusiones y material adicional

Esto ha sido todo por hoy, nada mal. Solo me queda felicitarte si has llegado hasta aquí. 

Por supuesto, si hay algun punto que no queda claro, házmelo saber en los comentarios on mandándome un mensaje a la cuenta de Instagram, trataré de responder lo antes posible.

A continuación te dejo un listado con enlaces a material de interés, para seguir ampliando tus conocimientos.

De nuevo, muchas gracias por leerme, hasta la próxima.

2 comentarios en «Crear un servidor web HTTPs con NodeJs y Express»

    • Una posible solución sería utilizar el método app.use(express.static(publicFolder));, donde publicFolder sería una ruta hacía un directorio que contenga tus archivos .html, .css y .js. Ese método sirve para atender y entregar recursos «estáticos». Espero que te sea de ayuda mi respuesta. Saludos

      Responder

Deja un comentario