Escrever SQL bruto em sua API é tão ultrapassado ou, na melhor das hipóteses, é reservado para consultas realmente complexas. Estes são tempos mais simples para o desenvolvimento e, para a maioria das APIs, usar um dos muitos mapeadores relacionais de objetos (ORMs) é suficiente.

Os ORMs também encapsulam convenientemente os detalhes intrincados da comunicação com um banco de dados e sua linguagem de consulta.

Isso significa que você pode usar um único ORM em vários tipos de banco de dados como MySQL, PostgreSQL ou MongoDb, tornando mais fácil alternar entre bancos de dados sem reescrever seu código! Você também pode conectar diferentes tipos de bancos de dados ao seu projeto, usando o mesmo código para acessá-los.

Neste artigo, você aprenderá a usar o Sequelize ORM com TypeScript. Portanto, pegue seus laptops, abra seu IDE e vamos começar!

Pré-requisitos

Para acompanhar este artigo, instale o seguinte:

Node.js gerenciador de pacotes JavaScript; usaremos yarn Um IDE ou editor de texto de sua escolha, como Texto Sublime ou Visual Studio Code

Configurando o projeto

Para começar nosso projeto, vamos configurar uma API Express.js simples para criar um livro de receitas virtual que armazena receitas e ingredientes e marca nossas receitas com categorias populares.

Primeiro, vamos criar nosso diretório de projeto digitando o seguindo em nosso terminal:

$ mkdir cookbook $ cd cookbook

Dentro do novo diretório do projeto do livro de receitas, instale as dependências de projeto necessárias usando yarn. Primeiro, execute npm init para inicializar o projeto Node.js com um arquivo package.json:

$ npm init

Após a inicialização do projeto Node.js, instale as dependências começando com express:

$ yarn add express

Em seguida, adicione TypeScript ao projeto executando o seguinte:

$ yarn add-D typescript ts-node @ types/express @ types/node

Observe que adicionamos um sinalizador ,-D, ao nosso comando de instalação. Este sinalizador diz ao Yarn para adicionar essas bibliotecas como dependências de desenvolvimento, o que significa que essas bibliotecas são necessárias apenas quando o projeto está em desenvolvimento. Também adicionamos definições de tipo para Express.js e Node.js.

Com o TypeScript adicionado ao nosso projeto, vamos inicializá-lo:

$ npx tsc–init

Isso cria nossa configuração do TypeScript arquivo ts.config e define os valores padrão:

//ts.config {“compilerOptions”: {“target”:”es5″,”module”:”commonjs”,”sourceMap”: true,”outDir”:”dist”,”strict”: true,”esModuleInterop”: true,”skipLibCheck”: true,”forceConsistentCasingInFileNames”: true}}

Encontre mais informações sobre personalizando ts.config aqui .

Finalmente, vamos definir uma estrutura de API simples para nosso projeto criando um projeto diretórios e arquivos para corresponda ao esquema abaixo:

-dist # o nome de nosso outDir definido em tsconfig.json-src-api-controladores-contratos-rotas-serviços-db-dal-dto-models config.ts init.ts-erros index.ts ts.config

Agora que definimos nossa estrutura de projeto no arquivo index.ts, que é o ponto de partida de nosso aplicativo, adicione o seguinte código para criar nosso servidor Express.js:

# src/index.ts import express, {Application, Request, Response} de’express’const app: Application=express () const port=3000//Análise do corpo Middleware app.use (express.json ()); app.use (express.urlencoded ({extended: true})); app.get (‘/’, async (req: Request, res: Response): Promise => {return res.status (200).send ({message: `Welcome to the cookbook API! \ n Endpoints disponíveis em http://localhost: $ {port}/api/v1`})}) tente {app.listen (port, ()=> {console.log (`Servidor em execução em http://localhost: $ {port } `)})} catch (error) {console.log (` Ocorreu um erro: $ {error.message} `)}

Também devemos incluir algumas bibliotecas adicionais para executar o aplicativo facilmente e passar as variáveis ​​de ambiente. Essas bibliotecas adicionais são nodemon using yarn add-D nodemon , eslint usando yarn add-D eslint e dotenv using yarn add dotenv .

