Desenvolvimento backend
APIs REST em Node.js e Python, modelagem de banco relacional, autenticação, autorização por papel e desenho de rotas para consumo humano e máquina.
Mais de 4 anos construindo e sustentando software que não pode parar. Atuei em decisão em tempo real, integrações e engenharia de dados para Claro, IRB Brasil RE, HDI, Tribanco e Grupo Kroton — e hoje desenvolvo, sozinho e de ponta a ponta, um sistema de gestão full-stack em produção com automação por IA. Backend, dados e a operação no meio: é onde eu trabalho melhor.
Comecei resolvendo incidente em produção de cliente enterprise — o tipo de trabalho que ensina a ler log, achar causa raiz e entender que por trás de cada erro existe uma operação parada. De lá para cá passei a construir também: APIs de cálculo integradas a motor de decisão, pipelines de dados, e hoje um sistema completo em produção. O que me move é o encontro entre a técnica e o negócio.
APIs REST em Node.js e Python, modelagem de banco relacional, autenticação, autorização por papel e desenho de rotas para consumo humano e máquina.
Fluxos de decisão sob evento (SAS RTDM/ESP), otimização de SQL, migração de bases para nuvem e integrações REST/SOAP entre sistemas corporativos.
Análise de log, reprodução de falha, investigação de causa raiz e correção sem derrubar o serviço — em ambientes de alto volume com operação ativa.
Clique em um card para ver o contexto, o diagrama da solução, o que eu fiz em cada frente e trechos de código comentados. Os projetos autorais têm o repositório aberto no GitHub.
Reproduções das telas e do fluxo dos meus sistemas, montadas aqui com dados fictícios. Os ambientes reais rodam em produção com dados de clientes — por isso nada aqui é captura de tela do sistema ativo, nem endereço, nem credencial.
Visão consolidada de eventos aprovados, valor contratado, parcelas recebidas e o que está em atraso — a tela que responde “como está o mês” em três segundos.
/admin
Leads distribuídos por etapa, com origem, temperatura e follow-up agendado. O lead entra pela automação e caminha até virar contrato sem sair do sistema.
/admin/crm
GET /api/assistant/resumo
x-automation-secret: •••••••••
200 OK
{
"leads": { "novos": 7, "emContato": 4 },
"agenda": [
{ "tipo": "visita", "data": "2026-03-12" },
{ "tipo": "evento", "data": "2026-03-14" }
],
"financeiro": {
"aReceber": 144500,
"atrasadas": 1
},
"contratos": { "semAssinatura": 2 }
}
Rota somente-leitura protegida por segredo compartilhado. O agente responde sobre a operação inteira — e, por não existir rota de escrita nesse módulo, não tem como alterá-la.
GET /api/assistant/resumo
Filtros combináveis, cartões com as fotos já otimizadas no upload e mapa dos imóveis. Cada página de detalhe monta os próprios metadados e entra sozinha no sitemap.
/buscar
Sobre estas telas: são reproduções da interface montadas com dados fictícios. O ambiente real atende clientes de verdade, então nada aqui é captura do sistema em produção — nem endereço, nem credencial, nem dado de cliente.
Desenvolvimento full-stack de sistemas em produção, do modelo de dados ao deploy: um sistema de gestão para espaço de eventos e um portal imobiliário, além de automação de processos comerciais e integração com IA conversacional.
Sustentação e evolução de fluxos de decisão em tempo real em ambientes produtivos de alto volume para clientes enterprise, com desenvolvimento de integrações, otimização de performance e investigação de incidentes.
Primeira atuação técnica: gestão da plataforma de e-commerce, unindo front-end, experiência de compra e apoio direto à operação da loja.
Trabalhei em squad de consultoria atendendo cliente enterprise e também sozinho, do banco ao deploy. Nos dois casos o que sustenta a entrega é o mesmo: ciclo curto, combinado claro e rastro do que foi feito.
Prefiro entregar a menor versão que já resolve o problema e evoluir a partir do uso real. A automação das jornadas do Tribanco foi escopada como MVP; meu sistema de gestão nasceu com orçamento e parcela, e só ganhou CRM, contrato e assinatura depois que a operação já rodava. Escopo que cresce sem entrega vira risco.
Sprints de duas semanas com planning, daily, review e retrospectiva — o modelo que conduziu a migração do ambiente analítico da HDI, onde migrar em lotes validados era justamente o que permitia achar o erro antes do fechamento do mês. Quadro Kanban para o fluxo contínuo de sustentação, que não cabe em sprint.
Versionamento com histórico legível: mensagem de commit que explica o porquê, não só o quê. Branch por entrega, pull request para revisão e código aberto no GitHub — incluindo este site e o sistema de gestão, que qualquer pessoa pode abrir, ler e conferir o que está escrito aqui.
Documentação técnica e teste integrado fazem parte da entrega, não são anexo. Assumi a revisão 7.0 da especificação funcional do precificador do IRB — o documento que serve de contrato entre negócio e engenharia, e de base para medir mudança de escopo quando ela aparece.
Formação em desenvolvimento backend com Python: orientação a objetos, acesso a banco de dados e aplicações web com Django.
Desenho de soluções, integração entre sistemas e decisão arquitetural com trade-off explícito.
Estruturação, administração e visão estratégica de ambientes de banco de dados.
Base que sustenta a construção de interface e a preocupação com a experiência de quem usa o sistema.
SAS Institute: Real-Time Decision Manager, Creating Resources for Inbound Campaigns, Designing the Database to Support SAS Marketing Automation, SAS 360 Plan e Programação SAS. Alura: Python (fundamentos, POO e avançado), Python para Data Science, SQL com Oracle, Modelagem de Banco de Dados, Databricks, Amazon S3, Power BI, Django e Java. Em andamento: Java 17, Spring Boot e microsserviços.
Português nativo e inglês intermediário (leitura de documentação técnica). Perfil analítico e investigativo, com comunicação direta entre time técnico e área de negócio, organização, autonomia e compromisso com prazo.
Desenvolvedor de Software e Analista de Dados com mais de 4 anos entre sustentação de sistemas críticos, engenharia de dados e desenvolvimento full-stack. Experiência real de produção: incidente em cliente enterprise, API integrada a motor de decisão e sistema autoral mantido do banco ao deploy.
Sistema completo de CRM, orçamento, contrato e financeiro para um espaço de casamentos: API Express com 185 rotas, banco MySQL de 21 tabelas, painel React e automação de captação de leads.
Um espaço de eventos operava em planilhas soltas, WhatsApp e PDF: lead chegava e se perdia, orçamento era refeito à mão, parcela era controlada de cabeça e contrato ia e voltava por e-mail. O desafio era transformar isso em um sistema único — do primeiro contato do casal até a última parcela paga — rodando em hospedagem compartilhada barata, sem equipe e sem poder ficar fora do ar.
Modelei 21 tabelas em MySQL cobrindo cliente, orçamento, itens, parcelas, cardápio, custos, fornecedores, leads, contrato e assinatura. Criei um sistema de migrations numeradas com script de verificação de schema, para evoluir a base em produção sem quebrar o que já estava rodando.
Construí 185 rotas organizadas em módulos isolados por domínio (visitas, CRM comercial, WhatsApp, contratos, social, compromissos), cada um recebendo os helpers de banco e autenticação por injeção de dependência. Isso permite adicionar um domínio novo sem tocar no núcleo — reduzindo o risco de regressão a cada entrega.
Implementei autenticação JWT com política fail-closed (sem segredo configurado, o login fica desabilitado em vez de abrir), hash bcrypt com comparação de tempo constante contra enumeração de e-mail, bloqueio progressivo por tentativa de login, rate limiting por IP e rota, autorização por papel e cabeçalhos de segurança HTTP.
Desenvolvi o front em React 19 com TypeScript, Vite e Tailwind: dashboard financeiro, funil de CRM, calendário de disponibilidade, construtor de cardápio, calculadora de custo, editor de contrato ao vivo e geração de proposta em PDF direto no navegador.
Escrevi em Python, com Playwright, uma automação que captura leads de portal de casamentos, faz upsert no sistema pela API (sem duplicar) e dispara a régua de primeiro contato no WhatsApp. Tem modo dry-run e flags para rodar cada etapa isolada.
Expus um conjunto de rotas somente-leitura, protegidas por segredo compartilhado, que entregam resumo operacional, agenda, financeiro e contratos para um agente de IA orquestrado em n8n. Por serem read-only, o agente responde sobre a operação sem poder alterá-la.
Ajustei a aplicação para hospedagem compartilhada: pool de conexões limitado ao teto do provedor, retry automático em queda de conexão, logger em arquivo com fallback de caminho e endpoint de health check. Também escrevi os scripts de empacotamento, backup do banco e seed de administrador.
Se a variável de ambiente com o segredo não estiver configurada, o login não fica aberto — ele fica indisponível. Falhar fechado é mais seguro que falhar aberto.
const JWT_SECRET = process.env.JWT_SECRET || '';
// Sem segredo próprio, o login admin fica DESABILITADO
// (o site público continua no ar normalmente).
const JWT_SECRET_OK = !!JWT_SECRET && JWT_SECRET !== 'change-me-in-production';
function authMiddleware(req, res, next) {
if (!JWT_SECRET_OK) {
return res.status(503).json({ error: 'Servidor sem JWT_SECRET configurado' });
}
const m = (req.headers.authorization || '').match(/^Bearer\s+(.+)$/i);
if (!m) return res.status(401).json({ error: 'Sem token' });
try {
req.user = jwt.verify(m[1], JWT_SECRET, {
issuer: 'ranch-sistema',
audience: 'ranch-admin',
});
next();
} catch {
return res.status(401).json({ error: 'Token invalido ou expirado' });
}
}
Errar a senha uma vez é humano. Errar dez é ataque. O tempo de bloqueio cresce junto com a suspeita, sem nunca travar o usuário legítimo de vez.
function recordLoginAttempt(emailOrIp, success) {
const key = String(emailOrIp).toLowerCase();
let r = _loginAttempts.get(key) || { fails: 0, lockedUntil: 0 };
if (success) { r.fails = 0; r.lockedUntil = 0; return; }
r.fails++;
// 5 erros -> 5 minutos. 10 erros -> 30 minutos.
if (r.fails >= 10) r.lockedUntil = Date.now() + 30 * 60000;
else if (r.fails >= 5) r.lockedUntil = Date.now() + 5 * 60000;
}
// Hash descartavel: mantem o tempo de resposta constante mesmo quando o
// e-mail nao existe, para nao entregar quais contas sao validas.
const _DUMMY_HASH = bcrypt.hashSync('x', 10);
O provedor limita conexões por hora e suspende processo ocioso. O pool é pequeno de propósito, e a query tenta de novo quando a conexão cai — o usuário não vê o erro.
_pool = mysql.createPool({
...cfg,
connectionLimit: 5, // teto baixo: hospedagem compartilhada
idleTimeout: 30000, // devolve a conexao apos 30s ocioso
enableKeepAlive: true,
connectTimeout: 10000,
});
// O host suspende o processo ocioso e derruba o pool.
// Vale 1 retry: o pool entrega uma conexao nova.
async function _poolQuery(sql, params) {
try {
return await getPool().query(sql, params);
} catch (e) {
if (!_isConnLost(e)) throw e;
log('DB', 'conexao perdida, refazendo query');
return await getPool().query(sql, params);
}
}
Cada domínio novo é um arquivo isolado que recebe banco e autenticação prontos. Adicionar um módulo custa duas linhas no núcleo e não arrisca o que já funciona.
// visitas-routes.js — modulo isolado, nao duplica banco nem auth
function visitasRouter({ mysqlQuery, dbInsert, authMiddleware, asyncH }) {
const router = express.Router();
router.get('/api/visitas', authMiddleware, asyncH(async (req, res) => {
const rows = await mysqlQuery(
'SELECT * FROM `Visita` WHERE `data` BETWEEN ? AND ? ORDER BY `data`',
[req.query.from, req.query.to]
);
res.json(rows);
}));
return router;
}
// no main.js, o modulo entra com 1 linha:
app.use(visitasRouter({ mysqlQuery, dbInsert, authMiddleware, asyncH }));
A automação lê os leads do portal, envia para a API do sistema com upsert (não duplica) e só então dispara a régua de contato. O modo dry-run permite testar sem falar com ninguém.
def enviar_para_sistema(lead: Lead, cfg: Config) -> bool:
"""Upsert do lead no Ranch Life. Nao duplica: a API casa pelo telefone."""
resp = requests.post(
f"{cfg.ranch_api_url}/api/leads/ingest",
json={
"name": lead.nome,
"phone": lead.telefone,
"eventDate": lead.data_evento,
"guests": lead.convidados,
"source": "casamentos.com.br",
},
headers={"x-automation-secret": cfg.automation_secret},
timeout=20,
)
if resp.status_code >= 400:
log.error("falha ao enviar lead %s: %s", lead.nome, resp.text[:200])
return False
log.info("lead %s registrado no sistema", lead.nome)
return True
Operação inteira dentro de um sistema só: lead entra pela automação, vira orçamento, vira contrato assinado e vira parcela acompanhada. Roda em produção em hospedagem compartilhada, com backup e health check, mantido e evoluído por uma pessoa.
Site completo de imobiliária: busca de imóveis com filtros, mapa interativo, galeria otimizada e um painel onde o corretor cadastra imóvel, gerencia fotos e responde mensagens — sem depender de programador.
A imobiliária dependia de terceiros para qualquer mudança: publicar um imóvel novo, trocar uma foto ou corrigir um preço virava chamado. Além disso, imóvel só vende se for encontrado — então o site precisava ser rápido, aparecer no Google e funcionar bem no celular, que é onde o cliente procura.
Construí o backend em Express com SQLite, modelando usuários, imóveis, fotos, mensagens e configurações do site. O banco em arquivo mantém o custo de hospedagem baixo e a operação simples — adequado ao volume real do negócio, sem servidor de banco dedicado.
Foto de imóvel vem do celular com vários megabytes e derruba o carregamento da página. Implementei um pipeline com Sharp que valida o tipo do arquivo, redimensiona e recomprime em JPEG progressivo antes de gravar — o corretor sobe a foto original e o site entrega a versão leve.
Helmet para os cabeçalhos HTTP, CORS restrito por lista, rate limit separado para login e para envio de mensagem, autenticação JWT com bcrypt e validação de tipo e extensão em todo upload. A aplicação também se recusa a subir se o segredo do JWT estiver ausente ou curto demais.
Gerei o sitemap.xml dinamicamente a partir dos imóveis disponíveis no banco, com data de atualização por página, além do robots.txt bloqueando as áreas administrativas. No front, cada página de imóvel monta seus próprios metadados.
Desenvolvi o site em React com Vite: busca com filtros, página de detalhe com galeria, mapa dos imóveis com Leaflet, lista de favoritos, formulário de contato e botão flutuante de WhatsApp.
Área administrativa para cadastrar e editar imóvel, ordenar e remover fotos, ler as mensagens recebidas e ajustar as configurações do site — o objetivo era tirar a imobiliária da dependência técnica.
Valida o tipo antes de tocar no arquivo, redimensiona respeitando o limite e grava em JPEG progressivo — que aparece por inteiro desfocado e vai ganhando nitidez, em vez de carregar de cima para baixo.
const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!ALLOWED_MIME.includes(foto.mimetype)) {
return res.status(400).json({
error: `Tipo de arquivo nao permitido: ${foto.name}`,
});
}
await sharp(foto.data, { failOn: 'none' })
.rotate() // respeita a orientacao do celular
.resize({
width: IMG_MAX_WIDTH,
height: IMG_MAX_HEIGHT,
fit: 'inside', // nao corta o imovel
withoutEnlargement: true, // nao estica foto pequena
})
.jpeg({ quality: IMG_QUALITY, mozjpeg: true, progressive: true })
.toFile(caminhoCompleto);
Cada imóvel disponível vira uma URL no sitemap com a própria data de atualização. Publicar um imóvel novo já o coloca na fila de indexação, sem passo manual.
app.get('/sitemap.xml', async (req, res) => {
const imoveis = await dbAll(
"SELECT id, atualizado_em FROM imoveis WHERE status = 'disponivel' " +
"ORDER BY atualizado_em DESC LIMIT 5000"
);
const urls = [
{ loc: `${baseUrl}/`, priority: '1.0', changefreq: 'daily' },
{ loc: `${baseUrl}/buscar`, priority: '0.9', changefreq: 'daily' },
];
imoveis.forEach((i) => urls.push({
loc: `${baseUrl}/imovel/${i.id}`,
priority: '0.7',
changefreq: 'weekly',
lastmod: formatarData(i.atualizado_em),
}));
res.set('Content-Type', 'application/xml; charset=utf-8').send(montarXml(urls));
});
Um segredo fraco não deve virar um problema descoberto depois. A aplicação verifica na inicialização e se recusa a subir — erro barulhento na hora certa.
if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 16) {
console.error(
'✗ JWT_SECRET ausente ou muito curto. ' +
'Defina uma chave de pelo menos 16 caracteres em .env'
);
process.exit(1);
}
// Limites separados: login e envio de mensagem sofrem abusos diferentes.
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 });
const writeLimiter = rateLimit({ windowMs: 5 * 60 * 1000, max: 30 });
Portal no ar em domínio próprio, com a imobiliária publicando e editando os imóveis sozinha. As fotos entram pesadas e saem otimizadas, e cada imóvel novo entra no sitemap automaticamente.
APIs REST em Python, em arquitetura de microsserviços, integradas ao motor de decisão SAS RTDM para automatizar o cálculo de prêmio de contratos de resseguro.
A precificação de contratos de catástrofe era feita em planilhas de cálculo mantidas à mão. Cada cotação dependia de alguém abrir o arquivo certo, preencher na ordem certa e não errar uma célula — sem rastreabilidade e sem governança das regras. O objetivo era transformar esse conhecimento em serviço: automatizado, versionado e auditável.
Desenvolvi APIs REST em Python dedicadas aos modelos de precificação, cada modelo isolado em seu próprio serviço, com contrato de entrada e saída definido em documento funcional antes da implementação.
Os serviços foram desenhados stateless: como uma cotação pode conter várias estruturas com várias faixas cada, o contrato da API recebe listas aninhadas e devolve o resultado no índice correspondente. Cada estrutura é calculada no próprio escopo, sem interferência das demais — o que torna o serviço previsível e permite ao front decidir quantas estruturas enviar.
Integrei os serviços de cálculo ao SAS RTDM, de forma que a regra de negócio permanecesse governada no motor de decisão e o cálculo pesado ficasse no serviço especializado — cada camada fazendo o que faz melhor.
Assumi a revisão 7.0 da especificação funcional do precificador, documentando dados de entrada, cálculos e dados de saída de cada modelo. É o documento que serve de contrato entre negócio e engenharia — e de base para medir mudança de escopo.
Ajustei rotinas de cálculo e o processamento dos dados, e sustentei a integração em produção: investigação de falha, validação de resultado contra a referência de negócio e correção.
Trecho ilustrativo do padrão adotado — não é código do cliente. O serviço aceita N estruturas de uma vez e devolve N resultados na mesma ordem, sem guardar estado entre chamadas.
@app.post("/precificar")
def precificar(payload: Payload) -> Resposta:
"""Stateless: cada estrutura e calculada no proprio escopo.
Entrada e saida sao listas de mesmo tamanho e mesma ordem."""
resultados = []
for estrutura in payload.estruturas: # N estruturas
faixas = [
calcular_faixa(faixa, estrutura.parametros)
for faixa in estrutura.faixas # M faixas por estrutura
]
resultados.append(
consolidar(faixas, umbrella=estrutura.eh_umbrella)
)
return Resposta(resultados=resultados)
Cálculo de prêmio automatizado e integrado ao fluxo de decisão, substituindo a planilha manual por serviço rastreável — com as regras sob governança e a especificação versionada.
Pipeline de eventos de navegação e compra (SAS ESP) alimentando fluxos de decisão (SAS RTDM) que identificam o abandono no instante em que acontece e disparam a campanha de recuperação.
Cliente que abandona o carrinho esfria rápido. A recuperação rodava em lote e chegava tarde demais — quando a campanha disparava, a intenção de compra já tinha passado. Era preciso detectar o abandono no momento exato e reagir na hora, em uma operação com volume de operadora nacional.
Trabalhei com SAS ESP no processamento contínuo dos eventos de navegação e compra, identificando o padrão de abandono conforme os eventos chegavam — e não em uma carga posterior.
Construí e sustentei os fluxos no SAS RTDM que aplicam as regras de negócio sobre o evento detectado e decidem se, quando e com qual oferta o cliente deve ser abordado.
Refatorei lógica de regras e otimizei consultas SQL das rotinas de campanha, entregando mais de 20% de redução no tempo de processamento — em ambiente onde latência é o produto.
Atendi incidentes com impacto direto em campanha ativa: leitura de log, reprodução da falha, investigação de causa raiz e correção sem interromper o serviço.
Campanha de recuperação acionada no instante do abandono, com regras estáveis em produção de altíssimo volume e ganho mensurável de performance nas rotinas.
Migração de bases e do ambiente SAS Grid de infraestrutura on-premise para AWS, com foco em integridade dos dados, rastreabilidade e continuidade da operação.
Mover um ambiente analítico inteiro — dados, bibliotecas, conexões de banco e centenas de rotinas — para a nuvem sem perder integridade e sem parar a operação. Migração desse tipo falha no detalhe: uma biblioteca mal apontada ou uma conexão esquecida quebra rotina que só vai rodar no fechamento do mês.
Atuei na movimentação das bases para a nuvem AWS, acompanhando consistência e rastreabilidade ao longo de todo o processo — validando que o dado do outro lado é o mesmo dado.
Participei da migração das rotinas analíticas, das bibliotecas e das conexões de banco para o novo ambiente, incluindo o reapontamento das origens de dados e a validação de cada rotina migrada.
A migração foi conduzida em sprints de duas semanas, com planejamento, execução, plano de teste e revisão a cada ciclo — modelo que permite migrar em lotes e validar continuamente, em vez de apostar tudo em uma virada única.
Apoiei a estabilização depois da virada: investigação dos incidentes relacionados à transição e ajustes até o ambiente novo rodar sozinho.
Ambiente analítico rodando na nuvem com as bases íntegras e as rotinas validadas, com operação assistida até a estabilização completa.
Segmentação de base e automação da régua de comunicação com SAS Customer Intelligence 360, Marketing Automation, Information Maps e dashboards de acompanhamento.
A régua de comunicação dependia de processo manual: segmentar público, exportar planilha, anexar arquivo e enviar para o canal. Isso engessava a segmentação, atrasava a comunicação com o cliente e deixava a área sem visibilidade de performance — não havia acompanhamento de KPI das jornadas.
Construí segmentações de público com SAS Enterprise Guide e SAS Viya, traduzindo a regra comercial em critério executável sobre a base.
Modelei a camada semântica de leitura dos dados, para que a área de negócio consultasse a informação com o nome e o significado corretos, sem depender de quem escreve SQL.
Atuei na automação das jornadas com SAS CI360 e Marketing Automation, substituindo o envio manual por processo orquestrado de carga, segmentação e exportação para os canais de e-mail e SMS.
Apoiei a administração da plataforma via SAS Management Console — usuários, grupos, bibliotecas e permissões do ambiente analítico.
Régua de comunicação automatizada e segmentação liberada da planilha, com acompanhamento de indicadores das jornadas em dashboard.
Construção e sustentação de campanhas e jornadas de cliente orquestradas por dados no SAS Customer Intelligence 360.
Manter campanhas e jornadas de cliente rodando de forma estável e contínua, atendendo às mudanças pedidas pelas áreas de marketing sem quebrar o que já estava no ar.
Atuei na construção de campanhas e jornadas no SAS CI360, com a orquestração baseada nos dados do cliente.
Mantive os fluxos estáveis em operação e atendi diretamente às solicitações de ajuste das áreas de marketing, com visão sistêmica do impacto de cada mudança.
Campanhas e jornadas apoiadas por dados, com sustentação estável e resposta rápida às demandas de marketing.
O site que você está lendo. Aplicação Node/Express com renderização no servidor, conteúdo data-driven em um único arquivo e zero dependência de front-end.
Currículo em PDF não mostra como alguém escreve software. Eu queria que a própria apresentação fosse a demonstração: um site que carrega rápido, é indexável pelo Google, funciona sem JavaScript pesado e pode ser atualizado editando um único arquivo de dados — sem mexer em HTML.
Express com EJS montando a página inteira no servidor. O buscador recebe o HTML pronto — nada de conteúdo que só aparece depois que o JavaScript roda.
Todo o texto, projeto, competência e formação vive em data/content.js. O template percorre os dados e se monta sozinho: adicionar um projeto novo é adicionar um objeto na lista, e ele já vira card, modal e diagrama.
CSS próprio com variáveis de tema, tema claro/escuro persistido, animação de entrada com IntersectionObserver e modais acessíveis por teclado — tudo em JavaScript puro, sem framework.
Os diagramas de arquitetura desta página são SVG escrito à mão dentro do template: acompanham o tema claro/escuro, escalam sem perder nitidez e não custam nenhuma requisição extra.
O servidor entrega o objeto de conteúdo para o template. Para publicar um projeto novo, edita-se apenas o arquivo de dados.
const content = require('./data/content');
app.set('view engine', 'ejs');
app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h' }));
// A pagina inteira e montada no servidor a partir do conteudo.
app.get('/', (req, res) => res.render('index', { c: content }));
// Health check: a hospedagem usa para saber se o app esta de pe.
app.get('/health', (req, res) => res.json({ ok: true, uptime: process.uptime() }));
Um currículo que é, ele mesmo, um projeto entregue: versionado no GitHub, publicado e atualizado editando um arquivo.