APIs com NodeJS

APIs com NodeJS

Você sabe o que é uma API? Faz ideia do que ela seja? Já criou uma API com NodeJS alguma vez na sua vida?

Fique tranquilo se a sua resposta for NÃO para tudo, pois aqui nesta lição, o meu principal objetivo é te dar todas as respostas para as principais perguntas relacionadas a construção de APIs com NodeJS 😉

Se você está comigo desde o início dessa jornada, já deve ter notado que até o momento, nós já temos a capacidade de criar nossas próprias aplicações com NodeJS 🥳

Aplicações estas que misturam back-end com front-end em um mesmo projeto, só que nem sempre toda aplicação segue o padrão monolítico.

Em pleno ano de 2024, mais e mais desenvolvedores vem adotando padrões modulares, em que as aplicações front-end estão totalmente separadas do back-end.

E quando eu digo "totalmente", eu digo totalmente mesmo 😂

Onde tudo o que é relacionado ao back-end, não só ficam em pastas separadas, como também em servidores separados. A mesma lógica pode ser aplicada com o front-end também.

E se você um dia sonha (ou já atua) como desenvolvedor back-end, é de vital importância que você saiba como construir APIs 👻

Nesta lição, nós vamos aprender a construir APIs usando a biblioteca Express, portanto, eu espero que você já tenha dado uma passada por lá também rs

O que é uma API?

Uma API (Application Programming Interface), cujo acrônico é Interface de Programação de Aplicações, nada mais é do que uma aplicação que conta com um conjunto de regras e definições que permitem que diferentes sistemas ou aplicativos se comuniquem entre si.

A ideia de uma API, é abstrair a complexidade interna de um software, permitindo que outras aplicações acessem suas funcionalidades sem precisar conhecer detalhes da sua implementação.

Um grande exemplo de APIs, são aplicativos de clima que retornam informações meteorológicas de um serviço remoto, ou um sistema de pagamento responsável por processar transações por meio de um gateway de pagamento.

Ainda não entendeu? Então vamos imaginar um exemplo mais prático 😋

Vamos imaginar que você tem um grande e-commerce de produtos eletrônicos, produtos estes que a maioria das pessoas não encontram em lugar algum, uma vez que é você quem os fabrica rs

Supondo que o seu e-commerce foi feito com as tecnologias: NodeJS + Express + MySQL, e que as informações dos seus produtos estão salvas em seu banco de dados relacional (MySQL).

É certo dizer que a sua aplicação é a única fonte que detém informações originais e atualizadas dos seus produtos.

Vamos supor que a maioria dos seus produtos estourou no mercado de tecnologia, e com isso, você acabou recebendo diversas propostas de outros e-commerces e lojas virtuais, que se sentiram interessados em revender tais produtos.

Para que essa revenda seja possível, você vai precisar encontrar alguma forma de repassar as informações dos seus produtos para os outros e-commerces. E pensando em resolver essa questão de uma maneira rápida, você tem algumas opções:

  • Você pode colocar os dados em uma planilha, e continuar enviando a mesma planilha com dados atualizados mensalmente aos revendedores.
  • Ou você também pode criar um painel administrativo onde só os revendedores poderão ter acesso às informações dos produtos que você vende.

Ou.... como última alternativa, você também pode criar uma pequena aplicação que vai ficar responsável por distribuir as informações dos seus produtos que estão salvos em seu banco de dados, de modo a retornar essa resposta de volta ao cliente em um formato JSON.

Ou seja, uma aplicação que não tem nenhuma interface de usuário (UI), onde foca exclusivamente no envio de informações que estão salvas em seu banco de dados.

Conseguiu imaginar esse sistema? Então... isso nada mais é do que uma API.

É claro que uma API vai muito além da consulta de dados, como também dá a possibilidade para que seus usuários possam de criar, atualizar ou até mesmo deletar registros.

Em termos práticos, as APIs são usadas para integrar sistemas, permitindo a troca de dados e a execução de operações entre diferentes softwares ou plataformas.

Pois dessa forma, um sistema feito em Python pode se comunicar diretamente com um sistema feito em NodeJS, ou quem sabe PHP, C#, Java.... tudo por meio de APIs que se comunicam via requisições HTTP por meio dos formatos JSON/XML.

Os diferentes tipos de APIs

No mundo das APIs, nós podemos ter diferentes classificações com base nas funcionalidades e protocolos de cada uma delas.

Começando pela mais famosa de todas, nós temos as APIs baseadas em protocolos da web (HTTP), onde são amplamente usadas para conectar serviços online e aplicativos.

Dentre elas podemos destacar:

API REST (Representational State Transfer): é baseada em princípios arquitetônicos simples, onde usa métodos como GET, POST, PUT, DELETE para realizar suas operações, trabalhando com dados nos formatos JSON ou XML.

API SOAP (Simple Object Access Protocol): é um protocolo mais estruturado que utiliza XML para troca de informações, e que pode operar sobre diferentes protocolos, como HTTP ou SMTP, sendo usada principalmente em serviços que exigem alta segurança e confiabilidade.

