# Explicación App Vite + REACT + Express.js

Esta es la explicación para la app que se encuentra en:

{% embed url="<https://github.com/aviladotgibert/MERN_example>" %}

## Estructura de Directorios y archivos

```bash
MERN_example/
├── backend/                    # Servidor Node.js + Express
│   ├── server.js              # Punto de entrada de la API
│   ├── package.json           # Dependencias del backend
│   └── .env                   # Variables de entorno (no incluido en repo)
│
├── frontend/                   # Aplicación React + Vite
│   ├── src/
│   │   ├── App.jsx           # Componente principal
│   │   ├── App.css           # Estilos
│   │   └── main.jsx          # Punto de entrada React
│   ├── package.json          # Dependencias del frontend
│   ├── vite.config.js        # Configuración de Vite
│   └── .env                  # Variables de entorno (no incluido en repo)
│
└── README.md                  # Este archivo
```

## Código del Backend (Express y Node)

### Variables de entorno

Contenido:

```env
PORT=3001
MONGODB_URI=mongodb://app_user:Passw0rdApp@localhost:27017/mi_aplicacion
NODE_ENV=production
```

### Archivos del backend

* **`server.js`**: Es la aplicación backend completa. Crea una API REST para gestionar tareas. Un modelo de arquitectura común de Express con Mongo:

<details>

<summary><code>server.js</code></summary>

{% code overflow="wrap" %}

