Nos últimos anos, o Node.js consolidou-se como uma das principais plataformas para desenvolvimento de servidores web, impulsionado principalmente pelo Express. O Express é amplamente utilizado devido à sua facilidade de uso, extensa compatibilidade com middlewares e enorme comunidade. No entanto, conforme o cenário de desenvolvimento evolui e novas demandas surgem, torna-se necessário repensar alguns aspectos da arquitetura de servidores web.
Ao decidir criar um novo servidor HTTP para o CMMV (Meu projeto pessoal), a pergunta óbvia foi: “Por que não simplesmente usar o Express?” A resposta para isso não é tão simples, mas se resume a alguns pontos principais:
Foco na compatibilidade em vez de performance
O Express prioriza a compatibilidade total com todas as aplicações que rodam nele, além de manter suporte a middlewares antigos e novos. Isso significa que qualquer mudança no framework precisa ser extremamente cuidadosa para não quebrar implementações existentes. Como resultado, a evolução do código é lenta e conservadora, pois mudanças precisam passar por longos períodos de avaliação e testes antes de serem implementadas.
O Express foi criado há mais de 10 anos e, mesmo com atualizações, ainda carrega algumas decisões arquitetônicas que não são as mais otimizadas para cenários modernos. Recentemente, ao participar de discussões na comunidade, percebi que algumas funções internas do JavaScript utilizadas na construção do objeto request/response
do Express são extremamente lentas, impactando diretamente a performance em altas cargas de requisições.
Embora alternativas como o Fastify tenham surgido com um foco mais agressivo em performance, introduzindo melhorias significativas como um sistema de hooks assíncronos e o uso do fast-json-stringify
— que já utilizo no CMMV —, elas também trazem um custo adicional de complexidade. No caso do Fastify, por exemplo, é necessário definir schemas explícitos para cada resposta JSON, tornando a escrita de código mais burocrática. Essa troca entre facilidade de desenvolvimento e desempenho pode ser válida dependendo do nível de maturidade da equipe e do projeto, mas, no contexto do CMMV, representa um obstáculo desnecessário. Como a arquitetura do CMMV já é baseada em contratos, nos quais as estruturas de API e dados são geradas automaticamente, a exigência de schemas JSON adicionais se tornaria redundante e impactaria negativamente a agilidade no desenvolvimento.
Mais do que a questão de performance, existe um fator ainda mais determinante: o tempo e o esforço necessários para implementar mudanças. Frameworks amplamente utilizados, como Express e Fastify, precisam atender a diferentes cenários de uso, o que os torna flexíveis, mas também faz com que sua evolução seja mais lenta ou exija configurações extras para funcionar de maneira ideal em cada caso. No CMMV, a ideia sempre foi minimizar a necessidade de configurações manuais e tornar a estrutura da aplicação o mais fluida possível. O modelo de contrato já define como a API deve se comportar, sem que o desenvolvedor precise se preocupar com a criação de schemas ou configurações adicionais. Isso significa que utilizar um servidor web genérico seria um desperdício de recursos, pois adicionaria camadas desnecessárias de processamento e configuração.
Diante desse cenário, ficou evidente que a melhor solução seria desenvolver um servidor HTTP otimizado especificamente para o CMMV. Esse servidor foi projetado desde o início para ser enxuto e eficiente, eliminando camadas desnecessárias e garantindo um ciclo de requisição-resposta mínimo. Além de oferecer suporte nativo a WebSockets e RPC, algo essencial para a comunicação no CMMV, ele se integra diretamente com Protobuf, garantindo uma comunicação binária rápida e eficiente. O processamento de request
e response
foi otimizado para evitar alocações desnecessárias e reaproveitar objetos sempre que possível, reduzindo o overhead e aumentando a escalabilidade. O resultado é um servidor que atende às necessidades do projeto sem comprometer a simplicidade e a flexibilidade.
O Express e o Fastify continuam sendo excelentes soluções para diversas aplicações, mas nenhum deles foi projetado levando em consideração o modelo do CMMV, que se baseia na automação da estrutura da API e na eliminação de configurações manuais. Criar um servidor HTTP próprio não foi uma decisão tomada para “reinventar a roda”, mas sim uma necessidade para garantir que o CMMV mantenha sua proposta de leveza, modularidade e alto desempenho. Nos próximos artigos, pretendo compartilhar os desafios enfrentados no desenvolvimento desse servidor e como conseguimos torná-lo a melhor opção para o ecossistema do CMMV.
Desafios na criação do servidor HTTP
O desenvolvimento de um servidor HTTP otimizado para o CMMV trouxe uma série de desafios, desde a definição da estrutura base até a otimização de cada componente crítico para garantir o máximo de desempenho. Para não reinventar completamente a roda e manter um design familiar para desenvolvedores acostumados com frameworks populares, utilizei Express, Fastify e Koa como referência. No entanto, todo o código foi reescrito em TypeScript, priorizando eficiência e flexibilidade.
Um dos primeiros desafios foi garantir uma estrutura leve e modular sem sacrificar a compatibilidade com padrões amplamente utilizados. Para isso, a implementação das principais funcionalidades seguiu um modelo muito próximo ao do Express, permitindo o uso de middlewares e manipulação de headers da mesma forma que os desenvolvedores já conhecem. Entretanto, algumas partes fundamentais precisaram ser reconstruídas do zero para evitar os gargalos presentes no Express.
A otimização começou na gestão de objetos e criação de instâncias. Sempre que possível, ao invés de instanciar novas classes com new
, utilizei Object.create(null)
, que é mais eficiente, pois evita a criação desnecessária de protótipos e métodos herdados. Essa abordagem foi aplicada principalmente nas estruturas internas de request e response, reduzindo a sobrecarga do garbage collector e melhorando a reutilização de objetos.
Para o roteamento, utilizei o find-my-way
do Fastify, que provou ser significativamente mais rápido que o roteador do Express. Essa mudança permitiu que a resolução de rotas fosse feita com maior eficiência, utilizando uma estrutura de árvore compacta para encontrar rapidamente a rota correspondente, ao invés de percorrer uma lista de middlewares como ocorre no Express.
Outro ponto crítico foi a definição do ciclo de vida da requisição (lifecycle). Ao invés de seguir a abordagem tradicional do Express, optei por um sistema de hooks e tratamento assíncrono para melhor aproveitamento do event loop do Node.js, que evoluiu consideravelmente nos últimos anos. Essa decisão permite que cada requisição passe por diferentes estágios de processamento de forma eficiente, aproveitando ao máximo a capacidade assíncrona do runtime. Em muitos casos, também utilizei symbols para armazenar propriedades internas do request e do response, reduzindo colisões e melhorando a integridade da estrutura, similar ao que é feito no Fastify.
Mesmo buscando compatibilidade com a sintaxe do Express, algumas diferenças estruturais são inevitáveis. O comportamento de certas funções, como server-static e middlewares padrão, não pode ser idêntico ao do Express sem uma camada adicional de compatibilidade. Para mitigar esse problema, reescrevi os principais middlewares do Express diretamente no servidor do CMMV, garantindo que funções essenciais como etag, body-parser, cookie-parser, compression, helmet, server-static e CORS funcionassem de maneira otimizada e integrada ao novo modelo. Essa abordagem permite que o servidor seja leve e rápido sem depender diretamente da arquitetura legada do Express, mas mantendo um ecossistema familiar para quem já desenvolve aplicações em Node.js.
Os testes também foram um ponto importante no desenvolvimento. Para garantir estabilidade e evitar regressões, grande parte dos testes aplicados ao servidor do CMMV são os mesmos utilizados no Express. Isso assegura que o comportamento básico se mantenha previsível, mesmo com mudanças estruturais internas.
Ao longo do processo, percebi que criar um servidor HTTP do zero exige um equilíbrio entre compatibilidade, performance e flexibilidade. A decisão de reescrever componentes críticos e otimizar cada etapa do processamento de requisições trouxe um ganho de desempenho significativo, mas também impôs desafios na integração com middlewares existentes. O desenvolvimento continua em evolução, com a reimplementação de mais middlewares e aprimoramentos constantes na eficiência do roteamento, do ciclo de requisição e da gestão de memória.
“Cereja do bolo”
Durante quase quatro meses, trabalhei intensamente não apenas no desenvolvimento do servidor HTTP do CMMV, mas também em diversos outros módulos do projeto para consolidar um ecossistema completo. Hoje, finalmente, libero a versão beta do CMMV no https://cmmv.io. Apesar de ainda não ter sido amplamente testado em produção, o próprio site da documentação já utiliza esse servidor e framework como base, o que tem permitido avaliar o comportamento da aplicação em um ambiente real.
Uma das adições mais significativas na última versão do servidor, 0.9.4, foi uma funcionalidade que, pela primeira vez, permitiu que ele superasse a performance do Fastify. Esse ganho foi obtido através da implementação de uma camada de cache curto em memória, um conceito que utilizo há anos em outras aplicações e que tem se mostrado extremamente eficiente.
Em um ambiente real, nunca sabemos quando uma aplicação pode ser submetida a um grande volume de acessos simultâneos ou mesmo a um ataque DDoS. Mesmo com camadas de cache tradicionais como CDN, Redis ou caches intermediários entre o banco de dados e o controller, a aplicação ainda precisa lidar com essas requisições, resolvendo rotas, processando requisições e montando headers de resposta. Cada uma dessas etapas adiciona um pequeno tempo de latência que, quando somado, pode impactar significativamente a escalabilidade.
Uma abordagem que implementei em uma das minhas empresas para mitigar esse problema foi a criação de camadas de cache complementares às soluções tradicionais. Em resumo, quando uma aplicação recebe uma grande quantidade de requisições para uma mesma rota — por exemplo, a homepage de um site ou endpoints de API frequentemente acessados —, é muito provável que o conteúdo entregue seja idêntico para diversos usuários dentro de um curto intervalo de tempo.
Com isso em mente, a solução foi implementar um cache curto na própria memória da aplicação, com duração de 2 a 5 segundos, para armazenar o resultado da requisição antes que ela chegue ao controller. Esse cache reduz drasticamente a necessidade de:
- Resolver a rota no roteador da aplicação, poupando processamento.
- Realizar consultas a serviços externos, como Redis ou bancos de dados, evitando a latência de conexões TCP.
- Construir headers de resposta repetidamente, pois as respostas armazenadas já incluem essas informações.
Como esse cache tem uma duração extremamente curta, não há impactos negativos na atualização de dados dinâmicos. Essa abordagem é especialmente útil para requisições GET, como conteúdos HTML, views renderizadas e APIs públicas de pesquisa, onde os dados não mudam constantemente a cada segundo.
Para tornar essa otimização possível, foi necessário desenvolver um sistema eficiente de chaves de cache (cache keys), evitando colisões e garantindo que cada entrada armazenada represente corretamente o conteúdo esperado. Após diversos testes com diferentes algoritmos, optei por usar MurmurHash3 para geração de hashes rápidos e de baixa colisão, combinado com URLSearchParams para normalizar os parâmetros das requisições. Essa combinação permite armazenar as respostas mais acessadas na memória de maneira eficiente, sem comprometer a consistência dos dados.
O resultado dessa implementação foi impressionante. Em benchmarks internos, o novo servidor do CMMV não apenas superou o Express, como também apresentou um desempenho superior ao Fastify em cenários onde a camada de cache é ativada. Esse avanço reforça a ideia de que otimizações específicas para um contexto bem definido podem trazer ganhos expressivos, sem necessariamente precisar de um framework genérico que impõe regras e estruturas que nem sempre são ideais para todas as aplicações.
Concluo
Embora projetos como Express e Fastify sejam amplamente utilizados e tenham resistido ao teste do tempo, contando com uma grande comunidade que aprimora seus códigos há décadas, a realidade é que qualquer mudança nesses frameworks leva um tempo considerável para ser implementada. Mesmo quando há evidências claras de problemas de segurança ou performance, o processo de adaptação e correção pode levar meses ou até anos, devido à necessidade de manter compatibilidade com aplicações legadas e às complexidades envolvidas em cada atualização.
Não sugiro que ninguém substitua imediatamente o Express ou o Fastify pela solução desenvolvida no CMMV. O objetivo deste artigo não é propor um novo padrão universal, mas sim provocar uma reflexão sobre como a escolha da stack pode impactar diretamente a infraestrutura de uma aplicação. Muitas vezes, um framework amplamente adotado pode estar ocultando custos operacionais que não são percebidos de imediato.
Nos últimos testes realizados, ficou evidente que otimizações bem planejadas podem reduzir drasticamente o consumo de CPU e memória, minimizar a latência e aumentar a escalabilidade da aplicação sem necessidade de investir em mais hardware. Pequenos detalhes, como o tempo de resposta de um roteador ou a necessidade de múltiplas camadas de cache, podem representar um impacto direto no custo final da infraestrutura.
O desenvolvimento do servidor HTTP para o CMMV foi um processo desafiador, mas extremamente gratificante. Construir uma solução otimizada, com ciclo de requisição mais enxuto, roteamento eficiente, cache em memória para mitigar picos de carga e compatibilidade com conceitos modernos de desenvolvimento abriu novas possibilidades para um modelo de servidor mais performático e escalável.
O projeto segue evoluindo, e a versão beta já está disponível em https://cmmv.io para quem quiser testar e explorar suas funcionalidades. Com o tempo, ajustes serão feitos, novos módulos serão adicionados e o feedback da comunidade será essencial para aprimorar essa abordagem. O importante é lembrar que não existe uma solução única para todos os problemas, e que entender as limitações e pontos fortes de cada tecnologia pode ser um diferencial na construção de sistemas eficientes e sustentáveis. 🚀
Benchmarks
- https://github.com/fastify/benchmarks
- Machine: linux x64 | 32 vCPUs | 128.0GB Mem
- Node: v20.17.0
- Run: Sun Mar 16 2025 14:51:12 GMT+0000 (Coordinated Universal Time)
- Method:
autocannon -c 100 -d 40 -p 10 localhost:3000
Version | Router | Requests/s | Latency (ms) | Throughput/Mb | |
---|---|---|---|---|---|
bare | v20.17.0 | ✗ | 51166.4 | 19.19 | 9.13 |
cmmv | 0.9.4 | ✓ | 46879.2 | 21.03 | 8.40 |
fastify | 5.2.1 | ✓ | 46488.0 | 21.19 | 8.33 |
h3 | 1.15.1 | ✗ | 34626.2 | 28.37 | 6.18 |
restify | 11.1.0 | ✓ | 34020.6 | 28.88 | 6.07 |
koa | 2.16.0 | ✗ | 31031.0 | 31.72 | 5.53 |
express | 5.0.1 | ✓ | 12913.6 | 76.87 | 2.30 |
Fontes: