Aguarde...

12 de março de 2021

Seu guia para construir um NodeJS, TypeScript Rest API com MySQL

Seu guia para construir um NodeJS, TypeScript Rest API com MySQL

Combinação matadora para construção de APIs, NodeJS, TypeScript e MySQL

O MySQL é, sem dúvida, uma das principais opções para um banco de dados relacional na pilha de tecnologia de todos os desenvolvedores do Node. A facilidade do Node de criar APIs de back-end emparelhada com a capacidade do MySQL de suportar operações de consulta complexas fornece uma maneira simples para os desenvolvedores criarem back-ends da web avançados.

Neste tutorial, vamos desenvolver uma API REST simples para uma loja online com estrutura Express. MySQL é nossa escolha de banco de dados. Em vez de usar Javascript simples para a implementação, decidimos construir esta API usando Typescript.

O suporte a tipos de script deixa pouco espaço para que os desenvolvedores usem indevidamente os tipos. Isso nos ajuda a escrever um código mais limpo e reutilizável. Se você é um iniciante em Typescript ou deseja refrescar sua memória da linguagem, leia nosso guia de Typescript para desenvolvedores de Javascript antes de ir para a próxima etapa.

Com a introdução inicial resolvida, vamos começar agora.


Antes de começarmos…

Antes de começarmos o tutorial, certifique-se de ter todas as ferramentas que precisamos configurar. Supondo que você já tenha o Node.js instalado, instale o MySQL em seu dispositivo antes de continuar.

Configure o banco de dados

Como mencionei antes, estamos criando uma API para uma loja online simples que armazena uma lista de produtos e clientes registrados em seu banco de dados. Quando os clientes fazem pedidos de produtos, seus detalhes também são armazenados neste banco de dados.

No total, nosso esquema de banco de dados tem 3 tabelas: Product, Customer e ProductOrder.

Seu guia para construir um NodeJS, TypeScript Rest API com MySQL
Seu guia para construir um NodeJS, TypeScript Rest API com MySQL

Vou criá-los usando consultas SQL regulares. Se desejar, você pode usar uma ferramenta GUI para criar o esquema do banco de dados.

Certifique-se de que o servidor MySQL esteja em execução e execute este comando na linha de comando. (Você teria que adicionar o MySQL às variáveis ​​de ambiente para usar diretamente o comando mysql).

mysql -u <username> -p <password>

Isso o levará ao shell do MySQL, onde você pode executar consultas SQL diretamente no banco de dados.

Agora, podemos criar um novo banco de dados para nosso projeto.

create database OnlineStore

Use o seguinte comando para alternar para o banco de dados recém-criado.

use OnlineStore;

Em seguida, execute as seguintes consultas para criar as tabelas de que precisamos.

CREATE TABLE Product (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    description VARCHAR(255),
    instock_quantity INT,
    price DECIMAL(8, 2)
);

CREATE TABLE Customer (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50),
    password VARCHAR(255),
    email VARCHAR(255) UNIQUE
);

CREATE TABLE ProductOrder (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    product_id INT,
    customer_id INT,
    product_quantity INT,
    FOREIGN KEY (product_id) REFERENCES Product(id),
    FOREIGN KEY (customer_id) REFERENCES Customer(id)
);

Use consultas semelhantes às abaixo para inserir alguns dados nas tabelas criadas.

INSERT INTO Product VALUES (1, "Apple MacBook Pro", "15 inch, i7, 16GB RAM", 5, 667.00);

INSERT INTO Customer VALUES (1, "Anjalee", "2w33he94yg4mx88j9j2hy4uhd32w", "anjalee@gmail.com");

INSERT INTO ProductOrder VALUES (1, 1, 1, 1);

Excelente! Nosso esquema de banco de dados agora está completo. Podemos ir para o Node.js e começar com a implementação da API na próxima etapa.


Configure o ambiente de projeto Node.js

Como de costume, usamos o npm initcomando para inicializar nosso projeto Node.js como a primeira etapa da configuração.

Em seguida, temos que instalar os pacotes npm que usaremos neste projeto. Há muito poucos. Vamos instalar as dependências do projeto primeiro.

npm install express body-parser mysql2 dotenv

Aqui, usamos dotenvpara importar variáveis ​​de ambiente para o projeto e mysql2para gerenciar a conexão com o banco de dados.

Em seguida, instale o Typescript como uma dependência de desenvolvimento.

npm install typescript --save-dev

Também temos que instalar as definições de tipo Typescript para os pacotes que estamos usando no projeto. Como a maioria desses pacotes não tem definições de tipo, usamos o namespace @types npm, onde as definições de tipo relevantes são hospedadas no projeto Definitely Typed .

npm install @types/node @types/express @types/body-parser @types/mysql @types/dotenv --save-dev

Em seguida, devemos inicializar nosso projeto como um projeto Typescript. Para isso, execute o seguinte comando.

npx tsc --init

Isso adicionará o tsconfig.jsonarquivo ao seu projeto. Nós o usamos para configurar opções de Typescript relacionadas ao projeto.

Ao abrir o tsconfig.jsonarquivo, você verá vários códigos comentados. Para nosso projeto, precisamos descomentar as seguintes opções e defini-las com os valores mostrados abaixo.

"compilerOptions": {
    "target": "es6",   
    "module": "commonjs",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
}

Essas opções serão consideradas no momento em que o Typescript for compilado em Javascript. O outDirque fornecemos aqui é onde os .jsarquivos compilados serão armazenados.

Como etapa final, temos que modificar o script de início no arquivo package.json para compilar o Typescript antes de iniciar o aplicativo Node.

"scripts": {
    "start": "tsc && node dist/app.js",
},

dist/app.jsarquivo é a versão compilada do app.tsarquivo que estamos usando para escrever nosso código.


A estrutura do diretório do projeto

Nosso projeto tem uma estrutura de diretório simples semelhante ao gráfico a seguir.

|-.env
|-package.json
|-tsconfig.json
|-dist/
|-app.ts
|-db.ts
|-models/
|-routes/
|-types/

Crie o arquivo .env

Usamos o arquivo .env para armazenar as variáveis ​​de ambiente do aplicativo.

PORT=3000
DB_HOST="localhost"
DB_USER="username"
DB_PWD="****"
DB_NAME="OnlineStore"

Como você pode ver, a maioria das variáveis ​​de ambiente está relacionada ao banco de dados que criamos anteriormente.


Defina novos tipos para a API

Temos que definir novos tipos de Texto para os objetos Produto, Cliente e Pedido. Estamos armazenando todos os arquivos de tipo no diretório de tipos.

//file types/customer.ts

export interface BasicProduct {
  id: number,
}

export interface Product extends BasicProduct {
  name: string,
  description: string,
  instockQuantity: number,
  price: number
}

Aqui, criamos dois tipos de produtos. O primeiro tipo, BasicProduct, contém apenas o ID do produto em seus campos. O segundo tipo, Produto, estende a primeira interface e cria um tipo com detalhes elaborados.

Em nosso aplicativo, às vezes, queremos processar apenas a ID de um produto. Outras vezes, queremos processar um objeto de produto detalhado. Por este motivo, utilizamos dois tipos de produtos e um alarga o outro. Você verá um comportamento semelhante ao definir os tipos de cliente e pedido.

//file types/customer.ts

export interface BasicCustomer {
  id: number,
}

export interface Customer extends BasicCustomer{
  name: string,
  email?: string,
  password?: string
}

Ao definir os tipos de pedido, podemos usar os tipos de cliente e produto criados anteriormente como os tipos de cliente e campos de produto, respectivamente. Nos três tipos de pedido que definimos, os tipos relevantes são usados ​​para os campos de cliente e produto.

//file types/order.ts

import {BasicProduct, Product} from "./product";
import {BasicCustomer, Customer} from "./customer";

export interface BasicOrder {
  product: BasicProduct,
  customer: BasicCustomer,
  productQuantity: number
}

export interface Order extends BasicOrder {
  orderId: number
}

export interface OrderWithDetails extends Order{
  product: Product,
  customer: Customer,
}

O tipo BasicOrder que é definido sem o id é útil ao criar um pedido pela primeira vez (porque o novo pedido ainda não tem um ID).


Conecte-se ao banco de dados

Com a ajuda do pacote mysql2, é uma etapa fácil conectar-se ao banco de dados que criamos antes.

import mysql from "mysql2";
import * as dotenv from "dotenv";
dotenv.config();

export const db = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PWD,
  database: process.env.DB_NAME
});

Exportamos o objeto de conexão estabelecido para facilitar a definição de operações de banco de dados para diferentes tipos separadamente.


Definir operações de banco de dados

A seguir, vamos criar funções para operações de criação, localização, localização e atualização para o banco de dados. Estou apenas implementando as operações relacionadas ao tipo de pedido. Mas você também pode implementar essas operações para outros tipos de dados.

Observe que escreveremos instruções SQL simples para esta tarefa. Se você quiser usar um ORM em vez de escrever manualmente as instruções, o Node oferece vários ORMs compatíveis com o Typescript, como TypeORM e Sequelize.

First, import the objects we will need for the implementation.

//file models/order.ts

import {BasicOrder, Order, OrderWithDetails} from "../types/order";
import {db} from "../db";
import { OkPacket, RowDataPacket } from "mysql2";

A seguir, vamos implementar a função de criação. É usado para inserir um novo registro de um pedido na tabela ProductRecord.

export const create = (order: BasicOrder, callback: Function) => {
  const queryString = "INSERT INTO ProductOrder (product_id, customer_id, product_quantity) VALUES (?, ?, ?)"

  db.query(
    queryString,
    [order.product.id, order.customer.id, order.productQuantity],
    (err, result) => {
      if (err) {callback(err)};

      const insertId = (<OkPacket> result).insertId;
      callback(null, insertId);
    }
  );
};

Usamos o objeto db importado para consultar o banco de dados e retornar o insertId do registro do pedido por meio de um retorno de chamada. Como o resultado retornado é uma união de vários tipos, fazemos uma conversão simples para convertê-lo para o tipo OkPacket, o tipo retornado para as operações de inserção.

Ao passar instruções SQL para a função de consulta, sempre tome cuidado para não usar as entradas fornecidas pelo usuário diretamente na string. É uma prática que torna seu sistema vulnerável a ataques de injeção de SQL. Em vez disso, use o? símbolo em locais onde as variáveis ​​devem ser adicionadas e passe as variáveis ​​como uma matriz para a função de consulta.

Se você não estiver familiarizado com as instruções SQL, consulte a documentação oficial do MySQL para entender as instruções básicas de inserção, seleção e atualização que usamos neste tutorial.

A seguir, vamos implementar a função findOne, que seleciona um registro da tabela ProductOrder com base no ID do pedido.

export const findOne = (orderId: number, callback: Function) => {

  const queryString = `
    SELECT 
      o.*,
      p.*,
      c.name AS customer_name,
      c.email
    FROM ProductOrder AS o
    INNER JOIN Customer AS c ON c.id=o.customer_id
    INNER JOIN Product AS p ON p.id=o.product_id
    WHERE o.order_id=?`
    
  db.query(queryString, orderId, (err, result) => {
    if (err) {callback(err)}
    
    const row = (<RowDataPacket> result)[0];
    const order: OrderWithDetails =  {
      orderId: row.order_id,
      customer: {
        id: row.cusomer_id,
        name: row.customer_name,
        email: row.email
      },
      product: {
        id: row.product_id,
        name: row.name,
        description: row.description,
        instockQuantity: row.instock_quantity,
        price: row.price
      },
      productQuantity: row.product_quantity
    }
    callback(null, order);
  });
}

Aqui também, seguimos um processo semelhante à função de criação. Na instrução SQL, temos que juntar as tabelas ProductRecord, Customer, Product para recuperar registros completos do cliente e do produto incluído no pedido. Usamos as chaves estrangeiras definidas na tabela OrderProduct para fazer a junção.

Depois de recuperar os dados, temos que criar um objeto do tipo Order. Como estamos recuperando detalhes de objetos de produto e cliente, escolhemos OrderWithDetails como nosso tipo entre os 3 tipos de pedido que definimos antes.

Agora podemos implementar as outras duas operações de banco de dados, findAll e update, seguindo o mesmo padrão.

export const findAll = (callback: Function) => {
  const queryString = `
    SELECT 
      o.*, 
      p.*,
      c.name AS customer_name,
      c.email
    FROM ProductOrder AS o 
    INNER JOIN Customer AS c ON c.id=o.customer_id
    INNER JOIN Product AS p ON p.id=o.product_id`

  db.query(queryString, (err, result) => {
    if (err) {callback(err)}

    const rows = <RowDataPacket[]> result;
    const orders: Order[] = [];

    rows.forEach(row => {
      const order: OrderWithDetails =  {
        orderId: row.order_id,
        customer: {
          id: row.customer_id,
          name: row.customer_name,
          email: row.email
        },
        product: {
          id: row.product_id,
          name: row.name,
          description: row.description,
          instockQuantity: row.instock_quantity,
          price: row.price
        },
        productQuantity: row.product_quantity
      }
      orders.push(order);
    });
    callback(null, orders);
  });
}

export const update = (order: Order, callback: Function) => {
  const queryString = `UPDATE ProductOrder SET product_id=?, product_quantity=? WHERE order_id=?`;

  db.query(
    queryString,
    [order.product.id, order.productQuantity, order.orderId],
    (err, result) => {
      if (err) {callback(err)}
      callback(null);
    }
  );
}

Com isso, concluímos as funções para operações de banco de dados relacionadas a pedidos.


Implementar os gerenciadores de rotas

Como a próxima etapa, implementaremos o manipulador de rotas para o /ordersponto de extremidade. Você pode seguir o padrão que usamos aqui para implementar os pontos de extremidade /customer/productposteriormente.

Para nossa API REST, vamos definir 4 endpoints para enviar solicitações do lado do cliente.

//get all order objects
GET orders/

//create a new order
POST orders/

//get order by order ID
GET orders/:id

//update the order given by order ID
PUT orders/:id

Como estamos usando o roteador expresso para definir as rotas, podemos usar os caminhos relativos à rota / orders na implementação a seguir.

Uma vez que implementamos a lógica de recuperação de dados no modelo de banco de dados, no manipulador de rotas, só temos que obter esses dados usando as funções relevantes e passá-los para o lado do cliente.

Vamos adicionar a lógica de tratamento de rota ao arquivo orderRouter.ts.

import express, {Request, Response} from "express";
import * as orderModel from "../models/order";
import {Order, BasicOrder} from "../types/order";
const orderRouter = express.Router();

orderRouter.get("/", async (req: Request, res: Response) => {
  orderModel.findAll((err: Error, orders: Order[]) => {
    if (err) {
      return res.status(500).json({"errorMessage": err.message});
    }

    res.status(200).json({"data": orders});
  });
});

orderRouter.post("/", async (req: Request, res: Response) => {
  const newOrder: BasicOrder = req.body;
  orderModel.create(newOrder, (err: Error, orderId: number) => {
    if (err) {
      return res.status(500).json({"message": err.message});
    }

    res.status(200).json({"orderId": orderId});
  });
});

orderRouter.get("/:id", async (req: Request, res: Response) => {
  const orderId: number = Number(req.params.id);
  orderModel.findOne(orderId, (err: Error, order: Order) => {
    if (err) {
      return res.status(500).json({"message": err.message});
    }
    res.status(200).json({"data": order});
  })
});

orderRouter.put("/:id", async (req: Request, res: Response) => {
  const order: Order = req.body;
  orderModel.update(order, (err: Error) => {
    if (err) {
      return res.status(500).json({"message": err.message});
    }

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

export {orderRouter};

Junte tudo em app.ts

Agora concluímos a adição da lógica interna de nossa API. A única coisa que resta a fazer é juntar tudo no arquivo app.ts, o ponto de entrada para nossa API, e criar o servidor que escuta e responde às solicitações.

import * as dotenv from "dotenv";
import express from "express";
import * as bodyParser from "body-parser";
import {orderRouter} from "./routes/orderRouter";

const app = express();
dotenv.config();

app.use(bodyParser.json());
app.use("/orders", orderRouter);

app.listen(process.env.PORT, () => {
console.log("Node server started running");
});

É isso! Criamos nossa API REST Node.js simples com MYSQL e Typescript em nenhum momento.


Resumo

Neste tutorial, aprendemos como criar uma API REST com Node.js e MySQL com suporte de tipo de Typescript. Essas 3 tecnologias são uma combinação perfeita para criar APIs de forma rápida e fácil. Portanto, espero que este tutorial seja inestimável para você escrever boas APIs de back-end no futuro. Para obter mais experiência no assunto, você pode tentar implementar as sugestões que fiz neste tutorial como sua próxima etapa.

Postado em Blog
Escreva um comentário