ESTRUCTURA COMPLETA DEL PROYECTO

text
mi-tienda/
├── app.js
├── views/
│   ├── index.ejs
│   ├── productos.ejs
│   └── partials/
│       ├── header.ejs
│       └── footer.ejs
├── public/
│   └── css/
│       └── custom.css
└── package.json

1. app.js (Completo)

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,
        mensaje: 'Bienvenido a nuestra tienda'
    });
});

// Ruta para ver todos los productos
app.get('/productos-vista', (req, res) => {
    res.render('productos', {
        title: 'Lista de Productos',
        productos: productos,
        total: productos.length,
        precioTotal: productos.reduce((sum, p) => sum + p.precio, 0)
    });
});

// Ruta para ver un producto específico
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).render('error', {
            title: 'Error 404',
            mensaje: 'Producto no encontrado'
        });
    }
    
    res.render('detalle-producto', {
        title: `Detalle de ${producto.nombre}`,
        producto: producto
    });
});

// ============ RUTAS API (JSON) ============

// Obtener todos los productos (API)
app.get('/api/productos', (req, res) => {
    res.json({
        success: true,
        data: productos,
        total: productos.length
    });
});

// Obtener un producto por ID (API)
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({
            success: false,
            mensaje: 'Producto no encontrado'
        });
    }
    
    res.json({
        success: true,
        data: producto
    });
});

// Crear nuevo producto (API)
app.post('/api/productos', (req, res) => {
    const { nombre, precio } = req.body;
    
    if (!nombre || !precio) {
        return res.status(400).json({
            success: false,
            mensaje: 'Faltan datos: nombre y precio son requeridos'
        });
    }
    
    const nuevoProducto = {
        id: productos.length + 1,
        nombre: nombre,
        precio: parseFloat(precio)
    };
    
    productos.push(nuevoProducto);
    
    res.json({
        success: true,
        data: nuevoProducto,
        mensaje: 'Producto creado exitosamente'
    });
});

// Actualizar producto (API)
app.put('/api/productos/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const { nombre, precio } = req.body;
    const index = productos.findIndex(p => p.id === id);
    
    if (index === -1) {
        return res.status(404).json({
            success: false,
            mensaje: 'Producto no encontrado'
        });
    }
    
    if (nombre) productos[index].nombre = nombre;
    if (precio) productos[index].precio = parseFloat(precio);
    
    res.json({
        success: true,
        data: productos[index],
        mensaje: 'Producto actualizado exitosamente'
    });
});

// Eliminar producto (API)
app.delete('/api/productos/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const index = productos.findIndex(p => p.id === id);
    
    if (index === -1) {
        return res.status(404).json({
            success: false,
            mensaje: 'Producto no encontrado'
        });
    }
    
    const productoEliminado = productos.splice(index, 1);
    
    res.json({
        success: true,
        data: productoEliminado[0],
        mensaje: 'Producto eliminado exitosamente'
    });
});

// ============ 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(`
            <h1>Error 400</h1>
            <p>Faltan datos: nombre y precio son requeridos</p>
            <a href="/">Volver al inicio</a>
        `);
    }
    
    const nuevoProducto = {
        id: productos.length + 1,
        nombre: nombre,
        precio: parseFloat(precio)
    };
    
    productos.push(nuevoProducto);
    res.redirect('/');  // Redirige a la vista con EJS
});

// Formulario para editar producto
app.get('/editar-producto/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const producto = productos.find(p => p.id === id);
    
    if (!producto) {
        return res.status(404).render('error', {
            title: 'Error 404',
            mensaje: 'Producto no encontrado'
        });
    }
    
    res.render('editar-producto', {
        title: 'Editar Producto',
        producto: producto
    });
});

// Actualizar producto desde formulario
app.post('/actualizar-producto/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const { nombre, precio } = req.body;
    const index = productos.findIndex(p => p.id === id);
    
    if (index === -1) {
        return res.status(404).send('Producto no encontrado');
    }
    
    if (nombre) productos[index].nombre = nombre;
    if (precio) productos[index].precio = parseFloat(precio);
    
    res.redirect('/productos-vista');
});

// Eliminar producto desde formulario
app.post('/eliminar-producto/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const index = productos.findIndex(p => p.id === id);
    
    if (index !== -1) {
        productos.splice(index, 1);
    }
    
    res.redirect('/productos-vista');
});

// ============ 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)`);
});

2. views/index.ejs

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">
    <link rel="stylesheet" href="/css/custom.css">
</head>
<body>
    <div class="container mt-5">
        <h1 class="text-center mb-4"><%= title %></h1>
        
        <% if (mensaje) { %>
            <div class="alert alert-info text-center">
                <%= mensaje %>
            </div>
        <% } %>
        
        <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">
                        <div class="table-responsive">
                            <table class="table table-striped table-hover">
                                <thead>
                                    <tr>
                                        <th>ID</th>
                                        <th>Nombre</th>
                                        <th>Precio</th>
                                        <th>Acciones</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>
                                                <a href="/editar-producto/<%= producto.id %>" class="btn btn-warning btn-sm">Editar</a>
                                            </td>
                                        </tr>
                                    <% }) %>
                                    
                                    <% if (productos.length === 0) { %>
                                        <tr>
                                            <td colspan="4" class="text-center">No hay productos disponibles</td>
                                        </tr>
                                    <% } %>
                                </tbody>
                            </table>
                        </div>
                        
                        <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>

