Skip to content

Stack Tecnológica - Sport Tech Club

Plataforma SaaS Multi-tenant para Gestão de Clubes Esportivos
Arquitetura Event-Driven | Clean Architecture | DDD | Real-time | IoT-enabled


📋 Sumário

  1. Visão Geral
  2. Stack Detalhada
  3. Arquitetura de Serviços
  4. Estrutura do Monorepo
  5. Padrões de Código
  6. Estratégia de Testes
  7. Deploy & CI/CD
  8. Observabilidade
  9. Segurança
  10. Performance & Escalabilidade

🎯 Visão Geral

Objetivos da Plataforma

  • SaaS Multi-tenant: Isolamento de dados, configuração por tenant
  • Real-time First: Placar ao vivo, presença, notificações push
  • IoT-enabled: Tablets, botões físicos, sensores, beacons
  • Event-Driven: Comunicação assíncrona entre serviços
  • Mobile-ready: PWA responsivo/adaptativo
  • Escalável: Suporta crescimento de usuários e clubes
  • DDD/Clean Architecture: Código manutenível e testável

Características Técnicas

┌─────────────────────────────────────────────────────────────┐
│                      SPORT TECH CLUB                         │
│                                                               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │   Web App    │  │  Admin Panel │  │  Mobile PWA  │      │
│  │   (Vue.js)   │  │   (Vue.js)   │  │   (Vue.js)   │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
│         │                  │                  │              │
│         └──────────────────┴──────────────────┘              │
│                           │                                  │
│                  ┌────────▼────────┐                         │
│                  │   API Gateway   │                         │
│                  │    (NestJS)     │                         │
│                  └────────┬────────┘                         │
│         ┌────────────────┼────────────────┐                 │
│         │                │                │                 │
│  ┌──────▼──────┐  ┌─────▼─────┐  ┌──────▼──────┐           │
│  │   Core API  │  │ IoT Service│  │Media Service│           │
│  │  (NestJS)   │  │  (NestJS)  │  │  (Node.js)  │           │
│  └──────┬──────┘  └─────┬─────┘  └──────┬──────┘           │
│         │                │                │                 │
│  ┌──────▼────────────────▼────────────────▼──────┐          │
│  │         Message Bus (RabbitMQ/Redis)           │          │
│  └────────────────────────────────────────────────┘          │
│         │                │                │                 │
│  ┌──────▼──────┐  ┌─────▼─────┐  ┌──────▼──────┐           │
│  │ PostgreSQL  │  │   Redis    │  │     S3      │           │
│  │ (Primary DB)│  │  (Cache)   │  │  (Storage)  │           │
│  └─────────────┘  └───────────┘  └─────────────┘           │
└─────────────────────────────────────────────────────────────┘

🛠 Stack Detalhada

Backend

Runtime & Framework

yaml
Runtime:
  - Node.js: v20.x LTS
  - TypeScript: ^5.3.0

Framework:
  - NestJS: ^10.0.0
    Motivo: |
      - Arquitetura modular (DDD-friendly)
      - Dependency Injection nativo
      - Decorators para validação, guards, interceptors
      - Suporte a WebSockets, GraphQL, CQRS
      - Ecossistema maduro

ORM:
  - Prisma: ^5.0.0
    Motivo: |
      - Type-safe queries
      - Migrations automáticas
      - Introspection de schema
      - Melhor performance que TypeORM
      - Developer Experience superior

Database:
  - PostgreSQL: 15.x
    Motivo: |
      - ACID compliance
      - JSON/JSONB para dados flexíveis
      - Full-text search
      - Row-level security para multi-tenancy
      - Extensões (PostGIS, pg_trgm)

Cache:
  - Redis: 7.x
    Motivo: |
      - Cache de sessões
      - Rate limiting
      - Pub/Sub para real-time
      - Locks distribuídos

Message Queue:
  - RabbitMQ: 3.12.x
    Motivo: |
      - Message routing complexo
      - Dead letter queues
      - Priorização de mensagens
      - Garantias de entrega
  
  - Redis Streams (alternativa):
    Motivo: |
      - Menor complexidade operacional
      - Consumer groups
      - Persistência configurável

Real-time:
  - Socket.io: ^4.6.0
    Motivo: |
      - Fallback automático (WebSocket → Polling)
      - Rooms e namespaces
      - Reconexão automática
      - Broadcasting eficiente

API:
  - REST: NestJS Controllers
  - GraphQL: @nestjs/graphql (opcional para casos complexos)

Bibliotecas Essenciais

json
{
  "dependencies": {
    "@nestjs/core": "^10.0.0",
    "@nestjs/common": "^10.0.0",
    "@nestjs/platform-express": "^10.0.0",
    "@nestjs/platform-socket.io": "^10.0.0",
    "@nestjs/config": "^3.0.0",
    "@nestjs/jwt": "^10.0.0",
    "@nestjs/passport": "^10.0.0",
    "@nestjs/swagger": "^7.0.0",
    "@nestjs/bull": "^10.0.0",
    "@prisma/client": "^5.0.0",
    "prisma": "^5.0.0",
    "class-validator": "^0.14.0",
    "class-transformer": "^0.5.1",
    "helmet": "^7.0.0",
    "bcrypt": "^5.1.0",
    "uuid": "^9.0.0",
    "date-fns": "^2.30.0",
    "ioredis": "^5.3.0",
    "amqplib": "^0.10.0",
    "socket.io": "^4.6.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.0.0",
    "prettier": "^3.0.0",
    "jest": "^29.0.0",
    "@nestjs/testing": "^10.0.0"
  }
}

Frontend

Framework & Build

yaml
Framework:
  - Vue.js: 3.4.x (Composition API)
    Motivo: |
      - Performance superior ao Vue 2
      - TypeScript first-class support
      - Composition API (reutilização de lógica)
      - Menor bundle size que React
      - Curva de aprendizado suave

Build Tool:
  - Vite: ^5.0.0
    Motivo: |
      - HMR instantâneo
      - Build otimizado (Rollup)
      - Zero-config para TypeScript
      - Plugin ecosystem

State Management:
  - Pinia: ^2.1.0
    Motivo: |
      - Store oficial do Vue 3
      - TypeScript inference automático
      - DevTools integration
      - Modular e simples

UI Framework:
  - Bootstrap 5: ^5.3.0
    Motivo: |
      - Componentes prontos e testados
      - Grid system responsivo
      - Customização via SASS
      - Menor que Vuetify
  
  - Alternativa: Vuetify 3 (Material Design)

Styling:
  - SASS/SCSS
  - CSS Modules
  - Design Tokens (variáveis de tema)

Mobile:
  - PWA (Progressive Web App)
    - Service Workers
    - App Manifest
    - Offline-first
    - Install prompt
    - Push notifications

Bibliotecas Frontend

json
{
  "dependencies": {
    "vue": "^3.4.0",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0",
    "axios": "^1.6.0",
    "socket.io-client": "^4.6.0",
    "vee-validate": "^4.12.0",
    "yup": "^1.3.0",
    "bootstrap": "^5.3.0",
    "bootstrap-icons": "^1.11.0",
    "chart.js": "^4.4.0",
    "vue-chartjs": "^5.3.0",
    "date-fns": "^2.30.0",
    "@vueuse/core": "^10.7.0",
    "vue-toastification": "^2.0.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "vite": "^5.0.0",
    "vite-plugin-pwa": "^0.17.0",
    "typescript": "^5.3.0",
    "sass": "^1.69.0",
    "@types/node": "^20.0.0",
    "eslint": "^8.0.0",
    "eslint-plugin-vue": "^9.0.0",
    "prettier": "^3.0.0",
    "vitest": "^1.0.0",
    "@vue/test-utils": "^2.4.0"
  }
}

