Projeto de Ecommerce Backend com Node.js, Banco de Dados SQL

Nesta explicação detalhada, abordaremos como criar um ecommerce

* work in progress *

template

- Nesta explicação detalhada, abordaremos como criar um backend para um projeto de e-commerce utilizando Node.js SQL para armazenar os dados e alguns módulos como o Express, Sequelize, MySQL2 e Bcrypt.

  1. Setup do Ambiente

Antes de iniciar o desenvolvimento do projeto, é necessário configurar o ambiente de desenvolvimento.

- Siga os passos abaixo:

- Certifique-se de ter o Node.js instalado em sua máquina. Você pode verificar executando o comando node -v no terminal ou prompt de comando.

- Instale o gerenciador de pacotes NPM ou Yarn, se ainda não o tiver instalado.

- Execute npm -v ou yarn -v para verificar.

- Crie um diretório para o projeto do ecommerce em seu computador.

- Acesse o diretório através do terminal ou prompt de comando.

- Inicialize o projeto Node.js executando o comando npm init -y

  1. Instalando os Módulos Necessários

Para criar o backend do projeto de ecommerce, é necessário instalar alguns módulos.

- Execute os seguintes comandos no terminal ou prompt de comando, dentro do diretório do projeto:

- npm install express sequelize mysql2 bcrypt

- Este comando instalará os módulos Express, Sequelize, MySQL2 e Bcrypt, que são utilizados para criar um servidor web, realizar operações no banco de dados SQL autenticação de usuários e encriptação de senhas, respectivamente.

  1. Criando a Estrutura de Diretórios

Agora, vamos criar a estrutura de diretórios para o projeto de ecommerce:

- src

- models

- controllers

- config

- utils

- index.js

- routes.js

- Crie os diretórios manualmente em seu projeto e adicione os arquivos necessários, conforme indicado na estrutura acima.

- O diretório models será usado para armazenar os modelos das tabelas do banco de dados, utilizando o Sequelize.

- O diretório controllers conterá os controladores que serão responsáveis por processar as requisições HTTP e interagir com o banco de dados.

- O arquivo Routes.js será usado para armazenar as variáveis de roteamento, onde serão definidas as rotas e as funções de callback correspondentes.

- O diretório config conterá os arquivos de configuração, como a conexão com o banco de dados.

- O diretório utils será usado para armazenar arquivos utilitários, como funções helper.

- O arquivo index.js será o ponto de entrada da aplicação.

  1. Configurando a Conexão com o Banco de Dados

- Dentro do diretório config, crie um arquivo chamado database.js.

- Neste arquivo, configure a conexão com o banco de dados utilizando o módulo Sequelize e o módulo MySQL2.

- Observação , MySQL2 não é necessários fazer importações é o Sequelize que controla as funcionalidades do MySQL2

- MySQL2 é um driver para Node.js que permite a conexão com um banco de dados MySQL.

- Já o Sequelize é uma biblioteca ORM (Object-Relational Mapping) que facilita o uso de bancos de dados relacionais, incluindo o MySQL.

- O Sequelize depende do driver MySQL2 para fazer a conexão e controlar as funcionalidades do banco de dados MySQL. 

- A conexão deve ser estabelecida utilizando as informações de host, usuário, senha e nome do banco de dados. Exemplo:

database.js

 const Sequelize = require('sequelize');

module.exports = new Sequelize('ecommerce', 'root', 'senha', {

  host: 'localhost',

  dialect: 'mysql',

});

- É claro que não é uma boa pratica de programação adicionar dados sensíveis no código como usuário e senha, para isso podemos utilizar variáveis de ambiente, mas isso vou deixar para o final.

- então já vou aproveitar e dar um tutorial básico de como instalar e configurar o mysql server e criar o banco de dados e as tabelas no mysql workbench

- Download

- Instalação : Selecione , Mysql Server versão 8 e Mysql Workbench8 .

ecmysql1

- No Mysql server durante a instalação configure a senha do usuário root

- Selecione o mysql para funcionar como um serviço do Windows  , nessa parte escolher a opção que não pede para digitar senha, se o botão next travar é so voltar e avançar que desbuga.

- quando finalizar abrir o mysql workbench e configurar a conexão , host : 127.0.0.1 , porta : 3306 , usuário root e a senha que criou na instalação

ecmysql2

- após conectar criar o banco de dados e tabelas

- clique no ícone create a new schema in the conected server , e adicione o nome ecommerce e clicar em aplicar e vai criar o banco de dados

ecmysql3

- Agora não precisa fazer mais nada no banco de dados , as tabelas serão criadas automaticamente assim que iniciar o servidor ao executar npm run dev ou npm run start.

- isso acontece pois nos modelos vou criar uma função de sincronização , vou explicar em seguida..

  1. Criando os Modelos do Banco de Dados

Dentro do diretório models, crie um arquivo para cada tabela que deseja criar no banco de dados.

Por exemplo, se você deseja criar uma tabela de usuários, crie um arquivo chamado User.js.

Dentro deste arquivo, defina o modelo da tabela utilizando o módulo Sequelize.