3. views/productos.ejs

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>Resumen:</strong> Total de productos: <%= total %> | 
                    Precio total: $<%= precioTotal.toFixed(2) %>
                </div>
                
                <table class="table table-bordered table-striped">
                    <thead class="table-dark">
                        <tr>
                            <th>ID</th>
                            <th>Nombre</th>
                            <th>Precio (USD)</th>
                            <th>Acciones</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">Detalle</a>
                                    <a href="/editar-producto/<%= producto.id %>" class="btn btn-sm btn-warning">Editar</a>
                                    <form action="/eliminar-producto/<%= producto.id %>" method="POST" style="display: inline;">
                                        <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('¿Estás seguro?')">Eliminar</button>
                                    </form>
                                </td>
                            </tr>
                        <% }) %>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    
    <script src="/js/bootstrap.bundle.min.js"></script>
</body>
</html>

4. views/detalle-producto.ejs

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="/editar-producto/<%= producto.id %>" class="btn btn-warning">Editar</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

5. views/editar-producto.ejs

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-warning">
                <h2><%= title %></h2>
            </div>
            <div class="card-body">
                <form action="/actualizar-producto/<%= producto.id %>" method="POST">
                    <div class="mb-3">
                        <label class="form-label">ID del Producto:</label>
                        <input type="text" class="form-control" value="<%= producto.id %>" disabled>
                    </div>
                    <div class="mb-3">
                        <label for="nombre" class="form-label">Nombre del producto:</label>
                        <input type="text" class="form-control" id="nombre" name="nombre" value="<%= producto.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" value="<%= producto.precio %>" required>
                    </div>
                    <button type="submit" class="btn btn-primary">Actualizar Producto</button>
                    <a href="/" class="btn btn-secondary">Cancelar</a>
                </form>
            </div>
        </div>
    </div>
</body>
</html>

6. views/error.ejs

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="alert alert-danger text-center">
            <h1><%= title %></h1>
            <p><%= mensaje %></p>
            <a href="/" class="btn btn-primary">Volver al inicio</a>
        </div>
    </div>
</body>
</html>

7. public/css/custom.css

css
body {
    background-color: #f5f5f5;
}

.card {
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
    margin-bottom: 20px;
}

.table-responsive {
    margin-top: 20px;
}

.btn {
    margin: 2px;
}

h1, h2, h3, h4 {
    margin-bottom: 20px;
}

.alert {
    border-radius: 10px;
}

8. package.json

json
{
  "name": "mi-tienda",
  "version": "1.0.0",
  "description": "Aplicación de tienda con Express y EJS",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "ejs": "^3.1.9",
    "bootstrap": "^5.3.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

EXPLICACIÓN DEL EJERCICIO

¿Qué hace esta aplicación?

Es una API RESTful + aplicación web que permite gestionar productos (CRUD completo: Crear, Leer, Actualizar, Eliminar).

Estructura y funcionalidades:

1. Middlewares configurados:

  • express.json() - Procesa JSON en las peticiones API

  • express.urlencoded() - Procesa formularios HTML

  • express.static() - Sirve archivos estáticos (CSS, JS, imágenes)

2. Dos formas de interactuar:

A) Vistas con EJS (HTML dinámico):

  • GET / - Página principal con formulario y lista de productos

  • GET /productos-vista - Lista completa de productos

  • GET /producto/:id - Detalle de un producto

  • GET /editar-producto/:id - Formulario para editar

  • POST /productos-formulario - Crear producto (formulario)

  • POST /actualizar-producto/:id - Actualizar producto

  • POST /eliminar-producto/:id - Eliminar producto

B) API REST (JSON):

  • GET /api/productos - Obtener todos

  • GET /api/productos/:id - Obtener uno

  • POST /api/productos - Crear producto

  • PUT /api/productos/:id - Actualizar completo

  • DELETE /api/productos/:id - Eliminar

3. Flujo de trabajo:

  1. El usuario accede a http://localhost:3000/

  2. Ve un formulario y la lista de productos

  3. Puede:

    • Agregar productos (formulario)

    • Ver detalle de cada producto

    • Editar productos (formulario pre-llenado)

    • Eliminar productos (con confirmación)

4. Tecnologías utilizadas:

  • Express - Framework web

  • EJS - Motor de plantillas (HTML dinámico)

  • Bootstrap - Estilos CSS

  • JavaScript - Lógica del servidor

Instalación y ejecución:

bash
# 1. Crear carpeta del proyecto
mkdir mi-tienda
cd mi-tienda

# 2. Inicializar npm
npm init -y

# 3. Instalar dependencias
npm install express ejs bootstrap

# 4. Instalar nodemon para desarrollo
npm install -D nodemon

# 5. Crear la estructura de carpetas
mkdir views public public/css

# 6. Copiar todos los archivos (app.js, vistas, CSS)

# 7. Ejecutar
npm run dev  # Modo desarrollo con auto-reinicio
# o
npm start    # Modo producción

Características clave del ejercicio:

CRUD completo con dos interfaces (web y API)
Manejo de errores (404, 400)
Validación de datos
Redirecciones después de operaciones
Estilos responsivos con Bootstrap
Código modular y comentado
Buenas prácticas de Express

Este ejercicio demuestra perfectamente cómo crear una aplicación web completa con Express, EJS y una API REST, permitiendo aprender tanto el desarrollo frontend como backend.

Comentarios

Entradas más populares de este blog

Cómo Iniciar un Proyecto Node.js

8-Template Engines

6-Middleware?