Home / DevOps & Cloud / Cómo desplegar un sitio con Express 5 y Heroku paso a paso
Cómo desplegar un sitio con Express 5 y Heroku paso a paso

Cómo desplegar un sitio con Express 5 y Heroku paso a paso

26 October 2026 · 17 min de lectura ··

De tu carpeta local a una URL pública en minutos

Hay pocas sensaciones tan satisfactorias para quien empieza en desarrollo web como compartir un enlace y ver que su proyecto funciona en el navegador de otra persona. El problema es que el salto desde localhost hasta internet suele parecer intimidante: servidores, puertos, dominios, certificados y procesos que deben seguir vivos aunque cierres la laptop.

Las plataformas como servicio (PaaS) nacieron precisamente para eliminar esa fricción. Tú subes el código y la plataforma se encarga de instalar dependencias, arrancar el proceso, enrutar el tráfico y servirlo con HTTPS. Heroku fue durante años la referencia de este modelo y sigue siendo una opción directa para desplegar una aplicación Express en Heroku con unos pocos comandos.

En esta guía vas a construir un pequeño sitio con Express 5, la versión actual del framework, sobre Node.js 24, la versión LTS que Heroku usa por defecto. Verás cómo servir archivos estáticos y una API sencilla, cómo preparar el proyecto para la plataforma, cómo desplegarlo con Git y la Heroku CLI, y cómo gestionar variables de entorno y registros. También repasaremos los errores más frecuentes, las prácticas recomendadas para producción y la situación actual de Heroku frente a sus alternativas, un punto importante desde los cambios que la plataforma anunció a comienzos de 2026.

Qué necesitas antes de empezar

Para seguir el tutorial necesitas tres herramientas instaladas en tu equipo y una cuenta en Heroku:

  • Node.js 24 LTS, que incluye npm. Express 5 requiere al menos Node.js 18, pero conviene trabajar con la misma versión que usarás en producción.
  • Git, porque el despliegue clásico en Heroku se basa en hacer push a un repositorio remoto.
  • La Heroku CLI, la herramienta de línea de comandos de la plataforma.
  • Una cuenta de Heroku con un método de pago registrado. La plataforma eliminó su plan gratuito en 2022, así que incluso el plan más económico, Eco, requiere tarjeta.

Comprueba las versiones instaladas:

node --version
npm --version
git --version
heroku --version
v24.22.1

La primera línea debe mostrar Node.js 24; Heroku incluye npm 11 con esa versión, así que lo ideal es usar también npm 11 en local. Los otros comandos solo confirman que Git y la CLI están instalados y accesibles desde tu terminal.

Si te falta la CLI, puedes instalarla con npm o, en macOS, con Homebrew:

npm install -g heroku
# o bien
brew install heroku/brew/heroku

Después inicia sesión. El comando abre el navegador para que completes la autenticación, que en Heroku exige verificación en dos pasos. La antigua opción heroku login -i, que pedía usuario y contraseña en la terminal, ya no es práctica con la autenticación multifactor obligatoria:

heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/...
Logging in... done
Logged in as [email protected]

La aplicación de ejemplo: un generador de nombres de proyectos

Para que el ejemplo tenga algo más que un “Hola mundo”, construirás un generador de nombres para proyectos de software. La página muestra un botón; al pulsarlo, el navegador consulta una pequeña API de Express que combina un adjetivo y un sustantivo al azar.

La estructura final del proyecto será esta:

generador-nombres/
├── public/
│   ├── index.html
│   ├── estilos.css
│   └── app.js
├── servidor.js
├── package.json
├── package-lock.json
├── Procfile
├── .gitignore
└── README.md

Crea la carpeta, inicializa Git y npm, e instala Express:

mkdir generador-nombres && cd generador-nombres
git init -b main
npm init -y
npm install express@5
echo "node_modules/" > .gitignore