Exemplo:

User.js

const Sequelize = require('sequelize');

const db = require('../config/database');



const User = db.define('users', {

  id: {

    type: Sequelize.INTEGER,

    primaryKey: true,

    autoIncrement: true,

  },

  username: {

    type: Sequelize.STRING,

    allowNull: false,

  },

  email: {

    type: Sequelize.STRING,

    allowNull: false,

    unique: true,

  },

  password: {

    type: Sequelize.STRING,

    allowNull: false,

  },

});



module.exports = User;

- observação

- A função responsável por registrar o datetime é a função "timestamps", que é adicionada ao modelo do Sequelize, por padrão, essa função cria automaticamente as colunas "createdAt" e "updatedAt" no banco de dados, que são atualizadas com a data e hora em que o registro foi criado e atualizado, respectivamente.

  1. Criando os Controladores

Dentro do diretório controllers, crie um arquivo para cada entidade que deseja controlar.

Por exemplo, se você deseja controlar as operações de usuários, crie um arquivo chamado UserController.js. Dentro deste arquivo, defina as funções de callback para cada rota correspondente às operações CRUD.

UserController.js

    // Importar o modelo de usuário
const User = require("../models/User");
    // Função para criar um novo usuário
exports.createUser = async (req, res) => {
    // Criar uma nova instância do modelo de usuário com os dados fornecidos
  const user = new User(req.body);
  try {
    // Salvar o novo usuário no banco de dados
    const newUser = await user.save();
    // Retornar o novo usuário em formato JSON com o código de status 201 (Created)
    res.status(201).json(newUser);
  } catch (error) {
    // Se ocorrer algum erro, retornar uma mensagem de erro com o código de status 400 (Bad Request)
    res.status(400).json({ message: error.message });
  }
};
    // Função para atualizar um usuário pelo ID
exports.updateUser = async (req, res) => {
  try {
    // Adicionando os parametros que devem ser atualizados no put
    const { id } = req.params;
    const { username, email } = req.body;
    //Função para atualizar os dados  username, email, pelo id
    const updatedUser = await User.update(
      { username, email },
      { where: { id } }
    );
    // Se nenhum usuário foi atualizado, devido a  id invalido apresenta erro 404
    if (updatedUser[0] === 0) {
      return res.status(404).json({ message: 'Usuário não encontrado.' });
    }
    // reposta de sucesso 200 ou 500 de erro interno
    res.status(200).json({ message: 'Usuário Atualizado com Sucesso.' });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Falha ao Atualizar Usuário.' });
  }
};
    // Função para excluir um usuário pelo ID
exports.deleteUser = async (req, res) => {
  try {
    // Buscar o usuário com o ID fornecido no banco de dados e removê-lo
    const result = await User.destroy({ where: { id: req.params.id } });
    // Se o usuário não existir, retornar uma mensagem de erro com o código de status 404 (Not Found)
    if (!result) {
      return res.status(404).json({ message: "Usuário não encontrado" });
    }
    // Retornar uma mensagem de sucesso com o código de status 204 (No Content)
    res.status(200).json({ message: "Usuário excluído com sucesso" });
  } catch (error) {
    // Se ocorrer algum erro, retornar uma mensagem de erro com o código de status 500 (Internal Server Error)
    res.status(500).json({ message: error.message });
  }
};
   // Função para buscar todos os pelo id
exports.getUsersbyID = async (req, res) => {
  try {
    const id = req.params.id; // obtém o ID da URL da requisição
    const users = await User.findByPk(id); // busca o usuário pelo ID
    if (!users) {
      return res.status(404).json({ message: "Usuário não encontrado" });
    }
    return res.json(users);
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Erro ao buscar usuário" });
  }
};
   // Função para obter todos os usuários
exports.getAllUsers = async (req, res) => {
  try {
     // Buscar todos os usuários no banco de dados
    const users = await User.findAll();
    // Retornar a lista de usuários em formato JSON
    res.status(200).json(users);
  } catch (error) {
    // Se ocorrer algum erro, retornar uma mensagem de erro com o código de status 500 (Internal Server Error)
    console.error(error);
    res.status(500).json({ error: 'Erro ao obter os usuários' });
  }
};

Neste código do userController.js, temos a definição das funções para controlar as operações relacionadas aos usuários. Cada função corresponde a uma ação CRUD (Create, Read, Update, Delete):

  • getAllUsers: busca todos os usuários no banco de dados e retorna a lista deles em formato JSON.
  • createUser: cria um novo usuário com base nos dados fornecidos e o salva no banco de dados. Retorna o novo usuário criado em formato JSON.
  • getUserById: busca um usuário pelo ID fornecido no banco de dados e retorna-o em formato JSON. Se o usuário não for encontrado, retorna uma mensagem de erro.
  • updateUser: busca um usuário pelo ID fornecido no banco de dados, atualiza seus dados com base nos dados fornecidos e retorna o usuário atualizado em formato JSON. Se o usuário não for encontrado, retorna uma mensagem de erro.
  • deleteUser: busca um usuário pelo ID fornecido no banco de dados e o exclui. Retorna uma mensagem de sucesso. Se o usuário não for encontrado, retorna uma mensagem de erro.

