JavaScript: Construindo um backlog de filmes com Node.js, Express e MySQL

Neste artigo vamos trabalhar a estruturação inicial de um sistema web para rastreamento de atividades. Imagine o seguinte cenário: você deseja registrar todos os filmes que você viu durante o ano, ou seja, você deseja rastrear (ou trackear) essa atividade. E é para isso que nosso sistema será utilizado.

Construiremos o sistema como uma API RESTful, utilizando o runtime Node.js, o framework Express e o sistema gerenciador de banco de dados MySQL. Caso não saiba o que é uma API RESTful, recomendo que assista o vídeo abaixo e depois continue o tutorial.

API RESTful em camadas

Nossa API seguirá o padrão Model-View-Controller (MVC), que nos permitirá uma organização eficiente do código e melhor manutenabilidade. Aliás, para ser mais correto, utilizaremos uma variação do padrão porque nosso foco será só a parte do back-end. Logo, não teremos as construções das views. Portanto, tirando a camada das Views (V), ficamos apenas com as camadas Model-Controller (MC).

Pré-requisitos

Para continuar este tutorial, você deve ter instaladas em sua máquina as seguintes ferramentas:

Estrutura de diretórios do projeto

/api-backlog
    ├── /src
    |   ├── /controllers
    |   │   ├── filmeController.js
    |   ├── /models
    |   │   ├── filmeModel.js
    |   ├── /routes
    |   │   ├── filmeRoutes.js
    |   ├── /config
    |   │   ├── dbConfig.js
    |   └── app.js
    └── server.js

Iniciando o projeto

Abra o terminal de seu SO. No caso do OpenSuse, sistema usado por mim, é o Konsole. Em seguida, acesse a pasta do seu projeto pelo terminal e digite o comando:

npm init

O processo será iniciado. Siga as instruções para criar um arquivo package.json que irá gerenciar as dependências do projeto. A cada solicitação feita na instalação, preencha os dados do projeto – ou deixe em branco o que não tiver resposta mesmo – e pressione Enter. Para te ajudar nessa etapa, use a Figura 1 como exemplo.

Figura 1 – Processo de inicialização de um projeto usando o NodeJS com a configuração básica do package.json.

O gerenciador de pacotes npm é instalado junto com o Node.js. Entretanto, todas as demais dependências que precisarmos no projeto devem ser instaladas caso a caso. Então, em seguida, vamos instalar as dependências necessárias para este projeto. Execute no terminal o comando:

npm install express mysql2 sequelize body-parser

Nesta linha de comando estamos solicitando para o gerenciador de pacotes do node instalar o Express, o MySQL2, o Sequelize e o Body-parser. O ExpressJS nos dará um middleware para criarmos o servidor back-end de nossa aplicação web, enquanto o Body-parser nos permitirá acessar o conteúdo das requisições HTTP vindas até nossa API, especialmente no contexto das requisições POST, PATCH ou PUT.

Em relação ao banco de dados, usaremos o Sequelize para simplificar a interação com o banco de dados e estruturá-la de uma forma com menos necessidade de escrever SQL manualmente. Assim, teremos mais facilidade para a manutenção e expansão do projeto. Concomitantemente ao Sequelize, usaremos o mysql2 que é um cliente MySQL para Node.js com foco em performance, já que ele é uma versão melhorada e otimizada do mysql, sendo mais rápido e menos consumidor de memória.

Instaladas as dependências de nosso projeto, podemos partir para as demais configurações e iniciar a programação efetiva do sistema. Mas, antes de continuarmos, um aviso: não se preocupe caso apareça uma nova pasta em seu diretório chamada node_modules. Isso acontece porque nós instalamos os frameworks e clientes de BD que nosso sistema precisará durante o uso. Em outras palavras, as dependências. Veja a Figura 2 como exemplo.

Figura 2 – Exemplo da pasta node_modules dentro do projeto.

Configurando a conexão com o banco de dados

O BD que iremos usar para esse projeto será baseado na modelagem da Figura 3. Após analisá-la, você pode copiar o código SQL abaixo da figura para importá-lo em seu SGBD.

