Se você já conhece o framework NestJS, tendo usado ou não, deve saber das facilidades que NestJS oferece em comparação a aplicações utilizando apenas o Express.js (o qual o próprio NestJS usa por baixo dos panos). Criar uma API se torna muito mais fácil utilizando os decorators do NestJS:
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
No código acima criamos um controller para o endpoint /cats
com o método findAll
para receber requisições do verbo GET
.
Resumindo a história longa, o NestJS é perfeito para criação de APIs simples e complexas (e não só APIs). Quando uma complexidade adicional é necessária, o NestJS se mostra altamente performático e amigo do código limpo trazendo design patterns como singleton, strategy, decorators e a maravilhosa injeção de dependências.
Para quem preza pela legibilidade e organização do código, é paixão à primeira vista. Que bom seria se pudéssemos utilizar o NestJS para aplicações em geral, sem serem APIs nem envolverem HTTP servers, não? Este artigo traz as boas notícias: é perfeitamente possível — e sem gambiarras.
Standalone mode do NestJS
O que nos permite construir e executar uma aplicação com quase todo o mesmo fluxo e mesmas facilidades do NestJS sem envolver o HTTP server é o modo Standalone do NestJS. Esse modo é basicamente o que foi descrito até então: o NestJS sem HTTP server. Porém, não só sem o HTTP server. Segundo a documentação, nós também perdemos os facilitadores relacionados ao HTTP server, como por exemplo: guards, interceptors e pipes (elementos que não utilizaremos no caso simples que construiremos neste artigo).
Como funciona? Como utilizar?
Uma aplicação convencional com NestJS é organizada em módulos, serviços e controllers, tendo um script principal que construirá o ambiente pro NestJS e iniciará o servidor HTTP. Com o Standalone mode não é muito diferente. A estrutura de módulos e serviços é mantida. Os controllers, em sua essência, não precisaremos mais, embora você ainda possa utilizá-los para mapear para outros meios de transporte de dados (como por exemplo, WebSockets), criando manualmente a sua devida strategy. Mas isso é assunto para outro artigo.
Vejamos abaixo a instanciação de uma aplicação convencional com NestJS:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
Em termos de código, a única diferença (além de que não chamaremos mais o app.listen()
) é que em vez do método NestFactory.create
, usaremos o método NestFactory.createApplicationContext
. Voilà! Criamos nossa aplicação standalone com NestJS:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule);
}
bootstrap();
Mas é claro que não é só isso. O código acima apenas cria a nossa aplicação e não faz mais nada. Como não há mais recebimento de dados via HTTP e processamento das requisições pelos controllers, precisamos manualmente selecionar o serviço que queremos utilizar para processar a entrada (se houver alguma) e passar a entrada em si. Isso é feito de forma simples em duas etapas:
Extrair o serviço do contexto criado com o
NestFactory.createApplicationContext
Executar o método desejado do serviço, passando a entrada desejada
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AppService } from './app.service';
async function handler(inputData: any) {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
return appService.process(inputData);
}
handler('Hello');
Agora sim! O nosso código cria o contexto do NestJS, extrai um serviço (AppService
) de lá de dentro e processa a entrada (inputData
) que recebemos ao executar a função handler
. Veja o contexto como a aplicação NestJS em estado inerte, sem poder de executar nada sozinha, apenas esperando para ser usada.
O que acontece ao criar o contexto é que o NestJS vai percorrer recursivamente toda a estrutura da sua aplicação partindo a partir do módulo especificado (no nosso caso, o AppModule
) e vai deixar carregado tudo o que ele encontrar, incluindo módulos importados pelo módulo especificado.
No código acima, nosso handler espera uma entrada no inputData
. Entrada que foi preenchida com a string "Hello"
. Mas não necessariamente precisamos ter uma entrada para executar nossa aplicação standalone:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AppService } from './app.service';
async function handler() {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
return appService.process();
}
handler();
Se seu AppService
não estiver requerindo nenhum argumento, não há motivos pra se preocupar.
O método handler
acima está servindo apenas para encapsular a criação e execução da nossa aplicação em uma função async
, pois precisamos executar o método NestFactory.createApplicationContext
, que também é assíncrono. Nada nos impede de utilizar o .then()
e .catch()
, mas utilizar async
e await
é mais legível.
Repare que a nossa função handler
está extraindo apenas um serviço para servir de ponto inicial da execução da nossa aplicação: o AppService
. Mas nada nos impede também de extrairmos vários services e escolhermos qual queremos executar baseado na entrada. Ou mesmo, executar mais de um serviço para processar a entrada (que continua sendo opcional, não se preocupe).
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CatService } from './cat.service';
import { DogService } from './dog.service';
async function handler(animalInfo: any) {
const appContext = await NestFactory.createApplicationContext(AppModule);
const catService = appContext.get(CatService);
const dogService = appContext.get(DogService);
switch (animalInfo.animal) {
case 'cat':
return catService.process(animalInfo);
case 'dog':
return dogService.process(animalInfo);
default:
throw new Error('Unknown animal: ' + animalInfo.animal);
}
}
const cururuInfo = {
name: "Cururu",
animal: "cat",
age: 2,
}
handler(cururuInfo);
Desta vez, em vez de extrairmos apenas o AppService
do contexto, extraímos o CatService
e DogService
e fizemos um bloco switch
para decidir qual serviço executar, lançando um erro caso não encontremos um serviço mapeado para o animal.
Standalone mode em Lambdas
Esse foi o cenário que me levou a conhecer e aprender sobre o modo standalone do NestJS. Eu precisava criar uma aplicação legível e bem construída seguindo design patterns. Sem hesitar, optei por utilizar TypeScript, que não é suportado diretamente pela AWS mas que é possível utilizar o código JavaScript transpilado. Não sabia que o NestJS poderia ser utilizado também, mas acabou que o resultado se mostrou muito melhor do que o imaginado.
A lógica não é diferente do que já vimos até então, nós só vamos adaptar para o cenário da Lambda, onde precisamos exportar uma função que receba o evento e retorne o resultado da execução (se você precisar de tal):
import { NestFactory } from '@nestjs/core';
import { Callback, Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';
import { AppService } from './app.service';
export const handler: Handler = async (
event: any,
context: Context,
callback: Callback,
) => {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
return appService.process(event);
};
No código acima, deixamos de chamar manualmente nossa função handler
. Agora estamos exportando ela para a Lambda e nosso parâmetro inputData
para receber a entrada a ser processada se tornou event
. De resto, a lógica é a mesma: criamos o contexto, extraímos um serviço e executamos o serviço, usando seu resultado como retorno da função handler
.
Conclusão
Neste artigo aprendemos sobre como criar aplicações NestJS que não são executadas através de requisições HTTP e visitamos diversos exemplos de utilização de services para processar uma entrada, independente do que vier e de onde ela vier, permitindo a utilização de tais aplicações até em Lambdas da AWS.
Espero que este artigo tenha lhe ajudado a entender sobre o que é o maravilhoso standalone mode do NestJS e quem sabe lhe convencido a usar NestJS para mais finalidades além da criação de HTTP servers. Obrigado pela leitura!