API GraphQL: permite que o cliente especifique exatamente quais dados deseja receber, o que evita o recebimento de dados desnecessários, sendo ideal para otimização de consultas em aplicações com grande volume de dados.

API de WebSocket: permite comunicação bidirecional e em tempo real entre cliente e servidor, sendo amplamente usada em aplicações que requerem atualizações dinâmicas, como sistemas de chat ou de negociação de ativos.

Em seguida, nós temos as APIs locais, que operam dentro de um sistema ou entre componentes locais de um aplicativo, como por exemplo:

  • APIs de Bibliotecas ou Frameworks
  • APIs de Sistema Operacional
  • APIs de Hardware
  • APIs de banco de dados (já usamos ela com mysql2)
  • APIs de pagamentos

É importante ressaltar que o foco desta lição será voltado para o entendimento das APIs do tipo REST, ok? 😉

Mergulhando em APIs Rest e RestFull

Como você já sabe, uma API REST segue um conjunto de princípios arquiteturais que definem como os recursos devem ser acessados e manipulados em uma aplicação web.

Ela se baseia no protocolo HTTP e utiliza métodos padrão como GET, POST, PUT, DELETE, entre outros, sempre buscando responder em formatos JSON ou XML.

  • GET: Para buscar ou recuperar dados.
  • POST: Para criar novos recursos.
  • PUT ou PATCH: Para atualizar um recurso existente.
  • DELETE: Para excluir um recurso.

Lembrando que em uma API Rest, cada requisição funciona de forma independente (stateless = sem estado), ou seja, o servidor não armazena informações sobre o cliente entre as requisições.

Agora, para que que uma API seja considerada RestFull, ela precisa seguir todos os princípios de uma API Rest, e mais algumas outras coisas como:

Stateless: o servidor não deve armazenar o estado das requisições do cliente. Cada solicitação deve conter todas as informações necessárias para ser processada.

Cacheabilidade: as respostas das requisições podem ser armazenadas em cache (quando aplicável), para melhorar o desempenho e reduzir a carga no servidor.

Interface uniforme: a API deve fornecer uma interface uniforme e consistente, onde as mesmas operações (como GET, POST, etc.) são utilizadas da mesma forma em todos os recursos.

Desacoplamento cliente-servidor: o cliente e o servidor devem ser completamente independentes. O cliente deve ser capaz de evoluir sem depender de mudanças no servidor, e vice-versa.

Representações de recursos: um recurso pode ser representado de várias formas, como JSON ou XML, e o cliente pode solicitar diferentes representações de um recurso usando cabeçalhos HTTP como Accept.

Consegue perceber que uma API RestFull atua de forma um pouco mais completa que a uma API Rest? 🙂

Observação: a construção de uma API não se limita a nível da linguagem, uma vez que ele é um princípio de arquitetural.

Conhecendo um pouco mais sobre os verbos HTTP

Antes de colocarmos a mão na massa, é deveras importante que você recapitule o funcionamento de alguns verbos HTTP que são usados durante uma requisição na web.

Quando criamos uma API para a web (como será o nosso caso), os verbos HTTP andam junto com nossas APIs, é isso é uma coisa bem óbvia, pois serão por meio deles que essa comunicação acontecerá rs

GET:

  • Utilizado para recuperar dados de um recurso (por exemplo: buscar uma lista de usuários ou os detalhes de um produto).
  • Não modifica o recurso, apenas faz a sua leitura.

POST:

  • Utilizado para criar um novo recurso (por exemplo: enviar os dados de um formulário para criar um novo usuário ou um novo pedido).
  • Envia dados ao servidor para serem processados.

PUT:

  • Utilizado para atualizar completamente um recurso existente (por exemplo, atualiza todos os dados de um usuário com base em uma nova requisição).
  • Substitui os dados antigos pelos novos.

PATCH

  • Utilizado para atualizar parcialmente um recurso existente (por exemplo, atualiza apenas o e-mail de um usuário sem alterar os outros campos).
  • Ao contrário do PUT, ele é usado para realizar modificações parciais.

DELETE:

  • Utilizado para remover um recurso (por exemplo, excluir um usuário de um banco de dados ou remover um arquivo).

HEAD:

  • Semelhante ao GET, mas apenas recupera os cabeçalhos da resposta, sem o corpo (por exemplo, verificar se um recurso está disponível sem baixar o conteúdo).

OPTIONS:

  • Utilizado para descrever as opções de comunicação com um recurso (por exemplo, usado para saber quais métodos HTTP são suportados por um endpoint específico)

CONNECT:

  • Estabelece um túnel de comunicação entre o cliente e o servidor (por exemplo, quando temos conexões seguras (via TLS) em proxies HTTP).

TRACE:

  • Utilizado para realizar um teste de loop-back, retornando a requisição como foi recebida pelo servidor (por exemplo, depurar o caminho que uma requisição percorre).

Para que uma API seja considerada RestFull, ela precisa implementar todos esses verbos?

Não necessariamente, pois o que define uma API RESTful são os princípios arquiteturais que ela segue, e não a obrigatoriedade de usar todos os verbos HTTP.

