1. Início
  2. Desenvolvimiento de software
  3. Twitter Clone full stack app – Parte 1: Node JS tutorial

Twitter Clone full stack app – Parte 1: Node JS tutorial

Node JS tutorial

 Node JS tutorial? Temos! Este artigo é um bom lugar para entender os conceitos e se sentir mais confiante na hora de criar aplicações de ponta a ponta. Criaremos uma réplica do Twitter com um passo a passo bem didático.

Dividiremos em 2 partes: na primeira parte aprenderemos a criar um servidor back-end para nossa aplicação utilizando Node JS tutorial, Express e outras ferramentas que nos ajudarão no processo de desenvolvimento. No fim, teremos uma aplicação completa, incluindo rotas, autenticação e persistência de dados.

Já na segunda parte faremos a parte do front-end integrando com o nosso servidor e criando uma versão totalmente funcional.

O tutorial visa atingir pessoas com pouco ou nenhum conhecimento sobre programação, sendo que não é necessário conhecimento avançado para conseguir acompanhar. Porém, é recomendado um conhecimento básico de JavaScript.

Também é indicado que você tente fazer seguindo os passos do tutorial para ir aprendendo com seus próprios erros e para que os conceitos fixem melhor na sua mente. O código da aplicação está em no meu GitHub caso prefira ir direto para a implementação.

Requisitos para fazer uma aplicação

É necessário que você:

  • Instale o Node JS;
  • YARN ou qualquer gerenciador de pacotes de sua preferência;
  • VSCode ou IDE de sua preferência.

Iniciando Node JS tutorial

Para criarmos o ambiente de desenvolvimento temos que criar um diretório e inicializar o projeto: 

mkdir twitter-app
cd twitter-app
yarn init -y

Dentro do diretório, você deve encontrar um arquivo chamado package.json, que é o arquivo de configuração inicial para aplicações que se baseiam em Node JS tutorial, por exemplo. Nele são declarados as dependências do projeto, os scripts disponíveis e versão atual do software. No nosso caso este arquivo está com a configuração mínima de um projeto.

Nosso código principal ficará dentro do arquivo src/app.js, então vamos criá-lo: mkdir/src && touch app.js. No caso de você não ter notado o package.json apresenta uma referência para o código principal chamado de main, como esse valor não bate com o nosso arquivo devemos alterá-lo para src/app.js.

Primeiros passos com Express

Express é uma biblioteca que facilita o desenvolvimento de servidores, ainda mais quando se trata de Node JS tutorial. Com ela podemos implementar rotas, middlewares e gerenciadores de erros de forma fácil e prática.

Atualmente, Express é uma das libs mais usadas para criação de servers em JavaScript, com uma curva de aprendizado é relativamente fácil de construir aplicações totalmente úteis.

A criação do Express será feita no arquivo src/app.js, precisamos de:

  • Uma instância do Express, chamaremos de app;
  • A porta em que o servidor ficará ouvindo;
  • A função listen do app que serve para fazer com que nosso server escute na porta específica.
const express = require("express");
const app = express();
const PORT = 3333;
app.listen(PORT, () => {
  console.log(`Server running on port: ${PORT}`);
});

Podemos iniciar nosso servidor utilizando node src/app.js, porém até agora não implementamos nenhuma interação com o usuário, nossa aplicação está isolada de qualquer requisição.

Para que possamos receber requisições, precisamos implementar as chamadas rotas de cada recurso que o usuário terá acesso. Em cada uma delas serão utilizados os métodos HTTP que servem para identificar cada ação do cliente.

Existem diversos métodos que podem ser usados, mas os mais utilizados são:

  • GET – Buscar um recurso no servidor;
  • POST – Criar um recurso no servidor;
  • PUT – Atualizar um recurso no servidor;
  • DELETE – Apagar um recurso no servidor.

No caso do Express, o objeto app já possui os métodos específicos para cada método HTTP, o que facilita muito o desenvolvimento.

const express = require("express");

const app = express();

app.get("/", (req, res) => {
  res.send("Hello GeekHunter! 🤓")
})

const PORT = 3333;

