ESTRUCTURA COMPLETA DEL PROYECTO
mi-tienda/ ├── app.js ├── views/ │ ├── index.ejs │ ├── productos.ejs │ └── partials/ │ ├── header.ejs │ └── footer.ejs ├── public/ │ └── css/ │ └── custom.css └── package.json
1. app.js (Completo)
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
<!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
<!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
<!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
<!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
<!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
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
{ "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 APIexpress.urlencoded()- Procesa formularios HTMLexpress.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 productosGET /productos-vista- Lista completa de productosGET /producto/:id- Detalle de un productoGET /editar-producto/:id- Formulario para editarPOST /productos-formulario- Crear producto (formulario)POST /actualizar-producto/:id- Actualizar productoPOST /eliminar-producto/:id- Eliminar producto
B) API REST (JSON):
GET /api/productos- Obtener todosGET /api/productos/:id- Obtener unoPOST /api/productos- Crear productoPUT /api/productos/:id- Actualizar completoDELETE /api/productos/:id- Eliminar
3. Flujo de trabajo:
El usuario accede a
http://localhost:3000/Ve un formulario y la lista de productos
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:
# 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
Publicar un comentario