express.static es el middleware integrado que sirve archivos desde una carpeta. app.use(express.static(path.join(__dirname, 'public'))) hace que cada archivo de public esté disponible en la misma ruta de la URL: public/style.css responde a /style.css, y public/index.html responde a /. Express pone el Content-Type según la extensión del archivo, y cuando ningún archivo coincide pasa la petición a tus rutas en lugar de responder 404.
La respuesta a GET /style.css es el texto del archivo, con Content-Type: text/css; charset=utf-8 en la pestaña Headers. Envía GET / para obtener index.html, y luego GET /api/time: no existe ese archivo, así que responde la ruta.
Por qué la ruta de la carpeta necesita __dirname
express.static('public') resuelve 'public' respecto a la carpeta desde la que lanzaste Node, no respecto a la carpeta en la que está server.js. Funciona cuando ejecutas node server.js dentro del proyecto y se rompe cuando un gestor de procesos o un script lanza la app desde otro sitio:
// project/server.js uses express.static('public')
// cd project && node server.js -> serves project/public
// node project/server.js -> looks for ./public, every file is 404
path.join(__dirname, 'public') es la carpeta que está junto al archivo que contiene esa línea, se lance Node desde donde se lance. En un ES module, usa path.join(import.meta.dirname, 'public') (Node 20.11 y posteriores). La página del módulo path explica __dirname y path.join en detalle.
Servir archivos bajo un prefijo
Pasa primero una ruta de montaje para servir la carpeta bajo un prefijo de URL. El prefijo no forma parte de la ruta del archivo en disco:
GET /assets/app.js devuelve console.log("hi"); como text/javascript, y GET /assets/img/dot.svg llega hasta la subcarpeta. Envía GET /app.js sin el prefijo: da 404, porque los archivos solo existen bajo /assets. Usa un prefijo para que las URLs de los archivos no choquen con las rutas de tu API.
Headers de caché con maxAge
Por defecto Express envía Cache-Control: public, max-age=0, así que los navegadores consultan al servidor cada vez que usan el archivo (y reciben un 304 Not Modified rápido cuando no ha cambiado, gracias al header ETag). Para archivos cuyo nombre cambia en cada build, como app.3f9a1c.js, dile a los navegadores que los guarden:
Abre la pestaña Headers: Cache-Control ahora es public, max-age=604800, siete días en segundos. index: false desactiva que se sirva index.html para las URLs de carpetas. Otras opciones útiles son extensions: ['html'], que permite que /about encuentre about.html, y dotfiles, que por defecto vale 'ignore' para que archivos como .env nunca se sirvan.
Single page apps: volver a index.html
Una app de React o Vue construida con routing en el cliente necesita que cada ruta desconocida devuelva index.html, para que recargar en /settings no dé 404. Pon ese fallback después de express.static y de las rutas de tu API:
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/api', apiRouter);
// Express 5 wildcard syntax
app.get('/*splat', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
Errores comunes
Incluir el nombre de la carpeta en la URL. Con app.use(express.static('public')) la URL es /style.css, no /public/style.css. Si quieres /public en la URL, móntala: app.use('/public', express.static(...)).
Enlazar archivos con rutas relativas en el HTML. <link href="style.css"> en la página /blog/post pide /blog/style.css. Empieza la ruta con una barra: /style.css.
Poner secretos en la carpeta estática. Todo lo que hay en ella es público. Deja fuera los archivos de configuración, las claves y el código fuente.
Preguntas frecuentes
¿Puedo usar express.static más de una vez?
Sí. Registra uno por carpeta; Express busca en ellas en el orden en que las añadiste y sirve el primer archivo que encuentra. Dale a cada una su propia ruta de montaje cuando las carpetas puedan tener archivos con el mismo nombre.
¿Debe Express servir archivos estáticos en producción?
Para apps pequeñas está bien. Con mucho tráfico, pon delante un CDN o un reverse proxy como Nginx, que sirve los archivos más rápido y los cachea más cerca de los usuarios, y deja Express para la API.
¿Cómo envío un archivo concreto desde una ruta?
Usa res.sendFile(path.join(__dirname, 'files', 'report.pdf')). La ruta tiene que ser absoluta (o pasas una opción root). Usa res.download() cuando el navegador deba guardar el archivo en lugar de mostrarlo.