DEV Community

Moprius
Moprius

Posted on

Iniciando com Express: do primeiro scaffold à organização em MVC

Node JS Express

Construir aplicações web usando apenas o módulo HTTP nativo do Node.js é educativo, mas chega rápido em um teto. À medida que o projeto cresce, você se vê reimplementando coisas que praticamente todo servidor web precisa: roteamento, parsing de corpo de requisição, sessões, servir arquivos estáticos, lidar com uploads. Em poucos dias o código vira um emaranhado difícil de manter.

É exatamente aí que entra o Express, um framework minimalista que se tornou o padrão de fato para desenvolvimento web em Node.js. Neste tutorial você vai entender por que ele é tão usado, vai instalá-lo, gerar seu primeiro projeto via linha de comando, dissecar o scaffold gerado, e ainda reorganizar a estrutura de diretórios para algo profissional, escalável e amigável ao padrão MVC.

Se você já passou pela dor de manter um servidor HTTP nativo cheio de if/else para roteamento, prepare-se: o que vem a seguir é um alívio.

Por que utilizá-lo?

Programar usando apenas a API HTTP nativa é tremendamente trabalhoso. Conforme novas funcionalidades vão sendo exigidas pelo projeto, códigos enormes são acrescentados, a complexidade aumenta de forma exponencial e qualquer manutenção futura vira um pesadelo. Para cada nova rota, você precisa adicionar mais um if no roteador artesanal. Para fazer upload de arquivos, vai precisar manipular streams na mão. Para servir arquivos estáticos, vai escrever lógica de leitura de disco com tratamento de tipos MIME. E por aí vai.

Foi a partir desse problema que surgiu o Express, um framework muito popular voltado ao desenvolvimento de aplicações web de qualquer porte, do pequeno protótipo single-page até sistemas com dezenas de rotas, models, views e controllers. Sua filosofia de trabalho foi fortemente inspirada em outro framework de larga adoção em Ruby, conhecido por sua sintaxe enxuta e expressiva, o que se reflete diretamente na maneira como o Express organiza rotas e middlewares.

Entre as características que o Express oferece, vale destacar:

  • MVR (Model-View-Routes) — uma estrutura mais leve que separa rotas, modelos e views;
  • MVC (Model-View-Controller) — o padrão clássico, totalmente suportado pela flexibilidade do framework;
  • Roteamento de URLs via callbacks — cada rota é uma função simples, sem precisar de configuração XML ou arquivos auxiliares;
  • Middleware — camadas de processamento que ficam entre a requisição e a resposta, permitindo modularizar autenticação, logs, parsing e muito mais;
  • Interface RESTful — suporte nativo aos métodos HTTP usados em APIs REST (GET, POST, PUT, DELETE);
  • Suporte a file uploads — pronto para receber arquivos multipart sem reinventar a roda;
  • Configuração baseada em variáveis de ambiente — facilita ter ambientes separados para desenvolvimento, teste e produção;
  • Suporte a helpers dinâmicos — funções acessíveis dentro das views;
  • Integração com template engines — múltiplos motores de templates suportados, como EJS, Jade (Pug), Handlebars;
  • Integração com SQL e NoSQL — não impõe uma camada de persistência, deixa você escolher.

Em outras palavras: Express resolve as partes chatas do desenvolvimento web e deixa você focar no que realmente importa, que é a lógica da sua aplicação.

Instalação e configuração

A instalação do Express é simples e existem algumas opções de configuração para começar um projeto. Para aproveitar todos os recursos, especialmente a ferramenta de linha de comando que ele oferece, a recomendação é instalar em modo global:

npm install -g express
Enter fullscreen mode Exit fullscreen mode

Feito isso, será necessário fechar e reabrir seu terminal para que o comando express fique disponível no PATH do sistema. Esse comando é um CLI (Command Line Interface) do framework, capaz de gerar a estrutura inicial de um projeto pronta para uso. Ele já configura suporte a sessões, inclui um template engine (por padrão, o Jade) e suporta diferentes engines de CSS (por padrão, CSS puro).

Para ver todas as opções disponíveis do CLI, execute:

express -h
Enter fullscreen mode Exit fullscreen mode

Você vai ver uma lista de flags que controlam o template engine, o motor de CSS, e outros detalhes da geração do projeto. Vale dar uma olhada antes de começar qualquer projeto novo, porque tomar a decisão certa nessa etapa economiza um bom tempo depois.

