Arquitetura de Microsserviços com NestJS e Fastify
Introdução
Como Desenvolvedor Back-End no SouJunior Labs, tive a oportunidade de arquitetar e implementar microsserviços escaláveis usando NestJS, TypeORM, Fastify e PostgreSQL para dois projetos principais: Care4You (sistema de gestão hospitalar) e Solicite-me (plataforma de serviços). Neste artigo, compartilho as estratégias e decisões arquiteturais que adotei, aplicando princípios SOLID e Clean Architecture para facilitar a manutenção e o desenvolvimento contínuo.
Por que microsserviços?
Antes de mergulharmos nos detalhes técnicos, é importante entender por que escolhemos a arquitetura de microsserviços para esses projetos:
- Escalabilidade independente: Cada serviço pode ser escalado conforme sua demanda específica
- Desenvolvimento paralelo: Equipes diferentes podem trabalhar simultaneamente em serviços distintos
- Isolamento de falhas: Problemas em um serviço não comprometem o sistema inteiro
- Flexibilidade tecnológica: Liberdade para escolher a melhor tecnologia para cada contexto
- Entrega contínua: Facilidade para atualizar serviços individuais sem interromper todo o sistema
Stack Tecnológica
Nossa stack foi cuidadosamente selecionada para atender aos requisitos de performance e manutenção:
- NestJS: Framework Node.js para construção de aplicações server-side eficientes e confiáveis
- Fastify: Substituto de alto desempenho para Express.js, com foco em velocidade e eficiência
- TypeORM: ORM TypeScript para facilitar a interação com o banco de dados
- PostgreSQL: Sistema de banco de dados relacional robusto e de alta performance
- Jest/Cypress: Ferramentas para testes unitários, de integração e end-to-end
Aplicando Princípios SOLID
Os princípios SOLID foram fundamentais para nossa arquitetura. Veja como os aplicamos:
Single Responsibility Principle (SRP)
Cada serviço e cada classe tem uma responsabilidade única e bem definida:
// Exemplo de um serviço com responsabilidade única
@Injectable()
export class PatientService {
constructor(
@InjectRepository(PatientEntity)
private patientRepository: Repository<PatientEntity>,
private readonly notificationService: NotificationService,
) {}
async createPatient(data: CreatePatientDto): Promise<PatientEntity> {
const patient = this.patientRepository.create(data);
await this.patientRepository.save(patient);
await this.notificationService.notifyAdmission(patient);
return patient;
}
}
Open/Closed Principle (OCP)
Nossas entidades são abertas para extensão, mas fechadas para modificação:
// Base class
export abstract class BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// Extended entities
@Entity('patients')
export class PatientEntity extends BaseEntity {
@Column()
name: string;
// Patient specific fields
}
@Entity('doctors')
export class DoctorEntity extends BaseEntity {
@Column()
name: string;
// Doctor specific fields
}
Clean Architecture
Implementamos Clean Architecture para garantir que nossos serviços sejam testáveis e independentes de frameworks:
src/
├── domain/
│ ├── entities/
│ └── interfaces/
├── application/
│ ├── use-cases/
│ └── services/
├── infrastructure/
│ ├── persistence/
│ ├── external-services/
│ └── http/
└── presentation/
├── controllers/
└── dtos/
Otimização de Performance
Uma das principais contribuições que fiz foi otimizar consultas SQL críticas:
Antes
// Consulta não otimizada
const appointments = await this.appointmentRepository.find({
relations: ['patient', 'doctor', 'department'],
});
Depois
// Consulta otimizada com joins e paginação
const appointments = await this.appointmentRepository
.createQueryBuilder('appointment')
.leftJoinAndSelect('appointment.patient', 'patient')
.leftJoinAndSelect('appointment.doctor', 'doctor')
.where('appointment.status = :status', { status })
.andWhere('appointment.date BETWEEN :start AND :end', { start, end })
.orderBy('appointment.date', 'ASC')
.skip(offset)
.take(limit)
.cache(60000) // Cache por 1 minuto
.getMany();
Esta otimização reduziu o tempo médio de resposta em 30%, melhorando significativamente a performance e experiência do usuário.
Sistema de Autenticação e Segurança
Desenvolvi um sistema de autenticação JWT robusto com múltiplas camadas de segurança:
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<any> {
const user = await this.userService.findByEmail(email);
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.email, sub: user.id, roles: user.roles };
return {
access_token: this.jwtService.sign(payload, { expiresIn: '1h' }),
refresh_token: this.jwtService.sign(
{ sub: user.id },
{ expiresIn: '7d' },
),
};
}
async refreshToken(token: string) {
try {
const decoded = this.jwtService.verify(token);
const user = await this.userService.findOne(decoded.sub);
return this.login(user);
} catch (e) {
throw new UnauthorizedException('Invalid refresh token');
}
}
}
Monitoramento e Logs
Implementamos um sistema robusto de monitoramento e logs usando Winston e Prometheus:
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
}),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
}),
],
}),
});
Resultados e Lições Aprendidas
A implementação dessa arquitetura resultou em:
- Maior escalabilidade: Facilidade para expandir serviços conforme necessidade
- Manutenção simplificada: Isolamento de problemas e facilidade para atualizar componentes
- Performance melhorada: Redução de 30% no tempo médio de resposta das APIs
- Maior resiliência: Tolerância a falhas com circuit breakers e retry patterns
Conclusão
A arquitetura de microsserviços com NestJS e Fastify provou ser uma escolha acertada para nossos projetos, proporcionando escalabilidade, manutenção facilitada e excelente performance. A aplicação de princípios SOLID e Clean Architecture foram fundamentais para o sucesso da implementação.
Essa experiência não apenas melhorou tecnicamente nossos sistemas, mas também estabeleceu um padrão de qualidade para futuros desenvolvimentos na empresa.
Recursos Adicionais
- Documentação NestJS
- Documentação Fastify
- Princípios SOLID em TypeScript
- Clean Architecture com Node.js
Se você tiver alguma dúvida sobre arquitetura de microsserviços ou quiser discutir mais sobre este tema, fique à vontade para entrar em contato comigo no LinkedIn ou GitHub.