Skip to content

Simple NodeJS API

Se le ha entregado una aplicación para que puede construirla y ejecutarla en contenedores.

Cree el directorio simple-node-api e ingrese al mismo.

mkdir simple-node-api
cd simple-node-api

Dependencias

Cree el archivo package.json, con el siguiente contenido:

{
  "name": "simple-node-api",
  "version": "1.0.0",
  "description": "Minimal API for testing on Node 18 and 22",
  "main": "server.js",
  "type": "commonjs",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "node --test --test-reporter=spec"
  },
  "engines": {
    "node": ">=18 <23"
  },
  "dependencies": {
    "express": "4.17.1",
    "supertest": "^7.0.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  },
  "keywords": [],
  "author": "",
  "license": "MIT"
}

NOTA

No cambie las versiones, se han utilizando versiones anteriores intencionalmente.

Código del API

Cree el archivo server.js, con el siguiente contenido:

// server.js
const express = require("express");

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// Health check
app.get("/health", (req, res) => {
  res.json({ status: "ok", node: process.version, time: new Date().toISOString() });
});

// Echo query string: /echo?msg=hello
app.get("/echo", (req, res) => {
  res.json({ msg: req.query.msg ?? null });
});

// Sum numbers: { "a": 1, "b": 2 }
app.post("/sum", (req, res) => {
  const { a, b } = req.body || {};
  const na = Number(a), nb = Number(b);
  if (Number.isNaN(na) || Number.isNaN(nb)) {
    return res.status(400).json({ error: "Invalid numbers 'a' and 'b'." });
  }
  res.json({ a: na, b: nb, sum: na + nb });
});

// 404 fallback
app.use((req, res) => res.status(404).json({ error: "Not found" }));

// Solo arrancar si se ejecuta directamente
if (require.main === module) {
  app.listen(PORT, () => {
    console.log(`API listening on http://0.0.0.0:${PORT}`);
  });
}

module.exports = app;

Pruebas Unitarias

Cree el directorio tests.

mkdir tests
Agregue el archivo tests/api.test.js con el siguiente contenido:

// tests/api.test.js
const { test, describe, before, after } = require('node:test');
const assert = require('node:assert/strict');
const request = require('supertest');
const app = require('../server');

let server;
before(() => {
  // Supertest puede usar app directamente; si quieres server real:
  server = app.listen(0); // puerto efímero
});
after(() => {
  server && server.close();
});

describe('API', () => {
  test('/health responde ok', async () => {
    const res = await request(server).get('/health');
    assert.equal(res.statusCode, 200);
    assert.equal(res.body.status, 'ok');
    assert.match(res.body.node, /^v\d+\./); // algo como v18.x o v22.x
    assert.ok(Date.parse(res.body.time));
  });

  test('/echo devuelve el query param', async () => {
    const res = await request(server).get('/echo').query({ msg: 'hola' });
    assert.equal(res.statusCode, 200);
    assert.equal(res.body.msg, 'hola');
  });

  test('/echo sin msg devuelve null', async () => {
    const res = await request(server).get('/echo');
    assert.equal(res.statusCode, 200);
    assert.equal(res.body.msg, null);
  });

  test('/sum suma números válidos', async () => {
    const res = await request(server)
      .post('/sum')
      .send({ a: 5, b: 7 })
      .set('content-type', 'application/json');
    assert.equal(res.statusCode, 200);
    assert.deepEqual(res.body, { a: 5, b: 7, sum: 12 });
  });

  test('/sum valida números inválidos', async () => {
    const res = await request(server)
      .post('/sum')
      .send({ a: 'x', b: 2 })
      .set('content-type', 'application/json');
    assert.equal(res.statusCode, 400);
    assert.equal(res.body.error, "Invalid numbers 'a' and 'b'.");
  });

  test('404 para rutas inexistentes', async () => {
    const res = await request(server).get('/no-existe');
    assert.equal(res.statusCode, 404);
    assert.equal(res.body.error, 'Not found');
  });
});