DevOps & Infraestrutura

yaml
Containers:
  - Docker: 24.x
  - Docker Compose: 2.x
  - Multi-stage builds
  - Distroless images para produção

Orchestration:
  - Docker Swarm (MVP/Stage 1)
  - Kubernetes (Stage 2+)
    - Helm Charts
    - Horizontal Pod Autoscaling
    - Ingress NGINX

CI/CD:
  - GitHub Actions
    - Workflows: test, build, deploy
    - Matrix strategy para múltiplas plataformas
    - Secrets management
    - Cache de dependências

Cloud Provider:
  - AWS (recomendado)
    - EC2 / ECS / EKS
    - RDS PostgreSQL
    - ElastiCache Redis
    - S3 + CloudFront
    - Route 53
    - AWS IoT Core
  
  - GCP (alternativa)
    - GKE
    - Cloud SQL
    - Memorystore
    - Cloud Storage + CDN
    - Cloud IoT Core

Infrastructure as Code:
  - Terraform: ^1.6.0
    - Módulos reutilizáveis
    - State remoto (S3 + DynamoDB)
    - Workspaces por ambiente

Monitoring:
  - Prometheus: Coleta de métricas
  - Grafana: Dashboards e alertas
  - Node Exporter: Métricas de sistema
  - Redis Exporter: Métricas de cache

Logging:
  - ELK Stack (self-hosted)
    - Elasticsearch: Armazenamento
    - Logstash: Pipeline
    - Kibana: Visualização
  
  - AWS CloudWatch (managed)

APM:
  - OpenTelemetry: Instrumentação
  - Jaeger: Distributed tracing
  - Tempo (alternativa do Grafana)

IoT Stack

yaml
Protocolos:
  - MQTT: Telemetria de sensores
  - HTTP/REST: Comandos síncronos
  - WebSocket: Streaming de dados

Edge Devices:
  - Raspberry Pi 4: Tablets, displays
  - ESP32: Botões, sensores
  - Arduino: Protótipos

Cloud IoT:
  - AWS IoT Core
    - Thing Registry
    - Device Shadows
    - Rules Engine
    - MQTT Broker
  
  - Mosquitto (self-hosted)
    - Bridge com cloud

Bibliotecas:
  - MQTT.js: Cliente Node.js
  - paho-mqtt: Cliente Python (edge)

Security:
  - X.509 certificates
  - IAM policies
  - TLS mutual authentication

Segurança

yaml
Authentication:
  - Keycloak: 23.x
    - OAuth 2.0 / OpenID Connect
    - Social login (Google, Facebook)
    - Multi-factor authentication
    - User Federation (LDAP)
  
  - JWT tokens
    - Access token: 15min
    - Refresh token: 7 dias
    - Token rotation

Authorization:
  - Role-Based Access Control (RBAC)
  - Attribute-Based Access Control (ABAC)
  - Multi-tenant isolation

API Security:
  - Helmet: HTTP headers
  - CORS: Cross-origin configuration
  - Rate Limiting: Redis + Express
  - Input Validation: class-validator
  - SQL Injection: Prisma parameterized queries
  - XSS: CSP headers

WAF:
  - AWS WAF
  - ModSecurity (self-hosted)

Secrets Management:
  - AWS Secrets Manager
  - HashiCorp Vault (alternativa)
  - Environment variables para dev

Mídia & Storage

yaml
Storage:
  - S3: Vídeos, imagens, documentos
  - CloudFront: CDN global
  - Lifecycle policies: Migração para Glacier

Video Processing:
  - AWS MediaConvert: Transcoding
  - FFmpeg (self-hosted): Alternativa
  - Adaptive Bitrate Streaming (HLS/DASH)

Image Processing:
  - Sharp: Resize, crop, optimize
  - AWS Lambda: Processamento serverless
  - Thumbor (alternativa)

🏗 Arquitetura de Serviços

Visão Macro (Event-Driven)

┌────────────────────────────────────────────────────────────────┐
│                         CLIENTS                                 │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐         │
│  │   Web App    │  │ Admin Panel  │  │  IoT Devices │         │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘         │
└─────────┼──────────────────┼──────────────────┼────────────────┘
          │                  │                  │
          │                  │                  │
┌─────────▼──────────────────▼──────────────────▼────────────────┐
│                      API GATEWAY                                │
│  - Authentication (JWT)                                         │
│  - Rate Limiting                                                │
│  - Request Routing                                              │
│  - Load Balancing                                               │
└─────────┬────────────────────────────────────────────┬─────────┘
          │                                             │
┌─────────▼─────────────────────────────────────────────▼─────────┐
│                     MICROSERVICES LAYER                          │
│                                                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐            │
│  │  Core API   │  │ IoT Service │  │Media Service│            │
│  │             │  │             │  │             │            │
│  │ - Accounts  │  │ - Devices   │  │ - Videos    │            │
│  │ - Clubs     │  │ - Telemetry │  │ - Images    │            │
│  │ - Members   │  │ - Commands  │  │ - Transcode │            │
│  │ - Events    │  └─────┬───────┘  └─────┬───────┘            │
│  │ - Bookings  │        │                 │                    │
│  └─────┬───────┘        │                 │                    │
│        │                │                 │                    │
│  ┌─────▼─────────┐  ┌───▼──────────┐  ┌──▼──────────┐        │
│  │Notification   │  │  Analytics   │  │  Billing    │        │
│  │   Service     │  │   Service    │  │  Service    │        │
│  └─────┬─────────┘  └───┬──────────┘  └──┬──────────┘        │
└────────┼────────────────┼─────────────────┼───────────────────┘
         │                │                 │
┌────────▼────────────────▼─────────────────▼───────────────────┐
│                  MESSAGE BUS (RabbitMQ)                        │
│                                                                │
│  Exchanges:                                                    │
│  - club.events    → [event.created, event.updated]            │
│  - member.events  → [member.joined, member.checkin]           │
│  - iot.events     → [device.telemetry, button.pressed]        │
│  - media.events   → [video.uploaded, video.transcoded]        │
└────────────────────────────────────────────────────────────────┘
         │                │                 │
┌────────▼────────────────▼─────────────────▼───────────────────┐
│                      DATA LAYER                                │
│                                                                │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌──────────┐  │
│  │PostgreSQL │  │   Redis   │  │     S3    │  │Time-series│  │
│  │           │  │           │  │           │  │ (TimescaleDB)│
│  │ - Accounts│  │ - Cache   │  │ - Media   │  │ - IoT Data│  │
│  │ - Clubs   │  │ - Sessions│  │ - Assets  │  │ - Metrics │  │
│  │ - Members │  │ - Pub/Sub │  │           │  │           │  │
│  └───────────┘  └───────────┘  └───────────┘  └──────────┘  │
└────────────────────────────────────────────────────────────────┘

Comunicação entre Serviços

typescript
// Event-Driven (Assíncrono)
// Caso: Membro faz check-in → Notificação + Atualização de Analytics

// 1. Core API publica evento
await eventBus.publish('member.events', {
  type: 'member.checkedin',
  payload: {
    memberId: '123',
    clubId: '456',
    timestamp: new Date(),
  },
});