Figura 3 – Modelagem conceitual e lógica do banco de dados que será utilizado pela aplicação.
CREATE TABLE Filmes (
    id int PRIMARY KEY,
    titulo varchar(50),
    diretor varchar(50),
    genero varchar(50),
    ano int
);

Depois da importação do código SQL na sua máquina, vamos configurar a conexão do sistema web com o banco de dados. Crie o arquivo dbConfig.js conforme vimos na estrutura de diretórios e escreva o código:

import { Sequelize } from 'sequelize';

const sequelize = new Sequelize('backlog', 'root', '', {
  host: 'localhost',
  dialect: 'mysql'
});

sequelize.authenticate()
  .then(() => {
    console.log('Conectado ao MySQL com Sequelize!');
  })
  .catch(err => {
    console.error('Não foi possível conectar ao banco de dados:', err);
  });

export default sequelize;

Na linha 1, estamos importando os métodos do Sequelize para que tenhamos meios de manipular a conexão com o banco de dados. Depois, criamos uma instância da constante sequelize com os dados de conexão, que são o nome do banco, o usuário, a senha, o host e o dialeto (neste caso, o Mysql). Modifique esses dados para corresponder à conexão do SGBD na sua máquina. Configurado o objeto sequelize, o código tenta autenticá-lo para conectar ao banco e retorna uma mensagem de acordo com o sucesso ou não da tentativa.

Por fim, exportamos o objeto para que ele possa ser utilizado por outros arquivos no projeto.

Criando os modelos (models)

A partir de agora, iremos criar os modelos para armazenamento dos dados no banco. De forma simplificada, podemos dizer que os models representam a estrutura das entidades no banco de dados. Em alguns casos, é possível encontrarmos a palavra schema como sinônimo.

Crie o arquivo filmeModel.js na pasta models conforme a estrutura definida para este projeto. Escreveremos o código abaixo para o esquema da entidade Filme prevista na modelagem. A explicação está em forma de comentário no próprio código.

// importação dos módulos necessários
import { DataTypes } from 'sequelize';
import sequelize from '../config/dbConfig.js';

// definição do modelo de dados da tabela Filmes
const filmeModel = sequelize.define('Filmes', {
  id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    primaryKey: true,
    autoIncrement: true
  },
  titulo: {
    type: DataTypes.STRING,
    allowNull: false
  },
  diretor: {
    type: DataTypes.STRING,
    allowNull: false
  },
  ano: {
    type: DataTypes.INTEGER,
    allowNull: false
  },
  genero: {
    type: DataTypes.STRING,
    allowNull: false
  }
}, {
  timestamps: false
});

export default filmeModel;

Criando os controladores (controllers)

Agora que já temos o model definido para a entidade, precisamos lidar com a lógica do sistema, muitas vezes conhecida como regra do negócio. Na programação em camadas MVC, a camada dos controllers são os responsáveis por implementar a lógica de negócio da API.

Da mesma forma como fizemos para o model, criaremos um arquivo respectivo para a entidade. Portanto, teremos o arquivo filmeController.js. O arquivo deve estar dentro do diretório controllers, seguindo a estrutura prevista.

Em cada arquivo controller definiremos as funções que serão capazes de lidar com os verbos do protocolo HTTP (GET, POST, PUT, DELETE). Maiores informações sobre os verbos HTTP podem ser obtidas diretamente na documentação MDN Web Docs, da Mozilla, e no portal Devmedia. O código abaixo foi utilizado para a criação do controller e a explicação está em forma de comentário no próprio código.

// Importação do model da tabela Filme
import filmeModel from "../models/filmeModel.js";

// Definição da classe Filmes com seus métodos para
// buscar todos os filmes, buscar um filme por ID,
// cadastrar um filme, atualizar um filme e excluir um 
// filme
export default class Filmes {