Configurando o Sequelize ORM

Neste ponto, o aplicativo Express.js está em execução, então é hora para trazer a diversão: Sequelize ORM!

Comece adicionando Sequelize ao projeto executando o seguinte:

$ yarn add sequelize $ yarn add mysql2

Embora tenhamos adicionado o driver de banco de dados para MySQL, que é exclusivamente baseado em preferências pessoais, você pode instalar qualquer driver de seu banco de dados preferido. Veja aqui para outros drivers de banco de dados disponíveis .

Iniciando a conexão do Sequelize

Após instalar o Sequelize, devemos iniciar sua conexão com nosso banco de dados. Uma vez iniciada, esta conexão registra nossos modelos:

# db/config.ts import {Dialect, Sequelize} from’sequelize’const dbName=process.env.DB_NAME as string const dbUser=process.env.DB_USER as string const dbHost=process.env.DB_HOST const dbDriver=process.env.DB_DRIVER as Dialeto const dbPassword=process.env.DB_PASSWORD const sequelizeConnection=new Sequelize (dbName, dbUser, dbPassword, {host: dbHost, dialeto: dbDriver} exportar

Criando e registrando modelos Sequelize

Sequelize fornece duas maneiras de registrar modelos: using sequelize.define ou estendendo a classe de modelo Sequelize. Neste tutorial, usaremos o método de extensão de modelo para registrar nosso modelo de ingrediente.

Começamos criando as interfaces do seguinte:

IngredientAttributes define todos os atributos possíveis de nosso modelo IngredientInput define o tipo do objeto passado para o model.create IngredientOuput do Sequelize define o objeto retornado de model.create, model.update e model.findOne # db/models/Ingredient.ts import {DataTypes, Model, Optional} da importação’sequelize’sequelizeConnection from’../config’interface IngredientAttributes {id: number; nome: string; lesma: string; descrição ?: string; foodGroup ?: string; createdAt ?: Data; updatedAt ?: Date; deletadaAt ?: Data; } export interface IngredientInput extends Optional {} export interface IngredientOuput extends Required {}

A seguir, crie uma classe Ingredient que estenda, inicialize e exporte a importação {Model} de’sequelize’Sequelize a classe de modelo:

# db/models/Ingredient.ts… class Ingredient extends Model implementa IngredientAttributes {public id !: número public name !: string public slug !: string public description !: string public foodGroup !: string//timestamps! public readonly createdAt !: Data; public readonly updatedAt !: Date; público somente leitura excluídoAt !: Data; } Ingredient.init ({id: {type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true,}, name: {type: DataTypes.STRING, allowNull: false}, slug: {type: DataTypes.STRING, allowNull: false, unique: true}, description: {type: DataTypes.TEXT}, foodGroup: {type: DataTypes.STRING}}, {timestamps: true, sequelize: sequelizeConnection, paranoid: true}) export default Ingrediente

Observe que adicionamos a opção paranoid: true ao nosso modelo; isso impõe uma exclusão reversível no modelo ao adicionar um atributo deletedAt que marca os registros como excluídos ao invocar o método destroy.

Para completar nosso modelo e criar sua tabela de destino no banco de dados conectado, execute o método de sincronização do modelo:

# db/init.ts import {Recipe, RecipeTags, Tag, Review, Ingredient, RecipeIngredients} from’./models’const isDev=process.env.NODE_ENV===’development’const dbInit=()=> {Ingredient.sync ({alter: isDev})} exportar dbInit padrão

O método de sincronização aceita as opções force e alter. A opção force força a recriação de uma mesa. A opção alter cria a tabela se ela não existir ou atualiza a tabela para corresponder aos atributos definidos no modelo.

Dica profissional: reserve usando force ou alter para ambientes de desenvolvimento para não recriar acidentalmente seu banco de dados de produção, perdendo todos os seus dados ou aplicando alterações em seu banco de dados que podem quebrar seu aplicativo.

Usando nossos modelos em DAL e serviços

A camada de acesso a dados (DAL) é onde implementamos nossas consultas SQL ou, neste caso, onde as consultas do modelo Sequelize são executadas:

# db/dal/ingrediente.ts import {Op} from’sequelize’import {Ingredient} from’../models’import {GetAllIngredientsFilters} from’./types’import {IngredientInput, IngredientOuput} from’../models/Ingredient’export const create=async (payload: IngredientInput): Promise => {const ingrediente=await Ingredient.create (payload) return ingrediente} export const update=assíncrono (id: número, carga útil: Parcial ): Promessa => {const ingrediente=espera Ingrediente.findByPk (id) if (! ingrediente) {//@todo lançar erro personalizado lançar novo Erro (‘não encontrado’)} const updatedIngredient=await (ingrediente como ingrediente).update (carga) return updatedIngredient} export const getById=async (id: number): Promise => {const ingrediente=espera Ingrediente.findByPk (id) se (! ingrediente) {//@todo throw custom error throw new Error (‘not found’)} return ingrediente} export const deleteById=async (id: number): Promise => {const deletedIngredientCount=await Ingredient.destroy ({onde: {id}}) return !! deletedIngredientCount} export const getAll=async (filters ?: GetAllIngredientsFilters): Promise => {return Ingredient.findAll ({where: {… (filters)?.éDeleted && {deletedAt: {[ Op.not]: null}})},… ((filtros?.IsDeleted || filtros?.includeDeleted) && {paranoid: true})})}

Adicionar a opção paranoid: true ao método do modelo findAll inclui os registros excluídos por software com deletedAt definido no resultado. Caso contrário, os resultados excluem registros excluídos de forma reversível por padrão.

Em nosso DAL acima, definimos algumas consultas CRUD comumente necessárias usando nossa definição de tipo ModelInput e colocando quaisquer tipos adicionais em db/dal/types.ts:

# db/dal/types.ts interface de exportação GetAllIngredientsFilters {isDeleted ?: boolean includeDeleted ?: boolean}

Sequelize ORM tem alguns métodos de modelo muito legais, incluindo findAndCountAll, que retorna uma lista de registros e uma contagem de todos os registros que correspondem aos critérios do filtro. Isso é realmente útil para retornar respostas de lista paginada em uma API.

Agora podemos criar nosso serviço, que atua como um intermediário entre nosso controlador e DAL:

# api/services/ingredienteService.ts import * as ingredienteDal from’../dal/ingredient’import {GetAllIngredientsFilters} from’../dal/types’import {IngredientInput, IngredientOuput} from’../models/Ingredient’export const create=(carga útil: IngredientInput): Promessa => {retornar ingredienteDal.create (carga útil)} exportar atualização constante=(id: número, carga útil: Parcial ): Promessa => {retornar ingredienteDal.update (id, carga útil)} export const getById=(id: number): Promise => {return ingredienteDal.getById (id)} export const deleteById=(id: number): Promise => {return ingredienteDal.deleteById (id)} export const getAll=(filters: GetAllIngredientsFilters): Promise => {return ingredienteDal.getAll (filters )}

Potencializando o modelo com rotas e controladores

Percorremos um longo caminho! Agora que temos serviços que buscam nossos dados em nosso banco de dados, é hora de trazer toda essa mágica ao público usando rotas e controladores.

Vamos começar criando nossas rotas de Ingredientes em src/api/routes/ingredientes. ts:

# src/api/routes/components.ts import {Router} from’express’const ingredienteRouter=Router () componentsRouter.get (‘:/slug’, ()=> {//obter ingrediente} ) IngredientesRouter.put (‘/: id’, ()=> {//atualizar ingrediente}) IngredientesRouter.delete (‘/: id’, ()=> {//excluir ingrediente}) IngredientesRouter.post (‘/’, ()=> {//criar ingrediente}) export default IngredientesRouter

Nossa API de livro de receitas eventualmente terá várias rotas, como Receitas e Marcas. Portanto, devemos criar um arquivo index.ts para registrar as diferentes rotas para seus caminhos de base e ter uma exportação central para conectar ao nosso servidor Express.js anterior:

# src/api/routes/index.ts import {Router} de’express’importar ingredientesRouter de’./ingredients’const router=Router () router.use (‘/Ingredientes’, IngredientesRouter) exportar roteador padrão

