DEV Community

Moprius
Moprius

Posted on

Dominando o Express: partials, sessões, CRUD REST, filtros e páginas de erro

Javascript

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>
Enter fullscreen mode Exit fullscreen mode

E agora o rodapé. Crie o arquivo footer.ejs:

<footer>
    <small>Ntalk - Agenda de contatos</small>
</footer>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

Cada peça nova tem um papel específico:

  • express.cookieParser('ntalk') — precisa vir antes do middleware de sessão, porque o session() 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í, todo req ganha o atributo req.session que você manipula livremente.
  • express.json() e express.urlencoded() — são os parsers de corpo. Eles transformam o conteúdo bruto das requisições POST em objetos JavaScript prontos para uso em req.body. O primeiro lida com requisições com Content-Type: application/json (típico de APIs), e o segundo com formulários HTML padrão. É graças a eles que aquela sintaxe name="usuario[nome]" vira req.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 %>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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});
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.ejs e exit.ejs para 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, json e urlencoded na stack, criamos as rotas /entrar e /sair, implementamos login e logout manipulando req.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, PUT e DELETE, com parâmetros de rota via :id. Descobrimos o truque do campo oculto _method combinado com o middleware methodOverride para 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.ejs e server-error.ejs, dois handlers em middleware/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)