Essas funções utilizam o modelo de usuário User, que é importado no início do arquivo.

O arquivo do modelo de usuário deve estar localizado em ../models/User.js.

Essas funções são definidas como exportações (exports), permitindo que sejam utilizadas em outros arquivos. Depois de adicionar esse código ao seu userController.js, você pode usá-lo para controlar as operações CRUD relacionadas aos usuários .

- ainda na pasta models podemos criar uma nova funcionalidade ao modelo User.js

User.js

db.sync()
 .then(() => console.log('Database synced'))
 .catch((err) => console.log('Error syncing database:', err));
  1. O método db.sync() é chamado. Esse método é fornecido pelo Sequelize e cria ou atualiza as tabelas no banco de dados com base nos modelos definidos. Ele sincroniza o esquema do banco de dados com a estrutura de modelos fornecida.
  2. O método then() é usado para adicionar um callback que será chamado quando a sincronização do banco de dados for bem-sucedida. Nesse caso, ele imprime a mensagem "Database synced".
  3. O método catch() é usado para adicionar um callback que será chamado caso ocorra um erro durante a sincronização do banco de dados. Nesse caso, ele imprime a mensagem de erro.

Em resumo, esse trecho de código importa o arquivo de configuração do banco de dados e o modelo de usuário, sincroniza o banco de dados com base nos modelos fornecidos e imprime mensagens de sucesso ou erro dependendo do resultado da sincronização.

Ele garante que a estrutura do banco de dados esteja atualizada com os modelos definidos antes de prosseguir com qualquer operação que dependa desse esquema.

  1. Criando as Rotas

Dentro de Routes.js crie uma rota para cada entidade que deseja rotear.

Utilize o módulo Express para definir as rotas e as funções de callback correspondentes.

Exemplo:

Routes.js:

const express = require('express');
const UserController = require('../controllers/UserController');

const routes = express.Router();

routes.get('/users', UserController.getAllUsers);
routes.get('/users/:id', UserController.getUsersbyID);
routes.post('/users', UserController.createUser);
routes.put('/users/:id', UserController.updateUser);
routes.delete('/users/:id', UserController.deleteUser);

module.exports = routes;

- Cada rota representa as funções de callback que foi definido no UserController.js

- Repita o processo para outras entidades que deseja rotear no projeto.

  1. Configurando o Servidor Web

No arquivo index.js, importe o módulo Express e os arquivos de roteamento que foram criados anteriormente. Configure as rotas utilizando o método app.use() do Express.

Inicialize o servidor web utilizando o método app.listen(). Exemplo:

index.js

const express = require('express');
const app = express();

// Configuração do middleware e do body parser
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

const routes = require('./routes/userRoutes');

// Rotas
app.use(routes);

// Configuração da porta do servidor
const PORT = process.env.PORT || 3000;

// Inicialização do servidor
app.listen(PORT, () => {
  console.log(`Servidor rodando na porta ${PORT}`);
});
  1. Criando Utilitários

Dentro do diretório utils, crie arquivos utilitários que possam ser reutilizados em diferentes partes do projeto.

Por exemplo, você pode criar um arquivo chamado passwordUtils.js para lidar com a encriptação de senhas utilizando o módulo Bcrypt.

  1. Executando o Projeto

Por fim, para iniciar o servidor web e testar o projeto de ecommerce, execute o comando node index.js no terminal ou prompt de comando, dentro do diretório do projeto.

O backend estará rodando na porta especificada no arquivo index.js, e você poderá enviar requisições HTTP para as rotas definidas.

Certifique-se de ter o banco de dados configurado corretamente para que as operações no banco de dados funcionem corretamente.

- realizando testes:

- se vocÊ já acessou o codigo no github já viu que eu adicionei 2 scripts no package.json

    "start": "node src/index.js",
    "dev": "nodemon src/index.js"

- então vamos executar npm run dev

- abrir o insomnia e testar as rotas de post e ver se os dados vão ser enviados ao mysql.

- post para o endereço : localhost:3000/users

- estrutura da requisição no formato json

{
 "username": "JohnDoe",
 "email": "john.doe@example.com",
 "password": "password123"
	
}

Resposta :

{
	"message": {
		"id": 01,
		"username": "JohnDoe",
		"email": "john.doe@example.com",
		"password": "password123",
		"updatedAt": "2023-10-11T00:57:53.426Z",
		"createdAt": "2023-10-11T00:57:53.426Z"
	}
}

retornou o código 201 created !! que maravilha agora vamos checar o banco de dados

post2

- como podemos ver todos os atributos da tabela users foi preenchidos nessa requisição.

- resultado no terminal :

post3

- agora teste todas as rotas :

  • Rotas para testes

  • get: localhost:3000/users

    • retorna todos usuários cadastrados no banco de dados
  • get: localhost:3000/users/1

    • retorna o usuário de id ' 1 '
  • post : localhost:3000/users

    • post cadastrar usuário
  • delete: localhost:3000/users/1

    • deleta o usuario exemplo id ' 1'
  • put : localhost:3000/users/1

    • atualiza os dados do usuário id ' 1 '