```javascript
/* server.js - Backend con Express y MongoDB
DEPENDENCIAS: 
dotenv: permite usar variables del archivo .env sin escribirlas directamente en el código.
express: el framework que crea el servidor HTTP.
mongoose: un conector entre Node.js y MongoDB (la base de datos).
cors: permite que el frontend de REACT pueda acceder al backend aunque estén en dominios diferentes.*/
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

//app es mi servidor de express
const app = express();
//Para el puerto coje el del .env o el 300 por defecto
const PORT = process.env.PORT || 3000;
//Habilita peticiones desde otros orígenes y permite recibir y leer formatos json
app.use(cors());
app.use(express.json());

// Conexión a MongoDB
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/mi_aplicacion')
  .then(() => console.log('✅ Conectado a MongoDB'))
  .catch(err => console.error('❌ Error conectando a MongoDB:', err));

/* Modelo de ejemplo: Tareas
Aquí se define cómo luce cada documento (o registro) de la colección tareas en MongoDB */
const tareaSchema = new mongoose.Schema({
  titulo: { type: String, required: true },
  descripcion: String,
  completada: { type: Boolean, default: false },
  fechaCreacion: { type: Date, default: Date.now }
});

const Tarea = mongoose.model('Tarea', tareaSchema);

// ========== RUTAS API ==========

// Health check
app.get('/health', (req, res) => {
  res.json({ 
    status: 'OK', 
    timestamp: new Date().toISOString(),
    database: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected'
  });
});

// Obtener todas las tareas
app.get('/tareas', async (req, res) => {
  try {
    const tareas = await Tarea.find().sort({ fechaCreacion: -1 });
    res.json(tareas);
  } catch (error) {
    res.status(500).json({ error: 'Error obteniendo tareas' });
  }
});

// Obtener una tarea por ID
app.get('/tareas/:id', async (req, res) => {
  try {
    const tarea = await Tarea.findById(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 obteniendo tarea' });
  }
});

// Crear nueva tarea
app.post('/tareas', async (req, res) => {
  try {
    const { titulo, descripcion } = req.body;
    
    if (!titulo) {
      return res.status(400).json({ error: 'El título es obligatorio' });
    }

    const nuevaTarea = new Tarea({ titulo, descripcion });
    await nuevaTarea.save();
    
    res.status(201).json(nuevaTarea);
  } catch (error) {
    res.status(500).json({ error: 'Error creando tarea' });
  }
});

// Actualizar tarea
app.put('/tareas/:id', async (req, res) => {
  try {
    const { titulo, descripcion, completada } = req.body;
    
    const tareaActualizada = await Tarea.findByIdAndUpdate(
      req.params.id,
      { titulo, descripcion, completada },
      { new: true, runValidators: true }
    );

    if (!tareaActualizada) {
      return res.status(404).json({ error: 'Tarea no encontrada' });
    }

    res.json(tareaActualizada);
  } catch (error) {
    res.status(500).json({ error: 'Error actualizando tarea' });
  }
});

// Eliminar tarea
app.delete('/tareas/:id', async (req, res) => {
  try {
    const tareaEliminada = await Tarea.findByIdAndDelete(req.params.id);
    
    if (!tareaEliminada) {
      return res.status(404).json({ error: 'Tarea no encontrada' });
    }

    res.json({ mensaje: 'Tarea eliminada correctamente' });
  } catch (error) {
    res.status(500).json({ error: 'Error eliminando tarea' });
  }
});

// Manejo de rutas no encontradas
app.use((req, res) => {
  res.status(404).json({ error: 'Ruta no encontrada' });
});

// Iniciar servidor
app.listen(PORT, () => {
  console.log(`🚀 Servidor ejecutándose en http://localhost:${PORT}`);
  console.log(`📊 Entorno: ${process.env.NODE_ENV || 'development'}`);
});
```

{% endcode %}

</details>

Para la colección de **tareas** en MongoDB usando el modelo de tareas tenemos que por cada una se registra:

<table><thead><tr><th width="201">Campo</th><th width="219">Tipo</th><th>Descripción</th></tr></thead><tbody><tr><td>titulo</td><td>String (obligatorio)</td><td>Título de la tarea</td></tr><tr><td>descripcion</td><td>String</td><td>Detalles opcionales</td></tr><tr><td>completada</td><td>Boolean</td><td>Si está hecha o no</td></tr><tr><td>fechaCreacion</td><td>Date</td><td>Fecha automática de creación</td></tr></tbody></table>

El modelo `Tarea` permite **crear**, **consultar**, **actualizar** o **borrar** documentos en MongoDB fácilmente.

Lo que convierte al servidor en una “API REST” es que usar métodos HTTP (GET, POST, PUT, DELETE) para manejar recursos (en este caso, las tareas de las que hemos hablado).

El servidor expone las siguientes rutas:

<table><thead><tr><th width="90">Método</th><th width="119">Ruta</th><th width="101">Acción</th><th>Descripción</th></tr></thead><tbody><tr><td>GET</td><td><code>/health</code></td><td>Leer</td><td>Devuelve si el servidor y la db están activos.</td></tr><tr><td>GET</td><td><code>/tareas</code></td><td>Leer</td><td>Devuelve todas las tareas (ordenadas por fecha).</td></tr><tr><td>GET</td><td><code>/tareas/:id</code></td><td>Leer</td><td>Devuelve una tarea concreta por su ID.</td></tr><tr><td>POST</td><td><code>/tareas</code></td><td>Crear</td><td>Crea una nueva tarea en la base de datos.</td></tr><tr><td>PUT</td><td><code>/tareas/:id</code></td><td>Actualizar</td><td>Modifica los datos de una tarea existente.</td></tr><tr><td>DELETE</td><td><code>/tareas/:id</code></td><td>Borrar</td><td>Elimina una tarea de la base de datos.</td></tr></tbody></table>

Cada una de estas rutas responde con datos **en formato JSON** para que el frontend (React en nuestro caso) pueda entender las respuestas.

* **`package.json`**: Este archivo simplemente lista las dependencias que Node.js necesita instalar. Es como el "requirements.txt" de Python o "composer.json" de PHP.

<details>

<summary><code>package.json</code></summary>

```json
{
  "name": "mern-backend-example",
  "version": "1.0.0",
  "description": "API REST con Express y MongoDB",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": ["express", "mongodb", "api", "rest"],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^8.0.0",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}