// 2. Notification Service consome
@EventPattern('member.checkedin')
async handleMemberCheckin(data: MemberCheckinEvent) {
  await this.sendNotification({
    userId: data.memberId,
    title: 'Check-in realizado!',
    body: 'Bem-vindo ao clube',
  });
}

// 3. Analytics Service consome
@EventPattern('member.checkedin')
async handleMemberCheckin(data: MemberCheckinEvent) {
  await this.incrementMetric('daily_checkins', {
    clubId: data.clubId,
    date: data.timestamp,
  });
}

// Request-Response (Síncrono)
// Caso: Validar dispositivo IoT antes de aceitar comando

// Core API chama IoT Service
const device = await this.iotService.validateDevice(deviceId);
if (!device.active) {
  throw new ForbiddenException('Device inactive');
}

Padrões de Integração

yaml
Padrão Saga (Transações Distribuídas):
  Cenário: Criar evento → Reservar quadra → Enviar notificações
  
  Fluxo:
    1. EventService.createEvent() → Salva evento
    2. Publica 'event.created' → RabbitMQ
    3. BookingService consome → Tenta reservar quadra
       - Sucesso: Publica 'booking.confirmed'
       - Falha: Publica 'booking.failed' (compensa evento)
    4. NotificationService consome 'booking.confirmed'
       → Envia notificações

Padrão CQRS (Command Query Responsibility Segregation):
  Write Model: PostgreSQL (source of truth)
  Read Model: Redis + Elasticsearch (projeções)
  
  Exemplo:
    - Comando: CreateClub → Escreve em PostgreSQL
    - Query: GetClubDetails → Lê de Redis cache
    - Query: SearchClubs → Lê de Elasticsearch

Padrão API Gateway:
  - Single entry point
  - Authentication/Authorization
  - Request aggregation
  - Protocol translation (REST → gRPC)

📁 Estrutura do Monorepo

Organização de Workspaces

sport-tech-club/
├── .github/
│   └── workflows/
│       ├── ci.yml                    # Testes, lint
│       ├── cd-api.yml                # Deploy da API
│       └── cd-web.yml                # Deploy do frontend

├── apps/
│   ├── api/                          # NestJS - API Principal
│   │   ├── src/
│   │   │   ├── modules/
│   │   │   │   ├── accounts/         # Contexto: Contas
│   │   │   │   │   ├── domain/
│   │   │   │   │   │   ├── entities/
│   │   │   │   │   │   │   ├── account.entity.ts
│   │   │   │   │   │   │   └── subscription.entity.ts
│   │   │   │   │   │   ├── repositories/
│   │   │   │   │   │   │   └── account.repository.ts
│   │   │   │   │   │   ├── services/
│   │   │   │   │   │   │   └── subscription.service.ts
│   │   │   │   │   │   └── events/
│   │   │   │   │   │       └── account-created.event.ts
│   │   │   │   │   ├── application/
│   │   │   │   │   │   ├── use-cases/
│   │   │   │   │   │   │   ├── create-account.use-case.ts
│   │   │   │   │   │   │   └── upgrade-plan.use-case.ts
│   │   │   │   │   │   ├── dtos/
│   │   │   │   │   │   │   ├── create-account.dto.ts
│   │   │   │   │   │   │   └── account-response.dto.ts
│   │   │   │   │   │   └── mappers/
│   │   │   │   │   │       └── account.mapper.ts
│   │   │   │   │   ├── infrastructure/
│   │   │   │   │   │   ├── repositories/
│   │   │   │   │   │   │   └── prisma-account.repository.ts
│   │   │   │   │   │   └── services/
│   │   │   │   │   │       └── stripe-payment.service.ts
│   │   │   │   │   └── presentation/
│   │   │   │   │       ├── controllers/
│   │   │   │   │       │   └── accounts.controller.ts
│   │   │   │   │       └── guards/
│   │   │   │   │           └── account-owner.guard.ts
│   │   │   │   │
│   │   │   │   ├── clubs/            # Contexto: Clubes
│   │   │   │   ├── members/          # Contexto: Membros
│   │   │   │   ├── events/           # Contexto: Eventos
│   │   │   │   ├── bookings/         # Contexto: Reservas
│   │   │   │   └── shared/           # Infraestrutura compartilhada
│   │   │   │       ├── guards/
│   │   │   │       ├── interceptors/
│   │   │   │       ├── filters/
│   │   │   │       └── decorators/
│   │   │   │
│   │   │   ├── config/
│   │   │   │   ├── database.config.ts
│   │   │   │   ├── redis.config.ts
│   │   │   │   └── rabbitmq.config.ts
│   │   │   │
│   │   │   ├── common/
│   │   │   │   ├── interfaces/
│   │   │   │   ├── types/
│   │   │   │   └── constants/
│   │   │   │
│   │   │   ├── app.module.ts
│   │   │   └── main.ts
│   │   │
│   │   ├── prisma/
│   │   │   ├── schema.prisma
│   │   │   ├── seed.ts
│   │   │   └── migrations/
│   │   │
│   │   ├── test/
│   │   │   ├── unit/
│   │   │   ├── integration/
│   │   │   └── e2e/
│   │   │
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   ├── web/                          # Vue.js - Aplicação Web
│   │   ├── src/
│   │   │   ├── modules/
│   │   │   │   ├── auth/
│   │   │   │   │   ├── composables/
│   │   │   │   │   │   └── useAuth.ts
│   │   │   │   │   ├── stores/
│   │   │   │   │   │   └── authStore.ts
│   │   │   │   │   ├── views/
│   │   │   │   │   │   ├── LoginView.vue
│   │   │   │   │   │   └── RegisterView.vue
│   │   │   │   │   └── router/
│   │   │   │   │       └── authRoutes.ts
│   │   │   │   │
│   │   │   │   ├── clubs/
│   │   │   │   ├── members/
│   │   │   │   ├── events/
│   │   │   │   └── bookings/
│   │   │   │
│   │   │   ├── shared/
│   │   │   │   ├── components/
│   │   │   │   │   ├── AppHeader.vue
│   │   │   │   │   ├── AppSidebar.vue
│   │   │   │   │   └── DataTable.vue
│   │   │   │   ├── composables/
│   │   │   │   │   ├── useApi.ts
│   │   │   │   │   ├── useWebSocket.ts
│   │   │   │   │   └── useNotifications.ts
│   │   │   │   └── utils/
│   │   │   │       ├── validators.ts
│   │   │   │       └── formatters.ts
│   │   │   │
│   │   │   ├── layouts/
│   │   │   │   ├── DefaultLayout.vue
│   │   │   │   └── AuthLayout.vue
│   │   │   │
│   │   │   ├── router/
│   │   │   │   └── index.ts
│   │   │   │
│   │   │   ├── stores/
│   │   │   │   └── index.ts
│   │   │   │
│   │   │   ├── assets/
│   │   │   ├── styles/
│   │   │   │   ├── variables.scss
│   │   │   │   └── main.scss
│   │   │   │
│   │   │   ├── App.vue
│   │   │   └── main.ts
│   │   │
│   │   ├── public/
│   │   │   ├── manifest.json          # PWA manifest
│   │   │   └── service-worker.js      # Service worker
│   │   │
│   │   ├── Dockerfile
│   │   ├── vite.config.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   └── admin/                        # Vue.js - Painel Admin
│       └── (estrutura similar ao web)

