🏗️ Ecossistema de Microfrontends - React + Angular + NestJS API

Um ecossistema enterprise escalável integrando React, Angular e NestJS API via Module Federation, Web Components e Autenticação JWT centralizada.

ecosystem-react-angular

📋 Arquitetura Geral

┌──────────────────────────────────────────────────────────────────┐
│                  NGINX REVERSE PROXY (Port 80)                   │
│               (Router, Load Balancing, CORS, Headers)            │
├──────────────────────────────────────────────────────────────────┤
│  │
│  ├─→ /                          → HOST REACT (Port 3000)
│  ├─→ /api/*                     → API NESTJS (Port 5000)
│  ├─→ /dashboard/*               → MFE DASHBOARD (Port 3001)
│  ├─→ /users/*                   → MFE USERS (Port 3002)
│  └─→ /forms/*                   → MFE ANGULAR (Port 4200)
│
└──────────────────────────────────────────────────────────────────┘
         │
         ▼
┌──────────────────────────────────────────────────────────────────┐
│              HOST REACT (Port 3000 - Container)                  │
│                  React 18 + Vite + Auth Context                  │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐   │
│  │ Dashboard    │  │ Users        │  │ Angular MFE          │   │
│  │ MFE (React)  │  │ MFE (React)  │  │ (Route /forms)       │   │
│  │ Port 3001    │  │ Port 3002    │  │ Port 4200 (isolated) │   │
│  └──────────────┘  └──────────────┘  └──────────────────────┘   │
│                                                                  │
│  SharedAuthContext (JWT Token, User Data, Refresh Logic)        │
│  InterMFE Communication (CustomEvents + Window.postMessage)     │
└──────────────────────────────────────────────────────────────────┘
         │
         ▼
┌──────────────────────────────────────────────────────────────────┐
│              NESTJS API (Port 5000 - Container)                  │
│               Auth Guard, JWT Strategy, DB Access                │
├──────────────────────────────────────────────────────────────────┤
│  POST   /auth/login        → Valida cidenciais, devolve JWT     │
│  POST   /auth/refresh      → Renova JWT com refresh token       │
│  POST   /auth/logout       → Invalida token                     │
│  GET    /auth/me           → Dados do usuário autenticado       │
│  GET    /users             → Lista usuários (com auth)          │
│  POST   /forms             → Processa formulários (com auth)    │
└──────────────────────────────────────────────────────────────────┘

🚀 Quick Start

# Instalar dependências
npm run setup

# Iniciar todos os serviços em paralelo (com Docker Compose)
docker compose up

# Ou iniciar em desenvolvimento local
npm run dev

# Build de produção
npm run build:all

GitHub Pages

A documentação é publicada automaticamente em GitHub Pages via .github/workflows/gh-pages.yml sempre que há um push para main. O site inclui README.md, docs/SETUP.md, docs/QUICK_REFERENCE.md, docs/ARCHITECTURE.md e outras páginas selecionadas.

Para ver o deploy, verifique a URL do GitHub Pages do repositório ou a branch gh-pages.

🔐 Autenticação JWT Centralizada

Arquitetura de Autenticação

A autenticação funciona de forma centralizada entre todas as aplicações (React + Angular):

┌──────────────◄────────────────────────────────┐
│           Browser Context                     │
│  localStorage.setItem('authToken', jwt)       │
│  → Acessível a todos os MFEs via               │
│    SharedAuthContext + window.postMessage     │
└──────────────┬────────────────────────────────┘
         │
         ▼
┌──────────────────────────────────────────────┐
│     Host React + MFE React + MFE Angular     │
│                                              │
│   1. User Login (cualquer MFE)               │
│   2. Interceptor HTTP adiciona JWT           │
│   3. Token compartilhado via Context         │
│   4. Angular recebe via window.postMessage   │
│   5. Token renovado automaticamente          │
└──────────────┬──────────────────────────────┘
         │
         ▼
┌──────────────────────────────────────────────┐
│        NestJS API (Port 5000)                │
│                                              │
│  @UseGuards(JwtAuthGuard)                    │
│  Valida token em cada request                │
│  Retorna dados protegidos se válido          │
└──────────────────────────────────────────────┘

Fluxo de Login

1. User submete credenciais no MFE (React ou Angular)
2. Request POST /auth/login → NestJS API
3. NestJS valida e retorna JWT + Refresh Token
4. React armazena em localStorage + sessionStorage
5. Host React cria SharedAuthContext
6. MFEs se conectam ao Context
7. Angular MFE recebe token via window.postMessage
8. Todos os requests posteriores incluem Header: Authorization: Bearer <token>

Implementação no Host React

// apps/host-react/src/context/AuthContext.tsx
import React, { createContext, useState, useCallback, useEffect } from 'react';

export const AuthContext = createContext<{
  user: User | null;
  token: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isLoading: boolean;
} | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [token, setToken] = useState<string | null>(null);
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    // Restore token from localStorage on mount
    const storedToken = localStorage.getItem('authToken');
    if (storedToken) {
      setToken(storedToken);
      validateToken(storedToken);
    }

    // Broadcast token to other MFEs
    if (storedToken) {
      window.postMessage({ 
        type: 'AUTH_TOKEN_UPDATED',
        token: storedToken 
      }, '*');
    }
  }, []);

  const login = useCallback(async (email: string, password: string) => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) throw new Error('Login failed');

      const data = await response.json();
      const { access_token, refresh_token, user } = data;

      setToken(access_token);
      setUser(user);
      localStorage.setItem('authToken', access_token);
      localStorage.setItem('refreshToken', refresh_token);

      // Broadcast to other MFEs
      window.postMessage({
        type: 'AUTH_TOKEN_UPDATED',
        token: access_token,
        user,
      }, '*');

      // Emit custom event para MFEs que usam CustomEvents
      window.dispatchEvent(new CustomEvent('mfe:auth-updated', {
        detail: { token: access_token, user }
      }));
    } finally {
      setIsLoading(false);
    }
  }, []);

  const logout = useCallback(() => {
    setToken(null);
    setUser(null);
    localStorage.removeItem('authToken');
    localStorage.removeItem('refreshToken');
    
    window.postMessage({ type: 'AUTH_LOGOUT' }, '*');
    window.dispatchEvent(new CustomEvent('mfe:auth-logout'));
  }, []);

  return (
    <AuthContext.Provider value={{ user, token, login, logout, isLoading }}>
      {children}
    </AuthContext.Provider>
  );
}

Integração no MFE Angular

// apps/mfe-angular-forms/src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { BehaviorSubject, fromEvent } from 'rxjs';
import { filter, map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private tokenSubject = new BehaviorSubject<string | null>(null);
  public token$ = this.tokenSubject.asObservable();

  constructor(private http: HttpClient) {
    this.listenToHostToken();
  }

  private listenToHostToken() {
    // Listen for token updates from Host React via window.postMessage
    fromEvent<MessageEvent>(window, 'message')
      .pipe(
        filter(event => event.data.type === 'AUTH_TOKEN_UPDATED'),
        map(event => event.data.token)
      )
      .subscribe(token => {
        if (token) {
          this.tokenSubject.next(token);
          localStorage.setItem('authToken', token);
        }
      });

    // Listen for logout events
    fromEvent<MessageEvent>(window, 'message')
      .pipe(
        filter(event => event.data.type === 'AUTH_LOGOUT')
      )
      .subscribe(() => {
        this.tokenSubject.next(null);
        localStorage.removeItem('authToken');
      });

    // Check for existing token on init
    const existingToken = localStorage.getItem('authToken');
    if (existingToken) {
      this.tokenSubject.next(existingToken);
    }
  }

  // Get JWT token for HTTP calls
  getAuthHeaders() {
    const token = this.tokenSubject.value;
    return new HttpHeaders({
      'Authorization': `Bearer ${token}`
    });
  }
}

HTTP Interceptor (React MFE)

// apps/host-react/src/services/apiClient.ts
import axios, { AxiosInstance, AxiosConfig } from 'axios';

export const createApiClient = (token?: string): AxiosInstance => {
  const instance = axios.create({
    baseURL: '/api',
  });

  instance.interceptors.request.use((config) => {
    const authToken = token || localStorage.getItem('authToken');
    
    if (authToken) {
      config.headers.Authorization = `Bearer ${authToken}`;
    }
    
    return config;
  });

  instance.interceptors.response.use(
    (response) => response,
    async (error) => {
      const originalRequest = error.config;

      // Se token expirou (401), tenta renovar
      if (error.response?.status === 401 && !originalRequest._retry) {
        originalRequest._retry = true;

        try {
          const refreshToken = localStorage.getItem('refreshToken');
          const response = await axios.post('/api/auth/refresh', {
            refresh_token: refreshToken,
          });

          const { access_token } = response.data;
          localStorage.setItem('authToken', access_token);

          originalRequest.headers.Authorization = `Bearer ${access_token}`;
          return instance(originalRequest);
        } catch (refreshError) {
          // Refresh falhou, fazer logout
          localStorage.removeItem('authToken');
          window.location.href = '/login';
          throw refreshError;
        }
      }

      return Promise.reject(error);
    }
  );

  return instance;
};

🛜 MFE Angular via rota /forms

A aplicação Angular agora é integrada como uma rota de proxy no host React, usando /forms/ em vez de carregar o app por injeção manual ou iframe.

Vantagens do modelo baseado em rota

Como acessar

Exemplo de configuração do Nginx

location = /forms {
    return 301 /forms/;
}

location /forms/ {
    proxy_pass http://mfe_angular/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Prefix /forms;
}

Autenticação

A sincronização do JWT continua usando localStorage e window.postMessage, garantindo que o Angular receba o mesmo token do host React.

🗂️ Estrutura

🗼 Proxy Reverso Nginx

O Nginx atua como gateway centralizado para todos os serviços:

Benefícios

Roteamento unificado - URL única para toda a aplicação

CORS automático - Gerenciado em um único lugar

Cache inteligente - Assets estáticos cacheados

Compressão - Gzip habilitado para todas as respostas

SSL/TLS - Terminação SSL centralizada

Rate limiting - Proteção contra abuso

Health checks - Monitoramento de serviços

Rotas Configuradas

/                    → Host React (localhost:3000)
/api/*              → NestJS API (localhost:5000)
/dashboard/*         → MFE Dashboard (localhost:3001)
/users/*             → MFE Users (localhost:3002)
/forms/*             → MFE Angular (localhost:4200)
/health             → Health check endpoint

Configuração Dockerfile

# docker-compose.yml
nginx:
  image: nginx:latest
  ports:
    - "80:80"
  volumes:
    - ./nginx.conf:/etc/nginx/nginx.conf:ro
  depends_on:
    - host-react
    - api-nestjs
    - mfe-dashboard
    - mfe-users
    - mfe-angular

Cache de Assets

# Arquivos estáticos são cacheados por 1 hora
proxy_cache_valid 200 1h;
proxy_cache_valid 404 5m;

# Adicionar header indicando status do cache
add_header X-Cache-Status $upstream_cache_status;

Headers de Segurança

add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";

# CORS headers (quando necessário)
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;

🔄 Comunicação Inter-MFE

React ↔︎ React (Module Federation + Context)

// Host React compartilha via Provider
<AuthProvider>
  <DashboardMFE />
  <UsersMFE />
</AuthProvider>

// MFEs consomem via Hook
const { token, user } = useAuth();

React ↔︎ Angular (Message events + CustomEvents)

// React emite evento
window.postMessage({
  type: 'USER_UPDATED',
  data: { userId, name }
}, '*');

// Angular escuta
window.addEventListener('message', (event) => {
  if (event.data.type === 'USER_UPDATED') {
    this.userService.update(event.data.data);
  }
});

// Alternativa: CustomEvents (agnóstico de framework)
window.dispatchEvent(new CustomEvent('mfe:user-updated', {
  detail: { userId, name }
}));

Broadcast de Eventos Globais

// Event bus compartilhado
export class EventBus {
  private static events = new Map<string, Set<Function>>();

  static on(event: string, callback: Function) {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }
    this.events.get(event)!.add(callback);
  }

  static emit(event: string, data: any) {
    if (this.events.has(event)) {
      this.events.get(event)!.forEach(cb => cb(data));
    }
  }

  static off(event: string, callback: Function) {
    this.events.get(event)?.delete(callback);
  }
}

// Uso em qualquer MFE
EventBus.emit('auth:logout');
EventBus.on('auth:logout', () => {
  // Limpar estado local
});

🛠️ Tecnologias

🔐 Configuração de Autenticação - NestJS API

Instalação de Dependências

cd apps/api-nestjs
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install @types/passport-jwt --save-dev

JWT Strategy

// src/auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email };
  }
}

JWT Auth Guard

// src/auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

Auth Service

// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(email: string, password: string) {
    const user = await this.usersService.findOne(email);
    if (user && await bcrypt.compare(password, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { email: user.email, sub: user.id };
    
    return {
      access_token: this.jwtService.sign(payload, {
        expiresIn: '15m',
        secret: process.env.JWT_SECRET,
      }),
      refresh_token: this.jwtService.sign(payload, {
        expiresIn: '7d',
        secret: process.env.JWT_REFRESH_SECRET,
      }),
      user,
    };
  }

  async refresh(refreshToken: string) {
    try {
      const payload = this.jwtService.verify(refreshToken, {
        secret: process.env.JWT_REFRESH_SECRET,
      });

      const user = await this.usersService.findById(payload.sub);
      return {
        access_token: this.jwtService.sign(
          { email: user.email, sub: user.id },
          { expiresIn: '15m', secret: process.env.JWT_SECRET }
        ),
      };
    } catch {
      throw new UnauthorizedException('Invalid refresh token');
    }
  }
}

Auth Controller

// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Get, Req } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  async login(@Body() loginDto: { email: string; password: string }) {
    const user = await this.authService.validateUser(
      loginDto.email,
      loginDto.password,
    );

    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    return this.authService.login(user);
  }

  @Post('refresh')
  async refresh(@Body() body: { refresh_token: string }) {
    return this.authService.refresh(body.refresh_token);
  }

  @UseGuards(JwtAuthGuard)
  @Get('me')
  getProfile(@Req() req: any) {
    return req.user;
  }

  @Post('logout')
  logout() {
    return { message: 'Logged out successfully' };
  }
}

⚙️ Configuração de Produção

Ver docs/SETUP.md para detalhes de: - Environment variables - Lazy loading - Error boundaries - Shared libraries versionadas - Deploy independente

Variáveis de Ambiente

# .env.example
API_BASE_URL=http://localhost:5000
JWT_SECRET=your-super-secret-key-change-in-production
JWT_REFRESH_SECRET=your-refresh-secret-key-change-in-production
NODE_ENV=production

Docker Compose com Proxy Nginx

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - host-react
      - api-nestjs
      - mfe-dashboard
      - mfe-users
      - mfe-angular

  host-react:
    build:
      context: ./apps/host-react
      dockerfile: ./Dockerfile
    environment:
      - VITE_API_URL=/api

  api-nestjs:
    build:
      context: ./apps/api-nestjs
      dockerfile: ./Dockerfile
    environment:
      - DATABASE_URL=postgresql://user:password@postgres:5432/ecossistema
      - JWT_SECRET=${JWT_SECRET}
      - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
    depends_on:
      - postgres

  mfe-dashboard:
    build:
      context: ./apps/mfe-react-dashboard
      dockerfile: ./Dockerfile

  mfe-users:
    build:
      context: ./apps/mfe-react-users
      dockerfile: ./Dockerfile

  mfe-angular:
    build:
      context: ./apps/mfe-angular-forms
      dockerfile: ./Dockerfile

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ecossistema
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

📚 Documentação

⚠️ Considerações Importantes

Use Module Federation para MFEs React que compartilham estado/libs

Use Web Components para funcionalidades isoladas (Angular, Vue, etc)

Centralize autenticação em um único contexto/serviço

Use proxy reverso para simplificar CORS e roteamento

Evite comunicação muito intensa entre MFEs (indica que não deveriam ser separados)

Nunca compartilhe globais (window, localStorage diretamente sem contexto)

Não repita lógica de autenticação em cada MFE

🎯 Status do Projeto

✅ Implementado

🔄 Em Progresso

📋 Roadmap

🚀 Como Usar

Desenvolvimento Local

# 1. Instalar dependências
npm run setup

# 2. Iniciar todos os serviços (requer Docker)
docker compose up

# 3. Acessar a aplicação
# Host React: http://localhost:3000
# API: http://localhost:5000
# Nginx: http://localhost (se configurado)

Build de Produção

# Build todos os serviços
npm run build:all

# Build e publicar imagens Docker
docker compose build --push

# Deploy com Docker Compose
docker compose -f docker-compose.prod.yml up

💡 Exemplos de Uso

Login em um MFE React

import { useAuth } from '@shared/auth';

export function LoginPage() {
  const { login, isLoading, error } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await login(email, password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input 
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button disabled={isLoading} type="submit">
        {isLoading ? 'Carregando...' : 'Login'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </form>
  );
}

Chamar API Protegida no NestJS

// Route protegida com JwtAuthGuard
@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Get()
  async findAll(@Req() req: any) {
    console.log('User:', req.user); // Dados do JWT
    return this.usersService.findAll();
  }

  @Post()
  async create(@Body() createUserDto: CreateUserDto, @Req() req: any) {
    return this.usersService.create(createUserDto);
  }
}

Comunicação entre MFEs

// Emit from MFE Dashboard
EventBus.emit('dashboard:filter-changed', { category: 'sales' });

// Listen in MFE Users
EventBus.on('dashboard:filter-changed', (data) => {
  console.log('Filtro alterado:', data.category);
  // Atualizar estado local
});

🔧 Troubleshooting

CORS Errors

Certifique-se de que o nginx.conf tem os headers CORS configurados corretamente e que todos os serviços estão atrás do proxy.

Token Expirado

O interceptor HTTP deve tentar renovar o token automaticamente. Se falhar, o usuário é redirecionado para login.

MFE Angular não carrega

Verifique se o Angular MFE está acessível via proxy em http://localhost:3000/forms/ e se o host Nginx está roteando corretamente para apps/mfe-angular-forms.

📄 Licença

MIT - Ecossistema corporativo para uso interno.