API Node.js en Docker con datos en PostgreSQL
Esta API de práctica guarda notas en PostgreSQL y permite crearlas y consultarlas. El ejemplo usa Node.js, Express y el cliente pg, con consultas parametrizadas. No presenta un arreglo en memoria como si fuera almacenamiento persistente.
Necesitas una base PostgreSQL accesible desde el contenedor y una cuenta limitada a sus datos. Prepárala por un procedimiento autorizado y conserva sus credenciales fuera del repositorio.
Prepara tabla y proyecto
En la base del laboratorio, crea una tabla con el usuario propietario apropiado:
CREATE TABLE notas (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
texto text NOT NULL CHECK (length(texto) <= 500)
);
En una carpeta nueva:
npm init -y
npm install express pg
mkdir src
Conserva package.json y package-lock.json. Revisa las versiones de dependencias y utiliza una rama Node con soporte.
Implementa src/index.js
const express = require('express');
const { Pool } = require('pg');
const pool = new Pool();
const app = express();
app.use(express.json({ limit: '16kb' }));
app.get('/health', async (req, res, next) => {
try {
await pool.query('SELECT 1');
res.json({ status: 'ok' });
} catch (error) { next(error); }
});
app.get('/notas', async (req, res, next) => {
try {
const result = await pool.query(
'SELECT id, texto FROM notas ORDER BY id DESC LIMIT 100'
);
res.json(result.rows);
} catch (error) { next(error); }
});
app.post('/notas', async (req, res, next) => {
const texto = req.body?.texto;
if (typeof texto !== 'string' || !texto.trim() || texto.length > 500) {
return res.status(400).json({ error: 'Texto no válido' });
}
try {
const result = await pool.query(
'INSERT INTO notas(texto) VALUES($1) RETURNING id, texto',
[texto.trim()]
);
res.status(201).json(result.rows[0]);
} catch (error) { next(error); }
});
app.use((error, req, res, next) => {
if (error.type === 'entity.parse.failed') {
return res.status(400).json({ error: 'JSON no válido' });
}
if (error.type === 'entity.too.large') {
return res.status(413).json({ error: 'Solicitud demasiado grande' });
}
console.error(error.message);
res.status(500).json({ error: 'No se pudo completar la operación' });
});
app.listen(3000, '0.0.0.0');
Pool utiliza las variables de conexión reconocidas por pg, como PGHOST, PGDATABASE, PGUSER y PGPASSWORD. Define valores correctos para la red del contenedor. En producción, configura también TLS y políticas del cliente según el servidor.
Construye la imagen
Guarda Dockerfile:
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node src ./src
USER node
EXPOSE 3000
CMD ["node", "src/index.js"]
Excluye node_modules, .git, .env y archivos privados con .dockerignore. No copies credenciales a la imagen: seguirían presentes en sus capas aunque después las borraras.
docker build -t api-notas:lab .
Al ejecutar, proporciona configuración por el mecanismo privado de tu entorno y la red que alcance PostgreSQL. Publica 3000 en 127.0.0.1 para el laboratorio. localhost dentro de la API no apunta automáticamente al contenedor de la base.
Comprueba comportamiento
Verifica /health y realiza un POST a /notas con un JSON de texto. Comprueba respuesta 201, lectura posterior y rechazo 400 para entradas inválidas. Reinicia únicamente la API y confirma que la nota sigue en PostgreSQL.
Este servicio no tiene autenticación y no debe publicarse como una aplicación terminada. Necesita diseño de acceso, pruebas más amplias, cierre ordenado, observabilidad y política de errores.
Errores de conexión y contrato HTTP
Si falla /health, comprueba primero la conexión desde la misma red de la API: nombre, puerto, base, usuario y política TLS. No otorgues acceso administrativo a la cuenta para ocultar un error de configuración.
Para comprobar el contrato, prueba también un cuerpo sin texto, JSON mal formado y una solicitud demasiado grande. Una respuesta controlada permite distinguir un dato inválido de una indisponibilidad del servidor. Registra el fallo de manera privada y evita devolver credenciales o consultas completas al cliente.
Preguntas frecuentes
¿Los datos sobreviven al reinicio de la API?
Sí si se guardaron correctamente en PostgreSQL y la base conserva su almacenamiento. Debes comprobar también la persistencia y respaldo de la propia base.
¿Las consultas parametrizadas sustituyen toda validación?
No. Ayudan a separar valores de SQL, pero necesitas validar tipo, tamaño, reglas del negocio y permisos.
¿Por qué la API no conecta a localhost?
Dentro del contenedor, localhost se refiere a ese mismo contenedor. Utiliza el host o nombre del servicio correspondiente a la red real.
En pocas palabras
Una API que promete persistencia debe escribir realmente en una base, no solo en un arreglo de memoria. La conexión, los permisos y las comprobaciones son parte del ejemplo.
Conclusión
Prueba altas, lecturas y errores y confirma persistencia tras reiniciar la API. Antes de exponerla, añade autenticación, límites, TLS y mantenimiento conforme a su uso.
Para continuar, revisa Docker: qué es y cómo usarlo para ejecutar contenedores y Cómo instalar Docker en Ubuntu 24.04: guía práctica paso a paso.
Fuentes y documentación
Documentación de expressjs.com.
