Objetivos de aprendizaje
Después de leer este artículo podrás:
- Entender el camino de una petición entre navegador, back end y base de datos
- Crear un back end en Node.js que lee y escribe en PostgreSQL con consultas parametrizadas
- Configurar la conexión con una variable de entorno y comprobar que funciona
En esta página
- ¿Cómo se comunica un sitio web con la base de datos?
- ¿Un sitio estático puede usar una base de datos?
- ¿Qué necesito antes de empezar?
- ¿Cómo escribo el back end con Node.js y PostgreSQL?
- ¿Cómo configuro las credenciales de forma segura?
- ¿Cómo compruebo que funciona?
- ¿Qué errores son comunes?
- Cómo hacerlo en Square Cloud
- Preguntas frecuentes
Casi todo sitio web que guarda algo (usuarios, pedidos, comentarios, publicaciones) tiene una base de datos detrás. La duda de quien empieza es dónde "encaja" la base: no forma parte de la página, y el navegador no puede (ni debe) hablar con ella.
Entre los dos hay un back end: un programa que corre en un servidor, recibe peticiones HTTP, consulta la base y devuelve el resultado. Esta guía muestra ese diseño y un ejemplo funcional con Node.js y PostgreSQL.
¿Cómo se comunica un sitio web con la base de datos?#
Una petición recorre tres puntos:
- Navegador: pide una página o llama a una ruta, por ejemplo
GET /api/posts. - Back end: valida la petición, toma una conexión del pool y ejecuta una consulta SQL (o su equivalente en MongoDB).
- Base de datos: devuelve las filas; el back end arma el JSON o el HTML y responde.
El navegador solo ve el resultado final. La contraseña de la base existe únicamente en el servidor, en una variable de entorno. Si estuviera en el JavaScript de la página, cualquier visitante podría leerla en las herramientas de desarrollo.
¿Un sitio estático puede usar una base de datos?#
Un sitio estático son solo archivos (HTML, CSS, JS) servidos tal cual. No ejecuta código en el servidor, así que no consulta ninguna base. Para mostrar datos dinámicos, llama a una API en otro lugar que hace la consulta.
Los frameworks que renderizan en el servidor (como Next.js) juntan las dos partes en un solo proyecto. La diferencia entre renderizar en el build o en cada petición está en SSR, SSG y sitio estático. En cualquier caso, quien habla con la base es código que corre en el servidor.
¿Qué necesito antes de empezar?#
- Una versión reciente de Node.js compatible con Express 5, y npm.
- Una base PostgreSQL accesible, local u hospedada, y su cadena de conexión con el formato
postgresql://usuario:contraseña@host:5432/base. Lee cómo hospedar una base de datos si todavía no tienes una. - Una tabla sencilla para probar:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);¿Cómo escribo el back end con Node.js y PostgreSQL?#
Instala las dependencias:
npm install express pgCrea el index.js. El archivo usa ES modules, así que agrega "type": "module" al package.json:
import express from "express";
import pg from "pg";
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
});
// sin este listener, un error en una conexión inactiva tumba el proceso
pool.on("error", (err) => console.error("error en el pool de PostgreSQL", err));
const app = express();
app.use(express.json());
app.get("/api/posts", async (req, res) => {
const { rows } = await pool.query(
"SELECT id, title, created_at FROM posts ORDER BY id DESC LIMIT 20"
);
res.json(rows);
});
app.post("/api/posts", async (req, res) => {
const { title } = req.body ?? {};
if (typeof title !== "string" || title.trim() === "") {
return res.status(400).json({ error: "title es obligatorio" });
}
const { rows } = await pool.query(
"INSERT INTO posts (title) VALUES ($1) RETURNING id, title, created_at",
[title.trim()]
);
res.status(201).json(rows[0]);
});
app.listen(process.env.PORT || 3000);Aquí importan tres cosas:
Pool: mantiene conexiones abiertas y las reutiliza. Abrir una conexión nueva en cada petición es lento y puede agotar el límite de conexiones de la base. La documentación de pg dice que normalmente basta con un único pool por aplicación. Elpool.on("error")maneja errores de conexiones inactivas, como un reinicio de la base; sin él, el proceso se cae.$1y el array: es una consulta parametrizada. El valor viaja separado del SQL, así que un texto malicioso no se convierte en comando. OWASP lo señala como la primera defensa contra la inyección SQL.asyncsintry/catch: en Express 5, un error lanzado o una promesa rechazada dentro de un handler se envía automáticamente al manejador de errores. En Express 4 había que llamar anext(err)a mano.
Nunca armes el SQL concatenando la entrada del usuario, como en "... WHERE title = '" + title + "'".
¿Cómo configuro las credenciales de forma segura?#
Define DATABASE_URL en el entorno, nunca en el código ni en el repositorio:
export DATABASE_URL="postgresql://usuario:contraseña@localhost:5432/mibase"
node index.jsSi la contraseña tiene caracteres con significado en una URL, como @, /, # o %, aplica percent-encoding (p@ss pasa a ser p%40ss, pa/ss pasa a ser pa%2Fss). En desarrollo sirve un archivo .env fuera de Git; en producción, usa la configuración de variables de entorno de tu proveedor. Más detalles en qué son las variables de entorno.
¿Cómo compruebo que funciona?#
Con el servidor corriendo, escribe y lee:
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"Primer post"}'
curl http://localhost:3000/api/postsLa primera respuesta trae el registro creado, con id y created_at. La segunda lista los posts. Para que el sitio lo use, el front end servido por el mismo back end llama a fetch("/api/posts") y renderiza el JSON.
¿Qué errores son comunes?#
connect ECONNREFUSED: nada aceptó la conexión en ese host y puerto. La base está apagada o el host o el puerto son incorrectos. Un firewall que descarta paquetes suele dar un timeout en su lugar.password authentication failed for user "...": usuario o contraseña incorrectos.Invalid URL: la cadena de conexión está mal formada, muchas veces por un carácter de la contraseña sin percent-encoding.sorry, too many clients already: la base llegó a su límite de conexiones. Usa un únicoPoolcompartido, creado una vez al iniciar el programa. Más en demasiadas conexiones.- Base y sitio lejos uno del otro: cada consulta paga la latencia de la red, y una página con varias consultas se vuelve lenta. Mantén ambos en el mismo datacenter.
- Contraseña en el front end o en el repositorio: genera una nueva contraseña en la base y muévela a una variable de entorno.
Cómo hacerlo en Square Cloud#
En Square Cloud, el back end y la base de datos están en la misma cuenta. Haces el deploy del sitio (Node.js, Python, PHP, Go y otros runtimes) y creas desde el panel una base PostgreSQL, MySQL, MongoDB o Redis, con una latencia media de alrededor de 1 ms entre aplicación y base porque ambas están en el mismo datacenter. El panel te entrega la URL de conexión completa y el certificado.
TLS es obligatorio: el cliente tiene que confiar en la CA descargada y, en PostgreSQL, presentar también el .pem descargado, así que pasa esos archivos en la opción ssl de pg.Pool. Cada base acepta hasta 512 conexiones, sin pooler delante, por eso mantén bajo el max del pool. El hosting de bases de datos empieza en el plan Standard; el Hobby no incluye bases.
Pon la URL de conexión como variable de entorno DATABASE_URL de la aplicación, desde el panel, la API o la CLI. El cambio de variable aplica en el siguiente reinicio, sin rebuild. Toda aplicación web recibe un subdominio *.squareweb.app con HTTPS, y el dominio personalizado está disponible desde el Standard. Las aplicaciones web usan al menos 512 MB de RAM.
Mira los planes y cómo publicar tu sitio en hosting de sitios web en Square Cloud.
Hosting de sitios web
Sitios estáticos y SSR con TLS automático y CDN global
Preguntas frecuentes
¿Un sitio estático puede usar una base de datos?
El HTML estático por sí solo no consulta la base. Para leer o escribir datos tiene que llamar a un back end (una API) que haga la consulta. El JavaScript del navegador nunca debe contener la contraseña de la base.
¿Puedo conectar el JavaScript del navegador directamente a la base de datos?
No. Todo lo que se envía al navegador es público, incluida la cadena de conexión. Quien se conecta a la base es siempre el servidor.
¿Necesito un ORM?
No. Un driver como pg, con consultas parametrizadas, es suficiente. Los ORM ayudan con esquemas grandes y migraciones, pero son opcionales.
¿Cómo evito la inyección SQL?
Usa consultas parametrizadas, con marcadores como $1, en lugar de concatenar texto del usuario en el SQL. OWASP señala las prepared statements como la primera defensa.
Más en Sitios web
Ver los 20 artículos- Qué es un sitio estático y cómo hospedarloUn sitio estático es un conjunto de archivos HTML, CSS y JavaScript ya listos, entregados igual a todos los visitantes. Mira cómo funciona y dónde hospedarlo.
- ¿Cuánto cuesta hospedar una página web?Hospedar una página web cuesta desde cero hasta unas decenas de dólares al mes. Mira qué entra en la cuenta: hosting, dominio, tráfico y extras, con límites reales.
- ¿Cuánto cuesta hospedar un sitio estático?Hospedar un sitio estático cuesta entre cero y unas decenas de dólares al mes en la mayoría de los casos. Mira cómo el tráfico define el precio, paso a paso.
- ¿Cuánto cuesta hospedar una aplicación Next.js?Hospedar una app Next.js cuesta desde cero, con export estático, hasta decenas de dólares al mes con servidor. Modelos de cobro, cuentas de tráfico y costos ocultos.