```

</details>

Vamos ahora sí, con la instalación de dependencias:

```bash
cd /var/www/mi-app-mern/backend
npm install
```

Ahora iniciamos el proyecto con PM2

```bash
pm2 start server.js --name "backend-api"
pm2 save
pm2 startup  # Seguir instrucciones para autoarranque
```

Aquí te dirá algo como:

<figure><img src="https://539580950-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiKvwltOme7zTxep3LVyW%2Fuploads%2F0PzPtokzDqUq3HrKRA5K%2Fimage.png?alt=media&#x26;token=c59e786c-bbed-4bd4-924f-6d224c658574" alt=""><figcaption></figcaption></figure>

Simplemente ejecuta ese comando. Ahora, para verificar el estado puedes:

```bash
pm2 status
```

<figure><img src="https://539580950-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiKvwltOme7zTxep3LVyW%2Fuploads%2FDNncSH8IliMbKYBaNBAd%2Fimage.png?alt=media&#x26;token=8f2cb82e-4ab4-4b80-9818-0ccdf5d41604" alt=""><figcaption></figcaption></figure>

Y puedes probar con:

```bash
pm2 logs backend-api  # Para ver logs
curl http://localhost:3000/health  # Endpoint de prueba

# Debe responder:
# {"status":"OK","timestamp":"...","database":"connected"}
```

## Código del Frontend (React + Vite)

Esta carpeta la empezariamos con Vite con el siguiente comando:

```bash
 npm create vite@latest frontend -- --template react
```

&#x20;Que hace **exactamente esto**:

{% code overflow="wrap" %}

```
npm create vite@latest    → Ejecuta el "creador oficial" de proyectos Vite (versión más reciente)de Internet sin necesidad de instalar
frontend                      → NOMBRE de la carpeta que crea (tu proyecto)
-- --template react           → OPCIONES para Vite: "usa la plantilla React"
```

{% endcode %}

Lo cual genera automáticamente:

```
textfrontend/             ← Carpeta creada
├── index.html            ← HTML base
├── package.json          ← Dependencias React + Vite listas
├── vite.config.js        ← Configuración Vite optimizada
├── src/
│   ├── main.jsx          ← Entry point React
│   ├── App.jsx           ← Tu componente principal
│   └── App.css           ← Estilos base
└── README.md
```

Ya después necesitariamos instalar:

```bash
npm install     # Instala React, Vite, etc.
npm run dev     # Inicia servidor desarrollo
#o bien
npm run build   # Iniciar la build
```

Los archivos a trabajar (como ya hemos indicado) son los siguientes:

<details>

<summary><strong>src/App.jsx</strong> → [Código del componente App] </summary>

```javascript
// src/App.jsx
import { useState, useEffect } from 'react';
import './App.css';

// Configuración de la URL del backend
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/';

