# Capítulo 6: Estructura del Backend y Despliegue de la Aplicación En este capítulo, exploraremos la estructura del backend que hemos implementado para nuestra aplicación de reservas de gimnasio y el proceso de despliegue para dispositivos móviles reales. ## 1. Arquitectura del Backend Nuestra aplicación ahora cuenta con un backend real basado en Node.js, Express y PostgreSQL que gestiona todas las operaciones de datos. Este enfoque tiene varias ventajas sobre el almacenamiento local: - Datos centralizados y accesibles desde cualquier dispositivo - Mayor seguridad y control de acceso - Escalabilidad mejorada para manejar más usuarios - Sincronización de datos en tiempo real ### Estructura de Carpetas del Backend ``` backend/ ├── config/ │ └── db.js # Configuración de la base de datos ├── controllers/ │ ├── authController.js # Controlador de autenticación │ ├── bookingController.js # Controlador de reservas │ ├── gymClassController.js # Controlador de clases │ ├── uploadController.js # Controlador para subida de archivos │ └── userController.js # Controlador de usuarios ├── models/ │ ├── Booking.js # Modelo de datos para reservas │ ├── GymClass.js # Modelo de datos para clases │ ├── User.js # Modelo de datos para usuarios │ └── index.js # Exporta todos los modelos ├── routes/ │ ├── authRoutes.js # Rutas de autenticación │ ├── bookingRoutes.js # Rutas de reservas │ ├── gymClassRoutes.js # Rutas de clases │ ├── uploadRoutes.js # Rutas para subida de archivos │ └── userRoutes.js # Rutas de usuarios ├── uploads/ # Directorio para archivos subidos ├── server.js # Punto de entrada principal └── package.json # Dependencias y scripts ``` ### Tecnologías Utilizadas en el Backend - **Node.js**: Entorno de ejecución JavaScript del lado del servidor - **Express**: Framework web para crear APIs REST - **PostgreSQL**: Base de datos relacional robusta para almacenar los datos - **Sequelize**: ORM para modelar los datos y conectar con PostgreSQL - **JWT (JSON Web Tokens)**: Para gestionar la autenticación de usuarios - **Multer**: Para manejar la subida de archivos ## 2. Implementación del Backend ### Configuración de la Base de Datos **config/db.js**: ```javascript const { Sequelize } = require('sequelize'); const dotenv = require('dotenv'); dotenv.config(); // Create Sequelize instance let sequelize; if (process.env.DATABASE_URL) { // Use connection string if available sequelize = new Sequelize(process.env.DATABASE_URL, { logging: process.env.NODE_ENV === 'development' ? console.log : false, define: { freezeTableName: true // Prevent Sequelize from pluralizing table names }, dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, pool: { max: 5, min: 0, acquire: 30000, idle: 10000 } }); } else { // Fall back to individual parameters sequelize = new Sequelize( process.env.DB_NAME || 'postgres', process.env.DB_USER || 'postgres', process.env.DB_PASSWORD || 'postgres', { host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, dialect: 'postgres', logging: process.env.NODE_ENV === 'development' ? console.log : false, define: { freezeTableName: true // Prevent Sequelize from pluralizing table names }, dialectOptions: { ssl: process.env.NODE_ENV === 'production' ? { require: true, rejectUnauthorized: false } : false }, pool: { max: 5, min: 0, acquire: 30000, idle: 10000 } } ); } // Test the connection const connectDB = async () => { try { await sequelize.authenticate(); console.log('Database connection established successfully.'); } catch (error) { console.error('Unable to connect to the database:', error); process.exit(1); } }; module.exports = { sequelize, connectDB }; ``` ### Modelos de Datos **models/User.js**: ```javascript const { DataTypes } = require('sequelize'); const { sequelize } = require('../config/db'); const bcrypt = require('bcryptjs'); const User = sequelize.define('user', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.STRING, allowNull: false }, email: { type: DataTypes.STRING, allowNull: false, unique: true, validate: { isEmail: true } }, password: { type: DataTypes.STRING, allowNull: false }, profilePicUrl: { type: DataTypes.STRING, defaultValue: '/uploads/default-avatar.jpg' }, notificationsEnabled: { type: DataTypes.BOOLEAN, defaultValue: true } }, { hooks: { beforeCreate: async (user) => { if (user.password) { const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(user.password, salt); } }, beforeUpdate: async (user) => { if (user.changed('password')) { const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(user.password, salt); } } }, tableName: 'users' // Explicitly set lowercase table name }); // Method to check if password matches User.prototype.matchPassword = async function(enteredPassword) { return await bcrypt.compare(enteredPassword, this.password); }; module.exports = User; ``` **models/GymClass.js**: ```javascript const { DataTypes } = require('sequelize'); const { sequelize } = require('../config/db'); const GymClass = sequelize.define('gymClass', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.STRING, allowNull: false }, description: { type: DataTypes.TEXT, allowNull: false }, instructor: { type: DataTypes.STRING, allowNull: false }, startTime: { type: DataTypes.DATE, allowNull: false }, endTime: { type: DataTypes.DATE, allowNull: false }, maxCapacity: { type: DataTypes.INTEGER, allowNull: false }, currentBookings: { type: DataTypes.INTEGER, defaultValue: 0 }, category: { type: DataTypes.STRING, defaultValue: 'Otros' }, imageUrl: { type: DataTypes.STRING, defaultValue: '/uploads/default-class.jpg' } }, { tableName: 'classes' // Explicitly set lowercase table name }); module.exports = GymClass; ``` **models/Booking.js**: ```javascript const { DataTypes } = require('sequelize'); const { sequelize } = require('../config/db'); const Booking = sequelize.define('booking', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, userId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' } }, classId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'classes', key: 'id' } }, className: { type: DataTypes.STRING, allowNull: false }, date: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, status: { type: DataTypes.ENUM('confirmed', 'cancelled', 'pending'), defaultValue: 'confirmed' } }, { tableName: 'bookings' // Explicitly set lowercase table name }); module.exports = Booking; ``` **models/index.js**: ```javascript const User = require('./User'); const GymClass = require('./GymClass'); const Booking = require('./Booking'); // Define associations User.hasMany(Booking, { foreignKey: 'userId' }); Booking.belongsTo(User, { foreignKey: 'userId' }); GymClass.hasMany(Booking, { foreignKey: 'classId' }); Booking.belongsTo(GymClass, { foreignKey: 'classId' }); module.exports = { User, GymClass, Booking }; ``` ### Controladores **controllers/authController.js**: ```javascript const jwt = require('jsonwebtoken'); const { User } = require('../models'); // Generate JWT Token const generateToken = (id) => { return jwt.sign({ id }, process.env.JWT_SECRET || 'secret_key_12345', { expiresIn: '30d' }); }; // @desc Register a new user // @route POST /api/auth/register // @access Public exports.registerUser = async (req, res) => { try { const { name, email, password } = req.body; // Check if user already exists const existingUser = await User.findOne({ where: { email } }); if (existingUser) { return res.status(400).json({ success: false, message: 'Email is already registered' }); } // Create user const user = await User.create({ name, email, password }); // Generate token const token = generateToken(user.id); res.status(201).json({ success: true, token, user: { id: user.id, name: user.name, email: user.email, profilePicUrl: user.profilePicUrl, notificationsEnabled: user.notificationsEnabled } }); } catch (error) { console.error('Error registering user:', error); res.status(500).json({ success: false, message: 'Server error', error: error.message }); } }; // @desc Login user // @route POST /api/auth/login // @access Public exports.loginUser = async (req, res) => { try { const { email, password } = req.body; // Check for user email const user = await User.findOne({ where: { email } }); if (!user) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Check if password matches const isMatch = await user.matchPassword(password); if (!isMatch) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Generate token const token = generateToken(user.id); res.json({ success: true, token, user: { id: user.id, name: user.name, email: user.email, profilePicUrl: user.profilePicUrl, notificationsEnabled: user.notificationsEnabled } }); } catch (error) { console.error('Error logging in:', error); res.status(500).json({ success: false, message: 'Server error', error: error.message }); } }; ``` ### Rutas API **routes/authRoutes.js**: ```javascript const express = require('express'); const { registerUser, loginUser } = require('../controllers/authController'); const router = express.Router(); router.post('/register', registerUser); router.post('/login', loginUser); module.exports = router; ``` ### Punto de Entrada del Servidor **server.js**: ```javascript const path = require('path'); const express = require('express'); const dotenv = require('dotenv'); const cors = require('cors'); const morgan = require('morgan'); const fs = require('fs'); const { connectDB, sequelize } = require('./config/db'); // Load env variables dotenv.config(); // Import route files const userRoutes = require('./routes/userRoutes'); const gymClassRoutes = require('./routes/gymClassRoutes'); const bookingRoutes = require('./routes/bookingRoutes'); const uploadRoutes = require('./routes/uploadRoutes'); const authRoutes = require('./routes/authRoutes'); // Initialize express app const app = express(); // Middleware app.use(express.json()); app.use(cors()); // Dev logging middleware if (process.env.NODE_ENV === 'development') { app.use(morgan('dev')); } // Create uploads directory if it doesn't exist const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Set static folder for uploads app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // Mount routers app.use('/api/users', userRoutes); app.use('/api/classes', gymClassRoutes); app.use('/api/bookings', bookingRoutes); app.use('/api/upload', uploadRoutes); app.use('/api/auth', authRoutes); // Root route app.get('/', (req, res) => { res.json({ message: 'Welcome to Gym API' }); }); // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ message: 'Server Error', error: err.message }); }); // Define port const PORT = process.env.PORT || 3000; // Connect to database and start server const startServer = async () => { try { // Connect to database await connectDB(); await sequelize.sync(); console.log('Database synchronized'); // Start server app.listen(PORT, () => { console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`); }); } catch (error) { console.error('Unable to start server:', error); process.exit(1); } }; startServer(); // Handle unhandled promise rejections process.on('unhandledRejection', (err) => { console.log(`Error: ${err.message}`); // Close server & exit process process.exit(1); }); ``` ## 3. Despliegue en Plataformas Móviles Una de las grandes ventajas de usar Ionic con Capacitor es la posibilidad de desplegar la misma base de código en múltiples plataformas. Vamos a revisar el proceso para Android. ### Preparación para Android 1. **Instalar las Dependencias de Android**: - Android Studio - JDK (Java Development Kit) - Configurar variables de entorno (ANDROID_HOME, JAVA_HOME) 2. **Añadir la Plataforma Android a nuestro Proyecto**: ```bash # Instalar @capacitor/android si no está instalado npm install @capacitor/android # Añadir Android al proyecto npx cap add android ``` 3. **Sincronizar el Proyecto**: Después de hacer cambios en el código web, necesitamos construir el proyecto y sincronizarlo con las plataformas nativas: ```bash # Construir la aplicación ionic build --prod # Sincronizar con Android npx cap sync android ``` 4. **Abrir el Proyecto en Android Studio**: ```bash npx cap open android ``` ### Configuración del AndroidManifest.xml El archivo `AndroidManifest.xml` es crucial para definir los permisos y configuraciones de la aplicación Android. Ya cubrimos algunos permisos esenciales: ```xml ``` ### Personalización de Iconos y Splash Screen Para personalizar los iconos y la pantalla de inicio: 1. **Reemplazar los Archivos de Iconos**: - Crear iconos para diferentes resoluciones - Reemplazar los archivos en `android/app/src/main/res/mipmap-*/` 2. **Reemplazar la Pantalla de Inicio (Splash Screen)**: - Crear imágenes para diferentes orientaciones y resoluciones - Reemplazar los archivos en `android/app/src/main/res/drawable-*/splash.png` 3. **Personalizar el Nombre de la Aplicación**: - Editar `android/app/src/main/res/values/strings.xml`: ```xml Gym Reservation Gym Reservation com.example.gymreservation com.example.gymreservation ``` ### Construir un APK de Prueba Para generar un APK que puedas compartir con probadores: 1. En Android Studio, selecciona **Build > Build Bundle(s) / APK(s) > Build APK(s)** 2. Cuando termine, haz clic en **locate** para encontrar el archivo APK 3. Este archivo se puede compartir directamente para instalarlo en dispositivos Android (se debe habilitar la instalación desde fuentes desconocidas) ### Construir un Bundle para Google Play Para publicar en la tienda de Google Play: 1. En Android Studio, selecciona **Build > Generate Signed Bundle / APK** 2. Elige **Android App Bundle** 3. Crea o selecciona una clave de firma (keystore) 4. Completa la información necesaria y genera el bundle (.aab) 5. Este archivo es el que se sube a la Google Play Console ## 4. Consideraciones de Seguridad Al desplegar una aplicación móvil que se comunica con un backend, es crucial considerar aspectos de seguridad: ### Autenticación Segura Nuestra aplicación implementa JWT (JSON Web Tokens) para autenticar usuarios, pero aquí hay algunas mejoras adicionales: - **Tokens de Actualización**: Implementar tokens de corta duración con tokens de actualización - **Almacenamiento Seguro**: Utilizar almacenamiento nativo seguro para tokens - **HTTPS**: Garantizar que todas las comunicaciones API usen HTTPS ### Manejo de Datos Sensibles - **No almacenar contraseñas en texto plano**: Ya estamos usando bcrypt para hash de contraseñas - **Minimizar los datos almacenados localmente**: Solo guardar lo necesario en el dispositivo - **Eliminar datos al cerrar sesión**: Limpiar datos sensibles cuando el usuario cierre sesión ### Permisos de Aplicación - **Solicitar solo los permisos necesarios**: No solicitar permisos que no sean esenciales - **Explicar por qué se necesitan permisos**: Proporcionar contexto cuando se solicitan permisos - **Manejar denegaciones graciosamente**: La aplicación debe funcionar aunque se denieguen permisos no críticos ## 5. Optimizaciones de Rendimiento Para garantizar que nuestra aplicación funcione de manera óptima en dispositivos reales: ### Optimización de Imágenes - **Redimensionar imágenes**: Utilizar tamaños apropiados para dispositivos móviles - **Compresión de imágenes**: Reducir el tamaño de archivo sin perder calidad visible - **Carga progresiva**: Implementar carga progresiva para imágenes grandes ### Optimización de Red - **Implementar caché**: Almacenar en caché respuestas de API para reducir solicitudes de red - **Carga perezosa (lazy loading)**: Cargar datos solo cuando sean necesarios - **Compresión de datos**: Utilizar gzip en el servidor para respuestas API ### Optimización de la UI - **Virtualización de listas**: Para listas largas, solo renderizar elementos visibles - **Animaciones eficientes**: Usar animaciones CSS en lugar de JavaScript cuando sea posible - **Evitar work blocking**: No bloquear el hilo principal con operaciones pesadas ## 6. Pruebas y Depuración Para identificar y resolver problemas en dispositivos reales: ### Herramientas de Depuración - **Chrome Remote Debugging**: Para depurar WebView en dispositivos Android - **Safari Web Inspector**: Para depurar WebView en dispositivos iOS - **Capacitor Logs**: Usar `npx cap logs android` para ver logs en tiempo real ### Pruebas en Dispositivos Reales - **Probar en diferentes tamaños de pantalla**: Asegurar que la UI sea responsiva - **Probar con diferentes velocidades de red**: Simular conexiones lentas - **Pruebas de batería**: Verificar que la aplicación no consuma demasiada batería ## Resumen En este capítulo, hemos explorado: 1. La arquitectura de nuestro backend Node.js/Express/PostgreSQL 2. Los detalles de implementación de los modelos, controladores y rutas del backend 3. El proceso de despliegue para dispositivos Android 4. Consideraciones de seguridad para aplicaciones móviles 5. Técnicas de optimización de rendimiento 6. Herramientas y enfoques para pruebas y depuración Con esto, nuestra aplicación Gym Reservation está lista para ser utilizada por usuarios reales, con un backend robusto y una experiencia móvil optimizada.