Observa la opción -b main en git init. Muchos tutoriales antiguos usan la rama master y despliegan con git push heroku master; hoy la convención por defecto es main, y Heroku acepta ambas, pero conviene que el nombre de tu rama local coincida con el que empujas.

Preparar package.json para la plataforma

Heroku detecta que el proyecto es de Node.js porque existe un package.json en la raíz. Ese archivo también le indica qué versión de Node instalar y cómo arrancar la aplicación. Ajusta el que generó npm para que quede así:

{
    "name": "generador-nombres",
    "version": "1.0.0",
    "type": "module",
    "main": "servidor.js",
    "scripts": {
        "start": "node servidor.js",
        "dev": "node --watch servidor.js"
    },
    "engines": {
        "node": "24.x"
    },
    "dependencies": {
        "express": "^5.3.0"
    }
}

Hay tres decisiones importantes en este archivo:

  • "type": "module" habilita la sintaxis moderna import y export, que es la recomendada hoy en Node.js.
  • engines.node con el valor 24.x fija la versión mayor y deja que Heroku aplique automáticamente los parches de seguridad. La documentación de la plataforma desaconseja rangos abiertos como >=24, porque una versión mayor nueva podría romper tu aplicación sin aviso.
  • El script dev usa node --watch, que reinicia el servidor al guardar cambios sin instalar herramientas adicionales como nodemon.

Mantén siempre el archivo package-lock.json en el repositorio. Heroku elige el gestor de paquetes según el archivo de bloqueo que encuentra, y con él instala exactamente las mismas versiones que probaste en tu equipo.

Escribir el servidor con Express 5

El servidor tiene tres responsabilidades: servir archivos estáticos con Express, exponer una ruta de API y escuchar en el puerto correcto. Crea servidor.js:

import express from "express";

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

const adjetivos = [
    "Veloz",
    "Silencioso",
    "Cuántico",
    "Brillante",
    "Nómada",
    "Lunar",
];
const sustantivos = [
    "Halcón",
    "Faro",
    "Cometa",
    "Engranaje",
    "Puente",
    "Atlas",
];

const elegir = (lista) => lista[Math.floor(Math.random() * lista.length)];

app.disable("x-powered-by");
app.use(express.static("public", { maxAge: "1h" }));

app.get("/api/nombre", (req, res) => {
    res.json({ nombre: `${elegir(adjetivos)} ${elegir(sustantivos)}` });
});

app.get("/salud", (req, res) => {
    res.json({ estado: "ok", entorno: process.env.NODE_ENV ?? "desarrollo" });
});

app.use((req, res) => {
    res.status(404).json({ error: "Recurso no encontrado" });
});

const servidor = app.listen(PUERTO, (error) => {
    if (error) throw error;
    console.log(`Servidor escuchando en el puerto ${PUERTO}`);
});

process.on("SIGTERM", () => {
    console.log("SIGTERM recibido, cerrando conexiones...");
    servidor.close(() => process.exit(0));
});

Vale la pena detenerse en varios detalles.

El puerto viene de la variable de entorno PORT que asigna Heroku en cada arranque. Nunca fijes un número a mano en producción: cada dyno recibe un puerto distinto y el enrutador de la plataforma solo enviará tráfico a ese. El valor 3000 es solo para desarrollo local.

En Express 5, el callback de app.listen() recibe un error si el servidor no puede arrancar, por ejemplo porque el puerto está ocupado. En Express 4 ese error pasaba desapercibido.

express.static("public") sirve todo lo que haya en la carpeta public: si existe public/index.html, la ruta / lo devuelve automáticamente, sin necesidad de definirla.

El manejador de SIGTERM permite un apagado ordenado. Heroku envía esa señal cuando reinicia los dynos, algo que ocurre al menos una vez al día y en cada despliegue, y concede unos segundos para terminar las peticiones en curso.

Crear el frontend estático

El frontend es deliberadamente sencillo y no necesita bibliotecas externas. Las versiones antiguas de este tipo de tutoriales solían cargar jQuery para hacer peticiones; hoy la API fetch nativa del navegador cubre ese caso con menos código.