function App() {
  const [tareas, setTareas] = useState([]);
  const [nuevaTarea, setNuevaTarea] = useState({ titulo: '', descripcion: '' });
  const [editando, setEditando] = useState(null);
  const [cargando, setCargando] = useState(false);
  const [error, setError] = useState(null);

  // Cargar tareas al inicio
  useEffect(() => {
    cargarTareas();
  }, []);

  // Función para obtener todas las tareas
  const cargarTareas = async () => {
    setCargando(true);
    setError(null);
    try {
      const response = await fetch(`${API_URL}/tareas`);
      if (!response.ok) throw new Error('Error al cargar tareas');
      const data = await response.json();
      setTareas(data);
    } catch (err) {
      setError('No se pudieron cargar las tareas. ¿Está el backend ejecutándose?');
      console.error(err);
    } finally {
      setCargando(false);
    }
  };

  // Crear nueva tarea
  const crearTarea = async (e) => {
    e.preventDefault();
    if (!nuevaTarea.titulo.trim()) return;

    try {
      const response = await fetch(`${API_URL}/tareas`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(nuevaTarea)
      });
      
      if (!response.ok) throw new Error('Error al crear tarea');
      
      await cargarTareas();
      setNuevaTarea({ titulo: '', descripcion: '' });
    } catch (err) {
      setError('Error al crear la tarea');
      console.error(err);
    }
  };

  // Actualizar estado de completada
  const toggleCompletada = async (tarea) => {
    try {
      const response = await fetch(`${API_URL}/tareas/${tarea._id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...tarea, completada: !tarea.completada })
      });
      
      if (!response.ok) throw new Error('Error al actualizar tarea');
      
      await cargarTareas();
    } catch (err) {
      setError('Error al actualizar la tarea');
      console.error(err);
    }
  };

  // Eliminar tarea
  const eliminarTarea = async (id) => {
    if (!confirm('¿Estás seguro de eliminar esta tarea?')) return;

    try {
      const response = await fetch(`${API_URL}/tareas/${id}`, {
        method: 'DELETE'
      });
      
      if (!response.ok) throw new Error('Error al eliminar tarea');
      
      await cargarTareas();
    } catch (err) {
      setError('Error al eliminar la tarea');
      console.error(err);
    }
  };

  // Editar tarea
  const iniciarEdicion = (tarea) => {
    setEditando({
      id: tarea._id,
      titulo: tarea.titulo,
      descripcion: tarea.descripcion || ''
    });
  };

  const guardarEdicion = async (e) => {
    e.preventDefault();
    if (!editando.titulo.trim()) return;

    try {
      const response = await fetch(`${API_URL}/tareas/${editando.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          titulo: editando.titulo,
          descripcion: editando.descripcion
        })
      });
      
      if (!response.ok) throw new Error('Error al actualizar tarea');
      
      await cargarTareas();
      setEditando(null);
    } catch (err) {
      setError('Error al guardar los cambios');
      console.error(err);
    }
  };

  return (
    <div className="app">
      <div className="container">
        <header>
          <h1>📝 Gestor de Tareas MERN</h1>
          <p className="subtitle">Ejemplo de Stack MERN completo</p>
        </header>

        {error && (
          <div className="error-message">
            ⚠️ {error}
            <button onClick={() => setError(null)}>×</button>
          </div>
        )}

        {/* Formulario para crear nueva tarea */}
        <form onSubmit={crearTarea} className="form-nueva-tarea">
          <h2>➕ Nueva Tarea</h2>
          <input
            type="text"
            placeholder="Título de la tarea"
            value={nuevaTarea.titulo}
            onChange={(e) => setNuevaTarea({ ...nuevaTarea, titulo: e.target.value })}
            required
          />
          <textarea
            placeholder="Descripción (opcional)"
            value={nuevaTarea.descripcion}
            onChange={(e) => setNuevaTarea({ ...nuevaTarea, descripcion: e.target.value })}
            rows="3"
          />
          <button type="submit" className="btn-primary">Crear Tarea</button>
        </form>

        {/* Lista de tareas */}
        <div className="tareas-container">
          <h2>📋 Mis Tareas ({tareas.length})</h2>
          
          {cargando ? (
            <p className="loading">Cargando tareas...</p>
          ) : tareas.length === 0 ? (
            <p className="empty-state">No hay tareas. ¡Crea una nueva!</p>
          ) : (
            <div className="tareas-lista">
              {tareas.map((tarea) => (
                <div key={tarea._id} className={`tarea-card ${tarea.completada ? 'completada' : ''}`}>
                  {editando && editando.id === tarea._id ? (
                    // Modo edición
                    <form onSubmit={guardarEdicion} className="form-editar">
                      <input
                        type="text"
                        value={editando.titulo}
                        onChange={(e) => setEditando({ ...editando, titulo: e.target.value })}
                        autoFocus
                      />
                      <textarea
                        value={editando.descripcion}
                        onChange={(e) => setEditando({ ...editando, descripcion: e.target.value })}
                        rows="2"
                      />
                      <div className="botones-edicion">
                        <button type="submit" className="btn-guardar">Guardar</button>
                        <button type="button" onClick={() => setEditando(null)} className="btn-cancelar">
                          Cancelar
                        </button>
                      </div>
                    </form>
                  ) : (
                    // Modo visualización
                    <>
                      <div className="tarea-contenido">
                        <div className="tarea-header">
                          <input
                            type="checkbox"
                            checked={tarea.completada}
                            onChange={() => toggleCompletada(tarea)}
                            className="checkbox"
                          />
                          <h3>{tarea.titulo}</h3>
                        </div>
                        {tarea.descripcion && (
                          <p className="tarea-descripcion">{tarea.descripcion}</p>
                        )}
                        <small className="tarea-fecha">
                          Creada: {new Date(tarea.fechaCreacion).toLocaleDateString('es-ES')}
                        </small>
                      </div>
                      <div className="tarea-acciones">
                        <button onClick={() => iniciarEdicion(tarea)} className="btn-editar">
                          ✏️ Editar
                        </button>
                        <button onClick={() => eliminarTarea(tarea._id)} className="btn-eliminar">
                          🗑️ Eliminar
                        </button>
                      </div>
                    </>
                  )}
                </div>
              ))}
            </div>
          )}
        </div>

        <footer>
          <p>🔧 Backend: Express + MongoDB | 🎨 Frontend: React + Vite</p>
        </footer>
      </div>
    </div>
  );
}