├── services/
│   ├── iot-gateway/                  # NestJS - Gateway IoT
│   │   ├── src/
│   │   │   ├── mqtt/
│   │   │   │   ├── mqtt.service.ts
│   │   │   │   └── mqtt.controller.ts
│   │   │   ├── devices/
│   │   │   │   ├── device.repository.ts
│   │   │   │   └── device.service.ts
│   │   │   ├── telemetry/
│   │   │   │   ├── telemetry.processor.ts
│   │   │   │   └── telemetry.storage.ts
│   │   │   └── main.ts
│   │   └── Dockerfile
│   │
│   ├── media-service/                # Node.js - Processamento de Mídia
│   │   ├── src/
│   │   │   ├── upload/
│   │   │   ├── transcode/
│   │   │   ├── storage/
│   │   │   └── main.ts
│   │   └── Dockerfile
│   │
│   └── notification-service/         # NestJS - Notificações
│       ├── src/
│       │   ├── push/
│       │   ├── email/
│       │   ├── sms/
│       │   └── main.ts
│       └── Dockerfile

├── libs/
│   ├── shared/                       # Código compartilhado
│   │   ├── src/
│   │   │   ├── dtos/                 # DTOs compartilhados
│   │   │   │   ├── account.dto.ts
│   │   │   │   ├── club.dto.ts
│   │   │   │   └── member.dto.ts
│   │   │   ├── types/                # Types/Interfaces
│   │   │   │   ├── auth.types.ts
│   │   │   │   └── pagination.types.ts
│   │   │   ├── utils/                # Utilitários
│   │   │   │   ├── date.utils.ts
│   │   │   │   └── string.utils.ts
│   │   │   ├── constants/
│   │   │   │   └── roles.constants.ts
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   ├── domain/                       # Domain entities compartilhadas
│   │   └── src/
│   │       ├── entities/
│   │       ├── value-objects/
│   │       └── events/
│   │
│   └── ui/                           # Componentes UI compartilhados
│       └── src/
│           ├── components/
│           │   ├── Button.vue
│           │   ├── Input.vue
│           │   └── Modal.vue
│           ├── composables/
│           └── styles/

├── infrastructure/
│   ├── docker/
│   │   ├── docker-compose.yml        # Dev environment
│   │   ├── docker-compose.prod.yml   # Production
│   │   └── nginx/
│   │       └── nginx.conf
│   │
│   ├── k8s/                          # Kubernetes manifests
│   │   ├── namespaces/
│   │   ├── deployments/
│   │   │   ├── api-deployment.yml
│   │   │   ├── web-deployment.yml
│   │   │   └── iot-deployment.yml
│   │   ├── services/
│   │   ├── ingress/
│   │   └── configmaps/
│   │
│   └── terraform/                    # Infrastructure as Code
│       ├── modules/
│       │   ├── networking/
│       │   ├── compute/
│       │   ├── database/
│       │   └── storage/
│       ├── environments/
│       │   ├── dev/
│       │   ├── staging/
│       │   └── production/
│       └── main.tf

├── docs/
│   ├── architecture/
│   │   ├── ADR/                      # Architecture Decision Records
│   │   ├── diagrams/
│   │   └── TECH-STACK.md
│   ├── api/
│   │   └── openapi.yml
│   └── guides/
│       ├── CONTRIBUTING.md
│       └── DEPLOYMENT.md

├── scripts/
│   ├── setup-dev.sh
│   ├── seed-database.ts
│   └── generate-types.ts

├── .editorconfig
├── .gitignore
├── .prettierrc
├── .eslintrc.js
├── package.json                      # Root package.json (workspaces)
├── pnpm-workspace.yaml               # PNPM workspaces
└── README.md

Workspaces Configuration

yaml
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'services/*'
  - 'libs/*'
json
// package.json (root)
{
  "name": "sport-tech-club",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "apps/*",
    "services/*",
    "libs/*"
  ],
  "scripts": {
    "dev:api": "pnpm --filter api dev",
    "dev:web": "pnpm --filter web dev",
    "dev:all": "concurrently \"pnpm dev:api\" \"pnpm dev:web\"",
    "build": "pnpm -r build",
    "test": "pnpm -r test",
    "lint": "pnpm -r lint",
    "format": "prettier --write \"**/*.{ts,tsx,vue,md}\"",
    "typecheck": "pnpm -r typecheck",
    "docker:up": "docker-compose -f infrastructure/docker/docker-compose.yml up -d",
    "docker:down": "docker-compose -f infrastructure/docker/docker-compose.yml down"
  },
  "devDependencies": {
    "concurrently": "^8.2.0",
    "prettier": "^3.0.0",
    "typescript": "^5.3.0"
  }
}

🎨 Padrões de Código

Backend (NestJS + DDD)

Entity (Domain Layer)

typescript
// libs/domain/src/entities/member.entity.ts
export class Member {
  private constructor(
    public readonly id: string,
    private _name: string,
    private _email: Email,
    private _status: MemberStatus,
    private _joinedAt: Date,
    private _expiresAt: Date | null
  ) {}

  static create(props: CreateMemberProps): Member {
    // Validações de domínio
    if (!props.name || props.name.trim().length < 3) {
      throw new DomainError('Member name must be at least 3 characters');
    }

    const email = Email.create(props.email);
    const id = crypto.randomUUID();

    return new Member(
      id,
      props.name.trim(),
      email,
      MemberStatus.ACTIVE,
      new Date(),
      null
    );
  }

  // Getters
  get name(): string {
    return this._name;
  }

  get email(): Email {
    return this._email;
  }

  get status(): MemberStatus {
    return this._status;
  }

  get isActive(): boolean {
    return this._status === MemberStatus.ACTIVE;
  }

  get isExpired(): boolean {
    return this._expiresAt !== null && this._expiresAt < new Date();
  }

  // Comportamentos de domínio
  changeName(newName: string): void {
    if (!newName || newName.trim().length < 3) {
      throw new DomainError('Invalid name');
    }
    this._name = newName.trim();
  }

  suspend(): void {
    if (this._status === MemberStatus.SUSPENDED) {
      throw new DomainError('Member is already suspended');
    }
    this._status = MemberStatus.SUSPENDED;
  }

  reactivate(): void {
    if (this._status === MemberStatus.ACTIVE) {
      throw new DomainError('Member is already active');
    }
    this._status = MemberStatus.ACTIVE;
  }

  renew(durationDays: number): void {
    const newExpiry = addDays(new Date(), durationDays);
    this._expiresAt = newExpiry;
    this._status = MemberStatus.ACTIVE;
  }
}

// Value Object
export class Email {
  private constructor(private readonly value: string) {}

  static create(email: string): Email {
    if (!this.isValid(email)) {
      throw new DomainError('Invalid email format');
    }
    return new Email(email.toLowerCase().trim());
  }

  private static isValid(email: string): boolean {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  }

  toString(): string {
    return this.value;
  }

  equals(other: Email): boolean {
    return this.value === other.value;
  }
}

// Domain Event
export class MemberCreatedEvent implements DomainEvent {
  readonly occurredAt = new Date();
  readonly eventType = 'member.created';

  constructor(
    public readonly memberId: string,
    public readonly clubId: string,
    public readonly name: string,
    public readonly email: string
  ) {}
}

Use Case (Application Layer)

typescript
// apps/api/src/modules/members/application/use-cases/create-member.use-case.ts
@Injectable()
export class CreateMemberUseCase {
  constructor(
    private readonly memberRepository: MemberRepository,
    private readonly clubRepository: ClubRepository,
    private readonly eventBus: EventBus,
    private readonly logger: Logger
  ) {}

