ENGENHARIA DE SOFTWARE · LEITURA DE ~12 MIN

Microsserviços na prática:
entendendo a arquitetura com Node.js e TypeScript

Pense em uma plataforma de IA. Em vez de colocar usuários, conversas, arquivos e requisições a modelos no mesmo servidor, cada parte vira um serviço separado.

Microsserviços na prática: um cliente envia uma requisição a um API Gateway, que a roteia para o User Service, Conversation Service, File Service ou Model Gateway, cada um com seu próprio banco ou armazenamento. Uma checklist lista: monólito vs microsserviços, limites de serviço, comunicação por APIs, deploys independentes, quando e por que usar.

SEÇÃO 01

O que é arquitetura de microsserviços

Arquitetura de microsserviços é uma forma de estruturar um sistema como um conjunto de serviços pequenos e independentes, em vez de uma única aplicação.

Cada serviço roda no seu próprio processo, tem sua própria responsabilidade e se comunica com os outros por API.

SEÇÃO 02

Exemplo de aplicação monolítica

Diagrama de um monólito: um único código-fonte contendo Usuários, Conversas, Arquivos e Requisições a modelo, ao lado de um trecho de código com rotas para cada domínio dentro da mesma aplicação Express. Uma observação diz: mais difícil de escalar, mais difícil de publicar, acoplamento forte.
MONÓLITO · TUDO EM UMA APLICAÇÃO SÓ

Tudo está dentro do mesmo projeto:

MONÓLITOTYPESCRIPT
import express from "express";

const app = express();
app.use(express.json());

const users = [
  { id: 1, name: "Marcelo" }
];

const conversations = [
  { id: 1, title: "Onboarding" }
];

const messages: any[] = [];
const files: any[] = [];

app.get("/users/:id", (req, res) => {
  const user = users.find(
    user => user.id === Number(req.params.id)
  );

  res.json(user);
});

app.get("/conversations/:id", (req, res) => {
  const conversation = conversations.find(
    conversation => conversation.id === Number(req.params.id)
  );

  res.json(conversation);
});

app.post("/conversations/:id/messages", (req, res) => {
  const message = {
    id: messages.length + 1,
    conversationId: Number(req.params.id),
    content: req.body.content
  };

  messages.push(message);

  res.status(201).json(message);
});

app.post("/files", (req, res) => {
  const file = {
    id: files.length + 1,
    name: req.body.name,
    size: req.body.size
  };

  files.push(file);

  res.status(201).json(file);
});

app.listen(3000);

Aqui, tudo roda junto:

ESTRUTURADIAGRAMA
Aplicação
├── Usuários
├── Conversas
├── Arquivos
└── Requisições a modelo

Se você alterar como as mensagens são geradas, normalmente precisa publicar novamente a aplicação inteira.

SEÇÃO 03

Separação em User, Conversation, File e Model Gateway

Quatro serviços delimitados por responsabilidade: User Service gerencia usuários, Conversation Service gerencia conversas e mensagens, File Service cuida de uploads e armazenamento, Model Gateway roteia requisições para provedores de IA. Cada serviço tem seu próprio código, banco ou armazenamento, API e deploy.
SERVIÇOS DELIMITADOS POR RESPONSABILIDADE

Agora dividimos o sistema em quatro aplicações independentes:

PORTASDIAGRAMA
User Service           porta 3001
Conversation Service   porta 3002
File Service           porta 3003
Model Gateway          porta 3004

Cada serviço possui sua própria responsabilidade.

USER SERVICETYPESCRIPT
import express from "express";

const app = express();

const users = [
  { id: 1, name: "Marcelo" },
  { id: 2, name: "Ana" }
];

app.get("/users/:id", (req, res) => {
  const user = users.find(
    user => user.id === Number(req.params.id)
  );

  if (!user) {
    return res.status(404).json({
      message: "User not found"
    });
  }

  res.json(user);
});

app.listen(3001, () => {
  console.log("User Service running on port 3001");
});

Responsabilidade:

RESPONSABILIDADETEXTO
Criar, consultar e atualizar usuários
FILE SERVICETYPESCRIPT
import express from "express";

const app = express();
app.use(express.json());

const files: any[] = [];

app.post("/files", (req, res) => {
  const file = {
    id: files.length + 1,
    name: req.body.name,
    size: req.body.size
  };

  files.push(file);

  res.status(201).json(file);
});