app.listen(PORT, () => {
  console.log(`Server running on port: ${PORT}`);
}

Perceba que utilizamos o método get do objeto app, ele recebe como primeiro parâmetro a url em que a rota será ativada  (no exemplo a seguir o ‘/’ é referente a raíz do projeto, no caso localhost:3333/), e como segundo parâmetro uma função que recebe 2 argumentos, req, que é a requisição vinda do nosso cliente, e res que é a resposta do servidor para quem o chamou.

Agora podemos iniciar nosso servidor através da chamada node src/app.js, que fará com que nosso server seja criado e fique ouvindo a porta 3333. Para sabermos se tudo ocorreu corretamente poderemos acessar nosso server pelo browser utilizando a url: localhost:3333

Para que não seja necessário que lembremos todos os scripts que nossa aplicação possui, podemos alterar nosso package.json para rodar scripts por nós. No caso, podemos adicionar o script que inicia o servidor através da seguinte alteração:

– Adicionamos a propriedade scripts que servirá para guardar os scripts necessários para nossa aplicação.

– Dentro de scripts foi adicionado o start que roda a aplicação por nós sem que precisemos utilizar o comando inteiro toda vez, apenas utilizando o comando yarn start.

 

Middlewares

A arquitetura do Express consiste em fazer a requisição passar por um caminho determinado de forma que seja tratada corretamente, esse caminho é feito de funções chamadas middlewares. Cada middleware é um diferente tipo de parâmetro, que serve para resolver diferentes tipos de funções dentro do servidor como roteamento, tratamento de erros e autenticação, por exemplo.

Precisaremos implementar alguns middlewares para que nosso servidor se consiga tratar as requisições da melhor maneira possível. Para isso, precisamos primeiro entender como são estruturados e como utilizá-los corretamente. Se formos até a documentação oficial do Express encontraremos uma seção específica para o uso de middlewares.

Pela definição, middlewares são funções que recebem como um dos parâmetros a requisição que chega ao servidor, nele podem ser acessadas propriedades, alteradas se preciso antes que cheguem na rota ou até adicionadas novas. Um exemplo de middleware que calcula o tempo a duração do tratamento de requisição seria como o seguinte:

var app = express()

app.use(function (req, res) {
  console.time(“duration”)
  next()
})

  • A variável app representa a instância do nosso servidor
  • O método use serve para que nossa requisição seja passada por esse middleware, que possui como parâmetros
    • a própria requisição;
    • a resposta;
    • uma função next que serve para dar continuidade à sequência de middewares (caso essa função não seja chamada, ou seja dada a resposta ao usuário, a cadeia é interrompida).
  • A função console.time serve para iniciar uma cronometragem.
app.use('/time, function (req, res) {
  const duration = console.timeEnd(“duration”)  

res.send(duration)
})

Nessa rota encerramos a cronometragem e enviamos ao usuário o tempo total da requisição.

Implementando middlewares

Body Parsing middleware

Para que nossas futuras rotas tenham acesso ao corpo da requisição, precisamos Informar ao Express que queremos acessar nossa request como json. Felizmente o Express já traz um middleware específico para isso.

app.use(express.json());

Not Found middleware

Agora que sabemos o conceito por trás dos middlewares podemos implementar nossos próprios. Criaremos um middleware que gerenciará as requisições que buscam recursos que não foram encontrados em nosso servidor, o popularmente conhecimento Not Found Middleware.

Nossa implementação ficaria como a seguinte:

app.use((req, res, next) => {
const error = new Error(`Not found - ${req.originalUrl}`);
  res.status(404);
  next(error);
});

  • Como sabemos criamos um middleware como uma função que recebe 3 parâmetros:
    • A requisição;
    • A resposta;
    • A função que dá continuidade à cadeia.
  • Nossa função é encarregada de
    • Criar um erro referente a rota que foi requisitada;
    • Atribuir o HTTP Status correto para requisições que não encontraram o que procuravam;
    • Passar o erro para o próximo middleware da cadeia.

Sabemos que a request faz um caminho sequencial em nosso servidor, então, para que a função de tratar um recurso não encontrado seja executada com sucesso, precisamos que a request passe por todos os middlewares anteriores para sabermos se em nenhum deles ela é resolvida. Tendo isso em vista devemos declarar nosso middleware de não encontrado após todas as rotas.

Error handling middleware

Agora que implementamos nosso todos os middlewares que tratam a requisição de forma correta, precisamos criar um que trate quando acontecer um erro e que retorne para nosso cliente uma mensagem que faça sentido e ajude a resolver o erro.

