7-1-express.static
Servir Archivos Estáticos en Express
1. Estructura de proyecto
mi-app/
├── public/ # Carpeta de archivos estáticos
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── app.js
│ └── images/
│ └── logo.png
├── app.js
└── package.json
🔍 ¿Qué es express.static?
Es un middleware incorporado en Express que sirve para entregar archivos estáticos (HTML, CSS, JS, imágenes) desde un directorio específico en tu servidor.
app.use(express.static('ruta/al/directorio'));
🛠️ Configuración Básica
1. Estructura de proyecto típica
mi-app/
├── public/ # Carpeta de archivos estáticos
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── app.js
│ └── images/
│ └── logo.png
├── app.js
└── package.json2. Implementación mínima
app.use() es un método que monta middlewares (funciones intermedias) que se ejecutan antes de que lleguen a tus rutasRegla de oro:
app.use()→ Se ejecuta para TODOS los métodos HTTP (GET, POST, PUT, DELETE)app.get()→ Solo para peticiones GETapp.post()→ Solo para peticiones POST
Explicación simple:
// SIN app.use() - La petición va directo a la ruta app.get('/productos', (req, res) => { res.send('Productos'); }); // CON app.use() - La petición pasa PRIMERO por el middleware app.use(express.json()); // ← Middleware global app.get('/productos', (req, res) => { // req.body YA fue procesado por express.json() res.send('Productos'); });
const express = require('express');
const app = express();
// Servir archivos estáticos desde la carpeta 'public'
app.use(express.static('public'));
app.listen(3000, () => {
console.log('Servidor en http://localhost:3000');
});3. Cómo acceder a los archivos
| Archivo | URL de acceso |
|---|---|
| public/css/style.css | http://localhost:3000/css/style.css |
| public/images/logo.png | http://localhost:3000/images/logo.png |
💡 Características Clave
1. Prefijos virtuales
app.use() es un método que monta middlewares (funciones intermedias) que se ejecutan antes de que lleguen a tus rutasRegla de oro:
app.use()→ Se ejecuta para TODOS los métodos HTTP (GET, POST, PUT, DELETE)app.get()→ Solo para peticiones GETapp.post()→ Solo para peticiones POST
Explicación simple:
// SIN app.use() - La petición va directo a la ruta app.get('/productos', (req, res) => { res.send('Productos'); }); // CON app.use() - La petición pasa PRIMERO por el middleware app.use(express.json()); // ← Middleware global app.get('/productos', (req, res) => { // req.body YA fue procesado por express.json() res.send('Productos'); });
Puedes agregar una ruta virtual diferente a la ruta física:
app.use('/static', express.static('public'));Ahora los archivos se acceden mediante:
http://localhost:3000/static/css/style.css2. Múltiples directorios estáticos
app.use(express.static('public'));
app.use(express.static('uploads')); // Segundo directorio3. Opciones de configuración
app.use(express.static('public', {
index: 'home.html', // Cambia el archivo índice
extensions: ['html'], // Extensiones a buscar
setHeaders: (res, path) => { // Headers personalizados
if (path.endsWith('.css')) {
res.set('Content-Type', 'text/css');
}
}
}));🚀 Casos de Uso Comunes
1. Aplicación web tradicional
app.use() es un método que monta middlewares (funciones intermedias) que se ejecutan antes de que lleguen a tus rutasRegla de oro:
app.use()→ Se ejecuta para TODOS los métodos HTTP (GET, POST, PUT, DELETE)app.get()→ Solo para peticiones GETapp.post()→ Solo para peticiones POST
Explicación simple:
// SIN app.use() - La petición va directo a la ruta app.get('/productos', (req, res) => { res.send('Productos'); }); // CON app.use() - La petición pasa PRIMERO por el middleware app.use(express.json()); // ← Middleware global app.get('/productos', (req, res) => { // req.body YA fue procesado por express.json() res.send('Productos'); });
app.use(express.static('public'));
app.use(express.static('node_modules/bootstrap/dist')); // Servir Bootstrap2. SPA (Single Page Application)
// Servir el frontend (React/Vue/Angular build)
app.use(express.static('client/dist'));
// Redirigir todas las rutas al index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'client/dist', 'index.html'));
});3. Archivos de descarga
app.use('/descargas', express.static('archivos-secretos'));Acceso: http://localhost:3000/descargas/informe.pdf
⚠️ Consideraciones de Seguridad
No uses esto para contenido sensible:
// ¡PELIGROSO! app.use(express.static('/')); // Expone todo el sistema de archivosProtege directorios con middleware:
const auth = (req, res, next) => { if (req.query.key === 'mi-clave') next(); else res.status(403).send('Acceso denegado'); }; app.use('/privado', auth, express.static('archivos-privados'));
🔥 Trucos Avanzados
1. Cache-Control para producción
app.use(express.static('public', {
maxAge: '1d' // Cache de 1 día
}));2. Compresión con gzip
const compression = require('compression');
app.use(compression());
app.use(express.static('public'));3. Servir desde múltiples proyectos
// Frontend y backend en el mismo servidor
app.use('/app', express.static('frontend/dist'));
app.use('/api', apiRouter); // Tus rutas API📌 Mejores Prácticas
Organiza bien tus archivos: Separa assets por tipo (CSS, JS, imágenes).
Usa subdirectorios lógicos:
/public/assets/cssen lugar de/public/css.Configura cache en producción: Mejora el rendimiento.
Combina con un CDN para archivos estáticos en producción.
💻 Ejemplo Completo
const express = require('express');
const path = require('path');
const app = express();
// Configuración de archivos estáticos
app.use('/static', express.static(path.join(__dirname, 'public'), {
index: false,
extensions: ['html', 'css', 'js'],
setHeaders: (res, filePath) => {
if (filePath.endsWith('.js')) {
res.set('Content-Type', 'application/javascript');
}
}
}));
// Ruta principal que usa archivos estáticos
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.listen(3000, () => {
console.log('Servidor listo en http://localhost:3000');
});🔗 Recursos Adicionales
manteniendo solo las rutas esenciales y eliminando las rutas POST de actualizar y eliminar.
app.js (Versión Sencilla)
const express = require('express');
const app = express();
const PORT = 3000;
// ============ CONFIGURACIÓN EJS ============
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views');
// ============ MIDDLEWARE ============
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use('/css', express.static('node_modules/bootstrap/dist/css'));
app.use('/js', express.static('node_modules/bootstrap/dist/js'));
// ============ DATOS ============
let productos = [
{ id: 1, nombre: 'Laptop', precio: 1000 },
{ id: 2, nombre: 'Mouse', precio: 25 },
{ id: 3, nombre: 'Teclado', precio: 50 }
];
// ============ RUTAS CON EJS ============
// Ruta principal - Home
app.get('/', (req, res) => {
res.render('index', {
title: 'Mi Tienda',
productos: productos
});
});
// Ruta para ver todos los productos
app.get('/productos-vista', (req, res) => {
res.render('productos', {
title: 'Lista de Productos',
productos: productos,
total: productos.length
});
});
// Ruta para ver detalle de un producto
app.get('/producto/:id', (req, res) => {
const id = parseInt(req.params.id);
const producto = productos.find(p => p.id === id);
if (!producto) {
return res.status(404).send('Producto no encontrado');
}
res.render('detalle-producto', {
title: 'Detalle del Producto',
producto: producto
});
});
// ============ RUTAS API (JSON) ============
// Obtener todos los productos
app.get('/api/productos', (req, res) => {
res.json(productos);
});
// Obtener un producto por ID
app.get('/api/productos/:id', (req, res) => {
const id = parseInt(req.params.id);
const producto = productos.find(p => p.id === id);
if (!producto) {
return res.status(404).json({ error: 'Producto no encontrado' });
}
res.json(producto);
});
// Crear nuevo producto (API)
app.post('/api/productos', (req, res) => {
const { nombre, precio } = req.body;
if (!nombre || !precio) {
return res.status(400).json({ error: 'Faltan datos' });
}
const nuevoProducto = {
id: productos.length + 1,
nombre: nombre,
precio: parseFloat(precio)
};
productos.push(nuevoProducto);
res.status(201).json(nuevoProducto);
});
// ============ RUTAS CON FORMULARIOS ============
// Crear producto desde formulario
app.post('/productos-formulario', (req, res) => {
const { nombre, precio } = req.body;
if (!nombre || !precio) {
return res.status(400).send('Faltan datos: nombre y precio son requeridos');
}
const nuevoProducto = {
id: productos.length + 1,
nombre: nombre,
precio: parseFloat(precio)
};
productos.push(nuevoProducto);
res.redirect('/'); // Redirige a la vista principal
});
// ============ INICIAR SERVIDOR ============
app.listen(PORT, () => {
console.log(`🚀 Servidor en http://localhost:${PORT}`);
console.log(`📋 Vistas disponibles:`);
console.log(` - http://localhost:${PORT}/`);
console.log(` - http://localhost:${PORT}/productos-vista`);
console.log(` - http://localhost:${PORT}/api/productos (API)`);
});views/index.ejs (Versión Sencilla)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
<h1 class="text-center mb-4"><%= title %></h1>
<div class="row">
<!-- Formulario para agregar producto -->
<div class="col-md-5">
<div class="card">
<div class="card-header bg-primary text-white">
<h4>Agregar Nuevo Producto</h4>
</div>
<div class="card-body">
<form action="/productos-formulario" method="POST">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre del producto:</label>
<input type="text" class="form-control" id="nombre" name="nombre" required>
</div>
<div class="mb-3">
<label for="precio" class="form-label">Precio (USD):</label>
<input type="number" class="form-control" id="precio" name="precio" step="0.01" required>
</div>
<button type="submit" class="btn btn-primary w-100">Agregar Producto</button>
</form>
</div>
</div>
</div>
<!-- Lista de productos -->
<div class="col-md-7">
<div class="card">
<div class="card-header bg-success text-white">
<h4>Productos Disponibles</h4>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Precio</th>
<th>Acción</th>
</tr>
</thead>
<tbody>
<% productos.forEach(producto => { %>
<tr>
<td><%= producto.id %></td>
<td><%= producto.nombre %></td>
<td>$<%= producto.precio.toFixed(2) %></td>
<td>
<a href="/producto/<%= producto.id %>" class="btn btn-info btn-sm">Ver</a>
</td>
</tr>
<% }) %>
<% if (productos.length === 0) { %>
<tr>
<td colspan="4" class="text-center">No hay productos disponibles</td>
</tr>
<% } %>
</tbody>
</table>
<div class="mt-3 text-end">
<a href="/productos-vista" class="btn btn-info">Ver todos los productos</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/bootstrap.bundle.min.js"></script>
</body>
</html>views/productos.ejs (Versión Sencilla)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
<div class="card">
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
<h2><%= title %></h2>
<a href="/" class="btn btn-light">Volver al inicio</a>
</div>
<div class="card-body">
<div class="alert alert-secondary">
<strong>Total de productos:</strong> <%= total %>
</div>
<table class="table table-bordered table-striped">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Precio (USD)</th>
<th>Detalle</th>
</tr>
</thead>
<tbody>
<% productos.forEach(producto => { %>
<tr>
<td><%= producto.id %></td>
<td><%= producto.nombre %></td>
<td>$<%= producto.precio.toFixed(2) %></td>
<td>
<a href="/producto/<%= producto.id %>" class="btn btn-sm btn-info">Ver detalle</a>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
<script src="/js/bootstrap.bundle.min.js"></script>
</body>
</html>views/detalle-producto.ejs (Versión Sencilla)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
<div class="card">
<div class="card-header bg-primary text-white">
<h2><%= title %></h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-body text-center">
<h3><%= producto.nombre %></h3>
<p class="display-4 text-success">$<%= producto.precio.toFixed(2) %></p>
<p><strong>ID:</strong> <%= producto.id %></p>
<hr>
<a href="/" class="btn btn-secondary">Volver al inicio</a>
<a href="/productos-vista" class="btn btn-info">Ver todos los productos</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>EXPLICACIÓN DE LA VERSIÓN SENCILLA
¿Qué hace esta versión?
Es una aplicación más simple que permite:
✅ Ver productos en la página principal
✅ Agregar nuevos productos (desde formulario)
✅ Ver detalle de cada producto
✅ Consultar productos via API (JSON)
¿Qué NO incluye?
❌ Editar productos
❌ Eliminar productos
❌ Rutas complejas de actualización
Rutas disponibles:
| Método | Ruta | Descripción |
|---|---|---|
| GET | / | Página principal con formulario y lista |
| GET | /productos-vista | Lista completa de productos |
| GET | /producto/:id | Detalle de un producto |
| GET | /api/productos | API: Obtener todos los productos |
| GET | /api/productos/:id | API: Obtener un producto |
| POST | /api/productos | API: Crear producto (JSON) |
| POST | /productos-formulario | Web: Crear producto (formulario) |
Flujo de trabajo:
Inicio (
/) - Muestra formulario + lista de productosAgregar producto - Completar formulario y enviar
Ver detalle - Click en "Ver" para ver información completa
Ver todos - Click en "Ver todos los productos"
Ventajas de esta versión:
🎯 Más fácil de entender (menos código)
🚀 Ideal para aprender los conceptos básicos
📚 Perfecta para principiantes
🔧 Menos complejidad en las rutas
Instalación:
# 1. Crear carpeta
mkdir mi-tienda-sencilla
cd mi-tienda-sencilla
# 2. Inicializar npm
npm init -y
# 3. Instalar dependencias
npm install express ejs bootstrap
# 4. Crear estructura de carpetas
mkdir views public
# 5. Crear los archivos:
# - app.js (copiar el código)
# - views/index.ejs
# - views/productos.ejs
# - views/detalle-producto.ejs
# 6. Ejecutar
node app.jsEjemplo de uso de la API:
# Obtener todos los productos
curl http://localhost:3000/api/productos
# Obtener un producto específico
curl http://localhost:3000/api/productos/1
# Crear un producto via API
curl -X POST http://localhost:3000/api/productos \
-H "Content-Type: application/json" \
-d '{"nombre":"Tablet","precio":300}'
Comentarios
Publicar un comentario