Probando aplicación en un contenedor de desarrollo

Su sistema no tiene Node instalado, por lo que utilizaremos el runtime asignado para realizar las pruebas.

Ejecute el siguinete comando para iniciar una terminar en un contenedor llamado node18, utilizando la última versión de la imágen de docker.io/node:18 y expone el puerto 30080 al puerto 3000 del contenedor.

podman run -p 30080:3000 -it --workdir /app --rm --name node18 -v $PWD:/app:Z docker.io/node:18 bash

Se mostrará una terminal similar a:

Note que el usuario el root.

root@XXXXXXXX:/app#

Instalar las dependencias del aplicativo.

npm install

Salida similar:

up to date, audited 121 packages in 1s

19 packages are looking for funding
  run `npm fund` for details

7 vulnerabilities (3 low, 4 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues, run:
  npm audit fix --force

Run `npm audit` for details.
npm notice
npm notice New major version of npm available! 10.8.2 -> 11.5.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.5.2
npm notice To update run: npm install -g npm@11.5.2
npm notice

Note que el comando de npm, nos indica que existen vulnerabilidades en algunas librerías.

NOTA

Siga las instrucciones para corregir este caso.

  npm audit fix

Debemos conocer el aplicativo para poder tomar decisión de actualizar la dependencias, trabaje con el equipo de desarrollo para determinar este caso.

En nuestro caso podemos utilizar la última versión de express.

Ejecute :

npm audit fix --force

Esto modificará el archivo package.json con el contenido similar a:

grep express package.json
Salida similar:
    "express": "^4.21.2",
Note la versión de express utilizada.

Ejecute de nuevo el comando

npm install

Note 0 vulnerabilidades en la salida.

Salida similar:

up to date, audited 120 packages in 776ms

22 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Ejecuta las pruebas unitarias:

npm run test

Salida similar:

> simple-node-api@1.0.0 test
> node --test --test-reporter=spec

▶ API
   /health responde ok (28.309986ms)
   /echo devuelve el query param (7.595734ms)
   /echo sin msg devuelve null (4.313194ms)
   /sum suma números válidos (13.780872ms)
   /sum valida números inválidos (5.511968ms)
   404 para rutas inexistentes (3.695555ms) API (68.934328ms) tests 6 suites 1 pass 6 fail 0 cancelled 0 skipped 0 todo 0 duration_ms 314.612184

Inicie la aplicación para pruebas.

npm run dev

Abrá/Inicie una segunda sesión/terminal en su sistema ejecute algunas pruebas manualmente.

curl "http://localhost:30080/health"

curl "http://localhost:30080/echo?msg=Hola%20Mundo"

curl -X POST http://localhost:30080/sum -H "content-type: application/json" -d '{"a":5,"b":7}'

Todas las pruebas deben ser exítosas.

Detenga la ejecución del contenedor de pruebas del APP. En la terminal del contenedor.

Ctrl+C
exit

NOTA

No cierre la segunda terminal/sesión. Se utilizará para otras pruebas.

El contenedor se borrará automaticamente por la opción --rm, si lo inicia de nuevo de la misma manera, podrá ver que los cambios están presentes ya que se ha utilizado una opción de -v para mapear el directorio de trabajo local a un volúmen en /app, opciones utilizadas al ejecutar el contenedor.

Este sirve para ambientes de prueba o desarrollo en dónde estámos creando el código y deseamos realizar varias pruebas del mismo.

Elimine el directorio creado en las pruebas node_modules.

rm -Rf node_modules package-lock.json

Creando un contenedor para la aplicación V. 1

Cree el archivo Dockerfile con el siguiente contenido:

ARG NODE_IMAGE="docker.io/node:18" # Misma versión utilizada en las pruebas anteriores
FROM ${NODE_IMAGE}

WORKDIR /app
COPY package*.json ./
RUN npm install && \
  npm run test
COPY . .

ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]