Até porque, o uso de métodos como GET, POST, PUT e DELETE já são mais do que o suficiente para que uma API seja considerada RestFull.

Criando sua primeira API com NodeJS + Express

Pronto para criar sua primeira API com NodeJS + a biblioteca do Express?

Então vamos lá!

Criando o projeto principal

Vamos começar criando uma pasta do nosso projeto, no meu caso, eu criei uma pasta chamada de API dentro da minha área de trabalho (desktop):  

Com o seu terminal (Prompt de Comando) aberto na pasta raiz que acabamos de criar, precisamos inicializar o nosso projeto por meio do NPM, sendo assim, execute o seguinte comando abaixo:

npm init -y

A flag -y, como você já deve saber, cria um novo projeto de forma enxuta, respondendo SIM para tudo 😅

Em seguida, vamos instalar a biblioteca do express, pois iremos precisar dela para criar as rotas da nossa API:

npm install express

Por fim, não se esqueça de criar seu index.js, com uma mensagem bem legal de boas vindas:

console.log('Olá Mundo!');

Criando as rotas e a lógica da nossa API

Se você acha que o processo de criação de uma API é algo super complexo e trabalhoso, você está muito enganado, pois o CORAÇÃO de uma API gira em torno da lógica abaixo (que pode ser implementada totalmente dentro do seu index.js):

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

app.use(
    express.urlencoded({
        extended: true
    }),
);

app.use(express.json());

//Rotas Endpoints

app.get('/', (req, res) => {
    res.status(200).json({ message: 'Seja bem vindos a API da Micilini, consulte a documentação para acessar mais detalhes 🥳' });
});

app.post('/newUser', (req, res) => {
    const { name, email, password } = req.body;
    console.log(name, email, password);
    res.status(201).json({ message: `Usuário ${name} criado com sucesso!` });
});

app.put('/updateUser/:idUser', (req,
    res) => {

    //Exemplo de status de erro caso idUser não seja informado
    if (!req.params.idUser) {
        res.status(422).json({ message: 'Informe o id do usuário que deseja atualizar' });
        return;
    }

    const { idUser } = req.params;
    const { name, email, password } = req.body;
    console.log(idUser, name, email, password);
    res.status(200).json({ message: `Usuário ${name} atualizado com sucesso!` });
});

