Sumário
O CaptaCliente é um SaaS (Software as a Service) de prospecção de leads. Ele faz:
- Busca automática de leads no Google Maps
- Campanhas de envio de mensagens via WhatsApp
- Follow-up automático para leads que não responderam
- Dashboard com estatísticas e métricas do negócio
Arquitetura do sistema
Banco de dados - Tabelas principais
| Tabela | O que guarda |
|---|---|
users |
Usuários do sistema (nome, email, senha, plano) |
leads |
Leads capturados (nome, telefone, endereço, status) |
campanhas |
Campanhas de mensagens (nome, status, mensagem) |
faturas |
Cobranças e pagamentos (valor, status, vencimento) |
sugestoes |
Sugestões de busca automática |
O objetivo: criar uma API que retorne os dados principais do CaptaCliente para um app de monitoramento em React Native.
2. Leads separados por status (novo, contatado, respondeu)
3. Quantidade de usuários ativos
4. Campanhas ativas e inativas
5. Faturamento mensal
Pensamento de programador aplicado
| Pergunta | Resposta |
|---|---|
| O que preciso entregar? | Uma URL que devolve dados do sistema em JSON |
| Que dados? | Leads, usuários, campanhas, faturamento |
| De onde vêm? | Banco de dados MySQL (tabelas: leads, users, campanhas, faturas) |
| Como entrego? | Rota GET que retorna JSON |
| O que preciso criar? | 1 rota + 1 controller + conectar no app.js |
Passo 1: Criar a rota (monitorRoutes.js)
A rota é o "endereço" da API. Ela define: quando alguém acessar GET /monitor/stats, execute a função
getStats.
// backend/src/routes/monitorRoutes.js
// 1. Importa o Express (biblioteca que cria servidores web)
const express = require('express');
// 2. Cria um "mini-servidor" de rotas
const router = express.Router();
// 3. Importa o controller (quem vai fazer o trabalho)
const monitorController = require('../controllers/monitorController');
// 4. Define: GET /stats → executa getStats do controller
router.get('/stats', monitorController.getStats);
// 5. Exporta para ser usado em outro arquivo
module.exports = router;
router.get = quando alguém fizer uma requisição GET'/stats' = nesse endereçomonitorController.getStats = execute essa função
Passo 2: Criar o controller (monitorController.js)
O controller é quem faz o trabalho pesado: consulta o banco de dados e monta a resposta.
// backend/src/controllers/monitorController.js
// Importa o pool de conexões do banco
// ATENÇÃO: usa { pool } com chaves! Isso é desestruturação.
const { pool } = require('../database/connection');
exports.getStats = async (req, res) => {
try {
// Conta total de leads
const [leadsCount] = await pool.query(
'SELECT COUNT(*) AS totalLeads FROM leads'
);
// Conta leads agrupados por status
const [leadsByStatus] = await pool.query(
'SELECT status, COUNT(*) AS count FROM leads GROUP BY status'
);
// Conta usuários ativos
const [userCount] = await pool.query(
'SELECT COUNT(*) AS totalUsers FROM users WHERE ativo = 1'
);
// Conta campanhas ativas
const [campanhasAtivas] = await pool.query(
'SELECT COUNT(*) AS totalCampanhasAtivas FROM campanhas WHERE status = "ativa"'
);
// Conta campanhas inativas
const [campanhasInativas] = await pool.query(
'SELECT COUNT(*) AS totalCampanhasInativas FROM campanhas WHERE status = "inativa"'
);
// Soma faturamento pago
const [faturamentoPago] = await pool.query(
'SELECT SUM(valor) AS faturamentoMensal FROM faturas WHERE status = "pago"'
);
// Monta e retorna o JSON de resposta
res.json({
totalLeads: leadsCount[0].totalLeads,
leadsByStatus: leadsByStatus.map(row => ({
status: row.status,
count: row.count
})),
totalUsers: userCount[0].totalUsers,
totalCampanhasAtivas: campanhasAtivas[0].totalCampanhasAtivas,
totalCampanhasInativas: campanhasInativas[0].totalCampanhasInativas,
faturamentoMensal: faturamentoPago[0].faturamentoMensal || 0
});
} catch (error) {
console.error('Erro ao obter estatísticas:', error);
res.status(500).json({ error: 'Erro ao obter estatísticas' });
}
};
Passo 3: Conectar no app.js
O app.js é o "ponto central" do backend. Toda rota nova precisa ser registrada aqui.
// No arquivo backend/src/app.js, adicionar 2 linhas:
// 1. Importar a rota (no topo do arquivo, junto com as outras)
const monitorRoutes = require('./routes/monitorRoutes');
// 2. Conectar a rota (junto com os outros app.use)
app.use('/monitor', monitorRoutes);
// Resultado: GET /monitor/stats → monitorController.getStats
app.use('/monitor', ...) define o prefixo /monitor.Dentro da rota,
router.get('/stats', ...) define o complemento /stats.O Express junta:
/monitor + /stats = /monitor/statsPasso 4: Reiniciar e testar
$ pm2 restart captacliente-api
# Reinicia o servidor para carregar as mudanças
$ curl http://localhost:3737/monitor/stats
# Testa a API no terminal
Cada query usada no controller, explicada em detalhe:
COUNT(*) - Contar registros
-- Conta TODOS os leads
SELECT COUNT(*) AS totalLeads FROM leads;
-- COUNT(*) = conta todas as linhas da tabela
-- AS totalLeads = dá um nome ao resultado
-- Resultado: { totalLeads: 212 }
COUNT + WHERE - Contar com filtro
-- Conta só usuários ATIVOS
SELECT COUNT(*) AS totalUsers FROM users WHERE ativo = 1;
-- WHERE ativo = 1 = só conta quem tem ativo = 1 (verdadeiro)
-- Conta campanhas com status "ativa"
SELECT COUNT(*) AS total FROM campanhas WHERE status = "ativa";
-- WHERE status = "ativa" = filtra só as ativas
GROUP BY - Agrupar resultados
-- Conta leads SEPARADOS por status
SELECT status, COUNT(*) AS count FROM leads GROUP BY status;
-- GROUP BY status = cria um grupo pra cada status diferente
-- Resultado:
-- | status | count |
-- | novo | 90 |
-- | contatado | 77 |
-- | respondeu | 45 |
SUM - Somar valores
-- Soma o valor de todas as faturas pagas
SELECT SUM(valor) AS faturamentoMensal FROM faturas WHERE status = "pago";
-- SUM(valor) = soma todos os valores da coluna "valor"
-- Se não tiver nenhuma fatura paga, retorna NULL
-- Por isso usamos || 0 no JavaScript (se for null, retorna 0)
Desestruturação de objeto: { pool }
// O arquivo connection.js exporta um OBJETO com várias propriedades:
module.exports = { pool, testConnection };
// ERRADO - pega o objeto inteiro:
const pool = require('../database/connection');
// pool agora é { pool: ..., testConnection: ... }
// Quando fizer pool.query() → ERRO! O objeto não tem .query()
// CERTO - pega só a propriedade pool:
const { pool } = require('../database/connection');
// pool agora é o pool de conexões MySQL
// pool.query() funciona!
const pool = require(...) e deu erro 500.Corrigimos para
const { pool } = require(...) e funcionou.Lição: Sempre verifique o que o arquivo exporta antes de importar.
Desestruturação de array: [rows]
// pool.query() retorna um ARRAY com 2 itens:
// [0] = os dados (rows)
// [1] = metadados (informações sobre as colunas)
// Sem desestruturação:
const resultado = await pool.query('SELECT * FROM leads');
// resultado[0] = dados, resultado[1] = metadados
// Com desestruturação:
const [rows] = await pool.query('SELECT * FROM leads');
// rows = dados (pegou só o primeiro item do array)
async/await - Esperar respostas do banco
// O banco de dados não responde instantaneamente.
// Precisamos ESPERAR a resposta antes de continuar.
// async = "essa função vai ter operações que demoram"
exports.getStats = async (req, res) => {
// await = "ESPERA essa operação terminar antes de continuar"
const [rows] = await pool.query('SELECT * FROM leads');
// Só executa a próxima linha depois que o banco responder
};
try/catch - Tratar erros
try {
// Tenta executar o código
const [rows] = await pool.query('SELECT * FROM leads');
res.json(rows);
} catch (error) {
// Se der QUALQUER erro, cai aqui
console.error('Deu ruim:', error);
res.status(500).json({ error: 'Erro interno' });
}
// Sem try/catch: o servidor CRASHA se der erro
// Com try/catch: o servidor retorna erro 500 e CONTINUA funcionando
|| 0 - Valor padrão
// Quando SUM() não encontra nenhum registro, retorna NULL
// NULL no JSON vira null, e no app pode causar erro
faturamentoMensal: faturamentoPago[0].faturamentoMensal || 0
// Se faturamentoMensal for null → usa 0
// Se faturamentoMensal for 150.00 → usa 150.00
Esses erros aconteceram durante a construção. Cada um é uma lição:
Erro 1: Conexão recusada
curl: (7) Failed to connect to localhost port 3001Causa: Testamos na porta 3001, mas o backend roda na porta 3737
Como descobrir a porta certa:
$ ss -tlnp | grep node
# Mostra todas as portas que o Node.js está usando
# Resultado: *:3737 → porta correta é 3737
$ pm2 list
# Mostra os processos e seus PIDs
# Compara o PID do pm2 com o PID do ss para confirmar
Erro 2: Erro 500 (desestruturação)
{"error":"Erro ao obter estatísticas"}Causa:
const pool = require('../database/connection')Solução:
const { pool } = require('../database/connection')Como descobriu:
pm2 logs captacliente-api --lines 20 mostrou o erro exato
Erro 3: Erros de SQL (typos)
WERE → deveria ser WHEREcampnhas → deveria ser campanhasfinanceiro → tabela não existe, a correta é faturasFaltou aspas em
'ativa' → WHERE status = ativa (sem aspas dá erro)
Resposta real da API
// GET https://captacliente.techfluxar.com/api/monitor/stats
{
"totalLeads": 212,
"leadsByStatus": [
{ "status": "novo", "count": 90 },
{ "status": "contatado", "count": 77 },
{ "status": "respondeu", "count": 45 }
],
"totalUsers": 5,
"totalCampanhasAtivas": 4,
"totalCampanhasInativas": 0,
"faturamentoMensal": 0
}
Arquivos criados
Fluxo completo da requisição
1. Pensar como programador - dividir o problema em partes menores
2. Criar rotas - Express Router, router.get(), module.exports
3. Criar controllers - async/await, try/catch, res.json()
4. Consultar banco - SQL com COUNT, SUM, WHERE, GROUP BY
5. Desestruturação - { pool } e [rows]
6. Conectar rotas - require() e app.use()
7. Debug - pm2 logs, curl, ss -tlnp
8. Resolver erros - ler a mensagem, entender, corrigir
/monitor/stats com uma API Key no header. Assim, somente o seu app React Native
consegue acessar os dados. Isso usa o conceito de middleware (capítulo 6 desta apostila).No módulo anterior você aprendeu a criar um bot standalone com whatsapp-web.js. Agora vamos ver como
o CaptaCliente usa WhatsApp de verdade em produção — com a Evolution API, que é uma API REST
completa para WhatsApp.
• whatsapp-web.js → biblioteca npm, o bot roda dentro do seu código Node.js
• Evolution API → serviço separado com API REST. Seu backend faz requisições HTTP para ela. Mais robusto, multi-instância e com dashboard web.
W8.1 Arquitetura — Como o CaptaCliente usa a Evolution API
O fluxo funciona assim:
- O usuário clica em "Conectar WhatsApp" no frontend React
- O frontend faz
POST /whatsapp/connectno backend - O backend chama a Evolution API para criar uma instância e gerar o QR Code
- O QR Code volta em base64 e é exibido na tela
- O usuário escaneia com o celular → WhatsApp conectado
- Agora o backend pode enviar mensagens via Evolution API
- Quando um lead responde, a Evolution envia um webhook para o backend
W8.2 Instalando a Evolution API no Servidor
A Evolution API roda como um container Docker. Instale no seu servidor (VPS):
Passo 1 — Instalar Docker (se não tiver)
$ curl -fsSL https://get.docker.com | bash
$ docker --version
Docker version 24.x.x
Passo 2 — Subir a Evolution API com Docker
$ docker run -d \
--name evolution-api \
-p 8080:8080 \
-e AUTHENTICATION_API_KEY=SUA_CHAVE_SECRETA_AQUI \
-v evolution_instances:/evolution/instances \
atendai/evolution-api
• Substitua
SUA_CHAVE_SECRETA_AQUI por uma chave forte (ex: minha-chave-segura-123)
• A flag
-v persiste as sessões — se reiniciar o Docker, os WhatsApp continuam conectados
• A porta
8080 é onde a API fica acessível
Passo 3 — Verificar se está rodando
$ curl http://localhost:8080
{"status":200,"message":"Welcome to Evolution API"}
Se aparecer essa mensagem, a Evolution API está rodando! ✅
W8.3 O Service — Encapsulando a Evolution API
No CaptaCliente, toda comunicação com a Evolution está no arquivo evolutionService.js. Ele funciona
como uma "ponte" entre o backend e a API:
// backend/src/services/evolutionService.js
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'http://localhost:8080';
const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || '';
class EvolutionService {
// Header de autenticação — todas as requisições usam a apikey
_headers() {
return {
'Content-Type': 'application/json',
apikey: EVOLUTION_API_KEY,
};
}
// Método genérico para qualquer requisição à Evolution
async _request(method, path, body) {
const url = `${EVOLUTION_API_URL}${path}`;
const options = { method, headers: this._headers() };
if (body) options.body = JSON.stringify(body);
const res = await fetch(url, options);
const data = await res.json();
if (!res.ok) {
throw new Error(data.message || `Evolution API erro: ${res.status}`);
}
return data;
}
// Criar instância (conectar um novo número)
async createInstance(instanceName) {
return this._request('POST', '/instance/create', {
instanceName,
qrcode: true,
integration: 'WHATSAPP-BAILEYS',
});
}
// Verificar status da conexão
async getConnectionStatus(instanceName) {
return this._request('GET', `/instance/connectionState/${instanceName}`);
}
// Gerar QR Code para escanear
async getQRCode(instanceName) {
return this._request('GET', `/instance/connect/${instanceName}`);
}
// Enviar mensagem de texto
async sendTextMessage(instanceName, phone, message) {
return this._request('POST', `/message/sendText/${instanceName}`, {
number: phone,
text: message,
});
}
// Enviar com efeito "digitando..." (humanizado)
async sendTextWithTyping(instanceName, phone, message, delay = 2500) {
try {
await this.sendPresence(instanceName, phone, 'composing', delay);
} catch (_) {}
await new Promise(r => setTimeout(r, delay));
return this.sendTextMessage(instanceName, phone, message);
}
// Verificar se números têm WhatsApp
async checkIsWhatsapp(instanceName, phones) {
return this._request('POST', `/chat/whatsappNumbers/${instanceName}`, {
numbers: phones,
});
}
// Configurar webhook para receber respostas
async setWebhook(instanceName) {
return this._request('POST', `/webhook/set/${instanceName}`, {
webhook: {
enabled: true,
url: `https://seudominio.com/whatsapp/webhook`,
webhook_by_events: true,
events: ['MESSAGES_UPSERT'],
},
});
}
}
module.exports = new EvolutionService();
Toda a lógica de comunicação com a Evolution está encapsulada neste service. O controller nunca faz
fetch direto — sempre chama evolutionService.sendTextMessage(), etc. Se amanhã trocar de
API, só altera este arquivo.
W8.4 O Controller — Rotas da API WhatsApp
O controller expõe as funcionalidades do WhatsApp como rotas REST que o frontend consome:
| Rota | Método | O que faz |
|---|---|---|
/whatsapp/connect |
POST | Cria instância e retorna QR Code |
/whatsapp/status |
GET | Verifica se está conectado |
/whatsapp/disconnect |
POST | Desconecta e apaga instância |
/whatsapp/send |
POST | Envia mensagem de texto |
/whatsapp/send-media |
POST | Envia imagem, PDF, áudio |
/whatsapp/check-number |
POST | Verifica se número tem WhatsApp |
/whatsapp/webhook |
POST | Recebe respostas (chamado pela Evolution) |
Exemplo: Rota de Conectar
// backend/src/controllers/whatsappController.js
async connect(req, res, next) {
try {
// 1. Verificar se já tem instância no banco
let instance = await WhatsappModel.findByUserId(req.user.id);
// 2. Se já está conectado, retorna direto
if (instance && instance.status === 'connected') {
return sendSuccess(res, { status: 'connected' });
}
// 3. Nome único da instância (ex: lpp_user_42)
const instanceName = `lpp_user_${req.user.id}`;
// 4. Criar na Evolution API (retorna QR Code)
const qrData = await evolutionService.createInstance(instanceName);
// 5. Salvar no banco de dados
await WhatsappModel.create({
user_id: req.user.id,
instance_name: instanceName,
status: 'connecting',
});
// 6. Registrar webhook para receber respostas
await evolutionService.setWebhook(instanceName);
// 7. Retornar QR Code para o frontend exibir
sendSuccess(res, {
qrcode: qrData.base64,
instanceName,
status: 'connecting',
});
} catch (error) {
next(error);
}
}
Exemplo: Rota de Enviar Mensagem
async sendMessage(req, res, next) {
const { phone, message } = req.body;
// 1. Normalizar telefone (sempre com 55 na frente)
let normalizedPhone = phone.replace(/\D/g, '');
if (!normalizedPhone.startsWith('55')) {
normalizedPhone = '55' + normalizedPhone;
}
// 2. Buscar instância do usuário
const instance = await WhatsappModel.findByUserId(req.user.id);
if (!instance || instance.status !== 'connected') {
return sendError(res, 'WhatsApp não conectado', 400);
}
// 3. Verificar se o número tem WhatsApp
const check = await evolutionService.checkIsWhatsapp(instance.instance_name, normalizedPhone);
// 4. Enviar com efeito "digitando..."
const result = await evolutionService.sendTextWithTyping(
instance.instance_name,
normalizedPhone,
message
);
// 5. Logar no banco + incrementar contador
await MensagemLogModel.create({ ... });
await UserModel.incrementUsage(req.user.id, 'mensagens_enviadas');
sendSuccess(res, result, 'Mensagem enviada com sucesso');
}
W8.5 Webhook — Recebendo Respostas dos Leads
Quando um lead responde uma mensagem, a Evolution API envia um POST para o endpoint
/whatsapp/webhook do seu backend. Esse é o poder do webhook:
// backend/src/controllers/whatsappController.js
async webhook(req, res) {
const { event, instance, data } = req.body;
// Sempre retorna 200 (senão a Evolution reenvia)
if (event !== 'messages.upsert') {
return res.status(200).json({ received: true });
}
// Ignorar mensagens enviadas por nós
if (data.key.fromMe) return res.status(200).json({ received: true });
// Extrair telefone do remetente
const phone = data.key.remoteJid.replace('@s.whatsapp.net', '');
// Buscar lead no banco pelo telefone
const lead = await LeadModel.findByTelefoneNormalized(userId, phone);
// Salvar mensagem recebida
await MensagemRecebidaModel.create({
user_id: userId,
lead_id: lead.id,
telefone: phone,
mensagem: messageText,
tipo_mensagem: messageType, // texto, imagem, audio, video
});
// Atualizar status do lead para "respondeu"
await LeadModel.updateStatus(userId, lead.id, 'respondeu');
res.status(200).json({ received: true });
}
• A rota do webhook não tem autenticação JWT — é chamada diretamente pela Evolution API
• Sempre retorne
status 200, senão a Evolution reenvia a mesma mensagem várias vezes
• Ignore
key.fromMe = true (mensagens que você mesmo enviou)
• Ignore mensagens de grupo (
@g.us)
W8.6 Frontend — Página de Conexão WhatsApp
No React, a página de WhatsApp é simples — ela consome as rotas do backend:
// src/pages/WhatsApp.tsx (simplificado)
const [status, setStatus] = useState('disconnected');
const [qrCode, setQrCode] = useState(null);
// Verificar status ao carregar a página
useEffect(() => {
const checkStatus = async () => {
const res = await apiFetch('/whatsapp/status');
setStatus(res.data.status);
};
checkStatus();
}, []);
// Conectar — gerar QR Code
const handleConnect = async () => {
const res = await apiFetch('/whatsapp/connect', { method: 'POST' });
if (res.data.qrcode) {
setQrCode(res.data.qrcode); // base64 da imagem
}
};
// Exibir QR Code na tela
return (
<div>
{qrCode && (
<img
src={`data:image/png;base64,${qrCode}`}
alt="QR Code WhatsApp"
/>
)}
</div>
);
W8.7 Configuração — Variáveis de Ambiente
Para tudo funcionar, adicione estas variáveis no seu .env do backend:
# .env do backend
EVOLUTION_API_URL=http://localhost:8080
EVOLUTION_API_KEY=SUA_CHAVE_SECRETA_AQUI
BACKEND_URL=https://seudominio.com
| Variável | O que é | Exemplo |
|---|---|---|
EVOLUTION_API_URL |
Onde a Evolution está rodando | http://localhost:8080 |
EVOLUTION_API_KEY |
Chave definida ao subir o Docker | minha-chave-123 |
BACKEND_URL |
URL pública do backend (para o webhook) | https://api.meusite.com |
W8.8 Fluxo Completo — Do Clique ao Lead Respondendo
W8.9 Endpoints da Evolution API — Referência Rápida
| Ação | Método | Endpoint |
|---|---|---|
| Criar instância | POST | /instance/create |
| Gerar QR Code | GET | /instance/connect/{nome} |
| Status conexão | GET | /instance/connectionState/{nome} |
| Deletar instância | DELETE | /instance/delete/{nome} |
| Enviar texto | POST | /message/sendText/{nome} |
| Enviar mídia | POST | /message/sendMedia/{nome} |
| Verificar número | POST | /chat/whatsappNumbers/{nome} |
| Enviar presença | POST | /chat/sendPresence/{nome} |
| Configurar webhook | POST | /webhook/set/{nome} |
1. Evolution API — API REST para WhatsApp, roda via Docker na porta 8080
2. Service Layer —
evolutionService.js encapsula toda comunicação com a Evolution
3. Fluxo de conexão — criar instância → QR Code → escanear → conectado
4. Envio humanizado —
sendTextWithTyping() simula digitação antes de enviar
5. Webhook — Evolution envia POST quando lead responde, backend salva e atualiza status
6. Verificação de número —
checkIsWhatsapp() verifica se o número tem WhatsApp antes de
enviar
7. Frontend simples — React consome as rotas REST, exibe QR Code em base64
8. Variáveis de ambiente —
EVOLUTION_API_URL, EVOLUTION_API_KEY,
BACKEND_URL