Construya la primera versión de la images de su aplicación.

podman build . -t api-image:v1 

Ejecute la primera versión de la images de su aplicación.

podman run -d -p 30080:3000 --rm --name api-container api-image:v1 

Listar contenedores en ejecución.

podman ps
Salida similar:
CONTAINER ID  IMAGE                    COMMAND     CREATED        STATUS        PORTS                    NAMES
4fe23dfc7d25  localhost/api-image:v1  npm start   2 seconds ago  Up 3 seconds  0.0.0.0:30080->3000/tcp  api-container

En la seguna terminal/sesicón, ejcute de nuevo las pruebas manuales.

curl "http://localhost:30080/health"

curl "http://localhost:30080/echo?msg=Hola%20Mundo"

curl -X POST http://localhost:30080/sum -H "content-type: application/json" -d '{"a":5,"b":7}'

Explore la imágen creada para su aplicación.

podman exec -it api-container id
Salida similar:
uid=0(root) gid=0(root) groups=0(root)
podman inspect api-image:v1 | head
Salida similar:
[
     {
          "Id": "6243fedf46c2bf7df152072a0ab3635fa81453961623ce4ad4c682479ce4e2af",
          "Digest": "sha256:bee0ef7fd3c63dc9245e962f2bc874888ed8dca2371b3f37c6b4840edd78af4c",
          "RepoTags": [
               "localhost/api-image:v1"
          ],
          "RepoDigests": [
               "localhost/api-image@sha256:bee0ef7fd3c63dc9245e962f2bc874888ed8dca2371b3f37c6b4840edd78af4c"
          ],
podman images api-image:v1
Salida Similar:
REPOSITORY            TAG         IMAGE ID      CREATED        SIZE
localhost/api-image  v1          6243fedf46c2  4 minutes ago  1.13 GB

Detenga el contenedor en ejecución.

podman stop api-container

Analizando la imagen de contenedor creada

Para esta prueba utilizamos la herramienta de seguridad trivy opensource para realizar pruebas en la imagen creada.

Inslación de Trivy.

cat << EOF |sudo tee /etc/yum.repos.d/trivy.repo
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/\$releasever/\$basearch/
gpgcheck=0
enabled=1
EOF
systemctl enable --now podman.socket --user

sudo dnf install trivy -y

Realizar análisis de seguridad. Esto puede tomar unos minutos.

trivy image localhost/api-image:v1 --scanners vuln --severity HIGH,CRITICAL

Note que existen vulnerabilidades en la imagen base y en algunas librerías node aunque no son las utilizadas por nuestra aplicación.

localhost/api-image:v1 (debian 12.11)

Total: 292 (HIGH: 281, CRITICAL: 11)

┌─────────────────────────┬────────────────┬──────────┬──────────────┬─────────────────────────┬─────────────────────────┬──────────────────────────────────────────────────────────────┐
│         Library          Vulnerability   Severity     Status        Installed Version          Fixed Version                                  Title                             │
├─────────────────────────┼────────────────┼──────────┼──────────────┼─────────────────────────┼─────────────────────────┼──────────────────────────────────────────────────────────────┤
│ gir1.2-gdkpixbuf-2.0     CVE-2025-7345   HIGH      fix_deferred  2.42.10+dfsg-1+deb12u1                            gdk‑pixbuf: Heap‑buffer‑overflow in gdk‑pixbuf               │
│                                                                                                                    https://avd.aquasec.com/nvd/cve-2025-7345                    │
├─────────────────────────┼────────────────┤          ├──────────────┼─────────────────────────┼─────────────────────────┼──────────────────────────────────────────────────────────────┤
... OMITED ...
│ zlib1g                   CVE-2023-45853  CRITICAL  will_not_fix  1:1.2.13.dfsg-1                                   zlib: integer overflow and resultant heap-based buffer       │
│                                                                                                                    overflow in zipOpenNewFileInZip4_6                           │
│                                                                                                                    https://avd.aquasec.com/nvd/cve-2023-45853                   │
├─────────────────────────┤                                                                 ├─────────────────────────┤                                                              │
│ zlib1g-dev                                                                                                                                                                      │
│                                                                                                                                                                                 │
│                                                                                                                                                                                 │
└─────────────────────────┴────────────────┴──────────┴──────────────┴─────────────────────────┴─────────────────────────┴──────────────────────────────────────────────────────────────┘
Node.js (node-pkg)

Total: 1 (HIGH: 1, CRITICAL: 0)

┌────────────────────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬───────────────────────────────────────────────────┐
│          Library            Vulnerability   Severity  Status  Installed Version  Fixed Version                        Title                       │
├────────────────────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼───────────────────────────────────────────────────┤
│ cross-spawn (package.json)  CVE-2024-21538  HIGH      fixed   7.0.3              7.0.5, 6.0.6   cross-spawn: regular expression denial of service │
│                                                                                                 https://avd.aquasec.com/nvd/cve-2024-21538        │
└────────────────────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴───────────────────────────────────────────────────┘

Creando un contenedor para la aplicación V. 2

Modifique el archivo Dockerfile con el siguiente contenido.

ARG NODE_IMAGE="docker.io/node:18-alpine" # Modifique la imagen utilizada por 18-alpine
FROM ${NODE_IMAGE}

WORKDIR /app
COPY package*.json ./
RUN npm install && \
  npm run test
COPY . .

ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]

Construya la segunda versión de la images de su aplicación.

podman build . -t api-image:v2

Ejecute la segunda versión de la images de su aplicación.

podman run -d -p 30080:3000 --rm --name api-container api-image:v2 

Listar contenedores en ejecución.

podman ps
Salida similar:
CONTAINER ID  IMAGE                    COMMAND     CREATED        STATUS        PORTS                    NAMES
4fe23dfc7d25  localhost/api-image:v2  npm start   2 seconds ago  Up 3 seconds  0.0.0.0:30080->3000/tcp  api-container

En la seguna terminal/sesicón, ejcute de nuevo las pruebas manuales.

curl "http://localhost:30080/health"

curl "http://localhost:30080/echo?msg=Hola%20Mundo"

curl -X POST http://localhost:30080/sum -H "content-type: application/json" -d '{"a":5,"b":7}'

Explore la imágen creada para su aplicación.

podman exec -it api-container id
Salida similar:
uid=0(root) gid=0(root) groups=0(root)
podman inspect api-image:v2 | head
Salida similar:
[
     {
          "Id": "08b8e64a2db8a42987e866715975066bf62eb520c81cbec1c01df45789d67af3",
          "Digest": "sha256:552101cebebdccb7f1cc19e0553ebc13826771c0d88ecedbef04a5d60de90d53",
          "RepoTags": [
               "localhost/api-image:v2"
          ],
          "RepoDigests": [
               "localhost/api-image@sha256:552101cebebdccb7f1cc19e0553ebc13826771c0d88ecedbef04a5d60de90d53"
          ],
podman images api-image:v2
Salida Similar:
REPOSITORY            TAG         IMAGE ID      CREATED        SIZE
localhost/api-image  v2          08b8e64a2db8  4 minutes ago  148 MB

Realizar análisis de seguridad. Esto puede tomar unos minutos.

trivy image localhost/api-image:v2 --scanners vuln --severity HIGH,CRITICAL

Note que NO existen vulnerabilidades en la imagen base, pero en algunas librerías node aunque no son las utilizadas por nuestra apliacación.

Salida similar:

... OMITED ...

Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)


Node.js (node-pkg)

Total: 1 (HIGH: 1, CRITICAL: 0)

┌────────────────────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬───────────────────────────────────────────────────┐
│          Library            Vulnerability   Severity  Status  Installed Version  Fixed Version                        Title                       │
├────────────────────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼───────────────────────────────────────────────────┤
│ cross-spawn (package.json)  CVE-2024-21538  HIGH      fixed   7.0.3              7.0.5, 6.0.6   cross-spawn: regular expression denial of service │
│                                                                                                 https://avd.aquasec.com/nvd/cve-2024-21538        │
└────────────────────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴───────────────────────────────────────────────────┘

Esto puede ser debido a la contrucción de la imagen base y la instalación de Node en la imagen seleccionada.

podman exec -it api-container sh -lc "npm ls -g cross-spawn || true"
Salida similar:
/usr/local/lib
└─┬ npm@10.8.2
  └─┬ glob@10.4.2
    └─┬ foreground-child@3.2.1
      └── cross-spawn@7.0.3 # <--- librería no utilizada por el APP vulnerable

Puede limpiar o hacer su nueva imagen base, pero esto puede ser una tarea que requiere bastante mantenimiento.

La selección la imágen base es muy importante.

Detenga el contenedor en ejecución.

podman stop api-container

Creando un contenedor para la aplicación V. 3

Utilizaremos la imagen: quay.io/itmlabs/nodejs-22-minimal.

La cual es una copia de: registry.redhat.io/rhel10/nodejs-22-minimal@sha256:24b62b7a2d9091e0bd2c91a7cb0188451a3b8914446088a4f1b89642a195bc6e.

Modifique el archivo Dockerfile con el siguiente contenido.

# Ya que nuestro código es compatible con Node22, utilizaremos esta versión de Red Hat en lugar de la 18
ARG NODE_IMAGE="quay.io/itmlabs/nodejs-22-minimal" 
FROM ${NODE_IMAGE}

# Debemos cambiar el workdir (no se ejecuta como root)
WORKDIR /opt/app-root 
COPY package*.json ./
RUN npm install && \
  npm run test
COPY . .

ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]

Construya la tercera versión de la images de su aplicación.

podman build . -t api-image:v3

Ejecute la tercera versión de la images de su aplicación.

podman run -d -p 30080:3000 --rm --name api-container api-image:v3 

Listar contenedores en ejecución.

podman ps
Salida similar:
CONTAINER ID  IMAGE                    COMMAND     CREATED        STATUS        PORTS                    NAMES
4fe23dfc7d25  localhost/api-image:v3  npm start   2 seconds ago  Up 3 seconds  0.0.0.0:30080->3000/tcp  api-container

Explore la imágen creada para su aplicación.

podman exec -it api-container id
Salida similar:
uid=1001(devops) gid=0(root) groups=0(root)
Note que esta imagen por defecto utiliza el usuario con el id 1001.
podman inspect api-image:v3 | head
Salida similar:
[
     {
          "Id": "770f4bb881b0bc89d0829f021778003a5d30e1a4fe224fa793b71c82d3ce7152",
          "Digest": "sha256:58eb70e9d07b9a6448d225e2363612977271e48648c3bd1fd245b70760ffd299",
          "RepoTags": [
               "localhost/api-image:v3"
          ],
          "RepoDigests": [
               "localhost/api-image@sha256:58eb70e9d07b9a6448d225e2363612977271e48648c3bd1fd245b70760ffd299"
          ],

podman images api-image:v3
Salida Similar:
REPOSITORY            TAG         IMAGE ID      CREATED        SIZE
localhost/api-image  v3          770f4bb881b0  4 minutes ago  250 MB

Realizar análisis de seguridad. Esto puede tomar unos minutos.

trivy image localhost/api-image:v3 --scanners vuln --severity HIGH,CRITICAL

Note que NO existen vulnerabilidades HIGH,CRITICAL en la imagen base o en las librerías de NodeJS.

NOTA

No se debe tomar el contenido de este repositorio, como una recomendación para producción o el ambiente actual. No se han utilizado otras técnicas sugeridas como optmización de uso de capas, multi stage builds o pipelines para simplificar el contenido en la contrucción o consideraciones importanes para NodeJS.