- até aqui já foi possível testar o banco de dados e fazer os primeiros registros e temos um código funcional

- vamos agora por para funcionar o "bcrypt"

- A função do bcrypt vai ser criptografar a senha digitada na criação de usuário e salvar no banco de dados a senha criptografada .

- No banco de dados o atributo password é varchar (60) pois a senha criptografada tem 60 caracteres

- agora temos que alterar o arquivo UserController.js e na pasta Utils , criar o arquivo passwordUtils.js

passwordUtils.js

const bcrypt = require('bcrypt'); 

module.exports = { 
  async hashPassword(password) {
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);
    return hashedPassword;
  },

  async comparePasswords(password, hashedPassword) {
    return bcrypt.compare(password, hashedPassword);
  }
};

Este código é um módulo que utiliza a biblioteca bcrypt para fazer o hash de senhas e comparar senhas.

  1. O primeiro passo é importar a biblioteca bcrypt através da linha de código const bcrypt = require('bcrypt');.
  2. Em seguida, o módulo exporta duas funções:
    • hashPassword: Esta função recebe uma senha como parâmetro e retorna uma promessa que será resolvida com a senha hashed. Primeiro, ela gera uma "salt" (uma string aleatória) utilizando a função genSalt do bcrypt e, em seguida, utiliza essa salt para fazer o hash da senha utilizando a função hash do bcrypt. Por fim, retorna o resultado.
    • comparePasswords: Esta função recebe uma senha e uma senha hashed como parâmetros e retorna uma promessa que será resolvida com um valor booleano indicando se as senhas são iguais. Ela utiliza a função compare do bcrypt para comparar as senhas.
  3. A biblioteca bcrypt é utilizada para garantir que as senhas sejam armazenadas de forma segura no banco de dados. Ela utiliza um algoritmo de hashing unidirecional, o que significa que não é possível descobrir a senha original a partir do seu hash. Isso ajuda a proteger as senhas em caso de vazamento de dados.

UserController.js

// Função para criar um novo usuário utilizando o bcrypt para criptografar as senhas no banco de dados
exports.createUser = async (req, res) => {
  const { password } = req.body;
  const hashedPassword = await passwordUtils.hashPassword(password);

     // Criar uma nova instância do modelo de usuário com a função hashedPassword
  const user = new User({ ...req.body, password: hashedPassword });

  try {
    // Salvar o novo usuário no banco de dados
    const newUser = await user.save();
    // Retornar o novo usuário em formato JSON com o código de status 201 (Created)
    res.status(201).json({ message: 'Usuário Cadastrado com Sucesso' });
  } catch (error) {
    // Se ocorrer algum erro, retornar uma mensagem de erro com o código de status 400 (Bad Request)
    res.status(400).json({ message: error.message });
  }
};

- agora ao criar usuário a senha digitada vai ser criptografada no banco de dados usando as funçoes do bcrypt

- request post : localhost:3000/users

{
 "username": "User01",
 "email": "user@teste.com",
 "password": "senha123xx"
	
}

response: 201 created

{
	"message": {
		"id": 02,
		"username": "User01",
		"email": "user@teste.com",
		"password": "$2b$10$wwsUR/DUQhoquH.lXMLAGeYF9U.jlbcqjKZ/Ou2sfC1Ng0CySSSzS",
		"updatedAt": "2023-10-11T00:57:53.426Z",
		"createdAt": "2023-10-11T00:57:53.426Z"
	}
}

foi salvo como uma hash criptografada no banco de dados

todos os dados foi exibidos no response então depois vamos mudar isso para apenas uma mensagem de sucesso.

- A partir daqui começamos a escalar o projeto e ao poucos vou adicionando versões atualizadas do código no GitHub e atualizando esse post.

- Além do UserController, podemos criar os controladores para cadastro de produtos, carrinho de compras e pedidos.

- ProductController: responsável por gerenciar o cadastro, edição, listagem e exclusão de produtos.

Também pode conter métodos para buscar produtos por categoria, atualização de estoque, entre outros.

- CartController: responsável por gerenciar o carrinho de compras do usuário.

Pode conter métodos para adicionar produtos ao carrinho, remover itens do carrinho, atualizar a quantidade de um item no carrinho, entre outros.

- OrderController: responsável por gerenciar os pedidos realizados pelos usuários.

Pode conter métodos para criação de um novo pedido, listagem dos pedidos do usuário, status do pedido, entre outros.

Além desses controladores principais, podemos ter outros controladores auxiliares, como:

- CategoryController: responsável por gerenciar as categorias dos produtos.

Ele pode conter métodos para criar, editar, listar e excluir categorias.

UserController (pode ser renomeado para AuthController): além das funcionalidades de cadastro e login do usuário, podemos adicionar métodos para resetar senha, atualizar informações do usuário, entre outros relacionados à autenticação e gerenciamento da conta.

