Um ecossistema enterprise escalável integrando React, Angular e NestJS API via Module Federation, Web Components e Autenticação JWT centralizada.
┌──────────────────────────────────────────────────────────────────┐
│ 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) │
└──────────────────────────────────────────────────────────────────┘
# 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:allA 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.
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 │
└──────────────────────────────────────────────┘
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>
// 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>
);
}// 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}`
});
}
}// 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;
};/formsA 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.
apps/mfe-angular-formshttp://localhost:3000/forms/http://localhost:4200apps/mfe-angular-forms/src/index.html tenha
base href="/forms/"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;
}
A sincronização do JWT continua usando localStorage e
window.postMessage, garantindo que o Angular receba o mesmo
token do host React.
O Nginx atua como gateway centralizado para todos os serviços:
✅ 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
/ → 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
# 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# 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;
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;
// Host React compartilha via Provider
<AuthProvider>
<DashboardMFE />
<UsersMFE />
</AuthProvider>
// MFEs consomem via Hook
const { token, user } = useAuth();// 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 }
}));// 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
});cd apps/api-nestjs
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install @types/passport-jwt --save-dev// 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 };
}
}// src/auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}// 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');
}
}
}// 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' };
}
}Ver docs/SETUP.md para detalhes de: - Environment variables - Lazy loading - Error boundaries - Shared libraries versionadas - Deploy independente
# .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=productionversion: '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:✅ 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
# 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 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 upimport { 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>
);
}
// 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);
}
}// 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
});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.
O interceptor HTTP deve tentar renovar o token automaticamente. Se falhar, o usuário é redirecionado para login.
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.
MIT - Ecossistema corporativo para uso interno.