Criando um projeto de verdade

Para tornar o aprendizado mais concreto, vamos construir uma aplicação que vai evoluir ao longo do estudo: uma agenda de contatos integrada a um chat funcionando em tempo real. Vamos chamar o projeto de Ntalk (Node talk).

Os requisitos do projeto são os seguintes:

  • O usuário deve poder criar, editar ou excluir um contato;
  • O usuário deve se logar informando seu nome e e-mail;
  • O usuário deve poder se conectar ou desconectar do chat;
  • O usuário deve enviar e receber mensagens no chat somente entre contatos que estejam online.

Para chegar lá, vamos usar uma stack bem variada, que cobre as principais peças de uma aplicação web moderna:

  • Node.js — backend do projeto;
  • MongoDB — banco de dados NoSQL orientado a documentos, para armazenar usuários e contatos;
  • Redis — banco de dados NoSQL focado em estruturas chave-valor, ótimo para sessões e dados voláteis;
  • Express — framework para a camada web;
  • Socket.IO — módulo para comunicação em tempo real via WebSockets;
  • MongooseJS — ODM (Object Data Mapper) que abstrai o MongoDB de forma elegante para Node.js;
  • Node Redis — cliente Redis para Node.js;
  • EJS — template engine para HTML dinâmico, com sintaxe próxima do JavaScript puro;
  • Mocha — framework para testes automatizados;
  • SuperTest — módulo que emula requisições HTTP, ideal para testes de integração;
  • Nginx — servidor web de alta performance para servir arquivos estáticos em produção.

Não se assuste com o tamanho da lista. Cada uma dessas tecnologias entrará em cena no momento certo. Por enquanto, foque na base: Express e EJS.

Para começar a criar o projeto usando o CLI, execute:

express ntalk --ejs
cd ntalk
npm install
Enter fullscreen mode Exit fullscreen mode

O primeiro comando cria a pasta ntalk com a estrutura inicial do projeto, já configurada para usar EJS como template engine. O segundo entra na pasta. O terceiro instala todas as dependências listadas no package.json gerado.

Parabéns: você acabou de criar a base do seu primeiro projeto Express de verdade.

Gerando scaffold do projeto

Ao entrar no diretório do projeto recém-criado, você vai encontrar uma estrutura limpa, gerada automaticamente pelo CLI. Os principais arquivos e pastas são:

  • package.json — contém as informações sobre a aplicação: nome, autor, versão, colaboradores, URL, dependências e muito mais. É a "carteira de identidade" do projeto para o ecossistema Node.js.
  • public — pasta destinada a conteúdo estático: imagens, arquivos CSS, JavaScripts do lado cliente, fontes etc.
  • app.js — arquivo que inicializa o servidor do projeto. É o ponto de entrada da aplicação, executado com node app.js.
  • routes — diretório que mantém todas as rotas da aplicação.
  • views — diretório que contém todas as views renderizadas pelas rotas.

Quando rodamos o npm install, ele instalou por padrão as dependências que já estavam declaradas no package.json gerado. Inicialmente, são apenas duas: o Express e o EJS.

Vamos fazer pequenas alterações no scaffold para deixá-lo limpo e didático. Primeiro, abra o package.json e ajuste para deixá-lo assim:

{
    "name": "ntalk",
    "description": "Node talk - Agenda de contatos",
    "private": false,
    "version": "0.0.1",
    "scripts": {
        "start": "node app.js"
    },
    "dependencies": {
        "express": "3.4.7",
        "ejs": "0.8.5"
    }
}
Enter fullscreen mode Exit fullscreen mode

As mudanças foram pequenas mas importantes: adicionamos uma description que documenta o propósito do projeto e setamos private como false. Esses metadados ajudam tanto humanos quanto ferramentas a entenderem do que se trata o projeto.

Agora vamos enxugar o app.js. O scaffold gera um arquivo com bastante código pronto, mas, para entendermos cada peça em "baby-steps", vale apagar tudo e começar com o mínimo possível:

var express = require('express'),
    routes = require('./routes');

var app = express();

app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.static(__dirname + '/public'));

app.get('/', routes.index);
app.get('/usuarios', routes.user.index);

app.listen(3000, function() {
    console.log("Ntalk no ar.");
});
Enter fullscreen mode Exit fullscreen mode