- Dotenv

- Não vamos esquecer das variáveis de ambiente , npm install dotenv

- crie o arquivo .env na raiz do projeto

- adicione as variáveis de ambiente

DB_NAME=ecommerce
DB_USER=root
DB_PASSWORD=senha123
DB_HOST=127.0.0.1
DB_DIALECT=mysql

Atualize o database.js

require('dotenv').config(); // Carrega as variáveis de ambiente do arquivo .env

const Sequelize = require('sequelize');

module.exports = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
  host: process.env.DB_HOST,
  dialect: process.env.DB_DIALECT,
});

- Agora vamos instalar o Morgan

- O que é Morgan ? Morgan é um modulo para node.js e Express para registrar solicitações e erros HTTP e simplificar o processo. é um middleware que tem acesso aos métodos de ciclo de vida de solicitação e resposta registrando todos os logs de requests no terminal

- instalando : npm install morgan

- importar para o index.js

const express = require('express');
const app = express();
const morgan = require('morgan') // importando morgan

// Configuração do middleware e do body parser
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use(morgan('dev'))  // logs for http requests in terminal

const routes = require('./Routes');

// Rotas
app.use(routes);

// Configuração da porta do servidor
const PORT = process.env.PORT || 3000;

// Inicialização do servidor
app.listen(PORT, () => {
  console.log(`Servidor rodando na porta ${PORT}`);
});

- Agora vamos falar um pouco mais sobre a pasta Utils.

sobre a pasta Utils, além do bcrypt para o arquivo passwordUtils.js , podemos  adicionar outros arquivos utils relacionados a funcionalidades.

Alguns exemplos:

emailUtils.js: contendo funções para enviar e gerenciar e-mails, como de confirmação de pedidos, recuperação de senha, entre outros.

imageUtils.js: para funções relacionadas ao tratamento e manipulação de imagens, como redimensionamento, compressão, etc.

validationUtils.js: com funções de validação de dados inputados pelos usuários, para garantir que estejam corretos antes de serem processados ou salvos no banco de dados.

dateUtils.js: para funções relacionadas à manipulação de datas, como formatação, cálculos de prazos, entre outros.

currencyUtils.js: contendo funções relacionadas à manipulação e formatação de moedas, como conversão, arredondamento, etc.

fileUtils.js: para funções relacionadas à manipulação de arquivos, como leitura, gravação, deleção, entre outras operações.

- vamos agora criar o validationUtils.js na pasta utils , ele é muito importante para impedir cadastros duplicados no banco de dados.

validationUtils.js

const User = require('../models/User');

// Verifica se o usuário já está cadastrado no banco de dados
const isUserAlreadyRegistered = async (username) => {
  const user = await User.findOne({ username });
  return !!user;
};

module.exports = {
  isUserAlreadyRegistered,
};

começo o código importando o modelo de usuário , é nele que temos os atributos que queremos comparar no caso ' username ' outros atributos poderia ser escolhido mas o ideal nesse caso seria username.

variável isUserAlreadyRegistered é essa variável que vai ser chamada no arquivo UserController.js no método de criar usuários 'createUser'

UserController.js

const { isUserAlreadyRegistered } = require('../utils/validationUtils'); // importando o validador de usuário já cadastrado
// Função para criar usuários
exports.createUser = async (req, res) => {
  try {
    const { username } = req.body; // variável para validar usarios pelo username cadastro no db
    const { password } = req.body; // variável para criar um novo usuário utilizando o bcrypt para criptografar as senhas no banco de dados

    const userAlreadyExists = await isUserAlreadyRegistered(username); // variável que instancia o validationUtils.ja e verifica se o usuário já está cadastrado
    if (userAlreadyExists) { // se username já é cadastrado retorna erro 400
      return res.status(400).json({ message: "Usuário já cadastrado" });
    }

    const hashedPassword = await passwordUtils.hashPassword(password); // variável que instancia o bcrypt passwordUtils.js
    const user = new User({ ...req.body, password: hashedPassword }); // convertendo senha digitada pelo usuário em senha criptografada
    const newUser = await user.save();  // salva o usuário no db com a senha criptografada
    // retorno de sucesso 201 
    res.status(201).json({ message: 'Usuário Cadastrado com Sucesso' });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Erro interno do servidor" });
  }
};

já adicionei comentários no código para melhor entendimento , o arquivo UserController.js foi oque mais teve alterações até o momento devido a sua importância , temos nele agora variáveis para criptografar as senhas e para verificar se já tem o usuário cadastrado .

agora vamos testar algumas rotas

ecommercevxscode1x

vemos que no terminal as tabelas já são criadas no banco de dados.

rota post - localhost:3000/users

{
 "username": "ELon-Musk",
 "email": "elon-musk@tesla.com",
 "password": "senha123xx"
	
}

resposta:

{
	"message": "Usuário Cadastrado com Sucesso"
}

teste de validador - ao reenviar a mesma solicitação de cadastro de usuário temos a resposta

"Usuário já cadastrado "

{
	"message": "O Usuário já está cadastrado 😒"
}