app.listen(3003, () => {
  console.log("File Service running on port 3003");
});

Responsabilidade:

RESPONSABILIDADETEXTO
Armazenar e servir arquivos enviados pelos usuários
MODEL GATEWAYTYPESCRIPT
import express from "express";

const app = express();
app.use(express.json());

app.post("/generate", async (req, res) => {
  const { prompt } = req.body;

  // Em produção, isso chama um provedor de IA
  // (OpenAI, Anthropic etc.) por trás dos panos.
  const reply = {
    text: `Reply to: ${prompt}`
  };

  res.json(reply);
});

app.listen(3004, () => {
  console.log("Model Gateway running on port 3004");
});

Responsabilidade:

RESPONSABILIDADETEXTO
Rotear requisições para provedores de modelos de IA

O serviço de conversas precisa conversar com os outros serviços por APIs.

CONVERSATION SERVICETYPESCRIPT
import express from "express";

const app = express();
app.use(express.json());

const conversations = [
  { id: 1, title: "Onboarding" }
];

const messages: any[] = [];

app.get("/conversations/:id", (req, res) => {
  const conversation = conversations.find(
    conversation => conversation.id === Number(req.params.id)
  );

  if (!conversation) {
    return res.status(404).json({
      message: "Conversation not found"
    });
  }

  res.json(conversation);
});

app.post("/conversations/:id/messages", async (req, res) => {
  const { userId, content } = req.body;

  const userResponse = await fetch(
    `http://localhost:3001/users/${userId}`
  );

  if (!userResponse.ok) {
    return res.status(400).json({
      message: "Invalid user"
    });
  }

  const modelResponse = await fetch(
    "http://localhost:3004/generate",
    { method: "POST", body: JSON.stringify({ prompt: content }) }
  );

  if (!modelResponse.ok) {
    return res.status(502).json({
      message: "Model request failed"
    });
  }

  const user = await userResponse.json();
  const reply = await modelResponse.json();

  const message = {
    id: messages.length + 1,
    conversationId: Number(req.params.id),
    user,
    content,
    reply: reply.text
  };

  messages.push(message);

  res.status(201).json(message);
});

app.listen(3002, () => {
  console.log("Conversation Service running on port 3002");
});

SEÇÃO 04

Comunicação entre serviços por API

Em produção, os clientes não chamam cada serviço diretamente. Um API Gateway fica na frente de tudo e roteia cada requisição para o serviço certo:

Cliente enviando uma requisição a um API Gateway, que a roteia para o User Service, Conversation Service, File Service ou Model Gateway, cada um com seu próprio banco ou armazenamento. Uma checklist lista: deploys independentes, escala só o que precisa, resiliência, flexibilidade de tecnologia.
COM MICROSSERVIÇOS: CLIENTE → API GATEWAY → SERVIÇOS

Atrás do API Gateway, os serviços continuam se chamando diretamente quando precisam de dados uns dos outros — como o Conversation Service perguntando ao User Service e ao Model Gateway antes de responder a uma mensagem. Quando alguém envia uma mensagem:

REQUISIÇÃOHTTP
POST http://localhost:8080/conversations/1/messages
Content-Type: application/json

{
  "userId": 1,
  "content": "Hello!"
}

O fluxo é:

FLUXODIAGRAMA
Cliente
   │
   ▼
API Gateway
   │
   ▼
Conversation Service
   │
   ├── GET User Service
   │
   └── POST Model Gateway

A resposta poderia ser:

RESPOSTAJSON
{
  "id": 1,
  "conversationId": 1,
  "user": {
    "id": 1,
    "name": "Marcelo"
  },
  "content": "Hello!",
  "reply": "Reply to: Hello!"
}

SEÇÃO 05

Independência de deploy e escalabilidade

Cada serviço pode ser desenvolvido e publicado separadamente.

STACK POR SERVIÇODIAGRAMA
User Service
- Node.js
- PostgreSQL
- 2 servidores

File Service
- Go
- Armazenamento de objetos
- 3 servidores

Conversation Service
- Python
- PostgreSQL
- 10 servidores

Model Gateway
- Node.js
- Sem banco de dados
- 5 servidores

Não é obrigatório usar tecnologias diferentes, mas é possível.

Se o serviço de conversas recebe muito mais tráfego, você escala somente ele:

ESCALADIAGRAMA
User Service
└── 1 instância

File Service
└── 1 instância

Conversation Service
├── instância 1
├── instância 2
├── instância 3
└── instância 4

SEÇÃO 06

Baixo acoplamento

O serviço de conversas não precisa conhecer o código interno do serviço de usuários.

Ele conhece apenas a API:

CONTRATOHTTP
GET /users/1

O User Service pode mudar completamente internamente:

USER SERVICE POR DENTROTYPESCRIPT
// Antes
const user = users.find(...);

// Depois
const user = await database.users.findById(id);

Enquanto a API continuar retornando o mesmo formato, o Conversation Service não precisa ser alterado.

SEÇÃO 07

Banco de dados por serviço

Normalmente, sim:

BANCO POR SERVIÇODIAGRAMA
User Service
└── User Database

Conversation Service
└── Conversation Database

File Service
└── File Storage

Model Gateway
└── Model Providers (externo)

Nem todo serviço tem banco próprio. O Model Gateway não guarda nada — ele existe só para abstrair provedores externos de IA, para que o resto do sistema nunca precise saber qual provedor está por trás.

O Conversation Service não deveria acessar diretamente o banco do User Service:

EVITARTYPESCRIPT
// Evitar
const user = await userDatabase.findById(userId);

O correto seria chamar a API:

CORRETOTYPESCRIPT
const response = await fetch(
  `http://user-service/users/${userId}`
);

Assim, o banco de usuários continua sendo responsabilidade exclusiva do User Service.

SEÇÃO 08

Microsserviços versus monólito modular

Microservices não significa apenas separar arquivos:

NÃO É ISSODIAGRAMA
src/
├── users/
├── conversations/
├── files/
└── model-gateway/

Isso ainda pode ser um monólito modular.

Microsserviços são aplicações realmente separadas:

É ISSODIAGRAMA
projects/
├── user-service/
│   ├── package.json
│   ├── Dockerfile
│   └── src/
│
├── conversation-service/
│   ├── package.json
│   ├── Dockerfile
│   └── src/
│
├── file-service/
│   ├── package.json
│   ├── Dockerfile
│   └── src/
│
└── model-gateway/
    ├── package.json
    ├── Dockerfile
    └── src/

Cada uma pode ter:

  • servidor próprio;
  • banco próprio;
  • repositório próprio;
  • Docker próprio;
  • deploy próprio;
  • equipe responsável.

SEÇÃO 09

Vantagens, desvantagens e quando usar

VANTAGENS

  • deploy independente por serviço;
  • escalabilidade independente — só escala o serviço que precisa;
  • liberdade para usar stacks diferentes por serviço;
  • falha isolada não derruba o sistema inteiro necessariamente.

DESVANTAGENS

  • comunicação por rede adiciona latência e pontos de falha;
  • consistência de dados fica mais difícil, sem transação única entre bancos;
  • mais infraestrutura para operar: múltiplos deploys, monitoramento, logs distribuídos;
  • exige mais maturidade de DevOps e observabilidade.

Monólito costuma ser suficiente para times pequenos ou produtos em estágio inicial. Microsserviços fazem mais sentido quando partes do sistema — como um motor de conversas que precisa escalar de repente, ou um gateway de modelo que precisa trocar de provedor — precisam escalar, evoluir ou ser publicadas de forma diferente umas das outras.

SEÇÃO 10

Conclusão

Comparação lado a lado: Monólito tem um único código-fonte, um único deploy, banco compartilhado, é mais difícil de escalar e tem acoplamento maior. Microsserviços têm múltiplos serviços com deploys independentes, banco ou armazenamento próprio, são mais fáceis de escalar e têm acoplamento menor. Nota: microsserviços não são sempre a resposta — escolha a arquitetura certa para o seu contexto.
MICROSSERVIÇOS NÃO SÃO SEMPRE A RESPOSTA

A ideia principal é:

RESUMOTEXTO
Monólito:
um sistema grande com várias responsabilidades.

Microsserviços:
vários sistemas pequenos, cada um com uma responsabilidade,
comunicando-se por APIs ou eventos.

FIM · OBRIGADO POR LER

Quer entender a arquitetura completa?

O artigo de System Design junta cache, filas, resiliência e observabilidade num guia interativo só.

LER SYSTEM DESIGN VOLTAR AOS CONTEÚDOS