Skip to content
English
On this page

Ejercicio: Agenda de Contactos con Python y AWS

Escenario

Implementaremos una agenda de contactos que incluye:

  • Backend con Python/Flask
  • Base de datos RDS PostgreSQL
  • AWS Elastic Beanstalk
  • Pipeline con CodeCommit/Build/Deploy
  • Variables con Parameter Store

Estructura del Proyecto

contact-manager/
├── .ebextensions/
│   ├── 01_packages.config
│   └── 02_python.config

├── .aws/
│   ├── buildspec/
│   │   ├── buildspec-dev.yml
│   │   ├── buildspec-stg.yml
│   │   └── buildspec-prod.yml
│   │
│   └── scripts/
│       ├── setup_codecommit.sh
│       └── initialize_repo.sh

├── app/
│   ├── api/
│   │   ├── __init__.py
│   │   ├── contacts.py
│   │   ├── auth.py
│   │   └── utils.py
│   │
│   ├── models/
│   │   ├── __init__.py
│   │   ├── contact.py
│   │   └── user.py
│   │
│   ├── schemas/
│   │   ├── __init__.py
│   │   └── contact.py
│   │
│   └── services/
│       ├── __init__.py
│       ├── contact_service.py
│       └── notification_service.py

├── config/
│   ├── __init__.py
│   ├── dev.py
│   ├── stg.py
│   ├── prod.py
│   └── base.py

├── migrations/
│   └── versions/

├── tests/
│   ├── unit/
│   │   └── test_contacts.py
│   └── integration/
│       └── test_api.py

├── requirements/
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt

├── application.py
├── requirements.txt
└── README.md

1. Configuración de CodeCommit

1.1 Script de Configuración de Repositorio

bash
# .aws/scripts/setup_codecommit.sh
#!/bin/bash

# Variables
REPO_NAME="contact-manager"
REGION="us-east-1"

# Crear repositorio
aws codecommit create-repository \
    --repository-name $REPO_NAME \
    --repository-description "Agenda de Contactos - Python/Beanstalk Application"

# Configurar credenciales Git
aws iam create-service-specific-credential \
    --user-name $USER \
    --service-name codecommit.amazonaws.com

# Configurar Git local
git config --global credential.helper '!aws codecommit credential-helper $@'
git config --global credential.UseHttpPath true

1.2 Script de Inicialización

bash
# .aws/scripts/initialize_repo.sh
#!/bin/bash

# Variables
REPO_URL=$(aws codecommit get-repository --repository-name contact-manager --query 'repositoryMetadata.cloneUrlHttp' --output text)

# Inicializar repo local
git init
git add .
git commit -m "Initial commit"

# Conectar con CodeCommit
git remote add origin $REPO_URL
git push -u origin master

# Crear branches para entornos
for env in dev stg prod; do
    git checkout -b $env
    git push origin $env
done

2. Configuración Base de la Aplicación

2.1 Configuración Flask

python
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_marshmallow import Marshmallow
from config import config

db = SQLAlchemy()
migrate = Migrate()
ma = Marshmallow()

def create_app(config_name):
    app = Flask(__name__)
    
    # Cargar configuración
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)
    
    # Inicializar extensiones
    db.init_app(app)
    migrate.init_app(app, db)
    ma.init_app(app)
    
    # Registrar blueprints
    from app.api import contacts_bp, auth_bp
    app.register_blueprint(contacts_bp, url_prefix='/api/contacts')
    app.register_blueprint(auth_bp, url_prefix='/api/auth')
    
    return app

2.2 Modelo de Contacto

python
# app/models/contact.py
from app import db
from datetime import datetime

