Providers são mais um exemplo de como Nest decidiu lidar com várias soluções em uma: se pensarmos em injeção de dependência, os providers nos solucionam. Se olharmos pra inversão de controle, os providers solucionam.
Classes que são atribuidas como providers serão injetadas em tempo de execução, então nosso Controller pode ser um provider, assim como nosso Repositório ou Service, iremos focar nesse último.
Quem já lidou provavelmente entende injeção de dependência como um ponto minimamente complexo e muitas vezes fundamental. Usar uma implementação com TSyringe ou Inversify e controlar dependências e as injeções dessas é algo de dia-a-dia com JS.
Mas o Nest traz uma forma simples e muito mais amigável, principalmente se for sua primeira vez com injeção de dependência e inversão de controle.
Teremos uma estrutura de pastas parecida com essa com os arquivos que iremos criar a seguir.
Obs: não será necessário seguir essa estrutura, mas para fácil compreensão nesse artigo, recomendo que siga.
📁 src/
│ └── 📄 app.module.ts
│
├── 📁 categoria/
│ ├── 📄 categoria.controller.ts
│ ├── 📄 categoria.module.ts
│ └── 📄 categoria.service.ts
│
└── 📁 jogo/
├── 📄 jogo.controller.ts
├── 📄 jogo.module.ts
└── 📄 jogo.service.ts
Teremos uma classe JogoService
, que será anotada com o decorator @Injectable()
, o que possibilatará que tenhamos uma instância dela em tempo de execução que poderá ser injetada em quem utilzar.
Nosso JogoService terá apenas um método .criar()
, que irá salvar um payload a um array de jogos.
Podemos representar com:
jogo.service.ts
interface Jogo {
nome: string;
descricao: string
categoria: string
}
@Injectable()
class JogoService {
private jogos: Jogo[] = [];
criar (jogoDto: Jogo): void {
const existeJogo = this.jogos.some((jogo) => jogo.nome === jogoDto.nome)
if (existeJogo ) {
return;
}
this.jogos.push(jogoDto);
}
}
Aprendemos no conteúdo anterior como lidar com os controllers, e vamos utilizar um controller para disparar esse método JogoService.criar
. Aqui veremos a "mágica" do Nest para injeção de dependência. Sem @Inject
(NestJS/Inversify/TSyringe), sem @Autowired
(Spring) ou qualquer injeção explicita que vemos por ai, uma instância de JogoService é injetada em tempo de execução, já que é marcada com o decorator @Injectable e está no mesmo módulo.
Aqui o que faremos é apenas declarar uma propriedade privada jogoService e inferir seu tipo como JogoService. Faremos isso no controller para que a dependencia (nosso provider JogoService) seja injetada ao instanciar a classe.
Obs: irei omitir nosso método do controller, pois não é o foco e foi visto no tema anterior.
jogo.controller.ts
import { JogoService } from './jogo.service'
@Controller("jogo")
class JogoController {
constructor (
private jogoService: JogoService;
) {}
@Post()
async criar (...) {...}
}
Toda classe com o decorator Injectable() terá uma instância dela injetada de forma "automática" em tempo de execução caso no mesmo módulo. Caso em outro módulo, será necessário exportar como provider e importar no módulo que utilizará, mas o resultado final será o mesmo: a instância da classe será injetada de forma "automática" em tempo de execução.
No mesmo módulo:
jogo.module.ts
import { Module } from '@nestjs/common';
import { JogoService } from './jogo.service'
import { JogoController } from './jogo.controller'
@Module({
controllers: [JogoController],
providers: [JogoService],
})
export class JogoModule {}
Módulos diferentes:
Vamos pensar em um caso: teremos um método .consultarPorCategoria()
em nosso JogoService
, e teremos um CategoriaService com apenas um método .buscar()
. Esse método checa se existe uma categoria semelhante a buscada (num array de categorias), em caso negativo retorna uma string avisando que não encontrou categoria, em caso positivo buscamos jogos da categoria no JogoService.
Nossa ideia aqui não é estudar exatamente separação de contextos, então use apenas como um exemplo para explicar provider externos ao módulo.
Obs: irei omitir nosso método do controller, pois não é o foco e foi visto no tema anterior.
categoria.controller.ts
import { CategoriaService } from './categoria.service'
@Controller("categoria")
class CategoriaController {
constructor (
private categoriaService: CategoriaService;
) {}
@Get()
async buscar (...) {...}
}
categoria.service.ts
import { JogoService } from './jogo.service'
@Injectable()
class CategoriaService {
constructor (
private jogoService: JogoService;
) {}
private categorias: string[] = [];
async buscar (categoriaParaBuscar: string) {
const categoria = this.categorias.find((categoria) => categoria === categoriaParaBuscar);
if(!categoria) {
return 'Categoria não encontrada';
}
const jogosDaCategoria = this.jogoService.consultarPorCategoria(categoria);
return {
categoria,
jogos: jogosDaCategoria
}
}
}
Para acessarmos JogoService
a partir do CategoriaService
(módulo de categoria) precisaremos adicionar nosso JogoService
aos exports do JogoModule
e nos imports no CategoriaModule
jogo.module.ts
import { Module } from '@nestjs/common';
import { JogoService } from './jogo.service'
import { JogoController } from './jogo.controller'
@Module({
controllers: [JogoController],
providers: [JogoService],
exports: [JogoService]
})
export class JogoModule {}
categoria.module.ts
import { Module } from '@nestjs/common';
import { CategoriaService } from './categoria.controller'
import { CategoriaController } from './categoria.controller'
import { JogoService } from './jogo/jogo.service'
@Module({
controllers: [CategoriaController],
providers: [CategoriaService],
imports: [JogoService]
})
export class CategoriaModule {}
E para fechar, iremos declarar nossos módulos no AppModule, nosso módulo global.
app.module.ts
import { Module } from '@nestjs/common';
import { CategoriaModule } from './categoria/categoria.module.ts'
import { JogoModule } from './jogo/jogo.module.ts'
@Module({
imports: [CategoriaModule, JogoModule],
})
export class AppModule {}