Neste artigo, vamos explorar a criação de um repositório genérico de leitura no contexto do NestJS, uma estrutura de aplicativo Node.js para construção de aplicativos escaláveis e eficientes. Utilizaremos Generics, TypeORM (um popular ORM para TypeScript/JavaScript) e SQLite (um banco de dados leve e eficiente) para criar um repositório de leitura flexível e reutilizável.
Iniciando um Projeto NestJS 🚀
Para começar um novo projeto NestJS, certifique-se de ter o Node.js e o npm instalados em sua máquina. Em seguida, abra o terminal e execute os seguintes comandos:
# Instale o NestJS CLI globalmente npm install -g @nestjs/cli # Crie um novo projeto NestJS nest new nome-do-projeto
Isso criará uma estrutura básica para o seu projeto NestJS. Navegue até o diretório recém-criado usando cd nome-do-projeto e continue com as próximas etapas da implementação do repositório genérico de leitura.
Agora, vamos começar a implementar o repositório genérico de leitura usando Generics, TypeORM e SQLite. Fique à vontade para adicionar seu código conforme avançamos no desenvolvimento.
Instalando SQLite e TypeORM no NestJS
Para o nosso projeto NestJS, vamos utilizar o SQLite como banco de dados e o TypeORM como ORM para interagir com o banco de dados. Siga os passos abaixo para configurar essas dependências:
1 - Instalando o SQLite:
- O SQLite é um banco de dados leve e eficiente, perfeito para ambientes de desenvolvimento. Para instalá-lo, utilize o seguinte comando:
npm install --save sqlite3
2 - Instalando o TypeORM:
- O TypeORM é um ORM (Object-Relational Mapping) para TypeScript e JavaScript. Ele facilita a interação com o banco de dados SQLite. Instale o TypeORM usando o comando:
npm install --save @nestjs/typeorm typeorm
3 - Configurando a Conexão com o Banco de Dados:
- No seu projeto NestJS, vá até o arquivo app.module.ts e importe os módulos do TypeORM e SQLite. Adicione a configuração da conexão ao banco de dados. Seu arquivo app.module.ts pode se parecer com algo assim:
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: 'db/sql', synchronize: true, entities: [User], // já vamos criar a entidade }), ], }) export class AppModule {}
4 - Criando uma Entidade:
- No diretório do seu projeto, crie uma pasta entities. Dentro dessa pasta, crie um arquivo user.entity.ts para representar a entidade do usuário. Exemplo:
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; }
5 - Criando a tabela e inserindo o dado:
- Rode o comando:
npm run start
Com isso deve criar o arquivo db/sql.
- Acesse o banco, no terminal rode o comando abaixo e entrará dentro do sqlite:
sqlite3 db/sql
- Faça um insert na base para ter as informações para recuperar:
INSERT INTO user (name,email) VALUES ('Jhones', 'email@dominio.com');
Criando uma Pasta para ViewModels
Para manter uma estrutura organizada e reutilizável em nosso projeto, vamos criar uma pasta chamada viewmodels
. Nela, teremos um arquivo chamado base.viewmodel.ts
que conterá a classe BaseViewModel
. Esta classe fornecerá um modelo base para as ViewModels do nosso sistema.
-
Criando a Pasta e o Arquivo:
- No diretório do seu projeto, crie uma nova pasta chamada
viewmodels
. Dentro dessa pasta, crie um arquivo chamadobase.viewmodel.ts
.
- No diretório do seu projeto, crie uma nova pasta chamada
-
Conteúdo do
base.viewmodel.ts
:- Adicione o seguinte conteúdo ao arquivo
base.viewmodel.ts
:
- Adicione o seguinte conteúdo ao arquivo
export default class BaseViewModel<T extends Record<string, any>> { constructor(data?: Partial<T>) { this.initializeDefaultValues(); Object.assign(this, data); } private initializeDefaultValues(): void { const prototype = Object.getPrototypeOf(this); const properties = Object.getOwnPropertyNames(prototype); for (const property of properties) { const defaultValue = this[property]; if (defaultValue !== undefined) { this[property] = defaultValue; } } } }
Esta classe BaseViewModel aceita um objeto parcial durante a inicialização e preenche suas propriedades com valores padrão, garantindo uma consistência em todas as ViewModels.
Agora, estamos prontos para utilizar a BaseViewModel como base para as ViewModels específicas do nosso sistema. Isso proporcionará uma estrutura coesa e reutilizável para representar dados no nosso contexto.
Adicionando um Decorator DefaultValue
Decorators são uma poderosa ferramenta em TypeScript, permitindo-nos adicionar metadados e comportamentos adicionais às nossas classes e propriedades. Vamos criar um decorator chamado DefaultValue
que garantirá valores padrão para propriedades específicas em nossas ViewModels.
-
Criando a Pasta e o Arquivo:
- No diretório do seu projeto, crie uma nova pasta chamada
decorators
. Dentro dessa pasta, crie um arquivo chamadodefault-value.decorator.ts
.
- No diretório do seu projeto, crie uma nova pasta chamada
-
Conteúdo do
default-value.decorator.ts
:- Adicione o seguinte conteúdo ao arquivo
default-value.decorator.ts
:
- Adicione o seguinte conteúdo ao arquivo
export default function DefaultValue(defaultValue: any) { return function (target: any, propertyKey: string) { let value = defaultValue; Object.defineProperty(target, propertyKey, { get() { return value; }, set(newValue) { value = newValue !== undefined ? newValue : defaultValue; }, enumerable: true, configurable: true, }); }; }
Este decorator DefaultValue permite definir um valor padrão para propriedades específicas em uma classe. Se a propriedade for configurada como undefined, o valor padrão será aplicado.
Criando uma ViewModel para Usuário
Agora que temos nossa classe base BaseViewModel
, podemos criar ViewModels específicas para diferentes entidades do sistema. Vamos começar com uma UserViewModel
que representa um usuário.
-
Criando o Arquivo
user.viewmodel.ts
:- Dentro da pasta
viewmodels
, crie um arquivo chamadouser.viewmodel.ts
.
- Dentro da pasta
-
Conteúdo do
user.viewmodel.ts
:- Adicione o seguinte conteúdo ao arquivo
user.viewmodel.ts
:
- Adicione o seguinte conteúdo ao arquivo
import DefaultValue from 'src/decorators/default-value.decorator'; import BaseViewModel from './base.viewmodel'; export default class UserViewModel extends BaseViewModel<UserViewModel> { constructor(data?: Partial<UserViewModel>) { super(data); } @DefaultValue('') name: string; @DefaultValue('') email: string; }
Nesta UserViewModel, estamos estendendo a BaseViewModel e definindo duas propriedades: name e email. Além disso, estamos utilizando um decorator @DefaultValue('') que é necessário para quando inicializar a classe o repositório reconheça as propriedades.
Criando uma Pasta para Repositórios
Agora, vamos organizar nossos repositórios em uma pasta chamada repositories
. Dentro dessa pasta, criaremos um arquivo chamado read.repository.ts
que conterá nosso repositório genérico de leitura.
-
Criando a Pasta e o Arquivo:
- No diretório do seu projeto, crie uma nova pasta chamada
repositories
. Dentro dessa pasta, crie um arquivo chamadoread.repository.ts
.
- No diretório do seu projeto, crie uma nova pasta chamada
-
Conteúdo do
read.repository.ts
:- Adicione o seguinte conteúdo ao arquivo
read.repository.ts
:
- Adicione o seguinte conteúdo ao arquivo
import { InjectEntityManager } from '@nestjs/typeorm'; import BaseViewModel from 'src/viewmodels/base.viewmodel'; import { EntityManager } from 'typeorm'; export class ReadRepository { constructor( @InjectEntityManager() private readonly entityManager: EntityManager, ) {} public async getAllOrBy<TModel extends BaseViewModel<TModel>>( modelType: new () => TModel, query: string, parameters?: any[], ): Promise<TModel[]> { const result = await this.entityManager.query(query, parameters); return result.map((row: any) => this.mapper<TModel>(row, modelType)); } public async getOne<TModel extends BaseViewModel<TModel>>( modelType: new () => TModel, query: string, parameters?: any[], ): Promise<TModel> { const result = await this.entityManager.query(query, parameters); const resultMapped = result.map((row) => this.mapper<TModel>(row, modelType), ); return resultMapped[0] ?? null; } private mapper<TModel extends BaseViewModel<TModel>>( row: any, modelType: new () => TModel, ): TModel { const instance = new modelType(); const prototype = Object.getPrototypeOf(instance); Object.keys(prototype).forEach((key) => { prototype[key] = row[key]; }); return prototype; } }
Este arquivo contém nosso repositório genérico de leitura (ReadRepository). Ele utiliza o TypeORM e o SQLite para realizar operações de leitura no banco de dados e mapeia os resultados para as ViewModels correspondentes.
Agora, estamos prontos para utilizar o ReadRepository em nosso serviço para interações de leitura com o banco de dados.
Utilizando o Repositório Genérico no Controller
Agora que temos nosso repositório genérico de leitura, podemos integrá-lo ao nosso controlador (AppController
). Vamos fazer algumas alterações no app.controller.ts
para utilizar o ReadRepository
e realizar operações de leitura no banco de dados.
- Conteúdo Atualizado do
app.controller.ts
:- Altere o conteúdo do arquivo
app.controller.ts
para o seguinte:
- Altere o conteúdo do arquivo
import { Controller, Get, Param } from '@nestjs/common'; import { ReadRepository } from './repositories/read.repository'; import UserViewModel from './viewmodels/user.viewmodel'; @Controller() export class AppController { constructor(private readonly readRepository: ReadRepository) {} @Get('/users') async getUsers() { const query = 'SELECT * FROM user'; const users = await this.readRepository.getAllOrBy<UserViewModel>( UserViewModel, query, ); return users; } @Get('/users/:id') async getUser(@Param('id') id: number) { // pegar o id da rota const query = 'SELECT * FROM user WHERE id = ?'; const users = await this.readRepository.getOne<UserViewModel>( UserViewModel, query, [id], ); return users; } }
Neste arquivo atualizado, utilizamos o método getAllOrBy para obter todos os usuários e o método getOne para obter um usuário específico com base no ID fornecido na rota.
Essas alterações garantem que nosso controlador utilize o repositório genérico para interações de leitura com o banco de dados SQLite. Continue a implementação do seu projeto, ajustando conforme necessário para atender aos requisitos específicos.
Configurando o Módulo para Injeção de Dependência
Para garantir que o ReadRepository
seja injetado corretamente no AppController
, é necessário fazer algumas configurações no módulo principal (AppModule
). Vamos ajustar o arquivo app.module.ts
para incluir o ReadRepository
como um provedor.
- Conteúdo Atualizado do
app.module.ts
:- No arquivo
app.module.ts
, atualize o conteúdo para incluir oReadRepository
como um provedor:
- No arquivo
@Module({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: 'db/sql', synchronize: true, entities: [User], }), TypeOrmModule.forFeature([User]), ], controllers: [AppController], providers: [AppService, ReadRepository], }) export class AppModule {} export class AppModule {}
Essas alterações configuram o módulo para reconhecer e injetar corretamente o ReadRepository quando necessário.
Continue a desenvolver seu projeto e, conforme necessário, ajuste essas configurações para atender aos requisitos específicos do seu aplicativo.
Resultado: Personalizando o Retorno da API 🎨
Ao implementar o repositório genérico de leitura e mapear os resultados para objetos ViewModel, conseguimos personalizar o retorno da API de acordo com a estrutura definida no ViewModel. Notavelmente, o campo "id" foi omitido do resultado, proporcionando uma resposta mais alinhada com a estrutura desejada.
Exemplo de Retorno:
[ { "name": "Jhones", "email": "email@dominio.com" } ]
O mapeamento inteligente no repositório garante que apenas os campos definidos no ViewModel sejam apresentados no resultado final. Isso não apenas simplifica a resposta da API, mas também reforça a consistência nos dados retornados, contribuindo para uma experiência de desenvolvimento mais previsível.
Conclusão 🚀
A implementação de um repositório genérico de leitura com Generics, TypeORM e SQLite no NestJS oferece uma série de benefícios significativos para o desenvolvimento de aplicativos. Ao abstrair as operações de leitura do banco de dados para um repositório genérico, podemos obter ganhos notáveis em termos de reutilização de código, manutenção simplificada e maior coesão.
Principais Benefícios:
Reutilização de Código: O repositório genérico permite que operações de leitura comuns sejam encapsuladas e reutilizadas em todo o aplicativo, reduzindo a duplicação de código. 🔄
Manutenção Simplificada: Mudanças nas operações de leitura podem ser gerenciadas centralmente no repositório genérico, simplificando a manutenção e evitando alterações em várias partes do código. 🛠️
Coerência no Acesso aos Dados: Ao padronizar o acesso aos dados por meio do repositório genérico, promovemos uma abordagem consistente na recuperação de informações do banco de dados. 📊
No exemplo apresentado, utilizamos o NestJS, TypeORM e SQLite para criar um repositório genérico de leitura. Este repositório foi integrado a um controlador que realiza operações de leitura em uma entidade User
. Os benefícios proporcionados por essa implementação servem como uma base sólida para expandir e adaptar conforme as necessidades específicas do seu projeto.
Ao seguir as práticas apresentadas neste artigo, você estará apto a criar repositórios de leitura genéricos em seus próprios projetos, promovendo uma arquitetura mais limpa e modular.
Continue explorando e personalizando conforme necessário para atender aos requisitos específicos do seu aplicativo. 🌟
Espero que este artigo tenha sido útil na compreensão e implementação de repositórios genéricos de leitura no NestJS. Boas práticas de programação e sucesso no desenvolvimento do seu projeto! 🚀
Top comments (1)
Show demais, ficou muito fácil seguir esse tutorial. A escrita bem feita facilitou muito o entendimento dos conceitos aplicados