Essa é a configuração mínima de uma aplicação Express funcional. Vamos dissecá-la em detalhes:

A função express() é o ponto de partida. Ao invocá-la, recebemos uma instância configurável do framework, que armazenamos na variável app. É a partir desse objeto que tudo será definido: rotas, middlewares, configurações de view, e a chamada para colocar o servidor no ar.

app.listen(3000, ...) funciona de forma muito parecida com o http.listen() do módulo nativo. Na prática, é um atalho (alias) que coloca a aplicação no ar na porta especificada e dispara um callback quando o servidor está pronto.

Os métodos app.get(), app.post(), app.put() e app.del() são as funções de roteamento. Cada uma corresponde a um método HTTP: GET, POST, PUT e DELETE, respectivamente. O primeiro parâmetro é a string com o caminho da rota, e o segundo é uma função callback que recebe a requisição e a resposta. Exemplo:

app.get('/contatos', function(request, response) {
    // lógica da rota aqui
});
Enter fullscreen mode Exit fullscreen mode

A diferença em relação ao módulo HTTP nativo é gritante. Em vez de um if/else gigante dentro de um único handler, agora cada rota tem sua própria função, registrada de forma declarativa. Isso é muito mais legível, testável e modular.

O método app.set(chave, valor) funciona como uma estrutura simples de chave-valor mantida dentro de app. Conceitualmente, é como se você estivesse fazendo app["chave"] = "valor". Um exemplo prático é a configuração das views: app.set('views', __dirname + '/views') diz onde estão os templates, e app.set('view engine', 'ejs') define qual engine usar para renderizá-los.

A maioria das funções chamadas diretamente pela variável express são herdadas de dois submódulos: o Connect e o módulo HTTP nativo.

Detalhes sobre o Connect

O Connect é um middleware para servidores HTTP. Com ele, é possível configurar aspectos do servidor através do conceito de pilha (stack): os primeiros itens inseridos são os primeiros a serem executados, sempre antes de a requisição chegar nos callbacks das rotas. O Express herda todas as funcionalidades do Connect, e por isso é fundamental compreender a ordem dos itens inseridos no stack de configuração.

Se você não respeitar essa ordem, a aplicação pode se comportar de forma estranha, gerar erros inesperados ou simplesmente deixar de executar rotinas que você esperava que rodassem. A documentação oficial lista esses itens já na ordem em que cada um deve ser incluído. Sempre que tiver dúvida, é para lá que você deve olhar.

Em nossa configuração inicial, inserimos apenas dois itens no stack: o template engine EJS e o diretório de arquivos estáticos. Outros itens, como parsers de corpo, sessões, logger, serão adicionados depois, conforme a aplicação for crescendo.

Para deixar isso concreto: imagine que você queira que toda requisição passe primeiro por um logger que registra a URL acessada, depois por um parser que transforma o corpo JSON da requisição em objeto JavaScript, depois por um middleware de sessão, e só então chegue na rota propriamente dita. Em Express, isso vira algo como:

app.use(logger);
app.use(bodyParser);
app.use(session);
app.get('/contatos', listarContatos);
Enter fullscreen mode Exit fullscreen mode

A ordem aqui não é detalhe: é a ordem real de execução. Se você colocar o parser depois das rotas, suas rotas vão receber um corpo de requisição vazio. Se colocar o logger no final, ele nunca vai logar requisições que terminam mais cedo. Essa é a essência do conceito de middleware: cada peça processa a requisição e decide se passa adiante (chamando next()) ou se encerra ali mesmo.

Reparou também que registramos apenas duas rotas: / e /usuarios. Note como seus callbacks vieram da variável routes, que por sua vez foi obtida com require('./routes'). Aí tem um detalhe interessante: passamos um diretório para o require, e não um arquivo. Por convenção, quando o require aponta para uma pasta, ele procura por um arquivo index.js dentro dela. Esse é o motivo de routes.index funcionar magicamente: ele está buscando o exports.index do arquivo routes/index.js. Já routes.user.index segue a regra normal: carrega routes/user.js e acessa o método exports.index exportado por ele.

Organizando os diretórios do projeto