rota get todos os usuários

get : localhost:3000/users

resposta:

[
	{
		"id": 33,
		"username": "fbs-dev",
		"email": "francisco.spadaro@outlook.com",
		"password": "$2b$10$J9FoaHjrF5WHN3rYtcGVDeRNjmWbC2JbFzmMVFmn0KpuIt1kwots2",
		"createdAt": "2023-10-10T18:11:12.000Z",
		"updatedAt": "2023-10-10T18:11:12.000Z"
	},
	{
		"id": 34,
		"username": "ELon-Musk",
		"email": "elon-musk@tesla.com",
		"password": "$2b$10$wwsUR/DUQhoquH.lXMLAGeYF9U.jlbcqjKZ/Ou2sfC1Ng0CySSSzS",
		"createdAt": "2023-10-10T19:49:38.000Z",
		"updatedAt": "2023-10-10T21:42:11.000Z"
	}
]

quando usamos a rota para listar todos os usuários vemos que as senhas estão criptografadas

rota put : localhost:3000/users/id

put : atualizar os dados , alterei o método de atualizar usuários para apenas atualizar o email.

request: método put : localhost:3000/users/34

{
 "email": "teste@example.com"
}

response :

{
	"message": "🤖 Email Alterado com Sucesso. 🤖"
}

vamos conferir

get user pelo id localhost:3000/users/34

response:

	{
		"id": 34,
		"username": "ELon-Musk",
		"email": "teste@example.com",
		"password": "$2b$10$wwsUR/DUQhoquH.lXMLAGeYF9U.jlbcqjKZ/Ou2sfC1Ng0CySSSzS",
		"createdAt": "2023-10-10T19:49:38.000Z",
		"updatedAt": "2023-10-10T21:42:11.000Z"
	}
]

o email foi alterado com sucesso.

o username não será mais alterado no db.

UserController.js

exports.updateUserEmail = async (req, res) => {
  try {
    // Adicionando os parametros que devem ser atualizados no put
    const { id } = req.params;
    const { email } = req.body;
    //Função para atualizar os dados  de email, pelo id
    const updatedUser = await User.update(
      { email },
      { where: { id } }
    );
    // Se nenhum usuário foi atualizado, devido a  id invalido apresenta erro 404
    if (updatedUser[0] === 0) {
      return res.status(404).json({ message: 'Usuário não encontrado.' });
    }
    // reposta de sucesso 200 ou 500 de erro interno
    res.status(200).json({ message: '🤖 Email Alterado com Sucesso. 🤖' });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Erro interno do servidor' });
  }
};

já que agora temos a função de alterar e-mail também precisa de uma para alterar a senhas,

para isso devemos criar um novo método como fiz no updateUserEmail,

porem para não dar conflito na rota de put de users é necessário criar esse método em um novo controlador e em uma nova rota.

PasswordController.js

const User = require("../models/User");

const passwordUtils = require('../utils/passwordUtils');  // importando o arquivo de configurações do bcrypt 

exports.updateUserPassword = async (req, res) => {
    try {
      // Adicionando os parametros que devem ser atualizados no put
      const { id } = req.params;
      const { password } = req.body;
      //Função para atualizar os dados  de email, pelo id
      const hashedPassword = await passwordUtils.hashPassword(password)
      const updatedUser = await User.update(
        { password: hashedPassword },
        { where: { id } }
      );
      // Se nenhum usuário foi atualizado, devido a  id invalido apresenta erro 404
      if (updatedUser[0] === 0) {
        return res.status(404).json({ message: 'Usuário não encontrado.' });
      }
      // reposta de sucesso 200 ou 500 de erro interno
      res.status(200).json({ message: '🤖 Senha Alterada com Sucesso. 🤖' });
    } catch (error) {
      console.error(error);
      res.status(500).json({ message: 'Erro interno do servidor' });
    }
  };

o método agora tem as funcionalidades de alterar o password seguindo o mesmo padrão dos outros métodos

nova rota localhost:3000/password/id

routes.js

const express = require('express');
const routes = express.Router();

const UserController = require('./controllers/UserController');

//rotas de usuários
routes.get('/users', UserController.getAllUsers);
routes.get('/users/:id', UserController.getUsersbyID);
routes.post('/users', UserController.createUser);
routes.put('/users/:id', UserController.updateUserEmail);

routes.delete('/users/:id', UserController.deleteUser);

// rota para alterar senhas
const PasswordController = require('./controllers/PasswordController');
routes.put('/password/:id', PasswordController.updateUserPassword);


module.exports = routes;

resultado do teste :

post : localhost:3000/password/1

{
 "password": "teste123"
}

// requisiçao do post em json

resposta 201 created

{
	"message": "🤖 Senha Alterada com Sucesso. 🤖"
}

//201 created 

post : localhost:3000/password/777 um id que nao existe

{
	"message": "Usuário não encontrado.🔍"
}
// 404 not found

- Vamos Criar agora o Modelo para Produtos , na pasta models seguindo o mesmo padrão do arquivo User.js