  async execute(dto: CreateMemberDto): Promise<MemberResponseDto> {
    // 1. Validar clube existe
    const club = await this.clubRepository.findById(dto.clubId);
    if (!club) {
      throw new NotFoundException(`Club ${dto.clubId} not found`);
    }

    // 2. Validar email único
    const existingMember = await this.memberRepository.findByEmail(
      dto.email,
      dto.clubId
    );
    if (existingMember) {
      throw new ConflictException('Email already registered in this club');
    }

    // 3. Criar entidade de domínio
    const member = Member.create({
      name: dto.name,
      email: dto.email,
      clubId: dto.clubId,
    });

    // 4. Persistir
    await this.memberRepository.save(member);

    // 5. Publicar evento
    const event = new MemberCreatedEvent(
      member.id,
      dto.clubId,
      member.name,
      member.email.toString()
    );
    await this.eventBus.publish(event);

    this.logger.log(`Member created: ${member.id}`);

    // 6. Retornar DTO
    return MemberMapper.toResponseDto(member);
  }
}

Controller (Presentation Layer)

typescript
// apps/api/src/modules/members/presentation/controllers/members.controller.ts
@Controller('members')
@ApiTags('members')
@UseGuards(JwtAuthGuard, TenantGuard)
export class MembersController {
  constructor(
    private readonly createMemberUseCase: CreateMemberUseCase,
    private readonly listMembersUseCase: ListMembersUseCase
  ) {}

  @Post()
  @ApiOperation({ summary: 'Create new member' })
  @ApiResponse({ status: 201, type: MemberResponseDto })
  @ApiResponse({ status: 400, description: 'Bad Request' })
  @ApiResponse({ status: 409, description: 'Email already exists' })
  async create(
    @Body() dto: CreateMemberDto,
    @CurrentUser() user: UserPayload
  ): Promise<MemberResponseDto> {
    return this.createMemberUseCase.execute(dto);
  }

  @Get()
  @ApiOperation({ summary: 'List members' })
  @ApiResponse({ status: 200, type: [MemberResponseDto] })
  async list(
    @Query() query: ListMembersQueryDto,
    @CurrentUser() user: UserPayload
  ): Promise<PaginatedResponseDto<MemberResponseDto>> {
    return this.listMembersUseCase.execute(query);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Get member by ID' })
  async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<MemberResponseDto> {
    // ...
  }
}

Repository Pattern

typescript
// apps/api/src/modules/members/domain/repositories/member.repository.ts
export interface MemberRepository {
  findById(id: string): Promise<Member | null>;
  findByEmail(email: string, clubId: string): Promise<Member | null>;
  findByClubId(clubId: string, options: PaginationOptions): Promise<PaginatedResult<Member>>;
  save(member: Member): Promise<void>;
  delete(id: string): Promise<void>;
}

// apps/api/src/modules/members/infrastructure/repositories/prisma-member.repository.ts
@Injectable()
export class PrismaMemberRepository implements MemberRepository {
  constructor(private readonly prisma: PrismaService) {}

  async findById(id: string): Promise<Member | null> {
    const data = await this.prisma.member.findUnique({
      where: { id },
      include: { club: true },
    });

    return data ? MemberMapper.toDomain(data) : null;
  }

  async save(member: Member): Promise<void> {
    const data = MemberMapper.toPersistence(member);

    await this.prisma.member.upsert({
      where: { id: member.id },
      create: data,
      update: data,
    });
  }

  async delete(id: string): Promise<void> {
    await this.prisma.member.delete({ where: { id } });
  }
}

Frontend (Vue.js 3 + Composition API)

Composable (Reusable Logic)

typescript
// apps/web/src/shared/composables/useApi.ts
export function useApi<T>(url: string, options?: UseApiOptions) {
  const data = ref<T | null>(null);
  const error = ref<Error | null>(null);
  const loading = ref(false);

  const execute = async (config?: AxiosRequestConfig) => {
    loading.value = true;
    error.value = null;

    try {
      const response = await api.get<T>(url, config);
      data.value = response.data;
    } catch (err) {
      error.value = err as Error;
      throw err;
    } finally {
      loading.value = false;
    }
  };

  if (options?.immediate) {
    execute();
  }

  return { data, error, loading, execute };
}

// apps/web/src/modules/members/composables/useMembers.ts
export function useMembers(clubId: string) {
  const { data: members, loading, error, execute } = useApi<Member[]>(
    `/api/clubs/${clubId}/members`
  );

  const createMember = async (dto: CreateMemberDto) => {
    await api.post(`/api/clubs/${clubId}/members`, dto);
    await execute(); // Refresh list
  };

  const deleteMember = async (memberId: string) => {
    await api.delete(`/api/members/${memberId}`);
    await execute();
  };

  return {
    members,
    loading,
    error,
    createMember,
    deleteMember,
    refresh: execute,
  };
}

Component (Vue SFC)

vue
<!-- apps/web/src/modules/members/views/MembersListView.vue -->
<template>
  <div class="members-list">
    <div class="d-flex justify-content-between align-items-center mb-4">
      <h1>Members</h1>
      <button class="btn btn-primary" @click="showCreateModal = true">
        <i class="bi bi-plus"></i> Add Member
      </button>
    </div>

    <div v-if="loading" class="text-center">
      <div class="spinner-border" role="status">
        <span class="visually-hidden">Loading...</span>
      </div>
    </div>

    <div v-else-if="error" class="alert alert-danger">
      {{ error.message }}
    </div>

    <div v-else>
      <DataTable
        :items="members"
        :columns="columns"
        @row-click="handleRowClick"
      />
    </div>

    <CreateMemberModal
      v-model="showCreateModal"
      :club-id="clubId"
      @created="handleMemberCreated"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useMembers } from '../composables/useMembers';
import { useToast } from 'vue-toastification';
import DataTable from '@/shared/components/DataTable.vue';
import CreateMemberModal from '../components/CreateMemberModal.vue';

const route = useRoute();
const toast = useToast();

const clubId = computed(() => route.params.clubId as string);
const showCreateModal = ref(false);

const { members, loading, error, refresh } = useMembers(clubId.value);

const columns = [
  { key: 'name', label: 'Name', sortable: true },
  { key: 'email', label: 'Email' },
  { key: 'status', label: 'Status', formatter: formatStatus },
  { key: 'joinedAt', label: 'Joined', formatter: formatDate },
];

const handleRowClick = (member: Member) => {
  router.push({ name: 'member-detail', params: { id: member.id } });
};

const handleMemberCreated = () => {
  showCreateModal.value = false;
  toast.success('Member created successfully');
  refresh();
};

function formatStatus(value: string): string {
  const badges = {
    ACTIVE: '<span class="badge bg-success">Active</span>',
    SUSPENDED: '<span class="badge bg-warning">Suspended</span>',
    EXPIRED: '<span class="badge bg-danger">Expired</span>',
  };
  return badges[value] || value;
}

function formatDate(value: string): string {
  return new Date(value).toLocaleDateString();
}
</script>

<style scoped lang="scss">
.members-list {
  padding: 2rem;
}
</style>

Store (Pinia)