Como nós mesmos implementaremos o lado do cliente também (na parte 2 deste tutorial), seria bom que enviássemos todas as informações que pudessem nos ajudar a resolver o problema que aconteceu no servidor. Assim sendo, nossa implementação ficaria como a seguinte:

app.use((error, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.statusCode = statusCode;
  res.json({
    message: error.message,
    stack: error.stack
  });
});

  • Diferente dos middlewares que já vimos, para tratarmos um erro chamamos uma função que recebe não 3, mas 4 parâmetros, sendo o primeiro o erro encontrado (o Express se encarrega de diferenciar se a função tem 3 ou 4 parâmetros e passar as variáveis corretas a cada um deles);
  • Caso nossa response tenha o status 200 (sucesso) significa que ela ainda não foi tratada e, se chegou até aqui, deve ser retornada como um erro ao cliente. Portanto, alteramos o status para 500 (error no servidor);
  • Retornamos um JSON com a mensagem do erro o caminho percorrido pela request dentro de nosso servidor, conhecido como stacktrace.

Ao acessarmos uma URL que nosso servidor não possui, devemos receber uma resposta como esta: 

Como podemos ver, a mensagem que é retornada ao nosso cliente apresenta toda a stacktrace do nosso servidor, esteja ele sendo rodado localmente ou em uma cloud.
Devemos concordar que, por questões de segurança, isso não seria uma boa prática já que a cada erro que aconteça dizemos todos os arquivos que a requisição passou, o que facilitaria muito a vida de algum mal-intencionado.
Para resolvermos esse problema sem perder os benefícios da stacktrace em tempo de desenvolvimento, podemos verificar qual é o ambiente em que nossa aplicação está rodando e, caso esteja em produção, envidar apenas a mensagem de erro.

app.use((error, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.statusCode = statusCode;
  res.json({
    message: error.message,
    stack: process.env.NODE_ENV === "production" ? "🤓" : error.stack  });
});

Usando middlewares de terceiros

Morgan

Aprendemos a criar nossos próprios middlewares, mas nem sempre teremos que implementar um novo para cada funcionalidades que quisermos que nosso servidor forneça.

Podemos querer, por exemplo, que nossa aplicação crie um registro a cada requisição que chega até ela, para isso podemos utilizar a library morgan.

yarn add morgan

Como ela já possui integração com o Express, podemos utilizar o middleware que a própria library exporta. Utilizamos da seguinte forma:

const morgan = require("morgan");

app.use(morgan("common"));

A partir de agora, todas as requisições, que chegarem ao nosso servidor, criarão um registro (log) contendo informações importantes como IP, hora, método, URL e status para que possamos corrigir bugs, analisar e tomar decisões em cima dos dados.
E essa capacidade ainda é bastante conveniente quando nossa aplicação estiver em produção, pois não expõe nenhuma informação sigilosa do servidor.

CORS

Por padrão nosso servidor aceita requisições vindas de qualquer domínio, seja de onde for, e, se estamos nos preocupando com segurança, temos que nos alertar para os possíveis ataques que isso nos impõe.

Com intuito de proteger nossa aplicação de ataques, utilizaremos o CORS – sigla para Cross-origin resource sharing – que nos permite determinar uma origem de onde aceitaremos as requests e bloquear as que não são bem-vindas.

Para isso utilizaremos a library cors em sua versão para o Express(CORS é agnóstico a linguagem e possui implementação para a grande parte delas). 

yarn add cors

Nossa implementação ficaria como a seguinte:

const cors = require("cors");

app.use(
  cors({
    origin: "http://localhost:3000"
  })
);

  • Importamos o CORS como um módulo.

Declaramos um novo middleware e nele utilizamos o CORS para atribuir a origem permitida das request para a URL http://localhost:3000, que é onde ficará nossa aplicação front-end.

Configuração do banco de dados

Em nossa aplicação utilizaremos MongoDB como o banco de dados para persistência dos dados. Nele podemos ter dados em estrutura de json de forma dinâmica devido ao Mongo de um banco NoSQL. 

Para facilitar o desenvolvimento, utilizaremos uma versão do banco que roda na Cloud, assim não precisamos configurar nada em nossa máquina. Mongo Atlas é uma versão do Mongo que pode ser acessada pela aplicação através de uma URL, caso você ainda não tenha uma conta, será necessário criar uma.

De início precisaremos criar um cluster que armazenará nosso banco, seguindo os seguintes passos:

Enquanto nosso cluster é criado, podemos ir até a aba security para configurarmos os usuários que terão acesso ao nosso banco. No menu a esquerda, na parte de Security > Database Access não encontramos nenhum usuário com permissões de acessar o banco, devemos criar um da seguinte forma:

As informações utilizadas aqui devem permanecer confidenciais. No meu caso, criarei um usuário como temporário para durar 1 semana com acesso à escrita e leitura ao banco de dados.

Na aba Security > Network Access devemos adicionar IP Addess e ativar a opção Allow Access From Anywhere, que se refere ao nosso banco poder ser acessado de qualquer IP do mundo.

A esse momento nosso cluster já deve ter terminado de ser criado e podemos avançar para parte de conexão com a aplicação.

Para que nossa aplicação se comunique com o banco de dados que criamos, precisamos copiar a URL alterando o campo <password> e outro campo referente ao nome do banco de dados que usaremos:

mongodb+srv://picollo:<password>@geeekhunter-0rjve.mongodb.net/geek-twitter?retryWrites=true&w=majority

Interações com o banco de dados

Esse Node JS tutorial precisa de um lugar onde possa persistir os dados que foram gravados desde o seu lançamento, porém até agora a aplicação não tem conhecimento algum sobre nenhum banco de dados que tenha sido criado e não há a comunicação entre essas partes.

Para facilitar essa integração de servidor com o banco de dados usaremos a lib mongoose. Com ela podemos lidar com nossas tabelas do banco de dados como se fossem objetos em nosso código, sem precisar utilizar nenhum tipo de linguagem de consulta de banco, o que facilita bastante no desenvolvimento.

yarn add mongoose

Utilizaremos a URL do nosso cluster como ponto de comunicação, porém, essa URL não deveria ficar visível a todos que mexem em nosso código, porque lá contém informações sigilosas sobre nossa base de dados.

A criação de variáveis feitas de forma secreta é responsabilidade de um arquivo de variáveis de ambiente, neste arquivo são guardadas todas as variáveis que não devem ser expostas (se você estiver usando um controle de versão como git, você deve adicionar esse arquivo ao .gitignore)

Por convenção chamamos este arquivo de .env, e todas as variáveis criadas nele devem ser compostas de um nome seguido do símbolo de atribuição (=), e o valor da variável.

DB_URL=mongodb+srv://picollo:[email protected]/geek-twitter?retryWrites=true&w=majority

Para termos acesso a essas variáveis dentro do código precisaremos utilizar outra lib chamada dotenv

yarn add dotenv

Em nosso código, principal devemos fazer o seguinte:

const express = require("express");
require("dotenv").config();

const app = express();
console.log(process.env.DB_URL);
  • Importar o dotenv como um módulo;
  • Chamar o método config;
  • Acessar a variável através do objeto process.env.

Agora será possível fazer a conexão com o banco de dados, precisaremos dos seguintes passos:

const express = require("express");
const mongoose = require("mongoose");require("dotenv").config();

const app = express();

mongoose.connect(process.env.DB_URL, {

useNewUrlParser: true,    
useUnifiedTopology: true  
},  () => console.log("Connected to the database!"))
  • Importar o mongoose como um módulo;
  • Chamar o método connect do mongoose passando como argumento:
    • a URL do nosso banco;
    • um objeto de configuração do mongoose (não se preocupe em entender para que exatamente esse objeto serve);
    • uma função que será chamada assim que a conexão for realizada.

Se tudo ocorreu corretamente você deve ver as duas mensagens no console.

Devemos criar as entidades que representarão nossas tabelas através do código, para o mongoose esses são os chamados models. Cada model possui um nome e um objeto json que representa sua estrutura, este objeto é chamada de Schema.

Dentro de uma nova pasta chamada model devemos criar nossos models:

const mongoose = require("mongoose")

const User = new mongoose.Schema({
username: {
type: String,
required: true
},
password: {
type: String,
required: true,
max: 1024
},
tweets: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Tweet'
}] }, {
timestamps: true
}
)

module.exports = mongoose.model("User", User)

  • Importamos o mongoose como módulo;
  • Criamos um mongoose schema que representará nosso usuário
    • username do tipo string e obrigatório;
    • password do tipo string, obrigatório e com no máximo 1024 caracteres;
    • tweets que servirá como relacionamento entre nossa tabela User com a Tweet.
  • O segundo parâmetro é um objeto de configuração do schema, nele declaramos que devem ser criados os campos created_at e updated_at automaticamente quando o registro for criado;
  • Exportamos um mongoose model.