Product.js

const Sequelize = require('sequelize');
const db = require('../config/database');

const Product = db.define('products', {
  id: {
    type: Sequelize.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: {
    type: Sequelize.STRING,
    allowNull: false
  },
  price: {
    type: Sequelize.FLOAT,
    allowNull: false
  },
  description: {
    type: Sequelize.TEXT,
    allowNull: true
  },
  quantity: {
    type: Sequelize.INTEGER,
    allowNull: false,
    defaultValue: 0
  }
});

// Sincroniza o modelo com o banco de dados e cria a tabela de produto automaticamente
Product.sync()
  .then(() => {
    console.log('🤖 Tabela de Produtos Criada com sucesso! ✔');
  })
  .catch((error) => {
    console.error('Erro ao criar tabela de produtos:', error);
  });

module.exports = Product;

- Nesse modelo temos um importante atributo o ' quantity ' que é a quantidade de produtos cadastrado oque deixa o banco de dados organizado afinal de contas nao queremos varios registros do mesmo produto no banco de dados , queremos apenas 1 registro por produto e cada produto com sua quantidade.

- Agora que temos o Product.js já podemos criar o controlador de produtos

ProductController.js

const Product = require('../models/Product');


const ProductController = {
  createProduct: async (req, res) => {            // createProduct não precisa importar validationUtils igual o createUser de UserController.js a logica de validação já está no proprio codigo                              
    try {
      const { name, price, description, quantity } = req.body;
      const produtoJaExiste = await Product.findOne({ where: { name } });         // validador verifica se o produto ja existe pelo nome
      if (produtoJaExiste) {                                                      // se produto ja existe adiciona a quantidade de produtos inserida no post
        const updatedQuantity = parseInt(quantity, 10) || 1;                      // se ao cadastrar o produto nao inserir a quantidade , vai adicionar 1 de quantidade toda vez
        produtoJaExiste.quantity += updatedQuantity;                              // parseInt(quantity, 10) o numero 10 representa a base decimal de parseInt  para garantir que o número seja interpretado como um decimal
        await produtoJaExiste.save();
        console.log(produtoJaExiste)
        return res.status(200).json({ message: `⚠ O Produto ${produtoJaExiste.name} já está cadastrado. A quantidade de itens foi atualizada para ${produtoJaExiste.quantity}. ⚠` });
      }
      const newProduct = new Product({
        name,
        price,
        description,
        quantity: quantity || 1                                                     // quantidade ou 1  ,  ou ele cadastra a quantidade definida no cadastrou ou se for cadastrado com quantidade 0 ele cadastrar com quantidade 1 , nao pode haver 0 produtos
      });
      const savedProduct = await newProduct.save();
      console.log(savedProduct)
      res.status(201).json(savedProduct);
    }
    catch (error) {
      res.status(401).json({ error: error.message });
    }
  },
  // Método para obter todos os produtos
  getAllProducts: async (req, res) => {
    try {
      const products = await Product.findAll();                                       // Obter todos os produtos do banco de dados
      res.status(200).json(products);                                                 // Retornar os produtos obtidos como resposta
    }
    catch (error) {
      res.status(401).json({ error: error.message });
    }
  },
  // Método para obter um produto pelo seu ID
  getProductById: async (req, res) => {
    try {
      const { id } = req.params;                                                      // Obter o ID do produto a partir dos parâmetros da requisição
      const product = await Product.findByPk(id);                                     // Buscar o produto no banco de dados pelo seu ID

      if (!product) {                                                                 // Se o produto não existe, retornar uma resposta com status 404 e uma mensagem de erro
        return res.status(404).json({ error: "Produto não encontrado" });
      }
      res.status(200).json(product);                                                  // Retornar o produto encontrado como resposta
    }
    catch (error) {
      res.status(500).json({ error: error.message });
    }
  },
  // Método para atualizar um produto pelo seu ID
  updateProductById: async (req, res) => {
    try {
      const { id } = req.params;
      const product = await Product.findByPk(id);

      if (!product) {                                                                    // Verificar se o produto foi encontrado
        return res.status(404).json({ error: "Produto não encontrado" });                // Se o produto não existe, retornar uma resposta com status 404 e uma mensagem de erro
      }

      const { quantity, price, description } = req.body;                                 // Extrair dados do corpo da requisição , como regra de negocio o nome nao pode ser alterado.

      product.quantity = quantity;                                                       // Atualizar os dados do produto encontrado com os novos dados fornecidos
      product.price = price;
      product.description = description;

      const updatedProduct = await product.save();
      console.log(`Atenção os Dados do Produto ID "${req.params.id}" Foram Atualizados.`)                                       // Salvar as alterações no produto no banco de dados
      res.status(200).json(updatedProduct);                                              // Retornar o produto atualizado como resposta
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  },
  // Método para excluir um produto pelo seu ID
  deleteProductById: async (req, res) => {
    try {
      const deletedProduct = await Product.destroy({ where: { id: req.params.id } });      // Excluir o produto do banco de dados pelo seu ID
      if (!deletedProduct) {                                                               // Verificar se o produto foi encontrado e excluído
        return res.status(404).json({ error: "Produto não encontrado" });                  // Se o produto não foi encontrado, retornar uma resposta com status 404
      }
      console.log(`Atenção o Produto ID "${req.params.id}" Foi Excluido.`)
      res.status(200).json({ message: "Produto Excluido com Sucesso." });
    }
    catch (error) {
      res.status(401).json({ error: error.message });
    }
  }
};

// Essa forma de exportar é diferente do metodo usado no UserController , fiz isso apenas para estudar as diferentes formas
// as cores dos metodos fica diferente no arquivo Routes.js devido as formas diferentes de exportação. 
module.exports = ProductController;

- (ProductController) para lidar com as operações relacionadas a produtos. Ele importa o modelo Product e define métodos para criar um novo produto, obter todos os produtos, obter um produto pelo seu ID, atualizar um produto pelo seu ID e excluir um produto pelo seu ID.

Cada método utiliza blocos try-catch para tratar qualquer erro que possa ocorrer durante as operações do banco de dados.

- foi adicionado o validador para validar se o produto cadastrado já existe , se ele já existe então o novo cadastro de produto apenas atualiza a sua quantidade de acordo com a quantidade inserida no cadastro

- para atualizar a quantidade de produtos a forma padrão seria via put mas com esse método é possível atualizar os dados tanto no post ou no put

- parâmetro parseInt(quantity, 10):

- vale observar que nesse caso não precisei importar o validador igual fiz no UserController.js que importa a logica de validação do arquivo validationUtils.js , a logica foi criada no próprio código , pois nesse caso ficou mais fácil para aplicar o funcionamento e também para mostrar as diferentes formas de escrever um código , até as formas de exportar UserController.js e UserController.js estão diferente apenas para caso de estudos.

- O parâmetro 10 no método parseInt significa que o número a ser analisado será convertido para base decimal. É usado para garantir que o número seja interpretado como um decimal, mesmo que haja um zero à esquerda.

- lembrando que não pode ter virgulas nos campos de dados numéricos , mas essa configuração de alterar virgula por ponto é feita no front-end

- Vamos testar as rotas

- agora precisamos importar as rotas para os produtos.

Routes.js

const express = require('express');
const routes = express.Router();

const UserController = require('./controllers/UserController');  // importando controlador de usuários

//define as rotas de usuários.
routes.get('/users', UserController.getAllUsers);
routes.get('/users/:id', UserController.getUsersbyID);
routes.post('/users', UserController.createUser);
routes.put('/users/:id', UserController.updateUserEmail);
routes.delete('/users/:id', UserController.deleteUser);

const ProductController = require('./controllers/ProductController');  // importando controlador de produtos

// Define as rotas relacionadas aos produtos.
routes.post('/products', ProductController.createProduct);
routes.get('/products/:id', ProductController.getProductById);
routes.get('/products', ProductController.getAllProducts);
routes.put('/products/:id', ProductController.updateProductById);
routes.delete('/products/:id', ProductController.deleteProductById);

const PasswordController = require('./controllers/PasswordController'); // importando controlador para alterar senhas
// define a rota de senhas. 
routes.put('/password/:id', PasswordController.updateUserPassword);


module.exports = routes;

testando o cadastro

post : localhost:3000/products

corpo da requisição :

{ 
	"name": "tv", 
	"price": "200.00",        // nao por virgulas
	"description": "test", 
	"quantity": 20
}

resposta : 201 created

{
	"id": 1,
	"name": "tv",
	"price": "200.00",
	"description": "test",
	"quantity": 20,
	"updatedAt": "2023-10-11T16:38:31.465Z",
	"createdAt": "2023-10-11T16:38:31.465Z"
}

agora enviamos o mesmo post de novo para testar o comportamento

resposta : 200 ok

{
	"message": "⚠ O Produto tv já está cadastrado. A quantidade de itens foi atualizada para 40. ⚠"
}

a primeira vez foi cadastrado 20 itens e ao fazer de novo o cadastro com 20 ele ja atualiza o banco de dados e informa que foi atualizado, legal né ?

- observe os comentários do código ProductController.js para entender melhor a logica disso.

agora testar a rota put

put : localhost:3000/products/1

corpo da requisição :

{
 "price": "300.22",
 "description": "isso é um teste",
 "quantity": "10"
	
}

resposta : 200 ok

{
	"id": 1,
	"name": "tv",
	"price": "300.22",
	"description": "isso é um teste",
	"quantity": "10",
	"createdAt": "2023-10-11T16:38:31.000Z",
	"updatedAt": "2023-10-11T16:42:47.427Z"
}

A partir de agora vou focar mais no desenvolvimento do código e com isso não vou atualizar esse post por um tempo.

Quando for possível colocar esse projeto em produção farei um novo post , caso esse post aqui tenha curtidas e comentários eu atualizo e esse post vai virar um livro de tão grande 😂

- Work in progress

OBS: Optei por deixar o repositório privado.

Update : PARTE 2 publicada clique aqui

Comentários