public/index.html:

<!doctype html>
<html lang="es">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Generador de nombres de proyectos</title>
        <link rel="stylesheet" href="/estilos.css" />
    </head>
    <body>
        <main>
            <h1>¿Cómo se llamará tu próximo proyecto?</h1>
            <p id="resultado" aria-live="polite">
                Pulsa el botón para descubrirlo
            </p>
            <button id="generar" type="button">Generar nombre</button>
        </main>
        <script src="/app.js" type="module"></script>
    </body>
</html>

public/estilos.css:

body {
    margin: 0;
    min-height: 100vh;
    display: grid;
    place-items: center;
    font-family: system-ui, sans-serif;
    background: #0f172a;
    color: #e2e8f0;
}

main {
    text-align: center;
    padding: 1rem;
}

#resultado {
    font-size: 2rem;
    font-weight: 700;
    min-height: 3rem;
}

button {
    padding: 0.75rem 1.5rem;
    border: 0;
    border-radius: 0.5rem;
    background: #38bdf8;
    color: #0f172a;
    font-size: 1rem;
    cursor: pointer;
}

public/app.js:

const boton = document.querySelector("#generar");
const resultado = document.querySelector("#resultado");

boton.addEventListener("click", async () => {
    boton.disabled = true;
    try {
        const respuesta = await fetch("/api/nombre");
        if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);
        const { nombre } = await respuesta.json();
        resultado.textContent = nombre;
    } catch (error) {
        resultado.textContent =
            "No se pudo generar un nombre. Inténtalo de nuevo.";
        console.error(error);
    } finally {
        boton.disabled = false;
    }
});

Prueba todo en local antes de desplegar:

npm run dev
curl http://localhost:3000/api/nombre
{"nombre":"Cuántico Faro"}

Probar en local es el paso que más errores evita. Si algo falla en tu equipo, fallará igual en la plataforma, y allí es más lento de diagnosticar.

Configurar el archivo Procfile

El Procfile le dice a Heroku qué procesos debe ejecutar. Es un archivo de texto sin extensión, llamado exactamente Procfile, con mayúscula inicial, y ubicado en la raíz del proyecto. Cada línea define un tipo de proceso:

web: node servidor.js

El tipo web es especial: es el único que recibe tráfico HTTP del enrutador de Heroku. Si tu aplicación tuviera tareas en segundo plano, como procesar una cola, añadirías otra línea con un tipo distinto, por ejemplo worker: node trabajador.js, que se escala de forma independiente.

Para aplicaciones Node.js, Heroku ejecuta npm start si no encuentra un Procfile. Aun así, saber configurar el archivo Procfile de forma explícita es una buena práctica: documenta cómo arranca la aplicación y evita que un cambio en los scripts de package.json altere el comportamiento en producción sin que lo notes.

Puedes crear el archivo desde la terminal:

echo "web: node servidor.js" > Procfile

Crear la aplicación en Heroku y desplegar

Con el proyecto listo, crea la aplicación en Heroku. El nombre que elijas formará parte de la URL pública, debe ser único en toda la plataforma y solo admite minúsculas, números y guiones:

heroku create generador-nombres-2026
Creating ⬢ generador-nombres-2026... done
https://generador-nombres-2026-3f8a1c2d9e4b.herokuapp.com/ | https://git.heroku.com/generador-nombres-2026.git

Observa que la URL incluye un sufijo aleatorio. Heroku añade ese identificador a los dominios de las aplicaciones nuevas, así que no des por hecho que la dirección será nombre.herokuapp.com; consulta siempre la que devuelve el comando o heroku info.

El comando también añadió un remoto de Git llamado heroku. Puedes comprobarlo:

git remote -v
heroku  https://git.heroku.com/generador-nombres-2026.git (fetch)
heroku  https://git.heroku.com/generador-nombres-2026.git (push)