const mongoose = require("mongoose")

const Tweet = new mongoose.Schema({
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
content: {
type: String,
required: true,
min: 1
},
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}] }, {
timestamps: true
}
)

module.exports = mongoose.model("Tweet", Tweet)

Configurando ambiente de desenvolvimento

Se você tem brincado um pouco com o servidor, alterado para ver como ele se comportava, deve ter percebido que a cada modificação feita é necessário que o servidor seja desligado e ligado novamente. Em tempo de desenvolvimento, isso acaba sendo uma grande dor para o dev devido à perda de tempo e trabalho repetitivo.

Para acabarmos com essa dor, podemos utilizar a library nodemon. Com ela nossos arquivos serão vigiados e a cada mudança nosso server será reiniciado automaticamente.

Podemos instalar o Nodemon através do comando yarn add nodemon -D para que seja adicionado como uma dependência de desenvolvimento, ou seja, que não vai interferir na experiência do usuário, apenas na experiência do dev.

Podemos também adicionar ao package.json o script para iniciar o ambiente de desenvolvimento para que seja possível rodar o comando yarn dev e todo nosso ambiente de desenvolvimento esteja rodando corretamente.

"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js"
}

Criando nossa primeira rota

Por enquanto nosso app não tem muitas utilidades a não ser retornar “Hello GeekHunter!” para quem chamá-lo na rota localhost:3333. Para mudarmos isso precisamos começar a implementar a interação com os clientes que utilizarão nosso app, para isso implementaremos as seguintes rotas:

  • Login (POST);
  • Registrar usuário (POST);
  • Encontrar usuário  (GET);
  • Criar tweet (POST);
  • Encontrar tweet (GET);
  • Listar todos os tweets (GET);
  • Listar tweets por usuário (GET);
  • Listar único tweet por usuário (GET);
  • Deletar tweet (DELETE);
  • Opção de like/unlike no tweet (PUT).

Cadastro de usuário

O cadastro de um novo usuário necessita que nosso servidor seja acessado pela rota /register utilizando o método POST, passando o username e password. Nossa rota ficaria como a seguinte:

// Criar usuário
 app.post("/register", async (req, res, next) => {
   try {
     const { username, password } = req.body;
// Verificar se username é valido
const userExists = await User.findOne({username});

if (userExists) return res.status(400).send({error: "Username already in use."});

//Criar novo usuário no banco
const user = await User.create({
  username,
  password
})

res.status(201).send({
  id: user.id,
  username: user.username
});    
} catch (err) {       
     res.status(400)
     next(err);
   }
 });
  • Criamos uma rota POST passando como argumento:
    • A url /register;
    • Uma função assíncrona que recebe a request e a response do usuário;
      • Acessamos as propriedades username e password do body da requisição;
      • Verificamos se o usuário já existe;
      • Se existir retornamos um erro 400 ao usuário informando que o username já está em uso;
      • Caso contrário criamos um novo usuário no banco e retornamos o usuário criado;
      • Em caso de erro atribuímos o status 400 à response e passamos o erro para o middleware de tratamento de erro.

Para testarmos se ocorreu tudo corretamente, precisaremos utilizar um software chamado Insomnia.

Nele podemos fazer requisições de qualquer tipo ao nosso servidor, passando as informações que quisermos sem precisar que tenhamos um <form> no front end (outra alternativa seria utilizar o Postman, que serve para o mesmo fim).

Ao instalar o Insomnia veremos esta primeira página:

Nossa rota /register espera por uma requisição do tipo POST, que tenha no body um username e um password, para fazermos isso no Insomnia basta que mudemos o tipo da requisição ao lado da URL e que no body do tipo JSON esteja um objeto contendo essas propriedades.

Se tudo ocorreu como esperado devemos receber como resposta um código 201 com o objeto criado no banco.

No caso de não ter funcionado corretamente, podemos imprimir no console o erro que chega no catch de nossa rota, assim será possível entender qual erro pode estar acontecendo.

Ao acessarmos o nosso banco, na aba de Collections será possível ver o usuário criado.

Repare no objeto que está criado no banco, você repara algo de estranho com os dados mostrados? Pense um pouco e depois continue a ler.