app.delete('/removeUser/:idUser', (req, res) => {
    const { idUser } = req.params;
    console.log(idUser);
    res.status(201).json({ message: `Usuário ${idUser} removido com sucesso!` });
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

Tudo o que você já viu em lições anteriores, como é o caso da lição do express, está representado alí dentro do index.js.

A única diferença é que em vez de carregar uma view, nós estamos retornando a resposta em formato JSON em conjunto com um status da requisição.

Note também que implementamos a grande maioria dos verbos HTTP como GET, POST, PUT e DELETE.

Tudo o que vai além disso, são implementações arquiteturais, comunicação com bancos de dados, e lógicas internas. Ou seja, tudo o que você já viu em lições anteriores 😉

Criando uma API Robusta com Express

Anteriormente, você aprendeu a criar uma API de forma bem simples utilizando o express, e apesar dela funcionar muito bem, no mundo real, uma API costuma fazer outras implementações a mais, como:

  • Seguir um padrão de arquitetura,
  • Implementar conexões com bancos de dados,
  • Organizar a lógica de cada arquivo dentro de suas respectivas pastas,
  • e etc...

Pensando nisso, agora eu vou te ensinar a criar uma API com Express seguindo um padrão de arquitetura inspirado no MVC, que será capaz de se comunicar com um banco de dados feito com MySQL 😁

Criando o projeto inicial

Vamos começar criando uma pasta do nosso projeto, no meu caso, eu criei uma pasta chamada de API-DRAGONS dentro da minha área de trabalho (desktop):    

Com o seu terminal (Prompt de Comando) aberto na pasta raiz que acabamos de criar, precisamos inicializar o nosso projeto por meio do NPM, sendo assim, execute o seguinte comando abaixo:

npm init -y

A flag -y, como você já deve saber, cria um novo projeto de forma enxuta, respondendo SIM para tudo 😅

Em seguida, vamos instalar todas as bibliotecas necessárias para o bom funcionamento do nosso projeto:

npm install bcrypt cookie-parser cors express jsonwebtoken mysql2 sequelize multer dotenv

bcrypt: utilizada para hashing de senhas, ou seja, criptografa senhas antes de armazená-las no banco de dados, oferecendo maior segurança.

cookie-parser: middleware para Express que faz o parsing de cookies, facilitando a leitura e manipulação de cookies enviados nas requisições HTTP.

cors: middleware para habilitar o Cross-Origin Resource Sharing (CORS), permitindo que aplicações em diferentes domínios acessem sua API de forma segura.

express: um framework leve para NodeJS que facilita a criação de servidores web e APIs, com uma sintaxe simples e extensível.

jsonwebtoken: usada para criar e verificar tokens JWT (JSON Web Tokens), comumente utilizados para autenticação de usuários em aplicações web.

mysql2: uma biblioteca para conectar aplicações NodeJS ao banco de dados MySQL, oferecendo suporte para promessas e compatibilidade com outras bibliotecas, como Sequelize.

sequelize: um ORM (Object-Relational Mapping) para NodeJS que facilita o uso de bancos de dados SQL (como MySQL e PostgreSQL), permitindo interagir com tabelas usando objetos em JavaScript.

multer: middleware para gerenciar upload de arquivos em aplicações feitas com NodeJS, se tornando útil para processar arquivos recebidos em requisições HTTP multipart/form-data.

dotenv: é uma biblioteca usada em projetos NodeJS para carregar variáveis de ambiente a partir de um arquivo .env para o process.env.

Após a instalação das bibliotecas, como iremos nos inspirar na estrutura MVC, o ideal é que criemos todas as pastas necessárias como:

  • controllers
  • models
  • db (arquivos de conexão com o banco de dados)
  • helpers (funções de ajuda e apoio às nossas classes)
  • files (usada para armazenamento de imagens e outros arquivos)
  • routes (usada para salvar nossos arquivos de rotas)
  • middleware (usada para salvar os middlewares da nossa aplicação)

Em seguida, como estamos trabalhando com a biblioteca dotenv, é certo dizer que precisamos trabalhar com o arquivo .env, sendo assim, não se esqueça de criá-lo na pasta raiz do seu projeto 🤓

O arquivo .env deve conter as seguintes chaves:

DB_NAME=nodejs
DB_USER=root
DB_PASS=SUASENHA
DB_HOST=localhost
DB_DIALECT=mysql
ACCESS_SECRET=MICILINIACCESSSECRET
REFRESH_SECRET=MICILINIREFRESHSECRET

Observação: não se esqueça de inserir os valores corretos de conexão com o seu banco de dados. As chaves ACCESS_SECRET e REFRESH_SECRET fazem parte da autenticação de tokens JWT, e que serão usados no decorrer deste projeto.

Para saber mais sobre a autenticação que iremos implementar aqui, não deixe de dar uma olhada neste artigo.

Feito isso, não se esqueça de criar o seu index.js na sua pasta raiz, ele pode ser vazio mesmo, pois no próximo tópico já iremos trabalhar com ele 😉

Criando seus models

Dentro da pasta models, vamos precisar criar nossos arquivos que representam as tabelas do nosso banco de dados, no meu caso, eu criei dois arquivos chamados de Usuario.js e Dragoes.js.

models > Usuario.js:

const { Sequelize, DataTypes } = require('sequelize');
const sequelize = require('../db/connection');

const Usuario = sequelize.define('Usuario', {
    id_usuario: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true
    },
    nome_usuario: {
        type: DataTypes.STRING,
        allowNull: false
    },
    email_usuario: {
        type: DataTypes.STRING,
        allowNull: false
    },
    senha_usuario: {
        type: DataTypes.STRING,
        allowNull: false
    },
    imagem_usuario: {
        type: DataTypes.STRING,
        allowNull: true
    },
    whatsapp_usuario: {
        type: DataTypes.STRING,
        allowNull: false
    }
}, {
    timestamps: true
});

module.exports = Usuario;

models > Dragoes.js:

const { Sequelize, DataTypes } = require('sequelize');
const sequelize = require('../db/connection');
const Usuario = require('./Usuario'); // Relacionamento com o model Usuario

const Dragao = sequelize.define('Dragao', {
    id_dragao: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true
    },
    nome_dragao: {
        type: DataTypes.STRING,
        allowNull: false
    },
    peso_dragao: {
        type: DataTypes.FLOAT, // Alterado de NUMBER para FLOAT
        allowNull: false
    },
    idade_dragao: {
        type: DataTypes.INTEGER,
        allowNull: false
    },
    cor_dragao: {
        type: DataTypes.STRING,
        allowNull: false
    },
    imagens_dragao: {
        type: DataTypes.TEXT, // Armazenar array como JSON
        allowNull: false,
        get() {
            const value = this.getDataValue('imagens_dragao');
            return value ? JSON.parse(value) : [];
        },
        set(value) {
            this.setDataValue('imagens_dragao', JSON.stringify(value));
        }
    },
    existe_dragao: {
        type: DataTypes.BOOLEAN,
        defaultValue: true
    }
}, {
    timestamps: true
});

// Relacionamento: Dragão pertence a um Usuário
Dragao.belongsTo(Usuario, { foreignKey: 'id_usuario' });

module.exports = Dragao;

Todos os conceitos presentes em ambos os códigos acima, já foram discutidos na lição de Sequelize.

Criando o arquivo de conexão com o banco de dados

Dentro da pasta db, nós vamos criar um novo arquivo chamado de connection.js, que vai representar nosso arquivo de conexão com o banco de dados.

db > connection.js:

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

const { Sequelize } = require('sequelize');// Carrega a biblioteca do Sequelize

//Realiza as configurações do Sequelize com as variáveis de ambiente (.env)
const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
    host: process.env.DB_HOST,
    dialect: process.env.DB_DIALECT
});

module.exports = sequelize;

Todos os conceitos apresentados no código acima, já foram discutidos na lição de Sequelize.

Criando o ponto de entrada da nossa aplicação