typescript
// apps/web/src/modules/auth/stores/authStore.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { api } from '@/shared/services/api';

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null);
  const token = ref<string | null>(localStorage.getItem('token'));

  // Getters
  const isAuthenticated = computed(() => token.value !== null);
  const userRole = computed(() => user.value?.role || 'guest');

  // Actions
  async function login(email: string, password: string) {
    const response = await api.post<AuthResponse>('/auth/login', {
      email,
      password,
    });

    token.value = response.data.accessToken;
    user.value = response.data.user;

    localStorage.setItem('token', token.value);
    api.defaults.headers.common['Authorization'] = `Bearer ${token.value}`;
  }

  async function logout() {
    token.value = null;
    user.value = null;
    localStorage.removeItem('token');
    delete api.defaults.headers.common['Authorization'];
  }

  async function fetchCurrentUser() {
    if (!token.value) return;

    const response = await api.get<User>('/auth/me');
    user.value = response.data;
  }

  // Inicialização
  if (token.value) {
    api.defaults.headers.common['Authorization'] = `Bearer ${token.value}`;
    fetchCurrentUser();
  }

  return {
    // State
    user,
    token,
    // Getters
    isAuthenticated,
    userRole,
    // Actions
    login,
    logout,
    fetchCurrentUser,
  };
});

Naming Conventions

typescript
// Classes: PascalCase
class MemberService {}
class CreateMemberUseCase {}

// Interfaces: PascalCase com 'I' prefix (opcional)
interface MemberRepository {}
interface IPaymentGateway {}

// Types: PascalCase
type MemberStatus = 'ACTIVE' | 'SUSPENDED' | 'EXPIRED';
type PaginationOptions = { page: number; limit: number };

// Constants: UPPER_SNAKE_CASE
const MAX_UPLOAD_SIZE = 5 * 1024 * 1024;
const DEFAULT_PAGE_SIZE = 20;

// Enums: PascalCase (enum) + UPPER_CASE (values)
enum UserRole {
  ADMIN = 'ADMIN',
  MANAGER = 'MANAGER',
  MEMBER = 'MEMBER',
}

// Functions: camelCase
function calculateMembershipFee() {}
async function sendWelcomeEmail() {}

// Variables: camelCase
const memberCount = 10;
let isLoading = false;

// Private properties: underscore prefix
class Member {
  private _email: string;
  private _status: MemberStatus;
}

// Composables (Vue): use + PascalCase
function useAuth() {}
function useMembers() {}

// Components (Vue): PascalCase
// MemberCard.vue
// DataTable.vue
// CreateMemberModal.vue

// Files:
// - Components: PascalCase.vue (MemberCard.vue)
// - Composables: camelCase.ts (useMembers.ts)
// - Stores: camelCase.ts (authStore.ts)
// - Services: kebab-case.service.ts (payment.service.ts)
// - Use Cases: kebab-case.use-case.ts (create-member.use-case.ts)

🧪 Estratégia de Testes

Pirâmide de Testes

         /\
        /E2E\         10% - Fluxos críticos end-to-end
       /______\
      /        \
     /Integration\ 20% - Integração entre camadas
    /____________\
   /              \
  /   Unit Tests   \  70% - Lógica de domínio e negócio
 /__________________\

Unit Tests (Jest)

typescript
// apps/api/src/modules/members/domain/entities/member.entity.spec.ts
describe('Member Entity', () => {
  describe('create', () => {
    it('should create a valid member', () => {
      const member = Member.create({
        name: 'John Doe',
        email: 'john@example.com',
        clubId: 'club-123',
      });

      expect(member.id).toBeDefined();
      expect(member.name).toBe('John Doe');
      expect(member.email.toString()).toBe('john@example.com');
      expect(member.status).toBe(MemberStatus.ACTIVE);
    });

    it('should throw if name is too short', () => {
      expect(() =>
        Member.create({
          name: 'Jo',
          email: 'john@example.com',
          clubId: 'club-123',
        })
      ).toThrow('Member name must be at least 3 characters');
    });

    it('should throw if email is invalid', () => {
      expect(() =>
        Member.create({
          name: 'John Doe',
          email: 'invalid-email',
          clubId: 'club-123',
        })
      ).toThrow('Invalid email format');
    });
  });

  describe('suspend', () => {
    it('should suspend active member', () => {
      const member = createTestMember({ status: MemberStatus.ACTIVE });

      member.suspend();

      expect(member.status).toBe(MemberStatus.SUSPENDED);
    });

    it('should throw if member is already suspended', () => {
      const member = createTestMember({ status: MemberStatus.SUSPENDED });

      expect(() => member.suspend()).toThrow('Member is already suspended');
    });
  });

  describe('renew', () => {
    it('should extend expiration date', () => {
      const member = createTestMember();
      const before = new Date();

      member.renew(30);

      expect(member.expiresAt).toBeInstanceOf(Date);
      expect(member.expiresAt!.getTime()).toBeGreaterThan(before.getTime());
    });
  });
});

// Test helpers
function createTestMember(overrides?: Partial<CreateMemberProps>): Member {
  return Member.create({
    name: 'Test Member',
    email: 'test@example.com',
    clubId: 'club-123',
    ...overrides,
  });
}

Integration Tests

typescript
// apps/api/test/integration/members.integration.spec.ts
describe('Members API Integration', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let authToken: string;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleRef.createNestApplication();
    prisma = moduleRef.get<PrismaService>(PrismaService);

    await app.init();
  });

  beforeEach(async () => {
    // Clean database
    await prisma.member.deleteMany();
    await prisma.club.deleteMany();
    await prisma.account.deleteMany();

    // Setup test data
    const account = await createTestAccount(prisma);
    const club = await createTestClub(prisma, account.id);

    // Get auth token
    authToken = await getAuthToken(app, 'admin@example.com', 'password');
  });

  afterAll(async () => {
    await prisma.$disconnect();
    await app.close();
  });

  describe('POST /api/members', () => {
    it('should create member with valid data', async () => {
      const response = await request(app.getHttpServer())
        .post('/api/members')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          name: 'John Doe',
          email: 'john@example.com',
          clubId: 'club-123',
        })
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(String),
        name: 'John Doe',
        email: 'john@example.com',
        status: 'ACTIVE',
      });

      // Verify in database
      const member = await prisma.member.findUnique({
        where: { id: response.body.id },
      });
      expect(member).toBeDefined();
      expect(member!.name).toBe('John Doe');
    });

    it('should return 409 if email already exists', async () => {
      // Create existing member
      await createTestMember(prisma, {
        email: 'john@example.com',
        clubId: 'club-123',
      });

      const response = await request(app.getHttpServer())
        .post('/api/members')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          name: 'John Doe',
          email: 'john@example.com',
          clubId: 'club-123',
        })
        .expect(409);

      expect(response.body.message).toContain('Email already registered');
    });

    it('should return 400 for invalid email', async () => {
      await request(app.getHttpServer())
        .post('/api/members')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          name: 'John Doe',
          email: 'invalid-email',
          clubId: 'club-123',
        })
        .expect(400);
    });

    it('should return 401 without auth token', async () => {
      await request(app.getHttpServer())
        .post('/api/members')
        .send({
          name: 'John Doe',
          email: 'john@example.com',
          clubId: 'club-123',
        })
        .expect(401);
    });
  });

  describe('GET /api/members', () => {
    it('should list members with pagination', async () => {
      // Create test members
      await Promise.all([
        createTestMember(prisma, { name: 'Member 1' }),
        createTestMember(prisma, { name: 'Member 2' }),
        createTestMember(prisma, { name: 'Member 3' }),
      ]);

      const response = await request(app.getHttpServer())
        .get('/api/members?page=1&limit=2')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body).toMatchObject({
        data: expect.arrayContaining([
          expect.objectContaining({ name: expect.any(String) }),
        ]),
        meta: {
          page: 1,
          limit: 2,
          total: 3,
          totalPages: 2,
        },
      });
    });
  });
});