  async BuscarTodosOsFilmes (req, res) {
    try {
      const filmes = await filmeModel.findAll();
      res.json(filmes);
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  }

  async BuscarFilmePorId (req, res) {
    try {
      const filmeEncontrado = await filmeModel.findByPk(req.params.id);
      if (filmeEncontrado) {
        res.json(filmeEncontrado);
      } else {
        res.status(404).json({ error: 'Filme não encontrado' });
      }
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  };

  async CadastrarFilme (req, res) {
    try {
      const filmeCadastrado = await filmeModel.create(req.body);
      res.json({ message: 'Filme criado com sucesso!', filmeCadastrado });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  };

  async AtualizarFilme (req, res) {
    try {
      const [atualizado] = await filmeModel.update(req.body, {
        where: { id: req.params.id }
      });
      if (atualizado) {
        const filmeAtualizado = await filmeModel.findByPk(req.params.id);
        res.json({ message: 'Filme atualizado com sucesso!', filme: filmeAtualizado });
      } else {
        res.status(404).json({ error: 'Filme não encontrado' });
      }
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  };

  async ExcluirFilme (req, res) {
    try {
      const excluido = await filmeModel.destroy({
        where: { id: req.params.id }
      });
      if (excluido) {
        res.json({ message: 'Filme excluído com sucesso!' });
      } else {
        res.status(404).json({ error: 'Filme não encontrado' });
      }
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  };

}

Criando as rotas (routes)

Agora que já temos a classe Filme e os métodos preparados para manipular os dados, precisamos enviá-los para o controller. Faremos isso por meio das rotas (em inglês, routes).

Cada rota usa um verbo HTTP (GET, POST etc.) para informar de que tipo será a requisição (request) e a resposta (response) da função callback que programarmos. Além disso, precisamos definir qual será o caminho a ser acionado para ativar aquela rota (pode ser apenas /, /:id etc.).

Seguindo a estrutura de diretórios definida, crie o arquivo filmeRoutes.js e digite o código abaixo. Breves explicações estão em forma de comentário no próprio código.

// Importação do ExpressJS e do Controller de Filmes
import express from "express";
import filmeController from "../controllers/filmeController.js";

// Criação de uma instância do Controller de Filmes
const router = express.Router();
const filme = new filmeController();

// Definição das rotas para o CRUD de Filmes
router.get('/', (req, res) => {
    try {
        filme.BuscarTodosOsFilmes(req, res);
    }catch(err){
        res.status(500).json({ error: err.message });
    }
});

router.get('/:id', (req, res) => {
    try {
        filme.BuscarFilmePorId(req, res);
    }catch(err){
        res.status(500).json({ error: err.message });
    }
});

router.post('/', (req, res) => {
    try {
        filme.CadastrarFilme(req, res);
    }catch(err){
        res.status(500).json({ error: err.message });
    }
});

router.put('/:id', (req, res) => {
    try {
        filme.AtualizarFilme(req, res);
    }catch(err){
        res.status(500).json({ error: err.message });
    }
});

router.delete('/:id', (req, res) => {
    try {
        filme.ExcluirFilme(req, res);
    }catch(err){
        res.status(500).json({ error: err.message });
    }
});

export default router;

Configurando o Express e o ponto de entrada da aplicação

Toda aplicação precisa de um ponto de entrada. Esse ponto costuma ser um arquivo ou função inicial que dá o start no restante das funções e tarefas.

Em nossa estrutura, definimos o arquivo server.js como sendo esse ponto. Nele, escreveremos o código a seguir para iniciar o servidor e configurar as rotas da API. Dessa forma, nossa app poderá receber e responder a requisições.

// importação do módulo app.js para dentro deste arquivo
import app from "./src/app.js";

// variável PORT recebe a indicação de qual porta o servidor deverá "ouvir" para
// pegar a requisição (request) e retornar a resposta (response)
const PORT = 3000;

// utilização da variável app que foi exportada do módulo app.js e do método .listen()
// para escutar a porta definida anteriormente.
app.listen(PORT, () => {
    console.log("Servidor escutando");
});

Se você é uma pessoa atenta, deve ter percebido que no código do arquivo server.js nós importamos um arquivo chamado app.js que não apresentamos ainda. Bem, este arquivo foi criado para que pudéssemos separar a inicialização do express, do body-parser e das rotas. Fizemos isso para melhorar a manutenção do código em caso de necessidade futura. Portanto, dentro do diretório src, crie o arquivo app.js e digite o código a seguir. A explicação do código está em forma de comentário.

import express from "express";
import bodyParser from "body-parser";
import filmeRoutes from "../src/routes/filmeRoutes.js";

// criação de uma instância do ExpressJS
const app = express();

// configuração do ExpressJS para utilizar o formato JSON
app.use(bodyParser.json());

// configuração do ExpressJS para utilizar as rotas definidas
// no arquivo filmeRoutes.js
app.use('/', filmeRoutes);

// exportação de variáveis e métodos do arquivo app.js para os demais arquivos do projeto.
// Aqui o arquivo app.js se torna um módulo dentro do projeto.
export default app;

Agora sim, finalmente criamos e programamos todos os arquivos que havíamos previsto na estrutura inicial. Se você seguiu os passos deste tutorial corretamente, seu sistema deve estar pronto para o teste de funcionamento.

Testando a aplicação

Antes de mais nada, confirme que seu banco de dados MySQL está em execução. Se você estiver usando o XAMPP como servidor para o banco de dados e o SO OpenSuse, você pode inicializar o MySQL digitando o comando no terminal:

sudo /opt/lampp/lampp start

Lembrando que o caminho /opt/lampp/lampp é o local para o diretório de instalação na minha máquina. Você precisa verificar onde o XAMPP está instalado na sua (provavelmente será no mesmo local, mas é bom confirmar).

Com o banco de dados inicializado, vamos subir nosso servidor e colocar nossa aplicação para funcionar. No terminal, entre na pasta raiz do projeto (nós havíamos definido na estrutura que a pasta raiz chama-se api-backlog) e digite:

node server.js

Se tudo estiver correto, você verá no terminal uma mensagem igual a que está demonstrada na Figura 4. Isso é sinal de que nosso servidor está rodando e pronto para receber requisições.

Figura 4 – Resultado que aparece no terminal quando a aplicação foi iniciada com sucesso.

Agora, abra um navegador de sua vontade e digite na barra de endereços o domínio http://localhost:3000/. O número 3000 representa a porta do servidor localhost que será “ouvida” pela aplicação. Nós definimos esse valor no arquivo server.js.

Como está é a primeira vez que você faz uma requisição no sistema, seu navegador não mostrará nada na tela. Afinal, não há nada armazenado no banco de dados. À medida que os testes forem feitos, o banco será “populado” e nas próximas chamadas básicas haverá o retorno de dados em um vetor de objetos JSON, conforme a Figura 5.

Figura 5 – Exemplo de retorno da aplicação na rota base.

O resultado que obtivemos até aqui é o retorno da rota get e parâmetro / que está no arquivo filmeRoutes.js a partir da linha 10. Para testarmos o funcionamento das rotas para os verbos POST, PUT e DELETE, precisaremos de outro API client (o Postman, por exemplo). Portanto, vamos parar o tutorial neste ponto e deixar o restante dos testes para outro post (clique aqui para acessar).

Conclusão

Ufa! Depois de várias camadas, linhas de código, arquivos e pastas, chegamos ao fim do nosso simplório tutorial. Entretanto, antes que você saia do post, quero deixar dois disclaimers importantes:

  1. Sabemos que este código não é a versão mais otimizada ou segura possível para criar-se uma API RESTful. Entretanto, este tutorial foi elaborado para iniciantes. Logo, optamos pela facilidade de entendimento dos conceitos base para separação de código em camadas, especificamente o padrão Model-View-Controller (MVC).
  2. Lembre-se: o ideal é que você sempre digite seu próprio código, ajustando-o e adaptando-o para o seu tipo de escrita. Não fique dependente da ferramenta Copia e Cola disponível nos códigos. Assim, você estará reforçando sua aprendizagem.

Obrigado pelo seu tempo e bons estudos!