class Contact(db.Model):
    __tablename__ = 'contacts'
    
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(50), nullable=False)
    last_name = db.Column(db.String(50), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    phone = db.Column(db.String(20))
    address = db.Column(db.String(200))
    notes = db.Column(db.Text)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def __repr__(self):
        return f'<Contact {self.first_name} {self.last_name}>'

2.3 Schema de Contacto

python
# app/schemas/contact.py
from app import ma
from app.models.contact import Contact
from marshmallow import fields, validates, ValidationError

class ContactSchema(ma.SQLAlchemySchema):
    class Meta:
        model = Contact

    id = ma.auto_field()
    first_name = fields.Str(required=True)
    last_name = fields.Str(required=True)
    email = fields.Email(required=True)
    phone = fields.Str()
    address = fields.Str()
    notes = fields.Str()
    created_at = fields.DateTime(dump_only=True)
    updated_at = fields.DateTime(dump_only=True)
    
    @validates('phone')
    def validate_phone(self, value):
        if value and not value.replace('+', '').isdigit():
            raise ValidationError('Invalid phone number format')

contact_schema = ContactSchema()
contacts_schema = ContactSchema(many=True)

2.4 Requerimientos Base

# requirements/base.txt
Flask==2.0.1
Flask-SQLAlchemy==2.5.1
Flask-Migrate==3.1.0
Flask-Marshmallow==0.14.0
marshmallow-sqlalchemy==0.26.1
psycopg2-binary==2.9.1
python-dotenv==0.19.0
boto3==1.18.40
requests==2.26.0

3. Configuración de Entorno

3.1 Configuración Base

python
# config/base.py
import os
from datetime import timedelta

class Config:
    # Configuración base
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # JWT
    JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'jwt-secret-string'
    JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
    
    # Configuración de AWS
    AWS_REGION = os.environ.get('AWS_REGION', 'us-east-1')
    
    @staticmethod
    def init_app(app):
        pass

3.2 Configuración por Entorno

python
# config/dev.py
from .base import Config

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL')

Verificación Parte 1

1. Verificar CodeCommit

  • [ ] Repositorio creado
  • [ ] Credenciales configuradas
  • [ ] Branches creados
  • [ ] Permisos correctos

2. Verificar Aplicación

  • [ ] Estructura creada
  • [ ] Modelos definidos
  • [ ] Schemas configurados
  • [ ] Requerimientos instalados

3. Verificar Configuración

  • [ ] Variables cargadas
  • [ ] Entornos separados
  • [ ] Base de datos configurada
  • [ ] Seguridad establecida

Troubleshooting Común

Errores de CodeCommit

  1. Verificar credenciales
  2. Revisar permisos IAM
  3. Verificar Git config

Errores de Aplicación

  1. Verificar dependencias
  2. Revisar imports
  3. Verificar modelos

Errores de Configuración

  1. Verificar variables
  2. Revisar conexiones
  3. Verificar permisos

Parte 2: Elastic Beanstalk y API

1. Configuración de Elastic Beanstalk

1.1 Configuración de Ambiente

yaml
# .ebextensions/01_packages.config
packages:
  yum:
    python3-devel: []
    postgresql-devel: []
    gcc: []

option_settings:
  aws:elasticbeanstalk:container:python:
    WSGIPath: application:application
    NumProcesses: 3
    NumThreads: 20
  
  aws:elasticbeanstalk:application:environment:
    FLASK_ENV: production
    PYTHONPATH: "/var/app/current"
    
  aws:autoscaling:asg:
    MinSize: 2
    MaxSize: 4
    
  aws:autoscaling:trigger:
    UpperThreshold: 80
    LowerThreshold: 40
    MeasureName: CPUUtilization
    Unit: Percent
    
  aws:elasticbeanstalk:environment:proxy:
    ProxyServer: apache

1.2 Configuración de Python

yaml
# .ebextensions/02_python.config
container_commands:
  01_migrate:
    command: |
      source /var/app/venv/*/bin/activate
      python manage.py db upgrade
    leader_only: true
  
  02_collectstatic:
    command: |
      source /var/app/venv/*/bin/activate
      python manage.py collectstatic --noinput
    leader_only: true

option_settings:
  aws:elasticbeanstalk:container:python:
    WSGIPath: application:application
  
  aws:elasticbeanstalk:application:environment:
    PYTHONPATH: "/var/app/current"

1.3 Script de Despliegue

bash
# scripts/deploy_to_eb.sh
#!/bin/bash

ENVIRONMENT=$1
APPLICATION_NAME="contact-manager"
S3_BUCKET="elasticbeanstalk-deployment-artifacts"

# Validar ambiente
if [[ ! "$ENVIRONMENT" =~ ^(dev|stg|prod)$ ]]; then
    echo "Environment must be dev, stg, or prod"
    exit 1
fi

# Crear archivo de despliegue
zip -r deployment.zip . -x "*.git*" "*.pyc" "__pycache__/*"

# Subir a S3
aws s3 cp deployment.zip s3://$S3_BUCKET/$APPLICATION_NAME-$ENVIRONMENT.zip

# Crear nueva versión
aws elasticbeanstalk create-application-version \
    --application-name $APPLICATION_NAME \
    --version-label $ENVIRONMENT-$(date +%Y%m%d%H%M%S) \
    --source-bundle S3Bucket=$S3_BUCKET,S3Key=$APPLICATION_NAME-$ENVIRONMENT.zip

# Actualizar ambiente
aws elasticbeanstalk update-environment \
    --environment-name $APPLICATION_NAME-$ENVIRONMENT \
    --version-label $ENVIRONMENT-$(date +%Y%m%d%H%M%S)

2. Implementación de API

2.1 Rutas de Contactos

python
# app/api/contacts.py
from flask import Blueprint, request, jsonify
from app.models.contact import Contact
from app.schemas.contact import contact_schema, contacts_schema
from app.services.contact_service import ContactService
from app.api.auth import jwt_required

contacts_bp = Blueprint('contacts', __name__)
contact_service = ContactService()

@contacts_bp.route('', methods=['GET'])
@jwt_required
def get_contacts():
    user_id = request.user.id
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    
    contacts = contact_service.get_contacts(user_id, page, per_page)
    return jsonify(contacts_schema.dump(contacts)), 200

@contacts_bp.route('/<int:contact_id>', methods=['GET'])
@jwt_required
def get_contact(contact_id):
    user_id = request.user.id
    contact = contact_service.get_contact(contact_id, user_id)
    if not contact:
        return jsonify({'message': 'Contact not found'}), 404
    return jsonify(contact_schema.dump(contact)), 200

@contacts_bp.route('', methods=['POST'])
@jwt_required
def create_contact():
    user_id = request.user.id
    data = request.get_json()
    
    errors = contact_schema.validate(data)
    if errors:
        return jsonify({'errors': errors}), 400
        
    contact = contact_service.create_contact(data, user_id)
    return jsonify(contact_schema.dump(contact)), 201

@contacts_bp.route('/<int:contact_id>', methods=['PUT'])
@jwt_required
def update_contact(contact_id):
    user_id = request.user.id
    data = request.get_json()
    
    errors = contact_schema.validate(data)
    if errors:
        return jsonify({'errors': errors}), 400
        
    contact = contact_service.update_contact(contact_id, data, user_id)
    if not contact:
        return jsonify({'message': 'Contact not found'}), 404
    return jsonify(contact_schema.dump(contact)), 200

@contacts_bp.route('/<int:contact_id>', methods=['DELETE'])
@jwt_required
def delete_contact(contact_id):
    user_id = request.user.id
    if contact_service.delete_contact(contact_id, user_id):
        return '', 204
    return jsonify({'message': 'Contact not found'}), 404

2.2 Servicio de Contactos

python
# app/services/contact_service.py
from app.models.contact import Contact
from app import db
from sqlalchemy.exc import SQLAlchemyError

class ContactService:
    def get_contacts(self, user_id, page=1, per_page=10):
        """Obtener lista paginada de contactos"""
        return Contact.query.filter_by(user_id=user_id)\
            .order_by(Contact.last_name)\
            .paginate(page=page, per_page=per_page, error_out=False)
    
    def get_contact(self, contact_id, user_id):
        """Obtener un contacto específico"""
        return Contact.query.filter_by(id=contact_id, user_id=user_id).first()
    
    def create_contact(self, data, user_id):
        """Crear un nuevo contacto"""
        try:
            contact = Contact(user_id=user_id, **data)
            db.session.add(contact)
            db.session.commit()
            return contact
        except SQLAlchemyError as e:
            db.session.rollback()
            raise e
    
    def update_contact(self, contact_id, data, user_id):
        """Actualizar un contacto existente"""
        contact = self.get_contact(contact_id, user_id)
        if not contact:
            return None
            
        try:
            for key, value in data.items():
                setattr(contact, key, value)
            db.session.commit()
            return contact
        except SQLAlchemyError as e:
            db.session.rollback()
            raise e
    
    def delete_contact(self, contact_id, user_id):
        """Eliminar un contacto"""
        contact = self.get_contact(contact_id, user_id)
        if not contact:
            return False
            
        try:
            db.session.delete(contact)
            db.session.commit()
            return True
        except SQLAlchemyError as e:
            db.session.rollback()
            raise e

2.3 Servicio de Notificaciones

python
# app/services/notification_service.py
import boto3
from botocore.exceptions import ClientError

class NotificationService:
    def __init__(self):
        self.sns = boto3.client('sns')
        self.topic_arn = os.environ.get('SNS_TOPIC_ARN')
    
    def notify_contact_created(self, contact):
        """Notificar cuando se crea un nuevo contacto"""
        try:
            message = {
                'action': 'contact_created',
                'contact_id': contact.id,
                'name': f'{contact.first_name} {contact.last_name}',
                'email': contact.email
            }
            
            self.sns.publish(
                TopicArn=self.topic_arn,
                Message=json.dumps(message),
                Subject='Nuevo Contacto Creado'
            )
        except ClientError as e:
            print(f"Error sending notification: {e}")
            
    def notify_contact_deleted(self, contact):
        """Notificar cuando se elimina un contacto"""
        try:
            message = {
                'action': 'contact_deleted',
                'contact_id': contact.id,
                'name': f'{contact.first_name} {contact.last_name}'
            }
            
            self.sns.publish(
                TopicArn=self.topic_arn,
                Message=json.dumps(message),
                Subject='Contacto Eliminado'
            )
        except ClientError as e:
            print(f"Error sending notification: {e}")

3. Manejo de Errores

3.1 Error Handlers

python
# app/api/errors.py
from flask import jsonify
from app import db
from sqlalchemy.exc import SQLAlchemyError

def register_error_handlers(app):
    @app.errorhandler(404)
    def not_found_error(error):
        return jsonify({'error': 'Resource not found'}), 404
        
    @app.errorhandler(400)
    def bad_request_error(error):
        return jsonify({'error': 'Bad request'}), 400
        
    @app.errorhandler(SQLAlchemyError)
    def database_error(error):
        db.session.rollback()
        return jsonify({'error': 'Database error occurred'}), 500
        
    @app.errorhandler(Exception)
    def internal_error(error):
        db.session.rollback()
        return jsonify({'error': 'Internal server error'}), 500

Verificación Parte 2

1. Verificar Elastic Beanstalk

  • [ ] Ambiente creado
  • [ ] Configuración correcta
  • [ ] Deploy funcional
  • [ ] Auto scaling configurado

2. Verificar API

  • [ ] Endpoints funcionando
  • [ ] CRUD completo
  • [ ] Validaciones activas
  • [ ] Notificaciones enviadas

3. Verificar Errores

  • [ ] Manejo correcto
  • [ ] Respuestas apropiadas
  • [ ] Logging configurado
  • [ ] Rollbacks funcionando

Troubleshooting Común

Errores de Beanstalk

  1. Verificar logs
  2. Revisar configuración
  3. Verificar permisos

Errores de API

  1. Verificar rutas
  2. Revisar validaciones
  3. Verificar servicios

Errores de Base de Datos

  1. Verificar conexión
  2. Revisar migraciones
  3. Verificar modelos

Parte 3: CodeBuild y CI/CD Pipeline

1. Configuración de CodeBuild

1.1 Buildspec Principal

yaml
# .aws/buildspec/buildspec-dev.yml
version: 0.2

env:
  variables:
    PYTHON_VERSION: 3.9
  parameter-store:
    DATABASE_URL: "/contact-manager/dev/database_url"
    SECRET_KEY: "/contact-manager/dev/secret_key"

phases:
  install:
    runtime-versions:
      python: 3.9
    commands:
      - pip install --upgrade pip
      - pip install -r requirements/dev.txt
      
  pre_build:
    commands:
      - echo "Running tests..."
      - pytest tests/
      - python -m flake8 app/
      
  build:
    commands:
      - echo "Running migrations..."
      - python manage.py db upgrade
      - echo "Building application..."
      - python setup.py sdist
      
  post_build:
    commands:
      - echo "Running security checks..."
      - bandit -r app/
      - safety check

artifacts:
  files:
    - '**/*'
  base-directory: dist

cache:
  paths:
    - '/root/.cache/pip'

1.2 BuildSpec por Entorno

yaml
# .aws/buildspec/buildspec-prod.yml
version: 0.2

env:
  variables:
    PYTHON_VERSION: 3.9
  parameter-store:
    DATABASE_URL: "/contact-manager/prod/database_url"
    SECRET_KEY: "/contact-manager/prod/secret_key"

phases:
  install:
    runtime-versions:
      python: 3.9
    commands:
      - pip install --upgrade pip
      - pip install -r requirements/prod.txt
      
  pre_build:
    commands:
      - echo "Running security checks..."
      - safety check
      - bandit -r app/
      
  build:
    commands:
      - echo "Building application..."
      - python setup.py sdist
      
  post_build:
    commands:
      - echo "Preparing deployment package..."
      - cd dist && zip -r ../deployment.zip .
      - aws s3 cp ../deployment.zip s3://my-deployment-bucket/contact-manager/prod/

artifacts:
  files:
    - deployment.zip

2. Configuración de Pipeline

2.1 Pipeline Definition

yaml
# infrastructure/pipeline/pipeline.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CodePipeline for Contact Manager Application'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]
    
Resources:
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        
  PipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodePipelineFullAccess
        
  ApplicationPipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub contact-manager-${Environment}-pipeline
      RoleArn: !GetAtt PipelineServiceRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration:
                RepositoryName: contact-manager
                BranchName: !Ref Environment
              OutputArtifacts:
                - Name: SourceCode
                
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              InputArtifacts:
                - Name: SourceCode
              OutputArtifacts:
                - Name: BuildOutput
                
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: ElasticBeanstalk
              Configuration:
                ApplicationName: contact-manager
                EnvironmentName: !Sub contact-manager-${Environment}
              InputArtifacts:
                - Name: BuildOutput

2.2 Build Project

yaml
# infrastructure/pipeline/build.yaml
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub contact-manager-${Environment}-build
      ServiceRole: !GetAtt CodeBuildServiceRole.Arn
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
        EnvironmentVariables:
          - Name: ENVIRONMENT
            Value: !Ref Environment
          - Name: DATABASE_URL
            Type: PARAMETER_STORE
            Value: !Sub "/contact-manager/${Environment}/database_url"
      Source:
        Type: CODEPIPELINE
        BuildSpec: !Sub buildspec-${Environment}.yml
      Cache:
        Type: S3
        Location: !Sub ${ArtifactBucket}/cache

3. Automatización de Tests

3.1 Test Configuration

python
# tests/conftest.py
import pytest
from app import create_app, db
from app.models.contact import Contact
from app.models.user import User

@pytest.fixture
def app():
    app = create_app('testing')
    
    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()

@pytest.fixture
def test_user(app):
    with app.app_context():
        user = User(
            email='test@example.com',
            password='testpass123'
        )
        db.session.add(user)
        db.session.commit()
        return user

@pytest.fixture
def test_contact(app, test_user):
    with app.app_context():
        contact = Contact(
            first_name='John',
            last_name='Doe',
            email='john@example.com',
            phone='1234567890',
            user_id=test_user.id
        )
        db.session.add(contact)
        db.session.commit()
        return contact

3.2 API Tests

python
# tests/test_api.py
def test_get_contacts(client, test_user, test_contact):
    token = create_access_token(test_user)
    headers = {'Authorization': f'Bearer {token}'}
    
    response = client.get('/api/contacts', headers=headers)
    assert response.status_code == 200
    data = response.get_json()
    assert len(data) == 1
    assert data[0]['email'] == test_contact.email

def test_create_contact(client, test_user):
    token = create_access_token(test_user)
    headers = {
        'Authorization': f'Bearer {token}',
        'Content-Type': 'application/json'
    }
    
    contact_data = {
        'first_name': 'Jane',
        'last_name': 'Doe',
        'email': 'jane@example.com',
        'phone': '0987654321'
    }
    
    response = client.post('/api/contacts', 
                         json=contact_data,
                         headers=headers)
    assert response.status_code == 201
    data = response.get_json()
    assert data['email'] == contact_data['email']

3.3 Test Script

bash
#!/bin/bash
# scripts/run_tests.sh

set -e

# Activar entorno virtual
source venv/bin/activate

# Instalar dependencias de test
pip install -r requirements/test.txt

# Configurar variables de entorno para tests
export FLASK_ENV=testing
export DATABASE_URL=postgresql://localhost/contacts_test

# Ejecutar tests con coverage
pytest --cov=app tests/ --cov-report=xml --cov-report=term-missing

# Ejecutar análisis de código
flake8 app/
black --check app/
bandit -r app/

# Verificar dependencias
safety check

# Desactivar entorno virtual
deactivate

Verificación Parte 3

1. Verificar CodeBuild

  • [ ] Build exitoso
  • [ ] Tests pasando
  • [ ] Artefactos generados
  • [ ] Cache funcionando

2. Verificar Pipeline

  • [ ] Stages completos
  • [ ] Roles configurados
  • [ ] Artefactos movidos
  • [ ] Despliegue exitoso

3. Verificar Tests

  • [ ] Unit tests pasando
  • [ ] Integration tests OK
  • [ ] Cobertura adecuada
  • [ ] Análisis de código OK

Troubleshooting Común

Errores de Build

  1. Verificar buildspec
  2. Revisar dependencias
  3. Verificar permisos

Errores de Pipeline

  1. Verificar roles
  2. Revisar artefactos
  3. Verificar stages

Errores de Tests

  1. Verificar fixtures
  2. Revisar cobertura
  3. Verificar entorno

Parte 4: Frontend e Integración

1. Estructura Frontend

1.1 Organización de Archivos

contact-manager/
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── contacts/
│   │   │   │   ├── ContactList.js
│   │   │   │   ├── ContactForm.js
│   │   │   │   ├── ContactDetail.js
│   │   │   │   └── ContactSearch.js
│   │   │   ├── auth/
│   │   │   │   ├── Login.js
│   │   │   │   └── Register.js
│   │   │   └── common/
│   │   │       ├── Header.js
│   │   │       ├── Footer.js
│   │   │       └── Loading.js
│   │   ├── services/
│   │   │   ├── api.js
│   │   │   ├── auth.js
│   │   │   └── contacts.js
│   │   ├── hooks/
│   │   │   ├── useAuth.js
│   │   │   └── useContacts.js
│   │   └── utils/
│   │       ├── constants.js
│   │       └── validators.js
│   ├── public/
│   │   └── index.html
│   ├── package.json
│   └── webpack.config.js

1.2 Componente de Lista de Contactos

javascript
// frontend/src/components/contacts/ContactList.js
import React from 'react';
import { useContacts } from '../../hooks/useContacts';
import { Loading } from '../common/Loading';

export const ContactList = () => {
  const { 
    contacts, 
    loading, 
    error, 
    deleteContact 
  } = useContacts();

  if (loading) return <Loading />;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div className="contact-list">
      <h2>Mis Contactos</h2>
      <div className="contact-grid">
        {contacts.map(contact => (
          <div key={contact.id} className="contact-card">
            <div className="contact-info">
              <h3>{contact.first_name} {contact.last_name}</h3>
              <p>{contact.email}</p>
              <p>{contact.phone}</p>
            </div>
            <div className="contact-actions">
              <button 
                onClick={() => deleteContact(contact.id)}
                className="delete-btn"
              >
                Eliminar
              </button>
              <button 
                onClick={() => editContact(contact)}
                className="edit-btn"
              >
                Editar
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

1.3 Formulario de Contacto

javascript
// frontend/src/components/contacts/ContactForm.js
import React, { useState } from 'react';
import { useContacts } from '../../hooks/useContacts';
import { validateContact } from '../../utils/validators';

export const ContactForm = ({ contact = null, onSuccess }) => {
  const [formData, setFormData] = useState(contact || {
    first_name: '',
    last_name: '',
    email: '',
    phone: '',
    address: ''
  });

  const [errors, setErrors] = useState({});
  const { createContact, updateContact } = useContacts();

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Validar formulario
    const validationErrors = validateContact(formData);
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }

    try {
      if (contact) {
        await updateContact(contact.id, formData);
      } else {
        await createContact(formData);
      }
      onSuccess();
    } catch (error) {
      setErrors({ submit: error.message });
    }
  };

  return (
    <form onSubmit={handleSubmit} className="contact-form">
      <div className="form-group">
        <label>Nombre</label>
        <input
          type="text"
          value={formData.first_name}
          onChange={(e) => setFormData({
            ...formData,
            first_name: e.target.value
          })}
          className={errors.first_name ? 'error' : ''}
        />
        {errors.first_name && (
          <span className="error-text">{errors.first_name}</span>
        )}
      </div>

      <div className="form-group">
        <label>Apellido</label>
        <input
          type="text"
          value={formData.last_name}
          onChange={(e) => setFormData({
            ...formData,
            last_name: e.target.value
          })}
          className={errors.last_name ? 'error' : ''}
        />
        {errors.last_name && (
          <span className="error-text">{errors.last_name}</span>
        )}
      </div>

      <div className="form-group">
        <label>Email</label>
        <input
          type="email"
          value={formData.email}
          onChange={(e) => setFormData({
            ...formData,
            email: e.target.value
          })}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && (
          <span className="error-text">{errors.email}</span>
        )}
      </div>

      <button type="submit" className="submit-btn">
        {contact ? 'Actualizar' : 'Crear'} Contacto
      </button>
    </form>
  );
};

2. Servicios y Hooks

2.1 API Service

javascript
// frontend/src/services/api.js
import axios from 'axios';

const api = axios.create({
  baseURL: process.env.REACT_APP_API_URL
});

// Interceptor para agregar token
api.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Interceptor para manejar errores
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default api;

2.2 Contacts Service

javascript
// frontend/src/services/contacts.js
import api from './api';

export const contactsService = {
  getContacts: async (page = 1) => {
    const response = await api.get(`/contacts?page=${page}`);
    return response.data;
  },

  getContact: async (id) => {
    const response = await api.get(`/contacts/${id}`);
    return response.data;
  },

  createContact: async (contactData) => {
    const response = await api.post('/contacts', contactData);
    return response.data;
  },

  updateContact: async (id, contactData) => {
    const response = await api.put(`/contacts/${id}`, contactData);
    return response.data;
  },

  deleteContact: async (id) => {
    await api.delete(`/contacts/${id}`);
  },

  searchContacts: async (query) => {
    const response = await api.get(`/contacts/search?q=${query}`);
    return response.data;
  }
};

2.3 Custom Hooks

javascript
// frontend/src/hooks/useContacts.js
import { useState, useEffect } from 'react';
import { contactsService } from '../services/contacts';

export const useContacts = () => {
  const [contacts, setContacts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [page, setPage] = useState(1);

  const loadContacts = async () => {
    try {
      setLoading(true);
      const data = await contactsService.getContacts(page);
      setContacts(data);
      setError(null);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadContacts();
  }, [page]);

  const createContact = async (contactData) => {
    try {
      const newContact = await contactsService.createContact(contactData);
      setContacts([...contacts, newContact]);
      return newContact;
    } catch (err) {
      setError(err);
      throw err;
    }
  };

  const updateContact = async (id, contactData) => {
    try {
      const updatedContact = await contactsService.updateContact(id, contactData);
      setContacts(contacts.map(contact => 
        contact.id === id ? updatedContact : contact
      ));
      return updatedContact;
    } catch (err) {
      setError(err);
      throw err;
    }
  };

  const deleteContact = async (id) => {
    try {
      await contactsService.deleteContact(id);
      setContacts(contacts.filter(contact => contact.id !== id));
    } catch (err) {
      setError(err);
      throw err;
    }
  };

  return {
    contacts,
    loading,
    error,
    page,
    setPage,
    createContact,
    updateContact,
    deleteContact
  };
};

3. Configuración de Build

3.1 Webpack Config

javascript
// frontend/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Dotenv = require('dotenv-webpack');

module.exports = (env) => {
  return {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: '[name].[contenthash].js',
      clean: true
    },
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env', '@babel/preset-react']
            }
          }
        },
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader']
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: './public/index.html'
      }),
      new Dotenv({
        path: `./.env.${env.environment}`
      })
    ],
    devServer: {
      historyApiFallback: true,
      port: 3000
    },
    optimization: {
      splitChunks: {
        chunks: 'all'
      }
    }
  };
};

Verificación Parte 4

1. Verificar Frontend

  • [ ] Componentes renderizando
  • [ ] CRUD funcionando
  • [ ] Validaciones activas
  • [ ] Estilos aplicados

2. Verificar Integración

  • [ ] API conectada
  • [ ] Auth funcionando
  • [ ] Errores manejados
  • [ ] Loading states OK

3. Verificar Build

  • [ ] Webpack configurado
  • [ ] Assets optimizados
  • [ ] Env variables OK
  • [ ] Bundle size óptimo

Troubleshooting Común

Errores de Frontend

  1. Verificar consola
  2. Revisar network
  3. Verificar props

Errores de API

  1. Verificar endpoints
  2. Revisar tokens
  3. Verificar CORS

Errores de Build

  1. Verificar dependencias
  2. Revisar configuración
  3. Verificar entorno