7-1-express.static

 

 Servir Archivos Estáticos en Express 

   5- EXPRESS
           |
          V
           |
          V
7-express.static

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.

javascript
Copy
Download
app.use(express.static('ruta/al/directorio'));

Cómo funciona express.static


🛠️ Configuración Básica

1. Estructura de proyecto típica

Copy
Download
mi-app/
├── public/          # Carpeta de archivos estáticos
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── app.js
│   └── images/
│       └── logo.png
├── app.js
└── package.json

2. Implementación mínima

app.use() es un método que monta middlewares (funciones intermedias) que se ejecutan antes de que lleguen a tus rutas

Regla de oro:

  • app.use() → Se ejecuta para TODOS los métodos HTTP (GET, POST, PUT, DELETE)

  • app.get() → Solo para peticiones GET

  • app.post() → Solo para peticiones POST

Explicación simple:

javascript
// 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');
});

javascript
Copy
Download
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

ArchivoURL de acceso
public/css/style.csshttp://localhost:3000/css/style.css
public/images/logo.pnghttp://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 rutas

Regla de oro:

  • app.use() → Se ejecuta para TODOS los métodos HTTP (GET, POST, PUT, DELETE)

  • app.get() → Solo para peticiones GET

  • app.post() → Solo para peticiones POST

Explicación simple:

javascript
// 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:

javascript
Copy
Download
app.use('/static', express.static('public'));

Ahora los archivos se acceden mediante:

Copy
Download
http://localhost:3000/static/css/style.css

2. Múltiples directorios estáticos

javascript
Copy
Download
app.use(express.static('public'));
app.use(express.static('uploads')); // Segundo directorio

3. Opciones de configuración

javascript
Copy
Download
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 rutas

Regla de oro:

  • app.use() → Se ejecuta para TODOS los métodos HTTP (GET, POST, PUT, DELETE)

  • app.get() → Solo para peticiones GET

  • app.post() → Solo para peticiones POST

 

Explicación simple:

javascript
// 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');
});

javascript
Copy
Download
app.use(express.static('public'));
app.use(express.static('node_modules/bootstrap/dist')); // Servir Bootstrap

2. SPA (Single Page Application)

javascript
Copy
Download
// 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

javascript
Copy
Download
app.use('/descargas', express.static('archivos-secretos'));

Acceso: http://localhost:3000/descargas/informe.pdf


⚠️ Consideraciones de Seguridad

  1. No uses esto para contenido sensible:

    javascript
    Copy
    Download
    // ¡PELIGROSO!
    app.use(express.static('/')); // Expone todo el sistema de archivos
  2. Protege directorios con middleware:

    javascript
    Copy
    Download
    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

javascript
Copy
Download
app.use(express.static('public', {
  maxAge: '1d' // Cache de 1 día
}));

2. Compresión con gzip

javascript
Copy
Download
const compression = require('compression');
app.use(compression());
app.use(express.static('public'));

3. Servir desde múltiples proyectos

javascript
Copy
Download
// Frontend y backend en el mismo servidor
app.use('/app', express.static('frontend/dist'));
app.use('/api', apiRouter); // Tus rutas API

📌 Mejores Prácticas

  1. Organiza bien tus archivos: Separa assets por tipo (CSS, JS, imágenes).

  2. Usa subdirectorios lógicos/public/assets/css en lugar de /public/css.

  3. Configura cache en producción: Mejora el rendimiento.

  4. Combina con un CDN para archivos estáticos en producción.


💻 Ejemplo Completo

javascript
Copy
Download
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)

javascript
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)

ejs
<!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)

ejs
<!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)

ejs
<!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étodoRutaDescripción
GET/Página principal con formulario y lista
GET/productos-vistaLista completa de productos
GET/producto/:idDetalle de un producto
GET/api/productosAPI: Obtener todos los productos
GET/api/productos/:idAPI: Obtener un producto
POST/api/productosAPI: Crear producto (JSON)
POST/productos-formularioWeb: Crear producto (formulario)

Flujo de trabajo:

  1. Inicio (/) - Muestra formulario + lista de productos

  2. Agregar producto - Completar formulario y enviar

  3. Ver detalle - Click en "Ver" para ver información completa

  4. 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:

bash
# 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.js

Ejemplo de uso de la API:

bash
# 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

Entradas más populares de este blog

Cómo Iniciar un Proyecto Node.js

5-Express--Curso de Node.js#05 Introducción a Express.js

8-Template Engines