Vamos atualizar nosso src/index.ts para importar nossos exportados rotas e registrá-las em nosso servidor Express.js:

# src/index.ts import express, {Application, Request, Response} from’express’import routes from’./api/routes’const app: Application=express ()… app.use (‘/api/v1’, routes)

Depois de criar e conectar as rotas, vamos criar um controlador para vincular às nossas rotas e chamar os métodos de serviço.

Para suportar a digitação de parâmetros e resultados entre as rotas e controladores, vamos adicionar objetos de transferência de dados (DTOs) e mapeadores para transformar os resultados:

# src/api/controllers/ingrediente/index.ts import * as service from’../../../db/services/IngredientService’import {CreateIngredientDTO, UpdateIngredientDTO, FilterIngredientsDTO} de’../../dto/ingredient.dto’import {Ingredient} de’../../interfaces’import * as mapper from’./mapper’export const create=async (payload: CreateIngredientDTO): Promise => {return mapper.toIngredient (await service.create (payload)) } export const update=async (id: number, payload: UpdateIngredientDTO): Promise => {return mapper.toIngredient (await service.update (id, payload))} export const getById=async (id: number): Promessa => {return mapper.toIngredient (await service.getById (id))} export const deleteById=async (id: number): Promise => {const isDeleted=await service.deleteById (id) return isDeleted} export const getAll=async (filters: FilterIngredientsDTO): Promise => {return (await service.getAll (filters)). map (mapper.toIngredient)}

Não w, atualize o roteador com as chamadas para o controlador:

# src/api/routes/components.ts import {Router, Request, Response} from’express’import * as ingredienteController from’../controllers/ingredient’import {CreateIngredientDTO, FilterIngredientsDTO, UpdateIngredientDTO} from’../dto/ingredient.dto’const ingredienteRouter=Router () componentsRouter.get (‘:/id’, assíncrono (req: Request, res: Response)=> {const id=Number (req.params.id) const result=await ingredienteController.getById (id) return res.status (200).send (result)}) componentsRouter.put (‘/: id’, assíncrono (req: Request, res: Response)=> {const id=Number (req.params.id) const payload: UpdateIngredientDTO=req.body const result=await ingredienteController.update (id, payload) return res.status (201).send (result) }) ingredientesRouter.delete (‘/: id’, assíncrono (req: Solicitar, res: Resposta)=> {const id=Número (req.params.id) const result=esperar ingredienteController.deleteById (id) return res.status (204).send ({sucesso: resultado})}) ingredientesRouter.post (‘/’, async (req: Request, res: Response)=> {carga útil const: CreateIngredientDTO=req.body const result=esperar ingredienteController.create (carga) return res.status (200).send (resultado)}) ingredientesRouter.get (‘/’, assíncrono (req: Solicitar, res: Resposta)=> {filtros const: FilterIngredientsDTO=req.query const results=await ingredienteController.getAll (filters) return res.status (200).send (results)}) export default IngredientesRouter

Neste ponto, podemos adicionar um script de construção para executar nossa API:

# package.json…”scripts”: {“dev”:”nodemon src/index.ts”,”build”:”npx tsc”},…

Para ver o produto final, execute a API usando yarn execute dev e visite nossos endpoints de ingredientes em http://localhost: 3000/api/v1/Ingredientes .

Conclusão

Neste artigo, configuramos um aplicativo TypeScript simples com Expres s.js para usar o Sequelize ORM e percorreu a inicialização do Sequelize, a criação de nossos modelos e a execução de consultas por meio do ORM.

Usar o Sequelize com TypeScript em nosso projeto nos ajuda a escrever menos código e abstrair o mecanismo de banco de dados enquanto definindo tipos estritos para entrada e saída do modelo. Isso torna nosso código mais consistente, mesmo se alterarmos os tipos de banco de dados, e pode evitar a ocorrência de injeção de SQL em nossas tabelas.

Todo o o código deste artigo está disponível no Github . Espero que você tenha achado este artigo fácil de seguir e eu adoraria ouvir suas idéias sobre maneiras legais de usar o Sequelize em seu aplicativo ou qualquer dúvida que você tenha na seção de comentários!