export default App;
```

</details>

<details>

<summary><strong>src/App.css</strong> → [Código de estilos]</summary>

```css
/* src/App.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding: 20px;
}

.app {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  min-height: 100vh;
}

.container {
  width: 100%;
  max-width: 800px;
  background: white;
  border-radius: 20px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  padding: 40px;
  margin: 20px;
}

header {
  text-align: center;
  margin-bottom: 40px;
}

header h1 {
  font-size: 2.5rem;
  color: #333;
  margin-bottom: 10px;
}

.subtitle {
  color: #666;
  font-size: 1rem;
}

/* Mensajes de error */
.error-message {
  background: #fee;
  color: #c33;
  padding: 15px 20px;
  border-radius: 10px;
  margin-bottom: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-left: 4px solid #c33;
}

.error-message button {
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: #c33;
  padding: 0 10px;
}

/* Formulario nueva tarea */
.form-nueva-tarea {
  background: #f8f9fa;
  padding: 25px;
  border-radius: 15px;
  margin-bottom: 40px;
  border: 2px solid #e9ecef;
}

.form-nueva-tarea h2 {
  color: #333;
  margin-bottom: 20px;
  font-size: 1.5rem;
}

.form-nueva-tarea input,
.form-nueva-tarea textarea {
  width: 100%;
  padding: 12px 15px;
  margin-bottom: 15px;
  border: 2px solid #dee2e6;
  border-radius: 8px;
  font-size: 1rem;
  transition: border-color 0.3s;
}

.form-nueva-tarea input:focus,
.form-nueva-tarea textarea:focus {
  outline: none;
  border-color: #667eea;
}

.form-nueva-tarea textarea {
  resize: vertical;
  font-family: inherit;
}

/* Botones */
.btn-primary {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border: none;
  padding: 12px 30px;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  width: 100%;
}

.btn-primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}

.btn-primary:active {
  transform: translateY(0);
}

/* Contenedor de tareas */
.tareas-container {
  margin-top: 40px;
}

.tareas-container h2 {
  color: #333;
  margin-bottom: 20px;
  font-size: 1.5rem;
}

.loading,
.empty-state {
  text-align: center;
  color: #999;
  padding: 40px;
  font-size: 1.1rem;
}

