Criando aplicações standalone com NestJS (sem servidor HTTP)

Criando aplicações standalone com NestJS (sem servidor HTTP)

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.

Nota: este artigo supõe que você tenha conhecimento básico em TypeScript e saiba o que é o NestJS (embora não seja requerido experiência prévia). Caso precise, recomendo a leitura dos seguintes artigos antes: Introdução ao TypeScript - O que é, suas vantagens, e conceitos fundamentais e Por que você deveria estudar Nest.js?

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:

  1. Extrair o serviço do contexto criado com o NestFactory.createApplicationContext

  2. 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.

💡
Leia mais sobre uso de TypeScript com AWS Lambda aqui.

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!