Ahora confirma los cambios y empuja la rama principal a ese remoto. Ese push es el despliegue:

git add .
git commit -m "Primera versión del generador de nombres"
git push heroku main

Heroku recibe el código, detecta Node.js, instala la versión indicada en engines, ejecuta npm ci, lanza el script build si existe y arranca el proceso web. El final de la salida debería parecerse a esto:

remote: -----> Building on the Heroku-24 stack
remote: -----> Determining which buildpack to use for this app
remote: -----> Node.js app detected
remote: -----> Installing binaries
remote:        engines.node (package.json):   24.x
remote:        Resolving node version 24.x...
remote:        Downloading and installing node 24.22.1...
remote: -----> Installing dependencies
remote:        Installing node modules (package-lock.json)
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote: -----> Launching...
remote:        https://generador-nombres-2026-3f8a1c2d9e4b.herokuapp.com/ deployed to Heroku
remote: Verifying deploy... done.

Abre la aplicación en el navegador directamente desde la terminal:

heroku open

Cada nuevo git push heroku main genera una nueva versión, llamada release, y la publica. Si algo sale mal, puedes volver a la anterior en segundos:

heroku releases
heroku rollback v4

Si prefieres no empujar a mano, Heroku también puede conectarse a un repositorio de GitHub desde el panel web y desplegar automáticamente cada cambio en la rama principal, con la opción de esperar a que pase la integración continua.

Variables de entorno, logs y escalado

Las credenciales, claves de API y ajustes que cambian entre entornos nunca deben ir en el código. Heroku las gestiona como “config vars”, que la aplicación lee en process.env:

heroku config:set NODE_ENV=production MENSAJE_BIENVENIDA="Hola desde producción"
heroku config
heroku config:unset MENSAJE_BIENVENIDA
=== generador-nombres-2026 Config Vars
MENSAJE_BIENVENIDA: Hola desde producción
NODE_ENV:           production

Cada cambio en las variables reinicia la aplicación automáticamente. Definir NODE_ENV=production es especialmente importante en Express, porque activa la caché de plantillas y mensajes de error menos detallados.

Para ver lo que ocurre en producción, consulta los registros en tiempo real. Todo lo que tu aplicación escribe en la salida estándar aparece aquí, junto con los eventos del enrutador y de la plataforma:

heroku logs --tail
2026-10-27T08:20:11.402Z app[web.1]: Servidor escuchando en el puerto 41873
2026-10-27T08:20:15.118Z heroku[router]: at=info method=GET path="/api/nombre" status=200 bytes=31 protocol=https

El estado de los procesos y el escalado se controlan con ps:

heroku ps
heroku ps:scale web=1
heroku ps:restart

Ten en cuenta que los dynos Eco se suspenden tras un periodo sin tráfico y tardan unos segundos en despertar con la siguiente petición. Para aplicaciones que deben responder siempre al instante necesitas un plan superior.

Buenas prácticas para producción

Que la aplicación cargue es solo el primer paso. Estas recomendaciones la hacen más segura y robusta.

Añade cabeceras de seguridad. El paquete helmet configura de una vez cabeceras como Content-Security-Policy, X-Content-Type-Options o Strict-Transport-Security, y compression reduce el tamaño de las respuestas:

npm install helmet compression
import compression from "compression";
import helmet from "helmet";

app.set("trust proxy", 1);
app.use(helmet());
app.use(compression());

La línea trust proxy es importante en Heroku. Las peticiones llegan a tu aplicación a través del enrutador de la plataforma, y sin esa opción Express vería la IP del enrutador en lugar de la del usuario y no sabría que la conexión original era HTTPS.

Redirige el tráfico HTTP a HTTPS. Heroku sirve ambos protocolos, pero puedes obligar a usar el seguro comprobando la cabecera que añade el enrutador:

app.use((req, res, next) => {
    if (
        process.env.NODE_ENV === "production" &&
        req.headers["x-forwarded-proto"] !== "https"
    ) {
        return res.redirect(
            301,
            `https://${req.headers.host}${req.originalUrl}`
        );
    }
    next();
});