Encontrou o erro? Você diria que o objeto criado está correto no nosso banco? Mesmo a senha sendo gravada como um texto legível por qualquer um? Obviamente que em aplicações reais nossas senhas não ficam expostas dessa maneira, precisamos que haja alguma tipo de criptografia que aumente ainda mais a segurança de nossos dados.

Utilizaremos a library bcryptjs para criptografar a senha de nossos usuários.

yarn add bcryptjs

Alteraremos nossa rota /register para gerar um hash a partir de um número aleatório criado pelo bcrypt.

 // Criptografar a senha
    const salt = await bcrypt.genSalt(10);
    const hash = await bcrypt.hash(password, salt);

    //Criar novo usuário no banco
    const user = await User.create({
      username,
      password: hash
    })

Agora para testarmos nossa rota novamente primeiro vamos excluir o registro antigo do banco, na opção delete do próprio Mongo Atlas. Após isso mandamos nossa requisição de novo para rota gerando um novo registro como o seguinte:

Se termos utilizar o mesmo username novamente devemos receber o seguinte output:

Rota de login

Seguindo a mesma linha da rota /register, nossa rota de login ficaria como a seguinte:

app.post("/login", async (req, res, next) => {
try {
const { username, password } = req.body;

// Verificar se username é valido
const user = await User.findOne({username})

if (!user) return res.status(400).send({error: "Username not found."});

// Verifica se a senha é válida
const validPassword = await bcrypt.compare(password, user.password);

if (!validPassword) return res.status(400).send({ error: "Invalid password."});

res.send({message: "User logged in."});

} catch (err) {
res.status(400);
send(err);
}
});

  • Criamos uma rota POST passando como argumento:
    • A url /login 
    • Uma função assíncrona que recebe a request e a response do usuário
    • Acessamos as propriedades username e password do body da requisição
    • Verificamos se o usuário já existe
    • Se não existir retornamos um erro 400 ao usuário informando que o user não foi encontrado
    • Caso contrário verificamos se a senha está correta, utilizamos a função compare do bcrypt que faz a comparação pelo hash da senha salva no banco
    • Caso não seja a senha correta retornamos um erro 400 informando que a senha está incorreta
    • Caso esteja correta mandamos uma mensagem que o usuário está logado

Ao acessarmos pelo postman podemos verificar se tudo ocrreu como esperado:

Agora digamos que após estar logado na aplicação, nosso usuário queira fazer mais ações como criar, curtir ou apagar um tweet, como poderíamos fazer com que nosso servidor saiba que o usuário está logado e qual a identidade dele? Necessitamos um jeito de identificar o usuário e todas as requisições feitas por ele, assim podemos controlar que apenas tweets criados pelo usuário logado possam ser excluídos.

Para isto utilizaremos a lib jsonwebtoken. Com ela será possível criar um token específico para cada usuário na hora do login, após isso todas as requisições que estiverem com este token serão referentes a este usuário. 

yard add jsonwebtoken 

Nossa rota ficaria como a seguinte:

// Login
app.post("/login", async (req, res, next) => {
try {
const { username, password } = req.body;

// Verificar se username é valido
const user = await User.findOne({username})

if (!user) return res.status(400).send({error: "Username not found."});

// Verifica se a senha é válida
const validPassword = await bcrypt.compare(password, user.password);

if (!validPassword) return res.status(400).send("Invalid password.");

// Criar token de validação de usuário
const token = jwt.sign({_id: user.id}, process.env.JWT_SECRET);    
res.header('auth-token', token).send(token);

} catch (err) {
res.status(400);
send(err);
}
});

Após a validação de senha adicionar a criação de um token JWT utilizando o ID do usuário no banco como identificador e uma variável de ambiente como segundo argumento (o JWT_SECRET pode ser qualquer texto que sirva para gerar o token, contudo essa variável deve ser secreta para  que não seja possível que nosso usuário crie um token como se fosse de outro usuário).

Enviamos como resposta o token no header e um objeto com o token gerado.

Nosso output ficaria como o seguinte:

Se copiarmos o token gerado e formos até o site oficial do JWT podemos descriptografá-lo e ver o que temos dentro dele:

A partir de agora, todas as requisições que formos fazer, que dependem do usuário estar logado, devem conter este token para que o server identifique o usuário que está logado e fazendo as ações.

Criando rotas privadas