Com nossas "tabelas" e o arquivo de comunicação com o banco de dados já criados.

Chegou o momento de criarmos a lógica do ponto de entrada da nossa aplicação, ou seja, configurar o express, cors e o cookie-parser dentro do nosso index.js:

const express = require('express');
const cors = require('cors');
const sequelize = require('./db/connection'); // Importa a conexão do Sequelize
const cookieParser = require('cookie-parser'); // Importa o cookie-parser

//Inicializa o express
const app = express();

//Configurações da resposta em JSON
app.use(express.json());

//Configurações do Parser de Coookies
app.use(cookieParser());

//Configuração do CORS
app.use(cors({ credentials: true, origin: 'http://localhost:3000' }));//Soluciona os erros de CORS na tentativa de acessarem a API estando no mesmo domínio

//Configuração e importação dos arquivos de rotas
app.use('/', require('./routes/GeralRotas'));
app.use('/usuarios', require('./routes/UsuarioRotas'));
app.use('/dragao', require('./routes/DragaoRotas'));

// Sincroniza o Sequelize com o banco de dados (cria as tabelas caso não existam)
sequelize.sync()
  .then(() => {
    console.log('Banco de dados sincronizado');
    
    // Configuração da porta do servidor
    app.listen(3333, () => {
        console.log('Servidor rodando em http://localhost:3333');
    });
  })
  .catch((error) => {
    console.error('Erro ao sincronizar o banco de dados:', error);
  });

Todos os conceitos apresentados no código acima, já foram discutidos nas seguintes lições:

Criando nosso arquivos de rotas

Em seguida, precisamos criar 3 novos arquivos dentro da pasta routes, que são:

routes > GeralRotas.js:

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

router.get('/', (req, res) => {
    res.send('Seja bem vindo a API de Dragões 🐉. Consulte a documentação para aprender a utilizar a API ️‍🔥');
});

module.exports = router;

routes > UsuarioRotas.js:

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

//Importa o Helper capaz de tratar imagens
const { imageUpload } = require('../helpers/uploadImage');

//Importa os controladores que serão usados
const UsuarioController = require('../controllers/UsuarioController');

router.post('/registrar', imageUpload.single("imagem"), UsuarioController.registrar);
router.post('/login', UsuarioController.login);
router.get('/authUser', UsuarioController.authUser);
router.post('/refreshToken', UsuarioController.refreshToken);
router.post('/logout', UsuarioController.logout);

module.exports = router;

routes > DragaoRotas.js:

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

//Importa o Helper capaz de tratar imagens
const { imageUpload } = require('../helpers/uploadImage');

//Importa os middlewares que serão usados
const authMiddleware = require('../middleware/authMiddleware');

//Importa os controladores que serão usados
const DragaoController = require('../controllers/DragaoController');

// Aplica o middleware para todas as rotas dentro de /dragao
router.use(authMiddleware);

//Restante das rotas de /dragao
router.post('/criar', imageUpload.array('imagens'), DragaoController.criar);
router.get('/retornaTodos', DragaoController.retornaTodos);
router.get('/retornaPorId/:id', DragaoController.retornaPorId);
router.put('/atualizar/:id', imageUpload.array('imagens'), DragaoController.atualizar);
router.delete('/deletar/:id', DragaoController.deletar);

module.exports = router;

Observe que as rotas de Dragao estão protegidas por um middleware chamado de authMiddlesware.

Todos os conceitos apresentados no código acima, já foram discutidos nas seguintes lições:

Criando nossos controllers

Dentro da pasta controllers, vamos precisar criar dois controladores, um para a rota de Usuario e outro para a rota de Dragao.

controllers > UsuarioController.js:

const Usuario = require('../models/Usuario');
const bcrypt = require('bcrypt');

//Importa as funções necessárias para gerar os tokens JWT
const { generateAccessToken, generateRefreshToken, verifyAccessToken, verifyRefreshToken } = require('../helpers/manageTokens');

