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
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
"express": "^4.21.2",
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
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
uid=0(root) gid=0(root) groups=0(root)
podman inspect api-image:v1 | head
[
{
"Id": "6243fedf46c2bf7df152072a0ab3635fa81453961623ce4ad4c682479ce4e2af",
"Digest": "sha256:bee0ef7fd3c63dc9245e962f2bc874888ed8dca2371b3f37c6b4840edd78af4c",
"RepoTags": [
"localhost/api-image:v1"
],
"RepoDigests": [
"localhost/api-image@sha256:bee0ef7fd3c63dc9245e962f2bc874888ed8dca2371b3f37c6b4840edd78af4c"
],
podman images api-image:v1
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
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
uid=0(root) gid=0(root) groups=0(root)
podman inspect api-image:v2 | head
[
{
"Id": "08b8e64a2db8a42987e866715975066bf62eb520c81cbec1c01df45789d67af3",
"Digest": "sha256:552101cebebdccb7f1cc19e0553ebc13826771c0d88ecedbef04a5d60de90d53",
"RepoTags": [
"localhost/api-image:v2"
],
"RepoDigests": [
"localhost/api-image@sha256:552101cebebdccb7f1cc19e0553ebc13826771c0d88ecedbef04a5d60de90d53"
],
podman images api-image:v2
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"
/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
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
uid=1001(devops) gid=0(root) groups=0(root)
1001.
podman inspect api-image:v3 | head
[
{
"Id": "770f4bb881b0bc89d0829f021778003a5d30e1a4fe224fa793b71c82d3ce7152",
"Digest": "sha256:58eb70e9d07b9a6448d225e2363612977271e48648c3bd1fd245b70760ffd299",
"RepoTags": [
"localhost/api-image:v3"
],
"RepoDigests": [
"localhost/api-image@sha256:58eb70e9d07b9a6448d225e2363612977271e48648c3bd1fd245b70760ffd299"
],
podman images api-image:v3
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.