Hospedar un sitio web con base de datos: cómo funciona

El navegador nunca accede a la base de datos directamente. Un back end (una API o un servidor web) recibe la petición, consulta la base con credenciales guardadas en una variable de entorno y devuelve la respuesta.

Actualizado el 5 min de lectura

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

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.

NavegadorGET / POSTJSON / HTMLBack endDATABASE_URLvariable deentornopoolconsulta parametrizadaBase dedatossin accesodirecto
El navegador solo habla con el back end, que guarda la contraseña en el entorno y consulta la base con una conexión del pool.

¿Cómo se comunica un sitio web con la base de datos?#

Una petición recorre tres puntos:

  1. Navegador: pide una página o llama a una ruta, por ejemplo GET /api/posts.
  2. Back end: valida la petición, toma una conexión del pool y ejecuta una consulta SQL (o su equivalente en MongoDB).
  3. 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:
sql
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:

bash
npm install express pg

Crea el index.js. El archivo usa ES modules, así que agrega "type": "module" al package.json:

js
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. El pool.on("error") maneja errores de conexiones inactivas, como un reinicio de la base; sin él, el proceso se cae.
  • $1 y 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.
  • async sin try/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 a next(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:

bash
export DATABASE_URL="postgresql://usuario:contraseña@localhost:5432/mibase"
node index.js

Si 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:

bash
curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Primer post"}'

curl http://localhost:3000/api/posts

La 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 único Pool compartido, 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

Más información

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.

Siguiente artículoError 404 al recargar una SPA de React/Vue después del deployUna SPA de React o Vue devuelve 404 al recargar porque el servidor busca un archivo en esa URL. Arréglalo con un fallback a index.html en nginx, Express o Apache.

Más en Sitios web

Ver los 20 artículos