diff --git a/docs/Material/Apuntes/apunte_backend.md b/docs/Material/Apuntes/apunte_backend.md new file mode 100644 index 00000000..f43551bd --- /dev/null +++ b/docs/Material/Apuntes/apunte_backend.md @@ -0,0 +1,684 @@ +--- +sidebar_position: 11 +--- + +# Backend con Node.js y Express + +Este apunte cubre cómo crear un servidor backend con Node.js y Express: desde el primer "Hola mundo" hasta una API completa con base de datos SQLite. + +## Introducción + +### ¿Qué es un backend? + +El **backend** es la parte de una aplicación que corre en el servidor. Se encarga de: +- Recibir peticiones HTTP del frontend (como vimos en el apunte de APIs REST). +- Procesar la lógica de negocio (validaciones, cálculos). +- Comunicarse con la base de datos (como vimos en el apunte de SQL). +- Devolver respuestas al frontend. + +``` + Navegador Servidor Base de Datos + (Frontend) (Backend) + ┌──────────┐ HTTP/JSON ┌──────────┐ SQL ┌──────────┐ + │ HTML │ ────────────> │ Node.js │ ───────────> │ SQLite │ + │ CSS │ <──────────── │ Express │ <─────────── │ │ + │ JS │ └──────────┘ └──────────┘ + └──────────┘ +``` + +--- + +## Node.js + +**Node.js** permite ejecutar JavaScript fuera del navegador. Mientras que en el apunte de JavaScript vimos JS corriendo en el navegador, Node.js lo ejecuta en el servidor. + +### npm y package.json + +**npm** (Node Package Manager) es el gestor de paquetes de Node.js. Permite instalar librerías de terceros. + +```bash +# Crear un proyecto nuevo +mkdir mi-api +cd mi-api +npm init -y +``` + +Esto crea un archivo `package.json` que describe tu proyecto y sus dependencias: + +```json +{ + "name": "mi-api", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js" + } +} +``` + +### Instalar dependencias + +```bash +npm install express +``` + +Esto descarga la librería en la carpeta `node_modules/` y la agrega a `package.json`. + +**Importante:** `node_modules/` nunca se sube al repositorio. Siempre incluila en el `.gitignore`: + +``` +node_modules/ +``` + +Cuando alguien clone tu proyecto, ejecuta `npm install` y npm descarga todas las dependencias automáticamente a partir del `package.json`. + +--- + +## Express + +**Express** es el framework más popular de Node.js para crear servidores HTTP. Simplifica enormemente el manejo de rutas, peticiones y respuestas. + +```bash +npm install express +``` + +--- + +## Primer servidor + +```javascript +// index.js +const express = require('express'); +const app = express(); + +app.get('/', (req, res) => { + res.json({ mensaje: 'Hola mundo desde Express!' }); +}); + +const PORT = 3000; +app.listen(PORT, () => { + console.log(`Servidor corriendo en http://localhost:${PORT}`); +}); +``` + +Ejecutalo: + +```bash +node index.js +``` + +Abrí `http://localhost:3000` en el navegador y vas a ver el JSON. También podés probarlo con curl (como vimos en el apunte de Bash): + +```bash +curl http://localhost:3000 +``` + +### ¿Qué hace cada parte? + +- `require('express')`: importa la librería Express. +- `express()`: crea una aplicación Express. +- `app.get('/', ...)`: define qué pasa cuando alguien hace un GET a `/`. +- `req`: el objeto con la información de la petición. +- `res`: el objeto para enviar la respuesta. +- `res.json(...)`: envía una respuesta en formato JSON. +- `app.listen(PORT, ...)`: inicia el servidor en el puerto indicado. + +--- + +## Rutas y métodos + +Las rutas definen qué hace tu servidor ante cada petición. Usando los métodos HTTP que vimos en el apunte de APIs REST: + +```javascript +// GET: obtener datos +app.get('/api/tareas', (req, res) => { + res.json([{ id: 1, titulo: 'Estudiar' }]); +}); + +// POST: crear un recurso +app.post('/api/tareas', (req, res) => { + const { titulo } = req.body; + res.status(201).json({ id: 2, titulo: titulo }); +}); + +// PUT: actualizar un recurso +app.put('/api/tareas/:id', (req, res) => { + const { id } = req.params; + const { titulo } = req.body; + res.json({ id: id, titulo: titulo }); +}); + +// DELETE: eliminar un recurso +app.delete('/api/tareas/:id', (req, res) => { + const { id } = req.params; + res.json({ mensaje: `Tarea ${id} eliminada` }); +}); +``` + +### Parámetros de ruta + +Los `:id` en la URL son **parámetros de ruta**. Se acceden con `req.params`: + +```javascript +app.get('/api/tareas/:id', (req, res) => { + const { id } = req.params; // si la URL es /api/tareas/5, id = "5" + res.json({ id: id }); +}); +``` + +### Parámetros de query + +Los parámetros de query (`?clave=valor`) se acceden con `req.query`: + +```javascript +// GET /api/tareas?completada=true&orden=fecha +app.get('/api/tareas', (req, res) => { + const { completada, orden } = req.query; + console.log(completada); // "true" + console.log(orden); // "fecha" +}); +``` + +### Body de la petición + +Para leer el body de peticiones POST/PUT, necesitás el middleware `express.json()`: + +```javascript +app.use(express.json()); // ANTES de las rutas + +app.post('/api/tareas', (req, res) => { + const { titulo, descripcion } = req.body; + // titulo y descripcion vienen del JSON que envió el cliente +}); +``` + +--- + +## Middleware + +Un **middleware** es una función que se ejecuta **entre** que llega la petición y se envía la respuesta. Se usa para tareas comunes como parsear JSON, loggear peticiones, manejar autenticación, etc. + +```javascript +// Middleware que loggea cada petición +function logger(req, res, next) { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); + next(); // IMPORTANTE: llamar a next() para pasar al siguiente middleware/ruta +} + +app.use(express.json()); // parsea el body JSON +app.use(logger); // loggea cada petición +``` + +### Middlewares comunes de Express + +```javascript +app.use(express.json()); // parsear JSON en el body +app.use(express.static('public')); // servir archivos estáticos (HTML, CSS, JS) +``` + +`express.static('public')` sirve todos los archivos de la carpeta `public/` automáticamente. Si ponés un `index.html` ahí, se sirve cuando alguien accede a `/`. + +### El orden importa + +Los middlewares se ejecutan en el orden en que los registrás. Por eso, ponelos **antes** de las rutas: + +```javascript +// 1. Primero los middlewares +app.use(express.json()); +app.use(logger); + +// 2. Después las rutas +app.get('/api/tareas', (req, res) => { /* ... */ }); +``` + +--- + +## Estructura de un proyecto + +Cuando tu proyecto crece, organizá el código en carpetas: + +``` +mi-api/ +├── src/ +│ ├── routes/ +│ │ └── tareas.routes.js +│ ├── controllers/ +│ │ └── tareas.controller.js +│ ├── database/ +│ │ └── connection.js +│ └── middleware/ +│ └── logger.js +├── .env +├── .gitignore +├── index.js +└── package.json +``` + +### Separando las rutas + +```javascript +// src/routes/tareas.routes.js +const express = require('express'); +const router = express.Router(); +const tareasController = require('../controllers/tareas.controller'); + +router.get('/', tareasController.obtenerTodas); +router.get('/:id', tareasController.obtenerPorId); +router.post('/', tareasController.crear); +router.put('/:id', tareasController.actualizar); +router.delete('/:id', tareasController.eliminar); + +module.exports = router; +``` + +### Separando los controladores + +```javascript +// src/controllers/tareas.controller.js + +function obtenerTodas(req, res) { + res.json({ mensaje: 'Listado de tareas' }); +} + +function obtenerPorId(req, res) { + const { id } = req.params; + res.json({ mensaje: `Tarea ${id}` }); +} + +function crear(req, res) { + const { titulo } = req.body; + res.status(201).json({ mensaje: 'Tarea creada' }); +} + +function actualizar(req, res) { + const { id } = req.params; + res.json({ mensaje: `Tarea ${id} actualizada` }); +} + +function eliminar(req, res) { + const { id } = req.params; + res.json({ mensaje: `Tarea ${id} eliminada` }); +} + +module.exports = { obtenerTodas, obtenerPorId, crear, actualizar, eliminar }; +``` + +### Conectando todo en index.js + +```javascript +// index.js +const express = require('express'); +const app = express(); +const tareasRoutes = require('./src/routes/tareas.routes'); + +app.use(express.json()); +app.use('/api/tareas', tareasRoutes); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Servidor corriendo en http://localhost:${PORT}`); +}); +``` + +Con `app.use('/api/tareas', tareasRoutes)` todas las rutas del router se montan bajo el prefijo `/api/tareas`. + +--- + +## Conectando a una base de datos SQL + +Como vimos en el apunte de SQL, las bases de datos permiten almacenar datos de forma estructurada. Vamos a usar **SQLite** — una base de datos liviana que guarda todo en un solo archivo, sin necesitar un servidor aparte. + +### Instalación + +```bash +npm install better-sqlite3 +``` + +### Creando la conexión + +```javascript +// src/database/connection.js +const Database = require('better-sqlite3'); +const path = require('path'); + +const dbPath = path.join(__dirname, 'tareas.db'); +const db = new Database(dbPath); + +db.pragma('journal_mode = WAL'); + +db.exec(` + CREATE TABLE IF NOT EXISTS tareas ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + titulo TEXT NOT NULL, + descripcion TEXT, + completada INTEGER DEFAULT 0, + fecha_creacion TEXT DEFAULT (datetime('now')) + ) +`); + +module.exports = db; +``` + +### Ejecutando consultas + +Con `better-sqlite3` usás **prepared statements**: + +```javascript +const db = require('./connection'); + +// SELECT todos +const tareas = db.prepare('SELECT * FROM tareas').all(); + +// SELECT uno por ID +const tarea = db.prepare('SELECT * FROM tareas WHERE id = ?').get(42); + +// INSERT +const resultado = db.prepare( + 'INSERT INTO tareas (titulo, descripcion) VALUES (?, ?)' +).run('Estudiar Node.js', 'Leer el apunte completo'); +console.log(resultado.lastInsertRowid); // el ID del nuevo registro + +// UPDATE +db.prepare( + 'UPDATE tareas SET completada = ? WHERE id = ?' +).run(1, 42); + +// DELETE +db.prepare('DELETE FROM tareas WHERE id = ?').run(42); +``` + +### SQL Injection: ¡cuidado! + +**Nunca concatenes** valores en el string SQL: + +```javascript +// PELIGROSO - vulnerable a SQL injection +const query = `SELECT * FROM tareas WHERE id = ${req.params.id}`; + +// SEGURO - consulta parametrizada +db.prepare('SELECT * FROM tareas WHERE id = ?').get(req.params.id); +``` + +Los placeholders `?` se reemplazan de forma segura, escapando caracteres especiales. + +--- + +## CRUD completo + +Actualizamos el controlador para usar la base de datos: + +```javascript +// src/controllers/tareas.controller.js +const db = require('../database/connection'); + +function obtenerTodas(req, res) { + try { + const tareas = db.prepare('SELECT * FROM tareas ORDER BY fecha_creacion DESC').all(); + res.json(tareas); + } catch (error) { + console.error('Error:', error.message); + res.status(500).json({ error: 'Error interno del servidor' }); + } +} + +function obtenerPorId(req, res) { + try { + const tarea = db.prepare('SELECT * FROM tareas WHERE id = ?').get(req.params.id); + if (!tarea) { + return res.status(404).json({ error: 'Tarea no encontrada' }); + } + res.json(tarea); + } catch (error) { + console.error('Error:', error.message); + res.status(500).json({ error: 'Error interno del servidor' }); + } +} + +function crear(req, res) { + try { + const { titulo, descripcion } = req.body; + + if (!titulo || titulo.trim() === '') { + return res.status(400).json({ error: 'El título es obligatorio' }); + } + + const resultado = db.prepare( + 'INSERT INTO tareas (titulo, descripcion) VALUES (?, ?)' + ).run(titulo.trim(), descripcion || null); + + const nuevaTarea = db.prepare('SELECT * FROM tareas WHERE id = ?').get( + resultado.lastInsertRowid + ); + res.status(201).json(nuevaTarea); + } catch (error) { + console.error('Error:', error.message); + res.status(500).json({ error: 'Error interno del servidor' }); + } +} + +function actualizar(req, res) { + try { + const { id } = req.params; + const { titulo, descripcion, completada } = req.body; + + const tareaExistente = db.prepare('SELECT * FROM tareas WHERE id = ?').get(id); + if (!tareaExistente) { + return res.status(404).json({ error: 'Tarea no encontrada' }); + } + + if (!titulo || titulo.trim() === '') { + return res.status(400).json({ error: 'El título es obligatorio' }); + } + + db.prepare( + 'UPDATE tareas SET titulo = ?, descripcion = ?, completada = ? WHERE id = ?' + ).run(titulo.trim(), descripcion || null, completada ? 1 : 0, id); + + const tareaActualizada = db.prepare('SELECT * FROM tareas WHERE id = ?').get(id); + res.json(tareaActualizada); + } catch (error) { + console.error('Error:', error.message); + res.status(500).json({ error: 'Error interno del servidor' }); + } +} + +function eliminar(req, res) { + try { + const { id } = req.params; + const tarea = db.prepare('SELECT * FROM tareas WHERE id = ?').get(id); + if (!tarea) { + return res.status(404).json({ error: 'Tarea no encontrada' }); + } + db.prepare('DELETE FROM tareas WHERE id = ?').run(id); + res.json({ mensaje: `Tarea ${id} eliminada correctamente` }); + } catch (error) { + console.error('Error:', error.message); + res.status(500).json({ error: 'Error interno del servidor' }); + } +} + +module.exports = { obtenerTodas, obtenerPorId, crear, actualizar, eliminar }; +``` + +### Probando con curl + +```bash +# Crear tareas +curl -X POST http://localhost:3000/api/tareas \ + -H "Content-Type: application/json" \ + -d '{"titulo": "Estudiar para el parcial", "descripcion": "Repasar SQL y Backend"}' + +# Listar todas +curl http://localhost:3000/api/tareas + +# Obtener una +curl http://localhost:3000/api/tareas/1 + +# Actualizar +curl -X PUT http://localhost:3000/api/tareas/1 \ + -H "Content-Type: application/json" \ + -d '{"titulo": "Estudiar para el parcial", "completada": true}' + +# Eliminar +curl -X DELETE http://localhost:3000/api/tareas/1 +``` + +--- + +## Manejo de errores + +### try/catch en las rutas + +Siempre envolvé las operaciones que pueden fallar en `try/catch`: + +```javascript +function obtenerTodas(req, res) { + try { + const tareas = db.prepare('SELECT * FROM tareas').all(); + res.json(tareas); + } catch (error) { + console.error('Error:', error.message); + res.status(500).json({ error: 'Error interno del servidor' }); + } +} +``` + +### Middleware de manejo de errores + +Express tiene un middleware especial con **4 parámetros** que se pone al final: + +```javascript +// Ruta 404 (después de todas las rutas) +app.use((req, res) => { + res.status(404).json({ error: 'Ruta no encontrada' }); +}); + +// Middleware de errores (siempre al final) +app.use((err, req, res, next) => { + console.error('Error:', err.message); + res.status(500).json({ error: 'Error interno del servidor' }); +}); +``` + +--- + +## Variables de entorno + +No hardcodees contraseñas, claves o configuraciones en el código. Usá **variables de entorno**. + +### Archivos .env + +```bash +npm install dotenv +``` + +Creá un archivo `.env`: + +``` +PORT=3000 +DB_PATH=./src/database/tareas.db +``` + +Leelo al inicio de tu app: + +```javascript +// index.js +require('dotenv').config(); + +const PORT = process.env.PORT || 3000; +``` + +**Importante**: `.env` nunca se sube al repositorio. Agregalo al `.gitignore`: + +``` +node_modules/ +.env +*.db +``` + +Creá un `.env.example` como referencia: + +``` +PORT=3000 +DB_PATH=./src/database/tareas.db +``` + +--- + +## Ejemplo integrador + +### Paso a paso para crear tu API + +```bash +# 1. Crear el proyecto +mkdir mi-api && cd mi-api +npm init -y +npm install express better-sqlite3 dotenv + +# 2. Crear la estructura +mkdir -p src/routes src/controllers src/database src/middleware +``` + +### index.js completo + +```javascript +require('dotenv').config(); +const express = require('express'); +const app = express(); +const tareasRoutes = require('./src/routes/tareas.routes'); + +app.use(express.json()); +app.use(express.static('public')); + +app.get('/', (req, res) => { + res.json({ + mensaje: 'API de Tareas funcionando', + endpoints: { + 'GET /api/tareas': 'Obtener todas las tareas', + 'GET /api/tareas/:id': 'Obtener una tarea por ID', + 'POST /api/tareas': 'Crear una nueva tarea', + 'PUT /api/tareas/:id': 'Actualizar una tarea', + 'DELETE /api/tareas/:id': 'Eliminar una tarea', + }, + }); +}); + +app.use('/api/tareas', tareasRoutes); + +app.use((req, res) => { + res.status(404).json({ error: 'Ruta no encontrada' }); +}); + +app.use((err, req, res, next) => { + console.error('Error:', err.message); + res.status(500).json({ error: 'Error interno del servidor' }); +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Servidor corriendo en http://localhost:${PORT}`); +}); +``` + +### Iniciá el servidor + +```bash +npm start +``` + +--- + +## Resumen + +1. **Node.js** permite ejecutar JavaScript fuera del navegador. +2. **npm** gestiona las dependencias del proyecto. +3. **Express** simplifica la creación de servidores HTTP. +4. Definimos **rutas** para cada método HTTP (GET, POST, PUT, DELETE). +5. Los **middleware** procesan peticiones antes de que lleguen a las rutas. +6. **Organizamos** el proyecto en carpetas (routes, controllers, database). +7. **SQLite** es una base de datos simple, sin servidor aparte. +8. Las **consultas parametrizadas** previenen SQL injection. +9. El **manejo de errores** con try/catch mantiene la API robusta. +10. Las **variables de entorno** protegen información sensible. + +Con estos conocimientos, ya tenés las herramientas para construir APIs funcionales. En el apunte de Integración vamos a conectar este backend con un frontend y Docker Compose. diff --git a/docs/Material/Apuntes/apunte_integracion.md b/docs/Material/Apuntes/apunte_integracion.md new file mode 100644 index 00000000..7653167e --- /dev/null +++ b/docs/Material/Apuntes/apunte_integracion.md @@ -0,0 +1,740 @@ +--- +sidebar_position: 12 +--- + +# Integración: Frontend + Backend + Base de Datos + +Este es el apunte que une todo lo que vimos durante la cursada. Vas a aprender cómo conectar un frontend (HTML/CSS/JS), un backend (Express), una base de datos (SQLite), y orquestar todo con Docker Compose. + +## Introducción + +### La foto completa + +Una aplicación web full-stack tiene tres capas: + +``` + ┌─────────────────────────────────────────────────────────────┐ + │ USUARIO │ + │ (Navegador web) │ + └──────────────────────────┬──────────────────────────────────┘ + │ Interactúa + ┌──────────────────────────▼──────────────────────────────────┐ + │ FRONTEND │ + │ HTML + CSS + JavaScript │ + │ (lo que el usuario ve y toca) │ + └──────────────────────────┬──────────────────────────────────┘ + │ fetch() con HTTP/JSON + ┌──────────────────────────▼──────────────────────────────────┐ + │ BACKEND │ + │ Node.js + Express │ + │ (API REST, validación, lógica) │ + └──────────────────────────┬──────────────────────────────────┘ + │ SQL (consultas parametrizadas) + ┌──────────────────────────▼──────────────────────────────────┐ + │ BASE DE DATOS │ + │ SQLite │ + │ (almacenamiento persistente) │ + └─────────────────────────────────────────────────────────────┘ +``` + +Cada capa tiene su responsabilidad: +- **Frontend**: la interfaz de usuario. Se comunica con el backend usando `fetch()` (como vimos en el apunte de JavaScript). +- **Backend**: la API REST. Recibe peticiones, las valida, opera sobre la base de datos y devuelve respuestas (como vimos en los apuntes de APIs REST y Backend). +- **Base de datos**: almacena los datos de forma persistente (como vimos en el apunte de SQL). + +--- + +## Estructura del proyecto + +``` +mi-proyecto/ +├── frontend/ +│ ├── index.html +│ ├── styles.css +│ └── app.js +├── backend/ +│ ├── package.json +│ ├── server.js +│ ├── database.js +│ └── routes/ +│ └── tareas.js +├── docker-compose.yml +├── Dockerfile.frontend +├── Dockerfile.backend +├── nginx.conf +└── README.md +``` + +Separar frontend y backend en carpetas distintas mantiene todo organizado y facilita el deploy con Docker. + +--- + +## Conectando el Frontend al Backend + +La conexión se hace con `fetch()` desde JavaScript en el navegador. El frontend envía peticiones HTTP al backend y recibe respuestas JSON. + +### Ejemplo: cargar y mostrar tareas + +```javascript +// frontend/app.js +const API_URL = 'http://localhost:3000/api/tareas'; + +async function cargarTareas() { + try { + const respuesta = await fetch(API_URL); + + if (!respuesta.ok) { + throw new Error(`Error ${respuesta.status}`); + } + + const tareas = await respuesta.json(); + mostrarTareas(tareas); + } catch (error) { + if (error.message.includes('fetch')) { + mostrarError('No se puede conectar al servidor. ¿Está corriendo el backend?'); + } else { + mostrarError(error.message); + } + } +} +``` + +### Enviar datos al backend + +```javascript +async function agregarTarea(titulo) { + try { + const respuesta = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ titulo }), + }); + + if (!respuesta.ok) { + const errorData = await respuesta.json(); + throw new Error(errorData.error || 'Error al crear la tarea'); + } + + return await respuesta.json(); + } catch (error) { + mostrarError(error.message); + return null; + } +} +``` + +--- + +## CORS: qué es y por qué aparece + +Cuando el frontend corre en `http://localhost:8080` y el backend en `http://localhost:3000`, el navegador bloquea las peticiones porque son **orígenes distintos**. Esto se llama **CORS** (Cross-Origin Resource Sharing). + +### La solución + +Instalar el paquete `cors` en el backend: + +```bash +cd backend +npm install cors +``` + +```javascript +// backend/server.js +const cors = require('cors'); +app.use(cors()); // permite peticiones de cualquier origen +``` + +Sin esto, vas a ver en la consola del navegador un error como: + +``` +Access to fetch at 'http://localhost:3000/api/tareas' from origin +'http://localhost:8080' has been blocked by CORS policy +``` + +--- + +## Flujo completo de una operación + +Veamos paso a paso qué pasa cuando el usuario hace click en "Agregar Tarea": + +``` +1. USUARIO hace click en "Agregar" + │ +2. FRONTEND (app.js) + │ evento "submit" del formulario + │ lee el valor del input + │ llama a fetch() con POST + │ + ▼ +3. HTTP: POST /api/tareas + Body: { "titulo": "Estudiar para el parcial" } + │ + ▼ +4. BACKEND (Express) + │ recibe la petición + │ express.json() parsea el body + │ valida que el título no esté vacío + │ ejecuta INSERT en la base de datos + │ + ▼ +5. BASE DE DATOS (SQLite) + │ INSERT INTO tareas (titulo) VALUES ('Estudiar para el parcial') + │ devuelve el ID del nuevo registro + │ + ▼ +6. BACKEND + │ hace SELECT del registro recién creado + │ responde con 201 Created + el JSON de la tarea + │ + ▼ +7. HTTP: 201 Created + Body: { "id": 5, "titulo": "Estudiar para el parcial", "completada": 0 } + │ + ▼ +8. FRONTEND + │ recibe la respuesta + │ actualiza la lista de tareas en el DOM + │ + ▼ +9. USUARIO ve la nueva tarea en la página +``` + +--- + +## Docker Compose para todo el stack + +Como vimos en el apunte de Docker, Docker Compose permite levantar múltiples servicios con un solo comando. + +### Dockerfile.backend + +```dockerfile +FROM node:18-alpine +WORKDIR /app +COPY backend/package*.json ./ +RUN npm install +COPY backend/ . +RUN mkdir -p /app/datos +EXPOSE 3000 +CMD ["node", "server.js"] +``` + +### Dockerfile.frontend + +```dockerfile +FROM nginx:alpine +COPY frontend/ /usr/share/nginx/html/ +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 8080 +``` + +### nginx.conf + +```nginx +server { + listen 8080; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} +``` + +### docker-compose.yml + +```yaml +services: + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + ports: + - "8080:8080" + depends_on: + - backend + + backend: + build: + context: . + dockerfile: Dockerfile.backend + ports: + - "3000:3000" + environment: + - PORT=3000 + - DB_PATH=/app/datos/datos.db + volumes: + - datos-db:/app/datos + +volumes: + datos-db: +``` + +Puntos clave: +- `depends_on`: el frontend espera a que el backend esté listo. +- `volumes`: los datos de la base de datos persisten entre reinicios. +- `environment`: variables de entorno para el backend. +- `ports`: mapeo de puertos del contenedor al host. + +### Comandos + +```bash +# Construir las imágenes +docker-compose build + +# Levantar todo +docker-compose up + +# Levantar en segundo plano +docker-compose up -d + +# Ver logs +docker-compose logs -f + +# Bajar todo +docker-compose down + +# Bajar todo + borrar volúmenes (borra la DB) +docker-compose down -v +``` + +--- + +## Manejo de errores end-to-end + +### En el Frontend + +```javascript +async function cargarTareas() { + try { + const respuesta = await fetch(API_URL); + + if (!respuesta.ok) { + const errorData = await respuesta.json(); + throw new Error(errorData.error || `Error ${respuesta.status}`); + } + + const tareas = await respuesta.json(); + mostrarTareas(tareas); + } catch (error) { + if (error.name === 'TypeError' && error.message === 'Failed to fetch') { + mostrarError('No se puede conectar al servidor.'); + } else { + mostrarError(error.message); + } + } +} +``` + +### En el Backend + +```javascript +router.post('/', (req, res) => { + try { + const { titulo } = req.body; + + if (!titulo || titulo.trim() === '') { + return res.status(400).json({ error: 'El título es obligatorio' }); + } + + const stmt = db.prepare('INSERT INTO tareas (titulo) VALUES (?)'); + const result = stmt.run(titulo.trim()); + + const nuevaTarea = db.prepare('SELECT * FROM tareas WHERE id = ?').get(result.lastInsertRowid); + res.status(201).json(nuevaTarea); + } catch (error) { + console.error('Error en POST /api/tareas:', error); + res.status(500).json({ error: 'Error interno del servidor' }); + } +}); +``` + +### Buenas prácticas + +1. **Siempre usá `try/catch`** en funciones async del frontend y handlers del backend. +2. **Nunca muestres detalles internos al usuario**. Los detalles van al `console.error`. +3. **Validá tanto en el frontend como en el backend**. La validación del frontend mejora la UX; la del backend protege los datos. + +--- + +## Ejemplo integrador completo: Lista de Tareas + +Este es un proyecto completo que podés copiar y correr. + +### frontend/index.html + +```html + + +
+ + +Cargando tareas...
+No hay tareas. ¡Agregá una!
'; + return; + } + tareas.forEach(tarea => { + const div = document.createElement('div'); + div.className = `tarea ${tarea.completada ? 'completada' : ''}`; + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = tarea.completada === 1; + cb.addEventListener('change', () => toggleTarea(tarea.id, cb.checked)); + + const span = document.createElement('span'); + span.textContent = tarea.titulo; + + const btn = document.createElement('button'); + btn.textContent = 'Eliminar'; + btn.addEventListener('click', () => eliminarTarea(tarea.id)); + + div.appendChild(cb); + div.appendChild(span); + div.appendChild(btn); + lista.appendChild(div); + }); +} + +function actualizarContador(tareas) { + const completadas = tareas.filter(t => t.completada === 1).length; + document.getElementById('contador').textContent = `${completadas} de ${tareas.length} completadas`; +} + +function mostrarError(msg) { + const div = document.getElementById('error-mensaje'); + div.textContent = msg; + div.style.display = 'block'; + setTimeout(() => div.style.display = 'none', 5000); +} + +document.getElementById('form-tarea').addEventListener('submit', async (e) => { + e.preventDefault(); + const input = document.getElementById('input-titulo'); + const titulo = input.value.trim(); + if (!titulo) return; + try { + await agregarTarea(titulo); + input.value = ''; + cargarTareas(); + } catch (error) { + mostrarError(error.message); + } +}); + +cargarTareas(); +``` + +### backend/package.json + +```json +{ + "name": "lista-de-tareas-backend", + "version": "1.0.0", + "main": "server.js", + "scripts": { "start": "node server.js" }, + "dependencies": { + "better-sqlite3": "^11.0.0", + "cors": "^2.8.5", + "express": "^4.18.2" + } +} +``` + +### backend/server.js + +```javascript +const express = require('express'); +const cors = require('cors'); +const tareasRoutes = require('./routes/tareas'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.json()); +app.use('/api/tareas', tareasRoutes); + +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +app.use((req, res) => { + res.status(404).json({ error: 'Ruta no encontrada' }); +}); + +app.listen(PORT, () => { + console.log(`Servidor corriendo en http://localhost:${PORT}`); +}); +``` + +### backend/database.js + +```javascript +const Database = require('better-sqlite3'); +const path = require('path'); + +const dbPath = process.env.DB_PATH || path.join(__dirname, 'datos.db'); +const db = new Database(dbPath); + +db.pragma('journal_mode = WAL'); + +db.exec(` + CREATE TABLE IF NOT EXISTS tareas ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + titulo TEXT NOT NULL, + completada INTEGER DEFAULT 0, + creada_en TEXT DEFAULT (datetime('now')) + ) +`); + +module.exports = db; +``` + +### backend/routes/tareas.js + +```javascript +const express = require('express'); +const router = express.Router(); +const db = require('../database'); + +router.get('/', (req, res) => { + try { + const tareas = db.prepare('SELECT * FROM tareas ORDER BY creada_en DESC').all(); + res.json(tareas); + } catch (error) { + console.error('Error:', error); + res.status(500).json({ error: 'Error interno del servidor' }); + } +}); + +router.get('/:id', (req, res) => { + try { + const tarea = db.prepare('SELECT * FROM tareas WHERE id = ?').get(req.params.id); + if (!tarea) return res.status(404).json({ error: 'Tarea no encontrada' }); + res.json(tarea); + } catch (error) { + res.status(500).json({ error: 'Error interno del servidor' }); + } +}); + +router.post('/', (req, res) => { + try { + const { titulo } = req.body; + if (!titulo || titulo.trim() === '') { + return res.status(400).json({ error: 'El título es obligatorio' }); + } + const result = db.prepare('INSERT INTO tareas (titulo) VALUES (?)').run(titulo.trim()); + const nueva = db.prepare('SELECT * FROM tareas WHERE id = ?').get(result.lastInsertRowid); + res.status(201).json(nueva); + } catch (error) { + res.status(500).json({ error: 'Error interno del servidor' }); + } +}); + +router.put('/:id', (req, res) => { + try { + const tarea = db.prepare('SELECT * FROM tareas WHERE id = ?').get(req.params.id); + if (!tarea) return res.status(404).json({ error: 'Tarea no encontrada' }); + + const { titulo, completada } = req.body; + const nuevoTitulo = titulo !== undefined ? titulo.trim() : tarea.titulo; + const nuevaCompletada = completada !== undefined ? (completada ? 1 : 0) : tarea.completada; + + db.prepare('UPDATE tareas SET titulo = ?, completada = ? WHERE id = ?') + .run(nuevoTitulo, nuevaCompletada, req.params.id); + + const actualizada = db.prepare('SELECT * FROM tareas WHERE id = ?').get(req.params.id); + res.json(actualizada); + } catch (error) { + res.status(500).json({ error: 'Error interno del servidor' }); + } +}); + +router.delete('/:id', (req, res) => { + try { + const tarea = db.prepare('SELECT * FROM tareas WHERE id = ?').get(req.params.id); + if (!tarea) return res.status(404).json({ error: 'Tarea no encontrada' }); + db.prepare('DELETE FROM tareas WHERE id = ?').run(req.params.id); + res.json({ mensaje: 'Tarea eliminada correctamente' }); + } catch (error) { + res.status(500).json({ error: 'Error interno del servidor' }); + } +}); + +module.exports = router; +``` + +### Cómo correr el proyecto + +**Con Docker Compose** (recomendado): + +```bash +docker-compose build +docker-compose up +``` + +Abrí `http://localhost:8080` en el navegador. + +**Sin Docker** (para desarrollo): + +```bash +# Terminal 1: Backend +cd backend && npm install && node server.js + +# Terminal 2: Frontend +cd frontend && npx serve -l 8080 +``` + +--- + +## Tips para el TP + +### Git workflow + +1. **Usá branches**: nunca trabajes directamente en `main`. + ```bash + git checkout -b feature/agregar-tareas + ``` +2. **Commits chicos y descriptivos**: "Agrego formulario de tareas" > "Hago todo el TP". +3. **Usá Pull Requests** (como vimos en el apunte de Git). +4. **`.gitignore`**: + ``` + node_modules/ + *.db + .env + .DS_Store + ``` + +### Errores comunes + +| Error | Causa | Solución | +|-------|-------|----------| +| CORS bloqueado | Frontend y backend en distintos puertos | Instalar y usar el paquete `cors` | +| "Failed to fetch" | Backend no está corriendo | Verificar que `node server.js` esté ejecutándose | +| Formulario recarga la página | Falta `event.preventDefault()` | Agregarlo en el handler del submit | +| Datos se pierden al reiniciar Docker | No hay volumen configurado | Agregar volumen en `docker-compose.yml` | +| `node_modules` en el repo | Falta en `.gitignore` | Agregar `node_modules/` al `.gitignore` | +| No funciona con Docker | Solo probaste sin Docker | Siempre probar con `docker-compose build && docker-compose up` | + +### Cómo debuggear + +- **Consola del navegador (F12)**: errores de JavaScript y peticiones HTTP. +- **Tab "Network" en DevTools**: ver cada petición, su status, headers y body. +- **Logs del backend**: `console.log()` en Express → se ven en la terminal o en `docker-compose logs`. +- **curl**: probar endpoints sin depender del frontend. + +--- + +## Resumen + +| Capa | Tecnología | Responsabilidad | +|------|-----------|----------------| +| Frontend | HTML + CSS + JavaScript | Interfaz de usuario, `fetch()` al backend | +| Backend | Node.js + Express | API REST, validación, lógica | +| Base de datos | SQLite | Almacenamiento persistente | +| Orquestación | Docker Compose | Levantar todo con un solo comando | + +El flujo siempre es: **Usuario → Frontend → Backend → Base de Datos** y viceversa. + +Si te trabás, revisá los apuntes individuales: +- [HTML y CSS](./apunte_html_cs) +- [JavaScript](./apunte_javascript) +- [APIs REST](./apunte_rest_api) +- [Backend](./apunte_backend) +- [SQL](./apunte_sql) +- [Docker](./docker) +- [Git](./git) diff --git a/docs/Material/Apuntes/apunte_javascript.md b/docs/Material/Apuntes/apunte_javascript.md new file mode 100644 index 00000000..364f97a2 --- /dev/null +++ b/docs/Material/Apuntes/apunte_javascript.md @@ -0,0 +1,813 @@ +--- +sidebar_position: 9 +--- + +# JavaScript en el Navegador + +Este apunte cubre JavaScript aplicado al navegador: cómo manipular páginas web, responder a acciones del usuario y comunicarse con servidores externos. + +## Introducción a JavaScript + +**JavaScript** es un lenguaje de programación que originalmente fue creado para darle interactividad a las páginas web. Mientras que HTML define la estructura y CSS el estilo (como vimos en el apunte de HTML y CSS), JavaScript agrega el **comportamiento**: qué pasa cuando hacés click en un botón, cómo se valida un formulario, cómo se cargan datos sin recargar la página. + +### Dónde se ejecuta + +JavaScript se ejecuta dentro del **navegador** (Chrome, Firefox, etc.). Cada navegador tiene un motor de JavaScript que interpreta y ejecuta el código. Cuando abrís una página web que tiene JavaScript, el navegador lo descarga y lo ejecuta automáticamente. + +### Cómo incluir JavaScript en HTML + +Hay dos formas principales: + +**1. Script interno** (dentro del HTML): + +```html + + + + +