Express funciona com uma arquitetura de chamados middlewares que funcionam como interceptadores da requisição que chega ao servidor. Com eles podemos adicionar e modificar a requisição antes mesmo que chega até a rota.

Criaremos um arquivo chamado auth.js, nele criaremos a seguinte função:

const jwt = require("jsonwebtoken")

function validateToken (req, res, next) {
const token = req.header('auth-token')
if (!token) return res.status(400).send({error: 'Access denied.'})

try {
    const verified = await jwt.verify(token, process.env.JWT_SECRET);
    req.user = verified;

    next();
} catch (err) {
    res.status(400).send({error: 'Invalid token.'})
}

}

module.exports = validateToken

  • Importamos jwt como um módulo
  • Criamos uma função que recebe a request, a response e uma função next que presenta chamar o próximo middleware da sequência, no nosso caso a própria rota
    • Pegamos o token do header da requisição
    • Caso não haja um auth-token informamos ao usuário que ele não tem acesso a rota e enviamos um erro 400
    • Caso haja um token verificamos se o token é válido utilizando nossa variável de ambiente JWT_SECRET
    • No caso de o token ser válido adicionamos à requisição uma flag para identificar um usuário logado e verificado e chamamos a função next para a requisição chegar até a rota

Devemos adicionar este middleware ao arquivo principal em todas as rotas que deveriam ser privadas. A implementação ficaria como a seguinte:

app.get("/private", authenticate, async (req, res, next) => {});

Sendo authenticate o nome do módulo que importamos do arquivo auth.js

No caso da requisição não ter passado na verificação devemos receber um output como o seguinte:

Criando um tweet

Agora que estamos corretamente logados na aplicação podemos começar a criar tweets normalmente. Nossa rota de criação de tweet ficaria como a seguinte:

// Criar tweets
app.post("/tweets", authenticate, async (req, res, next) => {
const { content } = req.body;

try {
const tweet = await Tweet.create({owner: req.user, content});

if (!tweet) res.status(400).send({error: "Unable to create tweet."})

res.status(201).send(tweet)

} catch (err) {
res.status(400);
send(err);
}
});

  • Buscamos o conteúdo do tweet no body da requisição
  • Criamos um novo tweet identificado pelo usuário que criou
  • Retornamos o tweet criado com o status (201 – Recurso criado)

Podemos testar a rota utilizando o Insomnia:

Remover e Atualizar tweet

Para que o usuário tenha a opção de deletar um tweet feito, devemos implementar a rota de DELETE para um tweet de id específico. Nossa rota ficaria como a seguinte:

// Deletar tweet específico
app.delete("/tweets/:id", authenticate, async (req, res, next) => {
const { id } = req.params;

try {
await Tweet.deleteOne(id);
res.status(200).send({message: "Tweet deleted."})
} catch (err) {
res.status(400);
next(err);}
});

Se quisermos que seja possível que tweets sejam curtidos devemos implementar a rota UPDATE de um tweet específico. Nossa rota ficaria como a seguinte:

// Atualizar tweet específico (like/unlike)
app.put("/tweets/:id", authenticate, async (req, res, next) => {
const { id } = req.params;

try {
const tweet = await Tweet.findById(id);

if (!tweet) return res.status(400).send({error: "Tweet not found."});

if (tweet.owner === req.user._id) return res.status(400).send({error: "Unable to update tweet."})

const tweetAlreadyLiked = tweet.likes.some(like => like == req.user._id)

if (tweetAlreadyLiked) {
  tweet.likes = tweet.likes.filter(like => like != req.user._id);
} else {
  tweet.likes.push(req.user._id)
}    

tweet.save();

res.status(200).send(tweet)

} catch (err) {
res.status(400);
next(err);

  }
});

  • Encontramos o tweet pelo parâmetro ID da url
  • Vemos se o tweet em questão pertence ao usuário que está logado
  • Caso seja, verificamos se este tweet já o foi curtido pelo usuário logado
  • Se não, adicionamos a curtida à propriedade likes
  • Se sim, removes a curtida da propriedade likes

Rotas de GET

As rotas a seguir são rotas para pegar um recurso no servidor, todas elas seguem um padrão, então ficarão todas abaixo:

Encontrar usuários