class UsuarioController {
    //Função para registrar novos usuários
    static async registrar(req, res) {
        try {
            const { nome, email, senha, imagem, whatsapp } = req.body;

            //Valida se todos os campos acima (que são obrigatórios) vieram na requisição, caso não, retorna um erro
            if (!nome || !email || !senha || !whatsapp) {
                return res.status(422).json({ mensagem: 'Os campos nome, email, senha e whatsapp são obrigatórios!' });
            }

            //Validação de imagens
            let image = null;

            if (req.file) {
                image = req.file.filename;//O middleware que configura o upload da imagem (declarado no arquivo de rotas) salva a imagem no req.file.filename
            }

            // Verifica se o email já existe no banco de dados
            const emailExiste = await UsuarioController.#verificarEmailExistente(email);
            if (emailExiste) {
                return res.status(409).json({ mensagem: 'Este email já está registrado!' });
            }

            //Fortifica a senha usando SALT + BCRYPT
            const salt = await bcrypt.genSalt(12);
            const passwordHash = await bcrypt.hash(senha, salt);

            //Salva o usuário no banco de dados

            const usuario = await Usuario.create({
                nome_usuario: nome,
                email_usuario: email,
                senha_usuario: passwordHash,
                imagem_usuario: image,
                whatsapp_usuario: whatsapp
            });

            return res.status(201).json(usuario);
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
    // Função privada para verificar se o email já existe (a # indica ao JS que é privada)
    static async #verificarEmailExistente(email) {
        const usuario = await Usuario.findOne({ where: { email_usuario: email } });
        return usuario !== null; // Retorna true se o email já estiver registrado
    }
    //Função de login
    static async login(req, res) {
        try {
            const { email, senha } = req.body;

            //Valida se todos os campos acima (que são obrigatórios) vieram na requisição, caso não, retorna um erro
            if (!email || !senha) {
                return res.status(422).json({ mensagem: 'Os campos email e senha são obrigatórios!' });
            }

            //Verifica se o email existe no banco de dados
            const usuario = await Usuario.findOne({ where: { email_usuario: email } });
            if (!usuario) {
                return res.status(401).json({ mensagem: 'Email ou senha incorretos!' });
            }

            //Verifica se a senha está correta
            const senhaCorreta = await bcrypt.compare(senha, usuario.senha_usuario);
            if (!senhaCorreta) {
                return res.status(401).json({ mensagem: 'Email ou senha incorretos!' });
            }

            //Gera os tokens JWT
            const accessToken = generateAccessToken(usuario.id_usuario, 60 * 15); // 15 minutos
            const refreshToken = generateRefreshToken(usuario.id_usuario);

            //Salva o RefreshToken em um cookie
            res.cookie('refreshToken', refreshToken, {
                expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // expira em 30 dias
                path: '/',
                secure: true, // apenas HTTPS
                httpOnly: true, // acessível apenas via HTTP
                sameSite: 'None' // ajuste conforme necessário
            });

            //Retorna somente o AccessToken
            return res.status(200).json({ accessToken });
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
    //Função de autenticação do usuário
    static async authUser(req, res) {
        try {
            //Pega o token de autorização da requisição
            const authHeader = req.headers['authorization'];//Não se esqueça de enviar no Header da Requisição o Authorization com o valor 'Bearer + AccessToken'
            const accessToken = authHeader && authHeader.split(' ')[1];

            //Verifica se o token é válido
            const user = verifyAccessToken(accessToken);
            if (!user) {
                return res.status(401).json({ mensagem: 'Token inválido!' });
            }

            //Retorna os dados do usuário
            const usuario = await Usuario.findByPk(user.user_id, { attributes: { exclude: ['senha_usuario'] } });
            return res.status(200).json(usuario);
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
    //Função para atualizar o AccessToken por meio do refreshToken
    static async refreshToken(req, res) {
        try {
            //Pega o RefreshToken do cookie
            const refreshToken = req.cookies.refreshToken;
            console.log(refreshToken);

            //Verifica se o RefreshToken é válido
            const user = verifyRefreshToken(refreshToken);
            console.log(user);
            if (!user) {
                return res.status(401).json({ mensagem: 'Token inválido!' });
            }

            //Gera um novo AccessToken
            const accessToken = generateAccessToken(user.user_id, 60 * 15); // 15 minutos

            //Retorna os dados do usuário
            const usuario = await Usuario.findByPk(user.user_id, { attributes: { exclude: ['senha_usuario'] } });
            return res.status(200).json({ accessToken, usuario });
        } catch (error) {
            console.log(error);
            return res.status(500).json({ error: error.message });
        }
    }
    //Função para fazer logout
    static async logout(req, res) {
        try {
            //Limpa o cookie do refreshToken
            res.clearCookie('refreshToken');

            return res.status(200).json({ mensagem: 'Logout realizado com sucesso!' });
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
}

module.exports = UsuarioController;

controllers > DragaoController.js:

const Dragao = require('../models/Dragoes');

class DragaoController {
    //Função para retornar todos os dragoes
    static async retornaTodos(req, res) {
        try {
            const dragoes = await Dragao.findAll();
            return res.status(200).json(dragoes);
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
    //Função para criar um dragao
    static async criar(req, res) {
        try {
            const { nome, peso, idade, cor, existe, imagens } = req.body;

            //Validar para ver se todos os campos vieram
            if (!nome || !peso || !idade || !cor || !existe) {
                return res.status(400).json({ error: 'Os campos nome, peso, idade, cor e existe são obrigatórios!' });
            }

            //Faz a validação de imagens
            let images = [];

            if (req.files) {
                images = req.files.map(file => file.filename);
            }

            //Criar um dragao
            const dragao = await Dragao.create({
                nome_dragao: nome,
                peso_dragao: peso,
                idade_dragao: idade,
                cor_dragao: cor,
                imagens_dragao: images,
                existe_dragao: existe,
            });
            return res.status(201).json(dragao);
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
    //Função para retornar um dragao por :id
    static async retornaPorId(req, res) {
        try {
            const { id } = req.params;
            const dragao = await Dragao.findByPk(id);
            if (!dragao) {
                return res.status(404).json({ error: 'Dragão não encontrado!' });
            }
            return res.status(200).json(dragao);
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
    //Função para atualizar um dragao por :id
    static async atualizar(req, res) {
        try {
            const { id } = req.params;
            const { nome, peso, idade, cor, existe, imagens } = req.body;

            //Validar para ver se todos os campos vieram
            if (!nome || !peso || !idade || !cor || !existe) {
                return res.status(400).json({ error: 'Os campos nome, peso, idade, cor e existe são obrigatórios!' });
            }

            //Faz a validação de imagens
            let images = [];

            if (req.files) {
                images = req.files.map(file => file.filename);
            }

            //Atualizar um dragao
            const dragao = await Dragao.findByPk(id);
            if (!dragao) {
                return res.status(404).json({ error: 'Dragão não encontrado!' });
            }
            await dragao.update({
                nome_dragao: nome,
                peso_dragao: peso,
                idade_dragao: idade,
                cor_dragao: cor,
                imagens_dragao: images,
                existe_dragao: existe,
            });
            return res.status(200).json(dragao);
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
    //Função para deletar um dragao por :id
    static async deletar(req, res) {
        try {
            const { id } = req.params;
            const dragao = await Dragao.findByPk(id);
            if (!dragao) {
                return res.status(404).json({ error: 'Dragão não encontrado!' });
            }
            await dragao.destroy();
            return res.status(204).end();
        } catch (error) {
            return res.status(500).json({ error: error.message });
        }
    }
}

module.exports = DragaoController;

Obeserve que é dentro dos controllers que a mágica acontece (nossas chamadas das consultas com o banco de dados), além disso, estamos chamando alguns métodos da classe manageTokens para validar nossos tokens JWT.

Todos os conceitos apresentados no código acima, já foram discutidos nas seguintes lições:

Criando nosso middleware de autenticação

Dentro da pasta middleware, nós iremos criar uma função responsável por verificar se o token refreshToken é válido, o que vai nos possibilitar verificar se o usuário pode acessar ou não as nossas rotas de Dragao.

middleware > authMiddleware.js:

//Importa as funções necessárias para gerar os tokens JWT
const { verifyRefreshToken } = require('../helpers/manageTokens');

//Middleware para verificar se o token de atualização é válido
async function authMiddleware(req, res, next) {
    // Pega o refreshToken dos cookies
    const refreshToken = req.cookies.refreshToken;
    console.log('RefreshToken:', refreshToken);

    if (!refreshToken) {
        return res.status(401).json({ mensagem: 'RefreshToken não fornecido!' });
    }

    try {
        // Verifica se o refreshToken é válido
        const decoded = verifyRefreshToken(refreshToken);

        if(!decoded){
            return res.status(403).json({ mensagem: 'RefreshToken inválido ou expirado!' });
        }

        // Chama o próximo middleware ou controlador
        next();
    } catch (error) {
        return res.status(403).json({ mensagem: 'RefreshToken inválido ou expirado!' });
    }

}

module.exports = authMiddleware;

Os conceitos apresentados neste código, já foram vistos na lição de Tokens JWT com NodeJS.

Criando nossos helpers

Dentro da pasta helpers, vamos criar dois arquivos que servirão de apoio a autenticação de tokens JWT e upload de imagens com a biblioteca multer.

helpers > manageTokens.js:

const jwt = require('jsonwebtoken');

// Definir segredos para assinar os tokens JWT
const ACCESS_SECRET = process.env.ACCESS_SECRET;
const REFRESH_SECRET = process.env.REFRESH_SECRET;

// Função para gerar AccessToken
function generateAccessToken(userID, expiresIn) {
    const accessTokenPayload = {
        user_id: userID,
        exp: Math.floor(Date.now() / 1000) + expiresIn // Expiração em segundos
    };

    // Gera e retorna o AccessToken
    return jwt.sign(accessTokenPayload, ACCESS_SECRET, { algorithm: 'HS256' });
}

// Função para gerar RefreshToken
function generateRefreshToken(userID) {
    const expirationTime = Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60); // 30 dias

    const refreshTokenPayload = {
        user_id: userID,
        exp: expirationTime
    };

    // Gera e retorna o RefreshToken
    return jwt.sign(refreshTokenPayload, REFRESH_SECRET, { algorithm: 'HS256' });
}

//Função para verificar se o AccessToken é válido
function verifyAccessToken(accessToken) {
    try {
        return jwt.verify(accessToken, ACCESS_SECRET, { algorithm: 'HS256' });
    } catch (error) {
        return null;
    }
}

//Função para verificar se o RefreshToken é válido
function verifyRefreshToken(refreshToken) {
    try {
        return jwt.verify(refreshToken, REFRESH_SECRET, { algorithm: 'HS256' });
    } catch (error) {
        console.log('Token verification error:', error.message); // Adicione este log
        return null;
    }
}

module.exports = {
    generateAccessToken,
    generateRefreshToken,
    verifyAccessToken,
    verifyRefreshToken
};

helpers > uploadImage.js:

const multer = require('multer');
const path = require('path');

//Destino para onde as imagens serão salvas
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        let folder = "files";//Pasta onde as imagens serão salvas
        cb(null, path.resolve(__dirname, '..', folder));//Caminho para a pasta, exemplo: /home/usuario/Documentos/Projeto/files
    },
    filename: function (req, file, cb) {
        cb(null, Date.now() + path.extname(file.originalname));//Nome do arquivo, exemplo: 123456789.jpg
    }
});

const imageUpload = multer({
    storage: storage,
    limits: { fileSize: 1000000 },//Tamanho máximo do arquivo em bytes
    fileFilter: function (req, file, cb) {
        if(!file.originalname.match(/\.(jpg|jpeg|png)$/)){//Extensões permitidas
            return cb(new Error('Por favor, envie apenas imagens!'));
        }
    cb(undefined, true);
    }
});//Nome do campo que será enviado no formulário

module.exports = { imageUpload };

Todos os conceitos apresentados no código acima, já foram discutidos nas seguintes lições:

Executando nossa API

Para executar a API que acabamos de criar, basta executar o seguinte código no seu terminal (Prompt de Comando) dentro da pasta raiz do seu projeto:

node ./index.js

Isso fará com que o NodeJS suba a sua API para localhost na porta 3333, ao mesmo tempo que suas tabelas do banco de dados serão criadas automaticamente (caso elas ainda não existam).

E pronto, a sua API está em ambiente local e pronta para uso 🥳

Testando nossa API com Postman

Para testar nossa API, nós podemos fazer o uso do Postman 🙃

Ele é uma ferramenta popular e muito prática para fazer requisições HTTP, permitindo simular e testar APIs de maneira eficiente.

Com ele, é possível enviar diferentes tipos de requisições, como GET, POST, PUT, DELETE, e visualizar as respostas em tempo real, facilitando a depuração e desenvolvimento de APIs.

Aqui na Micilini, nós já temos um artigo introdutório que vai te ensinar a instalar e usar o Postman, vale muito a pena conferir 😌

Com o Postman aberto, vamos realizar a nossa primeira chamada para a rota geral (http://localhost:3333/):

Como você mesmo viu, a rota geral é uma requisição do tipo GET que retorna uma mensagem de boas vindas:

Seja bem vindo a API de Dragões 🐉. Consulte a documentação para aprender a utilizar a API ️‍🔥

Como o restante dos endpoints dependem exclusivamente que você esteja logado (autenticado) na aplicação, precisamos criar o nosso primeiro usuário, e para isso, precisamos chamar a rota /usuarios/registrar (http://localhost:3333/usuarios/registrar):

Note que a rota em questão é do tipo POST, e que precisamos enviar algumas informações dentro do body (form-data).

Se você abrir o seu banco de dados, verá que um novo registro acaba de ser inserido:

O que indica que o INSERT foi um sucesso 🥳

Note também, que dentro da pasta files foi criado um arquivo (imagem) que representa o logo da micilini que foi enviado durante a requisição 😍

Com o seu usuário e senha criados, chegou o momento de realizarmos o login na plataforma (http://localhost:3333/usuarios/login)

Se as suas credências estiverem corretas, você receberá um accessToken, que deverá ser salvo na MEMÓRIA da sua aplicação Front-End (ou em uma session).

Observação: como dito neste artigo, o accessToken deve ser armazenado na memoria da aplicação, ao contrario do refreshToken que é armazenado em um cookie.

Com o accessToken em mãos, nós temos acesso a rota que é responsável por validar se o usuário está autenticado ou não, no caso dela, precisamos passar o accessToken dentro do Headers da requisição (http://localhost:3333/usuarios/authUser)

Note que estamos passando uma chave chamada de Authorization junto com Bearer + AccessToken que foi retornado no endpoint anterior.

Como resposta, recebemos algumas informações do nosso usuário, indicando que a autenticação foi um sucesso 🙃

Agora, caso o AccessToken estiver expirado, como última alternativa, você pode fazer uma requisição para a rota de refreshToken (http://localhost:3333/usuarios/refreshToken)

Note que essa rota é do tipo POST, e não precisamos enviar sequer nenhum parâmetro adicional, uma vez que por de baixo dos panos, ela só precisa acessar o cookie da requisição.

Se o refreshToken não estiver expirado, você receberá um novo AccessToken junto com as informações do seu usuário. E com o novo AccessToken em mãos, basta que você autentique seus usuário usando ele. 

Nesse caso a rota de refreshToken só deverá ser usada quando o AccessToken expirar.

Caso você queira deslogar o seu usuário, basta chamar a rota de /usuarios/logout (http://localhost:3333/usuarios/logout)

Essa rota vai remover o cookie cujo nome é refreshToken, impossibilitando que o usuário faça novas requisições para as rotas de /dragao.

Por fim, isto é, se você ainda estiver logado, você pode acessar as rotas de /dragao para interagir com o seu banco de dados:

Incrível, não acha?

Arquivos da lição

Os arquivos que você viu durante o decorrer desta lição, podem ser encontrados nos links abaixo:

Conclusão

Nesta lição, você aprendeu a criar APIs com o NodeJS em conjunto com outras bibliotecas 😋

A partir dos ensinamentos desta lição, acreditamos que você tenha dado um grande passo na sua jornada como desenvolvedor NodeJS 🥳

Até a próxima!

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.