Você já tem um projeto Express organizado em MVC, com sua rota inicial funcionando e uma view de login renderizando direitinho. Mas uma aplicação web de verdade exige muito mais: reaproveitamento de pedaços de HTML, controle de sessão para login e logout, um CRUD completo seguindo o padrão REST, filtros para impedir acesso indevido a áreas restritas e, claro, páginas de erro amigáveis em vez daqueles stack traces feios que assustam o usuário.
Neste tutorial, vamos juntar todas essas peças. Ao final você terá uma aplicação Express completa: usuários se autenticando, criando contatos, editando, excluindo, sendo barrados quando não logam, e vendo páginas customizadas quando algo dá errado. O projeto continua sendo o Ntalk, uma agenda de contatos que vai virar também um chat em tempo real lá na frente.
Bora dominar o Express?
Estruturando views
Antes de mergulhar em sessões e rotas REST, vale uma melhoria simples mas que faz diferença gigante na manutenção do código: a separação das views em partials.
O template engine EJS tem várias funcionalidades para programar conteúdo dinâmico dentro do HTML. Não vamos esgotar o assunto, mas vamos usar os recursos principais para renderizar dados dinâmicos e, principalmente, evitar repetição. A ideia dos partials é simples: pedaços de HTML que se repetem em várias telas (cabeçalhos, rodapés, menus laterais) ficam em arquivos próprios e são incluídos sob demanda.
Vamos criar dois partials que serão reaproveitados em quase todas as páginas. O primeiro é o cabeçalho. Dentro de views, crie o arquivo header.ejs:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Ntalk - Agenda de contatos</title>
</head>
<body>
E agora o rodapé. Crie o arquivo footer.ejs:
<footer>
<small>Ntalk - Agenda de contatos</small>
</footer>
</body>
</html>
Agora podemos enxugar a homepage. Modifique a views/home/index.ejs para usar os partials através da diretiva include do EJS:
<% include ../header %>
<header>
<h1>Ntalk</h1>
<h4>Bem-vindo!</h4>
</header>
<section>
<form action="/entrar" method="post">
<input type="text" name="usuario[nome]" placeholder="Digite o nome">
<br>
<input type="text" name="usuario[email]" placeholder="Digite o e-mail">
<br>
<button type="submit">Entrar</button>
</form>
</section>
<% include ../footer %>
A homepage agora está muito mais enxuta. O cabeçalho e o rodapé não estão mais duplicados em todas as views: estão em um único lugar. Se você precisar mudar o título do site ou adicionar uma tag <meta> nova, edita um arquivo só, e a mudança se reflete em toda a aplicação. Esse tipo de organização vai te poupar muito tempo no futuro.
Outro detalhe importante: repare que os campos do formulário usam name="usuario[nome]" e name="usuario[email]". Essa sintaxe de colchetes faz com que o body parser do Express transforme automaticamente os dados em um objeto aninhado, ou seja, no servidor você vai receber req.body.usuario.nome e req.body.usuario.email. É uma forma elegante de manter dados relacionados agrupados.
Controlando as sessões de usuários
Para o sistema fazer login e logout, precisamos de controle de sessão. A sessão é uma área de memória no servidor que mantém dados específicos de cada usuário entre uma requisição e outra, identificada por um cookie no navegador. Trabalhar com sessão em Express é muito simples: os dados são manipulados através de um objeto JSON acessível em req.session.
Primeiro, vamos adicionar duas novas rotas em routes/home.js, uma POST /entrar para receber o formulário de login e uma GET /sair para destruir a sessão:
module.exports = function(app) {
var home = app.controllers.home;
app.get('/', home.index);
app.post('/entrar', home.login);
app.get('/sair', home.logout);
};
Agora implemente as actions correspondentes no controllers/home.js. Na action login, vamos validar de forma simples se os campos nome e email foram preenchidos. Se sim, gravamos os dados na sessão e criamos um array vazio de contatos (que será usado mais tarde). Em seguida, redirecionamos para /contatos. Na action logout, chamamos req.session.destroy() para limpar tudo:
module.exports = function(app) {
var HomeController = {
index: function(req, res) {
res.render('home/index');
},
login: function(req, res) {
var email = req.body.usuario.email,
nome = req.body.usuario.nome;
if (email && nome) {
var usuario = req.body.usuario;
usuario['contatos'] = [];
req.session.usuario = usuario;
res.redirect('/contatos');
} else {
res.redirect('/');
}
},
logout: function(req, res) {
req.session.destroy();
res.redirect('/');
}
};
return HomeController;
};
Reinicie o servidor (CTRL+C para parar e node app.js para subir de novo) e tente fazer login no sistema. Surpresa: vai dar erro. E é justamente nesse erro que vamos aprender uma das peças mais importantes do Express.
Faltou habilitar o body parser e a sessão na stack
O erro acontece porque o Express, por padrão, não decodifica automaticamente os corpos de requisição vindos de formulários HTML. Ele também não tem sessão habilitada por padrão. Precisamos adicionar esses itens à stack de middlewares.
Atualize a configuração da stack no app.js para ficar assim, na ordem correta:
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.cookieParser('ntalk'));
app.use(express.session());
app.use(express.json());
app.use(express.urlencoded());
app.use(express.static(__dirname + '/public'));
Cada peça nova tem um papel específico:
-
express.cookieParser('ntalk')— precisa vir antes do middleware de sessão, porque osession()usa o cookie parser para codificar e decodificar o SessionID, que é justamente o identificador persistido no cookie do navegador. A string'ntalk'é a chave secreta usada para assinar os cookies, dificultando que alguém os forje. -
express.session()— habilita a sessão. A partir daí, todoreqganha o atributoreq.sessionque você manipula livremente. -
express.json()eexpress.urlencoded()— são os parsers de corpo. Eles transformam o conteúdo bruto das requisições POST em objetos JavaScript prontos para uso emreq.body. O primeiro lida com requisições comContent-Type: application/json(típico de APIs), e o segundo com formulários HTML padrão. É graças a eles que aquela sintaxename="usuario[nome]"virareq.body.usuario.nome.
Cuidados ao trabalhar com sessões
Tudo que você atribuir a req.session vira um atributo persistido no objeto da sessão daquele usuário. Por exemplo, req.session.mensagem = "Olá" cria a propriedade mensagem e ela vai estar disponível em requisições futuras do mesmo usuário.
Mas atenção: cuidado para não sobrescrever as funções nativas da sessão. Nomes como req.session.destroy ou req.session.regenerate são funções do próprio framework. Se você atribuir um valor a req.session.destroy, vai apagar a função e perder a capacidade de destruir aquela sessão. Isso é fonte de bugs inesperados difíceis de rastrear. Escolha nomes que não colidam com a API.
Criando a rota /contatos
Antes de testar tudo, precisamos criar a rota /contatos para onde o login está redirecionando. Vamos criar um controller, uma rota e uma view para essa área.
Crie o diretório views/contatos e dentro dele o arquivo index.ejs:
<% include ../header %>
<header>
<h2>Ntalk - Agenda de contatos</h2>
</header>
<section>
<p>Bem-vindo <%- usuario.nome %></p>
</section>
<% include ../exit %>
<% include ../footer %>
Note duas coisas: estamos exibindo o nome do usuário logado com <%- usuario.nome %>, e estamos incluindo um terceiro partial chamado exit. Esse partial vai conter o link de logout, reaproveitado por toda a área autenticada. Crie views/exit.ejs:
<section>
<a href='/sair'>Sair</a>
</section>
Agora crie o controller controllers/contatos.js, com uma única action por enquanto:
module.exports = function(app) {
var ContatoController = {
index: function(req, res) {
var usuario = req.session.usuario,
params = {usuario: usuario};
res.render('contatos/index', params);
}
};
return ContatoController;
};
E o arquivo de rotas routes/contatos.js (por convenção, o nome bate com o controller):
module.exports = function(app) {
var contatos = app.controllers.contatos;
app.get('/contatos', contatos.index);
};
Reinicie o servidor e tente novamente o login. Agora deve funcionar: você é redirecionado para a página de contatos, vê seu nome no topo e tem um link para sair. Parabéns, você acabou de implementar autenticação e sessão.
Criando rotas no padrão REST
A agenda de contatos precisa do CRUD clássico: criar, listar, atualizar e excluir contatos. A forma profissional de expor esse CRUD é via REST, que mapeia cada operação para um verbo HTTP específico:
-
GET /contatos— lista todos os contatos -
GET /contato/:id— mostra os detalhes de um contato -
POST /contato— cria um novo contato -
GET /contato/:id/editar— exibe o formulário de edição -
PUT /contato/:id— atualiza um contato existente -
DELETE /contato/:id— exclui um contato
Para que tudo isso funcione, precisamos adicionar dois itens novos na stack:
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.cookieParser('ntalk'));
app.use(express.session());
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
O express.methodOverride() permite simular os verbos PUT e DELETE em formulários HTML (já já explico o porquê disso). O app.router é o middleware que gerencia o roteamento da aplicação. Adicioná-lo explicitamente à stack nos dá controle sobre quando o roteamento acontece em relação aos outros middlewares, o que será crucial para a parte de páginas de erro mais à frente.
Implementando as rotas REST
Atualize routes/contatos.js para contemplar todas as rotas do CRUD:
module.exports = function(app) {
var contatos = app.controllers.contatos;
app.get('/contatos', contatos.index);
app.get('/contato/:id', contatos.show);
app.post('/contato', contatos.create);
app.get('/contato/:id/editar', contatos.edit);
app.put('/contato/:id', contatos.update);
app.del('/contato/:id', contatos.destroy);
};
Repare em :id — esses são parâmetros de rota. Qualquer valor que aparecer naquela posição da URL será capturado e disponibilizado em req.params.id. Por exemplo, ao acessar /contato/3, dentro da action o req.params.id vai valer "3".
Implementando as actions
Por enquanto, em vez de um banco de dados, vamos persistir os contatos na própria sessão do usuário. Isso não é uma boa ideia para produção (a sessão é volátil e tem tamanho limitado), mas é perfeito para entender o fluxo do framework sem se preocupar com infraestrutura. Substitua o conteúdo de controllers/contatos.js pelo seguinte:
module.exports = function(app) {
var ContatoController = {
index: function(req, res) {
var usuario = req.session.usuario,
contatos = usuario.contatos,
params = {usuario: usuario, contatos: contatos};
res.render('contatos/index', params);
},
create: function(req, res) {
var contato = req.body.contato,
usuario = req.session.usuario;
usuario.contatos.push(contato);
res.redirect('/contatos');
},
show: function(req, res) {
var id = req.params.id,
contato = req.session.usuario.contatos[id],
params = {contato: contato, id: id};
res.render('contatos/show', params);
},
edit: function(req, res) {
var id = req.params.id,
usuario = req.session.usuario,
contato = usuario.contatos[id],
params = {usuario: usuario, contato: contato, id: id};
res.render('contatos/edit', params);
},
update: function(req, res) {
var contato = req.body.contato,
usuario = req.session.usuario;
usuario.contatos[req.params.id] = contato;
res.redirect('/contatos');
},
destroy: function(req, res) {
var usuario = req.session.usuario,
id = req.params.id;
usuario.contatos.splice(id, 1);
res.redirect('/contatos');
}
};
return ContatoController;
};
Cada action faz algo bem específico: o index lista, o create adiciona ao array, o show exibe os detalhes de um item, o edit carrega o formulário com os dados atuais, o update aplica as alterações e o destroy remove pela posição no array usando splice.
Criando as views do CRUD
Atualize views/contatos/index.ejs para listar contatos e ter o formulário de cadastro:
<% include ../header %>
<header>
<h2>Ntalk - Agenda de contatos</h2>
</header>
<section>
<form action="/contato" method="post">
<input type="text" name="contato[nome]" placeholder="Nome">
<input type="text" name="contato[email]" placeholder="E-mail">
<button type="submit">Cadastrar</button>
</form>
<table>
<thead>
<tr>
<th>Nome</th>
<th>E-mail</th>
<th>Ação</th>
</tr>
</thead>
<tbody>
<% contatos.forEach(function(contato, index) { %>
<tr>
<td><%- contato.nome %></td>
<td><%- contato.email %></td>
<td><a href="/contato/<%- index %>">Detalhes</a></td>
</tr>
<% }) %>
</tbody>
</table>
</section>
<% include ../exit %>
<% include ../footer %>
Agora a view de edição, views/contatos/edit.ejs:
<% include ../header %>
<header>
<h2>Ntalk - Editar contato</h2>
</header>
<section>
<form action="/contato/<%- id %>" method="post">
<input type="hidden" name="_method" value="put">
<label>Nome:</label>
<input type="text" name="contato[nome]" value="<%- contato.nome %>">
<label>E-mail:</label>
<input type="text" name="contato[email]" value="<%- contato.email %>">
<button type="submit">Atualizar</button>
</form>
</section>
<% include ../exit %>
<% include ../footer %>
E por último a view de detalhes, views/contatos/show.ejs:
<% include ../header %>
<header>
<h2>Ntalk - Dados do contato</h2>
</header>
<section>
<form action="/contato/<%- id %>" method="post">
<input type="hidden" name="_method" value="delete">
<p><label>Nome:</label> <%- contato.nome %></p>
<p><label>E-mail:</label> <%- contato.email %></p>
<p>
<button type="submit">Excluir</button>
<a href="/contato/<%- id %>/editar">Editar</a>
</p>
</form>
</section>
<% include ../exit %>
<% include ../footer %>
O truque dos verbos PUT e DELETE em HTML
Você deve ter notado essas linhas estranhas:
<input type="hidden" name="_method" value="put">
<input type="hidden" name="_method" value="delete">
Esse é um detalhe importante e que pega muita gente de surpresa. A especificação atual do HTML não permite definir method="put" ou method="delete" na tag <form>. Os únicos métodos suportados nativamente são GET e POST. Para contornar isso, existe uma convenção que praticamente todos os frameworks web adotam: a tag <form> envia como POST, e um campo oculto chamado _method indica o verbo HTTP real que deveria ser usado.
O middleware express.methodOverride() é exatamente quem faz essa mágica. Ele intercepta a requisição, lê o valor de _method no corpo, e reescreve o verbo HTTP antes de chegar ao roteador. É por isso que tivemos que adicioná-lo à stack: sem ele, o servidor ignoraria o _method e trataria tudo como POST, jamais chegando nas actions update e destroy.
Reinicie o servidor e teste o CRUD completo. Cadastre alguns contatos, abra os detalhes, edite, exclua. Tudo deve funcionar, e os dados ficam persistidos enquanto a sessão durar (ou seja, até você fazer logout ou reiniciar o servidor).
Aplicando filtros antes de acessar as rotas
Já notou que se você acessar /contatos sem fazer login, a aplicação dá um erro feio? É fácil reproduzir: faça logout e tente abrir /contatos diretamente no navegador. Você vê algo como "Cannot read property 'usuario' of undefined".
A causa é simples. O controller tenta acessar req.session.usuario, mas, sem login, esse objeto não existe na sessão. JavaScript tenta ler a propriedade contatos de undefined e lança a exceção.
Para resolver isso, precisamos de um filtro de autenticação que rode antes da action propriamente dita. Frameworks de outras linguagens normalmente oferecem hooks explícitos chamados before ou after. O Express não tem isso de forma tão direta. Mas, graças aos callbacks de JavaScript, o próprio mecanismo de roteamento do Express suporta callbacks encadeados em uma rota. Resumindo: depois do path, você pode passar quantos callbacks quiser, e eles são executados em ordem, um após o outro.
Por exemplo:
app.get('/', callback1, callback2, callback3);
Esse é o mecanismo que vamos usar para criar nossos filtros.
Criando o middleware de autenticação
Na raiz do projeto, crie a pasta middleware e dentro dela o arquivo autenticador.js:
module.exports = function(req, res, next) {
if (!req.session.usuario) {
return res.redirect('/');
}
return next();
};
A lógica é direta: se não existe usuário na sessão, redireciona para a página inicial. Se existe, chama next() para passar para o próximo callback da cadeia (que será a action propriamente dita).
A função next é a peça-chave de qualquer middleware Express. Chamá-la significa "terminei meu trabalho, passe a bola adiante". Se você esquecer de chamar next() e não enviar uma resposta, a requisição fica travada para sempre, esperando algo que nunca vem.
Encaixando o filtro nas rotas
Agora vamos injetar esse middleware em todas as rotas que exigem autenticação. Modifique routes/contatos.js:
module.exports = function(app) {
var autenticar = require('./../middleware/autenticador'),
contatos = app.controllers.contatos;
app.get('/contatos', autenticar, contatos.index);
app.get('/contato/:id', autenticar, contatos.show);
app.post('/contato', autenticar, contatos.create);
app.get('/contato/:id/editar', autenticar, contatos.edit);
app.put('/contato/:id', autenticar, contatos.update);
app.del('/contato/:id', autenticar, contatos.destroy);
};
Pronto. Cada rota agora executa primeiro autenticar e só chega na action de contatos se o usuário estiver autenticado. Caso contrário, é redirecionado para a tela de login.
Esse é o padrão para emular um filtro before. Se você quiser um filtro after, basta colocar o callback do filtro depois do callback principal da rota. A ordem dos callbacks define a ordem de execução, simples assim.
Esse mesmo mecanismo pode ser usado para muito mais do que autenticação: logging por rota, verificação de permissões específicas, validação de dados, rate limiting. Cada filtro fica em seu próprio arquivo, com responsabilidade única, e você compõe-os onde precisar.
Indo além: criando páginas de erros amigáveis
Por padrão, quando uma rota não existe ou quando acontece um erro não tratado, o Express devolve uma página técnica feia, com stack trace, status numérico e nenhum carinho com o usuário. Isso é péssimo para qualquer aplicação em produção. A solução é interceptar esses erros e renderizar páginas customizadas.
O Express oferece dois mecanismos para isso: um para o famoso erro 404 (página não encontrada) e outro genérico para qualquer erro de servidor (geralmente o 500).
Criando as views de erro
Primeiro, crie duas novas views dentro de views. A primeira é a tela de "não encontrado", chame de not-found.ejs:
<% include header %>
<header>
<h1>Ntalk</h1>
<h4>Infelizmente essa página não existe :(</h4>
</header>
<hr>
<p>Vamos voltar <a href="/">home page?</a> :)</p>
<% include footer %>
A segunda é a tela de erro genérico, chame de server-error.ejs:
<% include header %>
<header>
<h1>Ntalk</h1>
<h4>Aconteceu algo terrível! :(</h4>
</header>
<p>
Veja os detalhes do erro:
<br>
<%- error.message %>
</p>
<hr>
<p>Que tal voltar <a href="/">home page?</a> :)</p>
<% include footer %>
A view de 500 recebe um objeto error com a mensagem do erro. Em produção, normalmente você esconderia essa mensagem do usuário final (para não vazar detalhes da implementação), mas em desenvolvimento é útil ver o erro direto na tela.
Registrando os handlers de erro
O Express tem uma regra interessante: qualquer middleware registrado depois das rotas e que receba uma requisição que nenhuma rota tratou vira automaticamente um handler de 404. Já um middleware com quatro parâmetros (error, req, res, next) é interpretado como handler de erro genérico, recebendo a exceção que foi lançada em qualquer ponto anterior.
Para organizar isso, vamos criar um arquivo dedicado em middleware/error.js:
exports.notFound = function(req, res, next) {
res.status(404);
res.render('not-found');
};
exports.serverError = function(error, req, res, next) {
res.status(500);
res.render('server-error', {error: error});
};
E modificar a stack no app.js para incluir esses handlers no final:
var express = require('express'),
app = express(),
load = require('express-load'),
error = require('./middleware/error');
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.cookieParser('ntalk'));
app.use(express.session());
app.use(express.json());
app.use(express.urlencoded());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
app.use(error.notFound);
app.use(error.serverError);
Repare na ordem: os handlers de erro vêm depois do app.router e do express.static. Isso é fundamental. Se viessem antes, eles interceptariam todas as requisições e nenhuma rota ou arquivo estático seria atendido. A regra é "trate o erro quando todo o resto já falhou".
Testando
Reinicie o servidor. Para testar o erro 404, acesse uma URL que não existe, como http://localhost:3000/url-errada. Em vez do erro técnico, você vai ver sua tela amigável.
Para testar o erro 500, force um bug deliberadamente. Remova o filtro autenticar de uma das rotas em routes/contatos.js e tente acessar /contatos sem estar logado. O req.session.usuario é undefined, a tentativa de ler .contatos lança uma exceção e o handler de erro 500 entra em ação, renderizando a tela customizada. Depois do teste, lembre-se de recolocar o filtro.
Existem códigos HTTP para os mais diversos tipos de erro (401 para não autenticado, 403 para acesso negado, 422 para dados inválidos, e por aí vai). À medida que sua aplicação cresce, faz sentido criar handlers e views específicas para cada um. O padrão é sempre o mesmo: registrar o middleware na stack após as rotas, definir o status com res.status(...) e renderizar a view apropriada.
Conclusão
Você acabou de transformar um esqueleto de aplicação em algo realmente funcional. Recapitulando:
-
Estruturando views com partials: extraímos
header.ejs,footer.ejseexit.ejspara eliminar duplicação, usando a diretiva<% include %>do EJS. Cada view agora tem só o conteúdo específico daquela tela. -
Controlando sessões de usuários: adicionamos
cookieParser,session,jsoneurlencodedna stack, criamos as rotas/entrare/sair, implementamos login e logout manipulandoreq.session. Aprendemos a evitar sobrescrever funções nativas da sessão. -
CRUD REST: implementamos as seis actions (index, show, create, edit, update, destroy) mapeadas para os verbos
GET,POST,PUTeDELETE, com parâmetros de rota via:id. Descobrimos o truque do campo oculto_methodcombinado com o middlewaremethodOverridepara simular PUT e DELETE em formulários HTML. -
Filtros antes das rotas: criamos nosso primeiro middleware customizado em
middleware/autenticador.js, aproveitando o suporte do Express a callbacks encadeados em rotas. Bloqueamos acesso à área autenticada sem precisar repetir verificação em cada action. -
Páginas de erro amigáveis: criamos
not-found.ejseserver-error.ejs, dois handlers emmiddleware/error.js, e os registramos no fim da stack. Entendemos a convenção de quatro parâmetros que define um middleware de erro no Express.
A aplicação agora tem todos os elementos básicos de um sistema web profissional: autenticação, CRUD, filtros, tratamento de erros, e tudo isso organizado de forma modular, sem código repetido, com responsabilidades bem separadas.
O próximo passo natural é tirar os dados da sessão e colocá-los em um banco de dados de verdade, persistindo os contatos entre sessões. Daí vamos abrir caminho para o chat em tempo real, validação robusta dos modelos, testes automatizados e tudo o mais que transforma uma aplicação de estudo em algo digno de ir para produção.
Mas isso já é assunto para o próximo tutorial. Por enquanto, aproveite o que você acabou de construir: uma aplicação Express completa, do zero, que faz tudo o que toda aplicação web precisa fazer. Boa codificação, e até lá.

Top comments (0)