// Encontrar usuários
app.get("/users", authenticate, async (req, res, next) => {
try {
const users = await User.find({})

if (!users.length) return res.status(400).send({error: "Unable to get users."});

res.status(200).send(users.map(user => (
  {
    _id: user.id,
    username: user.username
  }
)))

 

Encontrar todos os tweets

// Encontrar todos os tweets
app.get("/tweets", authenticate, async (req, res, next) => {
try {
const tweets = await Tweet.find({})

res.status(200).send(tweets);

} catch (err) {
res.status(400);
next(err);
}
});

Encontrar tweet específico

// Encontrar tweet específico
app.get("/tweets/:id", authenticate, async (req, res, next) => {
  const { id } = req.params;

  try {
    const tweet = await Tweet.findById(id)

    if (!tweet) return res.status(400).send({error: "Tweet not found."});

    res.status(200).send(tweet)

  } catch (err) {          
    res.status(400);    next(err);
  }

});

Organização do projeto

Nossa aplicação está terminada, porém nossas funções estão todas sendo aplicadas no arquivo principal. Isso atrapalha a manutenção e demonstra que não nos preocupamos em separar os módulos que compõem o app. Vamo organizar para que nossa experiência mantendo esse servidor vivo seja a melhor possível.

Para organizar as rotas podemos criar um arquivo src/routes.js e la declarar todas as rotas como o seguinte:

const { Router } = require("express");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");

const authenticate = require("./auth");
const User = require("../model/User");
const Tweet = require("../model/Tweet");

const router = new Router();

// Criar usuário
router.post("/register", authenticate, async (req, res, next) => {});

// Login
router.post("/login", authenticate, async (req, res, next) => {});

// … Demais rotas

module.exports = router;

Aqui utilizamos o Router do Express que é um objeto específico para lidar com as rotas, mas que funciona basicamente como o própria app.

Para organizarmos os middlewares podemos também criar um arquivo separado e chamá-lo de src/middleware.js, a implementação ficaria com a seguinte como o seguinte:

// Middleware recurso não encontrado
const notFound = (req, res, next) => {
const error = new Error(Not found - ${req.originalUrl});
res.status(404);
next(error);
};

// Middleware de tratamento de erro
const errorHandling = (error, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.statusCode = statusCode;
res.json({
message: error.message,
trace: process.env.NODE_ENV === "production" ? "🤓" : error.trace
});
};

module.exports = {
notFound,
errorHandling
};

Ao fim dos ajustes seu arquivo src/app.js deve estar parecido com o seguinte:

const express = require("express");
const mongoose = require("mongoose");
const morgan = require("morgan");
const cors = require("cors");
require("dotenv").config();

const router = require("./routes");
const middlewares = require("./middlewares");

const app = express();

app.use(morgan("common"));
app.use(express.json());
app.use(cors({
    origin: process.env.CORS_ORIGIN
  })
);

mongoose.connect(
  process.env.DB_URL,
  {
    useNewUrlParser: true,
    useUnifiedTopology: true
  },
  () => console.log("Connected to the database!")
);

app.use(router);

// Middleware recurso não encontrado
app.use(middlewares.notFound);
// Middleware de tratamento de erro
app.use(middlewares.errorHandling);

const PORT = 3333;
app.listen(PORT, () => {
  console.log(`Server running on port: ${PORT}`);
});

Com esse Node JS tutorial fica fácil, né?

Com isso terminamos a implementação do nosso back-end, implementamos as seguintes funcionalidades:

  • Rotas específicas para cada recurso;
  • Autenticação de usuário;
  • Integração com banco de dados NoSQL (MongoDB);
  • Registro de logs;
  • Teste de rotas com o Insomnia.

Com esse tutorial, minha intenção é mostrar como é simples implementar uma aplicação back-end utilizando as melhores tecnologias presentes no mercado. Todos os conceitos aqui mostrados são utilizados diariamente no dia a dia de um desenvolvedor web, não interessando a linguagem ou o framework.

Gostaria de deixar claro que estou aberto a responder qualquer dúvida através do email [email protected] ou pelo slack do React Brasil como Lucas Picollo. O código da aplicação está no meu GitHub.

Agradeço a todos que participaram deste tutorial e volto em breve com a segunda parte e, quem sabe, mais algumas…

Muito obrigado!

Quer contratar os melhores talentos tech em menos tempo?

Queremos te ajudar a contratar muito mais rápido e melhor. Podemos começar?

Leituras Recomendadas

Quer receber conteúdos incríveis como esses?

Assine nossa newsletter para ficar por dentro de todas as novidades do universo de TI e carreiras tech.