Quando o assunto é organização de código, o Express se comporta de forma bem flexível e liberal. Apesar de utilizar o scaffold inicial gerado pelo CLI, temos total liberdade para modificar a estrutura de diretórios e arquivos. A escolha vai depender da complexidade do projeto:

  • Se for um sistema single-page muito enxuto, você pode até desenvolver todo o backend dentro do app.js.
  • Se houver muitas rotas, views, models e controllers, o caminho natural é organizar tudo no padrão MVC (Model-View-Controller).

Para o nosso projeto, vamos adotar MVC. Faltam apenas dois diretórios: models e controllers. Crie-os no nível raiz do projeto. A estrutura ficará assim:

ntalk/
├── app.js
├── package.json
├── public/
├── routes/
├── views/
├── controllers/
└── models/
Enter fullscreen mode Exit fullscreen mode

Pronto. Agora temos cada responsabilidade em seu próprio diretório.

Vale aproveitar para reforçar a intenção de cada pasta dentro do MVC, porque essa clareza vai te poupar muita confusão no futuro:

  • models — responsáveis por representar os dados da aplicação e as regras de negócio relacionadas a eles. Aqui ficam as definições dos schemas do MongoDB, validações de campos, métodos que calculam coisas a partir dos dados. Não há nada de HTTP, nada de HTML aqui. Models não sabem que existe um navegador.
  • controllers — recebem a requisição, conversam com os models para obter ou alterar dados, e decidem o que renderizar de volta. São a "cola" entre os models e as views. A maior parte da lógica de aplicação fica aqui.
  • views — apenas exibem dados. Não fazem consultas, não tomam decisões de negócio. Apenas formatam o que receberam dos controllers em HTML, JSON ou qualquer outro formato.
  • routes — mapeiam URLs para actions específicas dos controllers. Devem ser bem enxutas: nada de lógica aqui, só roteamento.

Manter essa separação disciplinada desde o início é um dos hábitos que mais distingue projetos que envelhecem bem de projetos que viram bola de neve em poucos meses.

O problema dos requires espalhados

Cada model que for usado em um controller normalmente precisaria de uma chamada require('./models/nome-do-model'). Em um projeto com vários controllers que usam vários models, isso significa dezenas de chamadas de require espalhadas pelo código, e isso polui horrivelmente os arquivos. Para piorar, qualquer mudança de caminho ou renomeação exige procurar e ajustar essas chamadas em todo o projeto.

O ideal seria usar require apenas para módulos externos ou para coisas chamadas dentro do app.js. Para resolver esse problema, surgiu um plugin chamado express-load. Ele mapeia diretórios inteiros, carrega cada arquivo encontrado e injeta os módulos resultantes dentro de uma variável que você escolher.

Adicione-o como dependência no package.json:

"dependencies": {
    "express": "3.4.7",
    "express-load": "1.1.8",
    "ejs": "0.8.5"
}
Enter fullscreen mode Exit fullscreen mode

Em seguida, rode npm install para baixar a nova dependência.

Agora vamos refatorar o app.js para usar essa nova funcionalidade:

var express = require('express'),
    load = require('express-load'),
    app = express();

// ...stack de configurações do servidor...

load('models')
    .then('controllers')
    .then('routes')
    .into(app);

// ...app.listen(3000)...
Enter fullscreen mode Exit fullscreen mode

Repare na ordem dos itens carregados pela função load(). Isso é fundamental: primeiro carregamos os models, depois os controllers (que dependem dos models) e por último as routes (que dependem dos controllers). Se inverter essa ordem, na hora em que uma rota tentar acessar um controller, ele simplesmente não vai existir ainda.

Refatorando as rotas

Continuando o refactoring, exclua o arquivo routes/user.js gerado pelo Express, ele não será mais necessário. Em seguida, renomeie routes/index.js para routes/home.js. Coloque dentro dele o seguinte código, que já está adaptado para usar a variável app injetada pelo express-load:

module.exports = function(app) {
    var home = app.controllers.home;
    app.get('/', home.index);
};
Enter fullscreen mode Exit fullscreen mode

O que está acontecendo aqui? O express-load criou automaticamente um objeto chamado controllers dentro de app, espelhando a estrutura de diretórios. Ou seja, app.controllers.home faz referência direta ao arquivo controllers/home.js. Magia? Não. Convenção. E muito útil.

Criando o primeiro controller

Para a rota funcionar, precisamos criar o controller home. Em controllers/home.js, coloque o seguinte código, que define uma action chamada index:

module.exports = function(app) {
    var HomeController = {
        index: function(req, res) {
            res.render('home/index');
        }
    };
    return HomeController;
};
Enter fullscreen mode Exit fullscreen mode

A função res.render('home/index') é uma das mais úteis do Express: ela renderiza a view localizada em views/home/index.ejs e devolve o resultado como HTML para o navegador. Note que não precisamos passar o caminho completo nem a extensão — o Express já sabe que as views estão na pasta views (porque configuramos com app.set('views', ...)) e que a extensão é .ejs (porque configuramos com app.set('view engine', 'ejs')).

Criando a primeira view

Para fechar o fluxo entre route, controller e view, exclua o arquivo views/index.ejs gerado pelo scaffold e crie o diretório views/home. A homepage será uma simples tela de login para acessar o sistema. A lógica futura será de autocadastro: quando o usuário informar um nome novo, o sistema vai cadastrá-lo automaticamente.

Dentro de views/home, crie o arquivo index.ejs com um formulário contendo os campos de nome e e-mail:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Ntalk - Agenda de contatos</title>
</head>
<body>
    <header>
        <h1>Ntalk</h1>
        <h4>Bem-vindo!</h4>
    </header>
    <section>
        <form action="/entrar" method="post">
            <input type="text" name="nome" placeholder="Seu nome">
            <br>
            <input type="text" name="email" placeholder="Seu e-mail">
            <br>
            <button type="submit">Entrar</button>
        </form>
    </section>
    <footer>
        <small>Ntalk - Agenda de contatos</small>
    </footer>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Agora vamos rodar o projeto. No terminal, execute:

node app.js
Enter fullscreen mode Exit fullscreen mode

Em seguida, acesse http://localhost:3000 no navegador. Se tudo deu certo, você verá a tela de login do Ntalk, com o título, o formulário e o rodapé.

Um detalhe importante: ao longo deste tutorial, não vamos focar em CSS nem em refinamentos visuais de HTML, porque a meta é dominar o lado JavaScript da aplicação. Você é livre para customizar o visual depois, com folhas de estilo na pasta public/css, conforme sua preferência.

Conclusão

Em poucos passos, você saiu de um servidor HTTP nativo cheio de if/else para uma aplicação Express organizada no padrão MVC, com diretórios bem definidos, carregamento automático de módulos via express-load e o fluxo completo de uma requisição passando por rota, controller e view.

Recapitulando o que vimos:

  • Por que usar Express: ele resolve as partes repetitivas do desenvolvimento web (roteamento, middlewares, parsers, views) e fornece uma API enxuta inspirada em outros frameworks expressivos. Suas características cobrem MVC, roteamento via callbacks, middleware, REST, file uploads, configuração por ambiente e integração com diversos bancos de dados e template engines.
  • Instalação: o pacote é instalado globalmente via npm install -g express, e a partir daí o comando express fica disponível no terminal, gerando scaffolds completos de projeto.
  • Criando um projeto real: definimos os requisitos do Ntalk, conhecemos a stack completa que será usada ao longo do estudo (Node, MongoDB, Redis, Express, Socket.IO, MongooseJS, EJS, Mocha, SuperTest e Nginx) e geramos o projeto via CLI.
  • Scaffold gerado: entendemos o papel do package.json, da pasta public, do app.js, da pasta routes e da pasta views. Aprendemos sobre app.set(), app.use() e os métodos de roteamento app.get(), app.post(), app.put() e app.del(). Vimos como o Express herda boa parte do seu poder do middleware Connect e como a ordem do stack de configurações importa.
  • Organização em MVC: criamos as pastas controllers e models, adicionamos o express-load para evitar uma chuva de requires pelo projeto, refatoramos as rotas para o novo padrão e criamos um primeiro controller home ligado a uma view home/index.ejs.

A partir daqui, a aplicação está pronta para crescer. Próximos passos naturais incluem implementar a lógica de cadastro automático no /entrar, criar o model de usuários, conectar ao MongoDB com Mongoose, persistir os dados e expandir o fluxo do chat em tempo real. Cada peça nova vai se encaixar nessa estrutura sem precisar reescrever nada do que já foi feito.

E é justamente esse o sinal de uma boa arquitetura: ela aceita o crescimento sem dor. Você acabou de plantar essa semente. Agora é só regar.

Boa codificação, e até o próximo tutorial.

Top comments (0)