Mantén las dependencias de ejecución en dependencies. Heroku elimina las devDependencies después de construir, así que si el servidor importa un paquete que instalaste con --save-dev, fallará al arrancar.

Expón una ruta de salud, como la /salud de este ejemplo. Te sirve para monitorizar la aplicación desde servicios externos y comprobar rápidamente que el despliegue funciona.

Revisa periódicamente las vulnerabilidades conocidas con npm audit y actualiza Express y Node.js dentro de su línea mayor. Con engines.node en 24.x, cada despliegue recoge el último parche de seguridad disponible.

Errores comunes y cómo evitarlos

Estos tropiezos aparecen una y otra vez en el primer despliegue.

Fijar el puerto en el código. app.listen(3000) funciona en tu equipo, pero en Heroku el proceso no recibirá tráfico y la plataforma lo detendrá tras 60 segundos. Usa siempre process.env.PORT.

Nombrar mal el Procfile. procfile, Procfile.txt o ProcFile no funcionan. El nombre es exactamente Procfile y debe estar en la raíz.

Olvidar el archivo de bloqueo. Sin package-lock.json, Heroku puede resolver versiones distintas a las que probaste.

Subir node_modules al repositorio. Además de inflar el repositorio, puede introducir binarios compilados para tu sistema operativo que no funcionan en Linux. Exclúyelo con .gitignore.

Usar rutas comodín con la sintaxis de Express 4. Express 5 actualizó su motor de rutas y ya no acepta un asterisco suelto. Si necesitas una ruta que capture todo, por ejemplo para una SPA, debes darle nombre al comodín:

// Express 4
app.get("*", enviarIndex);

// Express 5
app.get("/{*ruta}", enviarIndex);

Confiar en el sistema de archivos. El disco de un dyno es efímero: lo que escribas se pierde en cada reinicio o despliegue. Guarda archivos subidos por usuarios en un almacenamiento externo, como Amazon S3, y los datos en una base de datos.

Solución de problemas: mensajes de error frecuentes

Los errores de Heroku tienen códigos que ayudan a localizar la causa. Estos son los más habituales con aplicaciones Express.

La aplicación se cae al arrancar:

heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" status=503

El proceso terminó con un error. Revisa las líneas anteriores de heroku logs --tail: casi siempre verás la excepción real, como un módulo inexistente o una variable de entorno que falta.

El proceso no escucha en el puerto asignado:

Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch

El servidor escucha en un puerto fijo o tarda demasiado en arrancar. Usa process.env.PORT y evita tareas lentas antes de llamar a app.listen().

No hay procesos web en marcha:

heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/"

Falta la línea web: en el Procfile o el proceso está escalado a cero. Revísalo y ejecuta heroku ps:scale web=1.

Dependencia no encontrada:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'helmet' imported from /app/servidor.js

El paquete no está en dependencies o no confirmaste el package-lock.json actualizado. Instálalo sin --save-dev, confirma ambos archivos y vuelve a desplegar.

La rama que empujas no existe:

error: src refspec main does not match any
error: failed to push some refs to 'https://git.heroku.com/generador-nombres-2026.git'

Tu rama local se llama de otra forma, normalmente master, o todavía no hiciste ningún commit. Comprueba el nombre con git branch y empuja la rama correcta, por ejemplo git push heroku master:main.

Heroku no reconoce el tipo de proyecto:

remote:  !     No default language could be detected for this app.

No existe package.json en la raíz del repositorio, quizá porque el proyecto está dentro de una subcarpeta. Mueve los archivos a la raíz o configura el buildpack para trabajar desde un subdirectorio.

El remoto de Heroku no está configurado:

fatal: 'heroku' does not appear to be a git repository

Ocurre al clonar el proyecto en otro equipo. Vuelve a vincularlo con heroku git:remote -a generador-nombres-2026.