E2E Tests (Playwright)

typescript
// apps/web/tests/e2e/members.e2e.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Member Management', () => {
  test.beforeEach(async ({ page }) => {
    // Login
    await page.goto('/login');
    await page.fill('input[name="email"]', 'admin@example.com');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');

    // Wait for redirect
    await page.waitForURL('/dashboard');
  });

  test('should create new member', async ({ page }) => {
    // Navigate to members page
    await page.goto('/members');

    // Click create button
    await page.click('button:has-text("Add Member")');

    // Fill form
    await page.fill('input[name="name"]', 'John Doe');
    await page.fill('input[name="email"]', 'john@example.com');
    await page.fill('input[name="phone"]', '+55 11 99999-9999');

    // Submit
    await page.click('button:has-text("Save")');

    // Verify success toast
    await expect(page.locator('.toast-success')).toContainText(
      'Member created successfully'
    );

    // Verify member appears in list
    await expect(page.locator('table')).toContainText('John Doe');
    await expect(page.locator('table')).toContainText('john@example.com');
  });

  test('should display validation errors', async ({ page }) => {
    await page.goto('/members');
    await page.click('button:has-text("Add Member")');

    // Try to submit empty form
    await page.click('button:has-text("Save")');

    // Verify error messages
    await expect(page.locator('.error-message')).toContainText(
      'Name is required'
    );
    await expect(page.locator('.error-message')).toContainText(
      'Email is required'
    );
  });

  test('should edit existing member', async ({ page }) => {
    await page.goto('/members');

    // Click edit button on first row
    await page.click('table tbody tr:first-child button[aria-label="Edit"]');

    // Change name
    await page.fill('input[name="name"]', 'Jane Doe Updated');
    await page.click('button:has-text("Save")');

    // Verify update
    await expect(page.locator('.toast-success')).toContainText(
      'Member updated successfully'
    );
    await expect(page.locator('table')).toContainText('Jane Doe Updated');
  });

  test('should delete member', async ({ page }) => {
    await page.goto('/members');

    // Click delete button
    await page.click('table tbody tr:first-child button[aria-label="Delete"]');

    // Confirm deletion
    await page.click('button:has-text("Confirm")');

    // Verify deletion
    await expect(page.locator('.toast-success')).toContainText(
      'Member deleted successfully'
    );
  });
});

Test Configuration

typescript
// jest.config.js (Backend)
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src', '<rootDir>/test'],
  testMatch: ['**/*.spec.ts', '**/*.test.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.dto.ts',
    '!src/**/*.interface.ts',
    '!src/main.ts',
  ],
  coverageThresholds: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@libs/(.*)$': '<rootDir>/../../libs/$1',
  },
};

// vitest.config.ts (Frontend)
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'dist/', '**/*.spec.ts'],
    },
  },
});

🚀 Deploy & CI/CD

GitHub Actions Workflows

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm format:check

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm typecheck

  test-api:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: sport_tech_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm --filter api prisma:generate
      - run: pnpm --filter api prisma:migrate:deploy
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sport_tech_test
      - run: pnpm --filter api test:cov
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sport_tech_test
          REDIS_URL: redis://localhost:6379
      - uses: codecov/codecov-action@v3
        with:
          files: ./apps/api/coverage/lcov.info
          flags: api

  test-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm --filter web test:unit
      - uses: codecov/codecov-action@v3
        with:
          files: ./apps/web/coverage/lcov.info
          flags: web

  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: npx playwright install --with-deps
      - run: pnpm --filter web test:e2e
      - uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: playwright-report
          path: apps/web/playwright-report/

  build:
    runs-on: ubuntu-latest
    needs: [lint, typecheck, test-api, test-web]
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
yaml
# .github/workflows/cd-api.yml
name: CD - API

on:
  push:
    branches: [main]
    paths:
      - 'apps/api/**'
      - 'libs/**'
      - '.github/workflows/cd-api.yml'

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push Docker image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: sport-tech-api
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
            -f apps/api/Dockerfile .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
            $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster sport-tech-cluster \
            --service sport-tech-api \
            --force-new-deployment

      - name: Run database migrations
        run: |
          # Execute migrations via ECS task
          aws ecs run-task \
            --cluster sport-tech-cluster \
            --task-definition sport-tech-migrations \
            --network-configuration "awsvpcConfiguration={subnets=[subnet-xxx],securityGroups=[sg-xxx]}" \
            --launch-type FARGATE

      - name: Notify deployment
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'API deployed to production'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}
        if: always()

Dockerfile (Multi-stage)

dockerfile
# apps/api/Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app

# Install pnpm
RUN npm install -g pnpm

# Copy package files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/api/package.json ./apps/api/
COPY libs/shared/package.json ./libs/shared/

# Install dependencies
RUN pnpm install --frozen-lockfile

# Copy source code
COPY apps/api ./apps/api
COPY libs/shared ./libs/shared

# Generate Prisma client
WORKDIR /app/apps/api
RUN pnpm prisma:generate

# Build application
RUN pnpm build

# Stage 2: Production
FROM node:20-alpine AS production

WORKDIR /app

# Install pnpm
RUN npm install -g pnpm

# Copy package files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/api/package.json ./apps/api/
COPY libs/shared/package.json ./libs/shared/

# Install production dependencies only
RUN pnpm install --frozen-lockfile --prod

# Copy built application from builder
COPY --from=builder /app/apps/api/dist ./apps/api/dist
COPY --from=builder /app/apps/api/prisma ./apps/api/prisma
COPY --from=builder /app/apps/api/node_modules/.prisma ./apps/api/node_modules/.prisma

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nestjs -u 1001

USER nestjs

WORKDIR /app/apps/api

EXPOSE 3000

CMD ["node", "dist/main.js"]

Docker Compose (Development)

yaml
# infrastructure/docker/docker-compose.yml
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: sport-tech-postgres
    environment:
      POSTGRES_DB: sport_tech_dev
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: sport-tech-redis
    ports:
      - '6379:6379'
    volumes:
      - redis_data:/data
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 10s
      timeout: 3s
      retries: 5

  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    container_name: sport-tech-rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: admin
    ports:
      - '5672:5672'
      - '15672:15672'
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    healthcheck:
      test: ['CMD', 'rabbitmq-diagnostics', 'ping']
      interval: 10s
      timeout: 5s
      retries: 5

  mailhog:
    image: mailhog/mailhog
    container_name: sport-tech-mailhog
    ports:
      - '1025:1025'
      - '8025:8025'

volumes:
  postgres_data:
  redis_data:
  rabbitmq_data:

📊 Observabilidade

Prometheus Configuration

yaml
# infrastructure/monitoring/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'api'
    static_configs:
      - targets: ['api:3000']
    metrics_path: /metrics

  - job_name: 'iot-gateway'
    static_configs:
      - targets: ['iot-gateway:3001']

  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres-exporter:9187']

  - job_name: 'redis'
    static_configs:
      - targets: ['redis-exporter:9121']

  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']

Grafana Dashboards