/* Lista de tareas */
.tareas-lista {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

/* Tarjeta de tarea */
.tarea-card {
  background: white;
  border: 2px solid #e9ecef;
  border-radius: 12px;
  padding: 20px;
  transition: all 0.3s;
}

.tarea-card:hover {
  border-color: #667eea;
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.tarea-card.completada {
  background: #f8f9fa;
  opacity: 0.7;
}

.tarea-card.completada h3 {
  text-decoration: line-through;
  color: #999;
}

.tarea-contenido {
  flex: 1;
}

.tarea-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}

.checkbox {
  width: 24px;
  height: 24px;
  cursor: pointer;
  accent-color: #667eea;
}

.tarea-card h3 {
  color: #333;
  font-size: 1.2rem;
  flex: 1;
}

.tarea-descripcion {
  color: #666;
  margin: 10px 0 10px 36px;
  line-height: 1.5;
}

.tarea-fecha {
  color: #999;
  font-size: 0.85rem;
  margin-left: 36px;
}

/* Acciones de tarea */
.tarea-acciones {
  display: flex;
  gap: 10px;
  margin-top: 15px;
}

.btn-editar,
.btn-eliminar {
  padding: 8px 16px;
  border: none;
  border-radius: 6px;
  font-size: 0.9rem;
  cursor: pointer;
  transition: all 0.2s;
  font-weight: 500;
}

.btn-editar {
  background: #ffc107;
  color: #333;
}

.btn-editar:hover {
  background: #ffb300;
  transform: translateY(-2px);
}

.btn-eliminar {
  background: #dc3545;
  color: white;
}

.btn-eliminar:hover {
  background: #c82333;
  transform: translateY(-2px);
}

/* Formulario de edición */
.form-editar {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.form-editar input,
.form-editar textarea {
  width: 100%;
  padding: 10px;
  border: 2px solid #dee2e6;
  border-radius: 6px;
  font-size: 1rem;
}

.form-editar input:focus,
.form-editar textarea:focus {
  outline: none;
  border-color: #667eea;
}

.botones-edicion {
  display: flex;
  gap: 10px;
}

.btn-guardar,
.btn-cancelar {
  padding: 8px 16px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  flex: 1;
}

.btn-guardar {
  background: #28a745;
  color: white;
}

.btn-guardar:hover {
  background: #218838;
}

.btn-cancelar {
  background: #6c757d;
  color: white;
}

.btn-cancelar:hover {
  background: #5a6268;
}

/* Footer */
footer {
  margin-top: 40px;
  padding-top: 20px;
  border-top: 2px solid #e9ecef;
  text-align: center;
  color: #666;
  font-size: 0.9rem;
}

/* Responsive */
@media (max-width: 600px) {
  .container {
    padding: 20px;
  }

  header h1 {
    font-size: 2rem;
  }

  .tarea-acciones {
    flex-direction: column;
  }

  .btn-editar,
  .btn-eliminar {
    width: 100%;
  }
}
```

</details>

<details>

<summary><strong>src/main.jsx</strong> →[Entry point de REACT]</summary>

```jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './App.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)
```

</details>

Algunos archivos más necesarios más del proyecto:

<details>

<summary><strong>index.html</strong> (raíz del proyecto):</summary>

```html
<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Gestor de Tareas MERN</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
```

</details>

<details>

<summary><strong>vite.config.js</strong>:</summary>

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})
```

</details>

<details>

<summary><strong>.gitignore</strong>:</summary>

```
# Dependencias
node_modules/

# Archivos de producción
dist/

# Variables de entorno
.env
.env.local

# Logs
npm-debug.log*
yarn-debug.log*

# Editor
.vscode/
.idea/
```

</details>

### Variables de entorno

Contenido (preparado para producción):

```dotenv
# .env
# Archivo de configuración para el frontend

# URL del backend API
# En desarrollo local:
VITE_API_URL=http://localhost:3001

# En producción con Nginx (mismo dominio):
# VITE_API_URL=/api

# En producción con backend separado:
# VITE_API_URL=https://api.tudominio.com
```
