Deivid Micael

Desenvolvedor De Software

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:

  1. Escalabilidade independente: Cada serviço pode ser escalado conforme sua demanda específica
  2. Desenvolvimento paralelo: Equipes diferentes podem trabalhar simultaneamente em serviços distintos
  3. Isolamento de falhas: Problemas em um serviço não comprometem o sistema inteiro
  4. Flexibilidade tecnológica: Liberdade para escolher a melhor tecnologia para cada contexto
  5. 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:

  1. Maior escalabilidade: Facilidade para expandir serviços conforme necessidade
  2. Manutenção simplificada: Isolamento de problemas e facilidade para atualizar componentes
  3. Performance melhorada: Redução de 30% no tempo médio de resposta das APIs
  4. 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


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.