json
// infrastructure/monitoring/dashboards/api-metrics.json
{
  "dashboard": {
    "title": "Sport Tech Club - API Metrics",
    "panels": [
      {
        "title": "Request Rate",
        "targets": [
          {
            "expr": "rate(http_requests_total[5m])"
          }
        ]
      },
      {
        "title": "Response Time (p95)",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))"
          }
        ]
      },
      {
        "title": "Error Rate",
        "targets": [
          {
            "expr": "rate(http_requests_total{status=~\"5..\"}[5m])"
          }
        ]
      }
    ]
  }
}

Application Metrics (NestJS)

typescript
// apps/api/src/shared/interceptors/metrics.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Counter, Histogram } from 'prom-client';

@Injectable()
export class MetricsInterceptor implements NestInterceptor {
  private readonly httpRequestsTotal = new Counter({
    name: 'http_requests_total',
    help: 'Total number of HTTP requests',
    labelNames: ['method', 'route', 'status'],
  });

  private readonly httpRequestDuration = new Histogram({
    name: 'http_request_duration_seconds',
    help: 'HTTP request duration in seconds',
    labelNames: ['method', 'route', 'status'],
    buckets: [0.1, 0.5, 1, 2, 5],
  });

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, route } = request;
    const start = Date.now();

    return next.handle().pipe(
      tap({
        next: () => {
          const response = context.switchToHttp().getResponse();
          const duration = (Date.now() - start) / 1000;

          this.httpRequestsTotal.inc({
            method,
            route: route.path,
            status: response.statusCode,
          });

          this.httpRequestDuration.observe(
            { method, route: route.path, status: response.statusCode },
            duration
          );
        },
        error: (error) => {
          const duration = (Date.now() - start) / 1000;

          this.httpRequestsTotal.inc({
            method,
            route: route.path,
            status: error.status || 500,
          });

          this.httpRequestDuration.observe(
            { method, route: route.path, status: error.status || 500 },
            duration
          );
        },
      })
    );
  }
}

Logging (Winston)

typescript
// apps/api/src/config/logger.config.ts
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';

export const loggerConfig = WinstonModule.createLogger({
  transports: [
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.colorize(),
        winston.format.printf(({ timestamp, level, message, context, ...meta }) => {
          return `${timestamp} [${context}] ${level}: ${message} ${
            Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
          }`;
        })
      ),
    }),
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
    }),
    new winston.transports.File({
      filename: 'logs/combined.log',
      format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
    }),
  ],
});

Distributed Tracing (OpenTelemetry)

typescript
// apps/api/src/config/tracing.config.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';

const traceExporter = new JaegerExporter({
  endpoint: process.env.JAEGER_ENDPOINT || 'http://localhost:14268/api/traces',
});

export const otelSDK = new NodeSDK({
  traceExporter,
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': { enabled: true },
      '@opentelemetry/instrumentation-express': { enabled: true },
      '@opentelemetry/instrumentation-nestjs-core': { enabled: true },
      '@opentelemetry/instrumentation-pg': { enabled: true },
      '@opentelemetry/instrumentation-redis': { enabled: true },
    }),
  ],
  serviceName: 'sport-tech-api',
});

// Start tracing
otelSDK.start();

// Graceful shutdown
process.on('SIGTERM', () => {
  otelSDK
    .shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((error) => console.log('Error terminating tracing', error));
});

🔒 Segurança

Helmet Configuration

typescript
// apps/api/src/main.ts
import helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Security headers
  app.use(
    helmet({
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          styleSrc: ["'self'", "'unsafe-inline'"],
          scriptSrc: ["'self'"],
          imgSrc: ["'self'", 'data:', 'https:'],
        },
      },
      hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true,
      },
    })
  );

  await app.listen(3000);
}

CORS Configuration

typescript
// apps/api/src/config/cors.config.ts
export const corsConfig: CorsOptions = {
  origin: (origin, callback) => {
    const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];

    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
};

Rate Limiting

typescript
// apps/api/src/shared/guards/rate-limit.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Redis } from 'ioredis';

@Injectable()
export class RateLimitGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private redis: Redis
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const ip = request.ip;
    const key = `rate_limit:${ip}`;

    const limit = this.reflector.get<number>('rateLimit', context.getHandler()) || 100;
    const window = 60; // 1 minute

    const current = await this.redis.incr(key);

    if (current === 1) {
      await this.redis.expire(key, window);
    }

    if (current > limit) {
      throw new TooManyRequestsException('Rate limit exceeded');
    }

    return true;
  }
}

// Usage
@Controller('members')
@UseGuards(RateLimitGuard)
export class MembersController {
  @Post()
  @RateLimit(10) // 10 requests per minute
  async create(@Body() dto: CreateMemberDto) {
    // ...
  }
}

Input Validation

typescript
// apps/api/src/modules/members/dtos/create-member.dto.ts
import { IsEmail, IsString, MinLength, MaxLength, Matches } from 'class-validator';

export class CreateMemberDto {
  @IsString()
  @MinLength(3)
  @MaxLength(100)
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @Matches(/^\+?[1-9]\d{1,14}$/, {
    message: 'Phone must be a valid E.164 format',
  })
  phone: string;

  @IsString()
  @IsUUID()
  clubId: string;
}

SQL Injection Prevention (Prisma)

typescript
// Prisma queries são parametrizadas automaticamente
const member = await prisma.member.findUnique({
  where: { id: memberId }, // Safe
});

// RAW queries devem usar parameterized queries
const result = await prisma.$queryRaw`
  SELECT * FROM members
  WHERE email = ${email} AND club_id = ${clubId}
`;

⚡ Performance & Escalabilidade

Caching Strategy

typescript
// apps/api/src/shared/decorators/cache.decorator.ts
export function Cache(ttl: number = 300) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const cacheKey = `cache:${propertyKey}:${JSON.stringify(args)}`;
      const redis = this.redis; // Inject Redis

      // Try to get from cache
      const cached = await redis.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }

      // Execute method
      const result = await originalMethod.apply(this, args);

      // Store in cache
      await redis.setex(cacheKey, ttl, JSON.stringify(result));

      return result;
    };

    return descriptor;
  };
}

// Usage
@Injectable()
export class ClubService {
  constructor(private redis: Redis) {}

  @Cache(600) // 10 minutes
  async getClubDetails(clubId: string): Promise<ClubDetailsDto> {
    // Expensive operation
    return this.clubRepository.findById(clubId);
  }
}

Database Indexing

prisma
// apps/api/prisma/schema.prisma
model Member {
  id        String   @id @default(uuid())
  name      String
  email     String
  clubId    String
  status    String
  createdAt DateTime @default(now())

  club      Club     @relation(fields: [clubId], references: [id])

  @@unique([email, clubId])
  @@index([clubId])
  @@index([status])
  @@index([createdAt])
  @@map("members")
}

Connection Pooling

typescript
// apps/api/src/config/database.config.ts
export const databaseConfig = {
  url: process.env.DATABASE_URL,
  pool: {
    min: 2,
    max: 10,
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 2000,
  },
  log: ['query', 'error'],
};

Horizontal Scaling (Kubernetes HPA)

yaml
# infrastructure/k8s/deployments/api-hpa.yml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: sport-tech-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sport-tech-api
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

📚 Referências

Documentação Oficial

Livros Recomendados

  • Clean Architecture - Robert C. Martin
  • Domain-Driven Design - Eric Evans
  • Designing Data-Intensive Applications - Martin Kleppmann
  • Building Microservices - Sam Newman

Atualizado em: 2026-01-09
Versão: 1.0.0
Mantenedor: Equipe Sport Tech Club