Node.js instalado. Caso não possua o node intalado ainda clique aqui
Criando aplicação e instalando as dependências
Vamos iniciar uma aplicação com o comando npm init -y e criar na pasta raiz os arquivos server.js, bd.js e quatro novas pastas chamadas router, controller, model e service.
Agora podemos instalar as dependências do projeto através do comando npm install express dotenv pg postgres sequelize sequelize-cli cors bcrypt .
Instale também como dependencias de desenvolvimento npm install nodemon morgan --save-dev .
Configure também um comando de inicialização da aplicação atrelado ao nodemon no package.json
Ao final do processo você deve ter a seguinte estrutura de arquivo e package.json.
{"name":"apiexemplo","version":"1.0.0","description":"","main":"index.js","scripts": {"test":"echo \"Error: no test specified\" && exit 1","start":"nodemon start server.js" },"keywords": [],"author":"","license":"ISC","dependencies": {"bcrypt":"^5.1.1","cors":"^2.8.5","dotenv":"^16.3.1","express":"^4.18.2","pg":"^8.11.3","postgres":"^3.4.0","sequelize":"^6.33.0","sequelize-cli":"^6.6.1" },"devDependencies": {"morgan":"^1.10.0","nodemon":"^3.0.1" }}
Banco de Dados
Crie um banco de dados local ou então siga op tutorial abaixo para criar uma instância do Elephant SQL.
Agora vamos prosseguir com a configuração do banco no arquivo bd.js que criamos anteriormente. Importe o sequelize e do dotenv conforme mostrado. Crie também na pasta raiz do projeto um arquivo .env. Nesta etapa vamos utilizar a url copiada por você na última etapa do tutorial acima.
constSequelize=require("sequelize"); //importação do sequelizerequire("dotenv").config(); //importação do .env com as variáveis de ambienteconstbdUser=process.env.user; //variavel de ambienteconstbdPassword=process.env.password; //variavel de ambienteconstsequelize=newSequelize(`postgres://${bdUser}:${bdPassword}@silly.db.elephantsql.com/${bdUser}`,// sua url de conexão com o banco { dialect:"postgres" } //define o dialeto do banco);module.exports= sequelize; //exporte padrão do Node.
#Dados BDuser="seuUsuario"password="suaSenha"
Configurando o servidor e definindp uma porta
Agora podemos configurar o servidor em server.js
constexpress=require("express");constcors=require("cors");constmorgan=require("morgan");constbd=require("./bd");constapp=express(); //inicia o expressapp.use(morgan("dev")); //imprime os logs das operações durante o desenvolvimentoapp.use(cors()); //libera o cross originapp.use(express.json()); // configura a API para receber e enviar json (Headers HTTP)//rota padrão na qual podemos verificar se o servidor está disponívelapp.get("/",async (req, res) => {res.status(200).json({ msg:"Api ok!" });});//inicia o servidor através de uma self-invoked function que executa na inicialização da aplicação(async () => {try {awaitbd.sync({ force:false });// sincroniza o bancoconsole.log("Banco conectado!");app.listen(3000, () =>console.log("Service is live!")); //iniciar efetivamente o servidor na porta 3000 } catch (err) {console.log(err); //imprime o erro caso este ocorra } })();
Observe o objeto passado de parâmetro na função que sincroniza o banco.
force: true: Quando você define force como true, a sincronização do banco de dados recriará todas as tabelas, ou seja, ele irá descartar todas as tabelas existentes e criá-las novamente do zero. Isso é útil em situações de desenvolvimento ou testes, quando você deseja recriar o esquema do banco de dados para refletir as mudanças no seu modelo de dados ou quando deseja começar com um banco de dados vazio.
force: false: Ao definir force como false, a sincronização do banco de dados não irá recriar as tabelas se elas já existirem. Isso é a configuração padrão e é mais apropriado para ambientes de produção, onde você deseja manter os dados existentes. Usar false é uma abordagem mais segura para evitar a perda de dados acidental, pois não apagará ou recriará tabelas existentes.
Em resumo, ao definir force: true, você está instruindo o sistema a recriar o esquema do banco de dados do zero, enquanto force: false garante que o esquema atual do banco de dados seja mantido, evitando a perda de dados. A escolha entre essas opções dependerá dos requisitos do seu ambiente, sendo true mais comum durante o desenvolvimento e false mais adequado para produção.
Podemos agora testar a conexão com o banco de dados iniciando a aplicação através do comando npm start . Se a conexão com o banco for bem sucedida a mensagem de "Banco conectado” será exibida no console bem como “Service is live!”que indica que o servidor está rodando.
mackleaps@222816-MackFCIapiExemplo%npmstart> apiexemplo@1.0.0 start> nodemon start server.js[nodemon] 3.0.1[nodemon] to restart at any time, enter `rs`[nodemon] watchingpath(s):*.*[nodemon] watching extensions: js,mjs,cjs,json[nodemon] starting `nodestartserver.js`Executing (default): SELECT 1+1 AS resultBancoconectado!Serviceislive!
Também podemos verificar que a aplicação foi iniciada corretamente através da rota que definimos para esta finalidade no server. Em seu navegador use a url http://localhot:3000 e verifique se a mensagem {msg:Api ok!} foi exibida.
Criando um Schema com Sequelize
Neste passo vamos criar o esquema para a tabela usuários de nossa aplicação. Crie um arquivo chamado userModel.js dentro da pasta model que criamos anteriormente.
constsequelize=require("sequelize"); // importamos o sequelize que gerencia a criação da tabelaconstdb=require("../bd"); //importamos o banco no qual conectamos anteriormenteconstUser=db.define("usuarios",//define o nome da tabela no banco de dados { id: { type:sequelize.INTEGER.UNSIGNED, primaryKey:true,//define que esta coluna é a chame primária autoIncrement:true,//incrmenta o id em uma unidade evitando duplicidade de chave primaria allowNull:false,//define que este atributo não pode ser vazio }, nome: { type:sequelize.STRING, allowNull:false, unique:true,//define que todos os valores desta coluna deve ser único }, password: { type:sequelize.STRING, allowNull:false, }, permissao: { type:sequelize.STRING, allowNull:false, }, }, { timestamps:false,//desabilita o armazenamento de datas em que foi criado e atualizado });module.exports= User;
Para entender melhor o funcionamento e os tipos de dados acesse a documentação do sequelize no link abaixo.
O sequelize também permite definir relacionamento entre as tabelas.
Criando o CRUD da API através de funções controller
Agora vamos criar as funções que são responsáveis por receber as requisições processa-las recuperando as informações do banco de dados e retornar um json como resposta para o cliente. Crie um arquivo userController.js na pasta controller.
Geralmente, um arquivo de controlador é responsável por conter todas as funções que manipulam as operações de CRUD (Create, Read, Update, Delete) lidando diretamente com as operações HTTP para um determinado modelo de dados em um aplicativo. Neste exemplo, o foco está em operações de usuário usando o modelo User.
recuperarUsuarios: Essa função lida com a operação de leitura para recuperar todos os usuários da tabela. Ela usa o método User.findAll() para buscar todos os registros e responde com os usuários encontrados ou uma mensagem de erro.
adicionarUsuario: Essa função lida com a operação de criação para adicionar um novo usuário à tabela. Ela desestrutura os dados do corpo da requisição, usa o método User.create() para criar um novo registro e responde com o novo usuário ou uma mensagem de erro.
findById: Esta função trata da operação de leitura para encontrar um usuário específico com base no ID. Ela extrai o ID da URL da rota, usa o método User.findByPk(id) para buscar o registro correspondente e responde com o usuário encontrado ou uma mensagem de erro.
excluiUsuario: Essa função lida com a operação de exclusão para remover um usuário com base no ID. Ela extrai o ID da URL da rota, usa o método User.destroy() para excluir o registro e responde com uma mensagem de sucesso ou uma mensagem de erro.
atualizarUsuario: Esta função trata da operação de atualização para modificar um usuário com base no ID. Ela extrai o ID da URL da rota e os novos dados do corpo da requisição. Ela também inicia uma transação para garantir a integridade do banco de dados, atualiza o registro com os novos dados e responde com uma mensagem de sucesso ou uma mensagem de erro.
Por fim, as funções são exportadas como um objeto no final do arquivo, tornando-as disponíveis para uso em outras partes do aplicativo.
constUser=require("../model/userModel"); //import do model de user que criamos anteiormenteconstsequelize=require("../bd"); //importe do sequelize para definir controle de transações (Transaction Control Language, TCL)//req-> paramentro que recebe a requisição//res ->parametro que configura a resposta da requisição//READ->GETasyncfunctionrecuperarUsuarios(req, res) {try {constusuarios=awaitUser.findAll(); //método findAll do sequelize retorna um sequelize object com todos os usuários do banco//verifica se usuarios for vazio retorna 404 not found como respostaif (usuarios.length===0) {res.status(404).json({ msg:"Não foram encontados usuários" });return; }res.status(200).json(usuarios); //se a busca for bem sucedida retorna 200 e um array que contém json com as informações dos usuários } catch (err) {//tratamento de excessõesconsole.log(err);res.status(500).json({ msg:"Falha ao recuperar usuários!" }); //caso algum erro ocorra retorna internal server error 500 junto da mensagem de erro }}//CREATE->POSTasyncfunctionadicionarUsuario(req, res) {const { nome,password,permissao } =req.body; //desestruturação do objeto presente no corpo da requisiçãotry {//recebe json como argumento do novo elemento que será criado e retorna elemento que foi criadoconstnovoUsuario=awaitUser.create({ nome: nome, password: password, permissao: permissao, });res.status(201).json(novoUsuario); //retorna para o cliente código 201 created e o json do novo elemento } catch (err) {console.log(err);res.status(500).json({ msg:"Falha ao criar usuário!" }); //retorna internal server error 500 caso ocorra algum erro durante a criação }}//READ->GETasyncfunctionfindById(req, res) {const { id } =req.params; //desestrutura a url da rota recuperando o idtry {constusuario=awaitUser.findByPk(id); // findByPk é um método do sequelize que pesquisa e retorna um objeto que corresponde aquela chave primáriaif (!usuario) {//verifica se usuario for vazio retorna 404 not found como respostares.status(404).json({ msg:"Usuario não encontrado" });return; }res.status(200).json(usuario); //se a busca for bem sucedida retorna 200 e um json com as informações do usuário } catch (err) {res.status(500).json({ msg:"Falha ao criar usuário!" }); //retorna internal server error 500 caso ocorra algum erro durante a operação }}//DELETE->DELETEasyncfunctionexcluiUsuario(req, res) {const { id } =req.params; //desestrutura a url da rota recuperando o id do elemnto a ser excluidotry {//exclui a linha da tabela através do id passado na uri através do método destroy do sequelizeawaitUser.destroy({ where: { id: id, }, });res.status(200).json({ msg:"Usuário excluido!" }); } catch (err) {console.log(err);res.status(500).json({ msg:"Falha ao criar usuário!" }); //retorna internal server error 500 caso ocorra algum erro durante a operação }}//UPDATE->PUTasyncfunctionatulizarUsuario(req, res) {const { id } =req.params; //desestrutura a url da rota recuperando o id do elemnto a ser atualizadoconst { novoNome,novoPassword,novaPermissao } =req.body; //desestruturação do objeto presente no corpo da requisiçãoconstt=awaitsequelize.transaction(); //abre uma transação para evitar que o banco perda sua integridadetry {//update é metodo do sequelize que atualiza linha da tabelaawaitUser.update( { nome: novoNome, password: novoPassword, permissao: novaPermissao, }, { where: { id: id }, transaction: t,// Associa a transação à atualização } );// Confirme a transaçãoawaitt.commit();res.status(200).json({ msg:"Usuário atualizado!" }); } catch (err) {// Em caso de erro, reverta a transaçãoawaitt.rollback();res.status(500).json({ msg:"Falha ao atualizar" }); }}//Exportação das funções para acesso no routermodule.exports= { recuperarUsuarios, adicionarUsuario, findById, excluiUsuario, atulizarUsuario,};
Definindo as rotas no Router
Em um aplicativo web, o roteamento desempenha um papel fundamental na definição de como as diferentes URLs são tratadas, quais ações devem ser executadas e como os recursos são servidos. Para facilitar essa funcionalidade em aplicativos Node.js, a biblioteca Express fornece um recurso chamado Router, que é uma parte essencial na criação de rotas e na manipulação de solicitações HTTP.
O que é um Router?
Um Router no contexto do Express é um módulo que permite organizar e mapear rotas em seu aplicativo Node.js. Ele funciona como um middleware que ajuda a direcionar solicitações HTTP para as funções de controle adequadas. Essas funções de controle executam a lógica de negócios necessária com base no caminho da URL e no método HTTP da solicitação.
O que um Router Define?
Um Router define como as rotas devem se comportar em relação a uma série de critérios, incluindo o método HTTP, a URL e qualquer parâmetro que possa estar presente na rota. Com um Router, você pode criar rotas para lidar com:
GET: Para buscar informações.
POST: Para criar novos recursos.
PUT: Para atualizar recursos existentes.
DELETE: Para excluir recursos.
Rotas Dinâmicas
No Express, você pode definir rotas dinâmicas usando notações especiais, como :id. Essa notação indica que um valor variável será capturado a partir da URL e disponibilizado como um parâmetro na função de controle associada. Por exemplo, a rota /usuarios/:id permitirá que você acesse o valor id na função de controle quando um URL como /usuarios/123 for acessado, onde 123 é o valor do parâmetro id.
A rota /produtos/:categoria? no Express define um parâmetro chamado categoria como opcional, permitindo que os clientes acessem a rota com ou sem um valor para categoria. Quando um valor é fornecido, ele é acessível na função de controle através de req.params.categoria, mas se nenhum valor for especificado na URL, o Express considera o valor padrão como "todos". Isso oferece flexibilidade ao usuário, tornando possível listar produtos de uma categoria específica quando desejado, ou todos os produtos quando a categoria não é especificada.
Agora podemos criar no diretório router o arquivo userRouter.js . Neste arquivo vamos mapear as funções do controller para as rotas e métodos HTTP.
As funções do controlador são importadas no início do arquivo routes.js.
O módulo Express é importado para criar e configurar o objeto Router.
Cada rota é definida usando o método correspondente (get, post, delete, put) no objeto routes. A função do controlador correspondente é associada a cada rota.
Nas rotas que exigem um parâmetro, como /usuarios/:id, a notação :id indica que esse parâmetro será parte da URL da requisição. Por exemplo, uma solicitação para /usuarios/123 fornecerá o valor 123 como o parâmetro id.
Finalmente, o objeto routes é exportado para que ele possa ser utilizado no arquivo principal (geralmente server.js) para configuração do servidor Express.
constexpress=require("express"); //importação do expressconstroutes=express.Router(); //importação do Router da biblioeca express//importação das funções do controller que serão mapeadas para as rotasconst {recuperarUsuarios,adicionarUsuario,findById,excluiUsuario,atulizarUsuario,} =require("../controller/userController");//mapeamento das funções do controller para seus respectivos métodos HTTP e rotasroutes.get("/usuarios", recuperarUsuarios);routes.post("/usuario", adicionarUsuario);routes.get("/usuario/:id", findById); //notação :id indica que o parametro id sera informado na url da requisiçãoroutes.delete("/usuario/:id", excluiUsuario); //notação :id indica que o parametro id sera informado na url da requisiçãoroutes.put("/usuario/:id", atulizarUsuario); //notação :id indica que o parametro id sera informado na url da requisição//exportamos o objeto routesmodule.exports= routes;
Configurando as rotas no aplicativo principal
Para concluir esta seção do guia, vamos integrar as rotas que definimos no router ao servidor, tornando-as finalmente acessíveis para uso por aplicativos externos.
No arquivo server.js, realize a importação do objeto routes e, em seguida, passe esse objeto como argumento para a função app.use(). Isso permitirá a integração das rotas definidas no arquivo de rotas com o aplicativo principal.
constexpress=require("express");constcors=require("cors");constmorgan=require("morgan");constbd=require("./bd");//importamos as rotas do usuárioconstuserRoutes=require("./router/userRouter");constapp=express(); //inicia o expressapp.use(morgan("dev")); //imprime os logs das operações durante o desenvolvimentoapp.use(cors()); //libera o cross originapp.use(express.json()); // configura a API para receber e enviar json.app.use(userRoutes); // torna as rotas disponíveis para uso//rota padrão na qual podemos verificar se o servidor está disponívelapp.get("/",async (req, res) => {res.status(200).json({ msg:"Api ok!" });});//inicia o servidor através de uma self-invoked function(async () => {try {awaitbd.sync({ force:false }); // sincroniza o bancoconsole.log("Banco conectado!");app.listen(3000, () =>console.log("Service is live!")); //iniciar efetivamente o servidor na porta 3000 } catch (err) {console.log(err); }})();
Agora execute novamente a aplicação através do comando npm start . Observe que o sequelize se encarregara da criação da tabela usuarios no banco.
Neste momento a API já está pronta para receber requisições.