Ruta comodín incompatible con Express 5:

TypeError: Missing parameter name at index 1: *

Estás usando la sintaxis antigua de comodines. Cambia * por un comodín con nombre, como /{*ruta}.

Heroku en 2026 y sus alternativas

Conviene conocer el contexto antes de elegir plataforma para un proyecto nuevo. En febrero de 2026, Salesforce anunció que Heroku pasaba a un modelo de ingeniería de mantenimiento: la plataforma sigue funcionando y recibiendo parches de seguridad y estabilidad, pero no desarrollará funciones nuevas y dejó de ofrecer contratos empresariales a clientes nuevos. Los clientes que pagan con tarjeta continúan sin cambios por ahora y no se ha anunciado una fecha de cierre.

Para proyectos personales, prototipos o aplicaciones que ya están allí, Heroku sigue siendo perfectamente válido. Para proyectos nuevos con horizonte largo, vale la pena evaluar estas alternativas a Heroku en 2026 antes de decidir:

  • Render: modelo muy parecido, con servicios web, trabajos en segundo plano y bases de datos gestionadas, y despliegue automático desde Git.
  • Railway: experiencia de uso muy rápida, buena para prototipos y proyectos con varios servicios conectados.
  • Fly.io: ejecuta tu aplicación como contenedores en regiones de todo el mundo, útil cuando la latencia geográfica importa.
  • Vercel y Netlify: ideales para sitios estáticos y frameworks frontend con funciones serverless; menos adecuados para un servidor Express de larga duración.
  • Un VPS o una plataforma de contenedores: máximo control y coste predecible a cambio de administrar más piezas tú mismo.

La buena noticia es que una aplicación bien preparada se mueve entre plataformas casi sin cambios. Si lees el puerto de process.env.PORT, guardas la configuración en variables de entorno, no dependes del disco local y fijas la versión de Node en engines, migrar consiste básicamente en conectar el repositorio en otro servicio. Empaquetarla en un contenedor amplía todavía más las opciones:

FROM node:24-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "servidor.js"]

Casos de uso reales

Este patrón de servidor Express con archivos estáticos y una API ligera cubre muchos escenarios prácticos.

Portafolios y páginas personales que necesitan algo más que HTML estático, como un formulario de contacto que envía correos mediante una API externa.

Prototipos y demostraciones para clientes o inversores, donde lo importante es tener una URL funcional en minutos y poder iterar con cada git push.

Backends ligeros para aplicaciones móviles o frontends en otros dominios, que exponen unos pocos endpoints JSON y se conectan a una base de datos gestionada.

Herramientas internas para equipos pequeños, como paneles de consulta o pequeños formularios, protegidas con autenticación básica o un proveedor de identidad.

Proyectos educativos y talleres, donde el objetivo es aprender el ciclo completo de desarrollo, control de versiones y despliegue sin administrar servidores.

Conclusiones

Desplegar un sitio con Express en una plataforma como servicio se reduce a un puñado de pasos bien definidos: un package.json con la versión de Node fijada en engines, un servidor que escucha en el puerto que indica la plataforma, un Procfile que declara el proceso web y un git push que construye y publica la aplicación. Con Express 5 y Node.js 24, el código resultante es moderno, usa módulos ES, la API fetch nativa y un apagado ordenado ante las señales de la plataforma.

Más allá del primer despliegue, las prácticas que marcan la diferencia son las mismas en cualquier proveedor: configuración en variables de entorno, dependencias de ejecución en el lugar correcto, cabeceras de seguridad, HTTPS forzado, registros consultables y ningún dato importante en el disco efímero.

Heroku sigue siendo una forma directa de poner una aplicación en línea, aunque su paso a un modelo de mantenimiento en 2026 aconseja valorar alternativas para proyectos nuevos de largo plazo. Si construyes tu aplicación siguiendo los principios de esta guía, podrás cambiar de plataforma cuando lo necesites sin reescribir una sola línea de lógica.