Monorepo - Quasar V2 + NestJS

tobymosque

Tobias Mesquita

Posted on April 13, 2021

Monorepo - Quasar V2 + NestJS

1 - Introdução

Este artigo tem como objetivo introduzir um novo recurso do Quasar V2, Middlewares para SSR, este recurso nos permite extender/configurar a instancia do ExpressJS de forma modular, assim como já fazíamos com os boots.

Como caso de uso, iremos criar um Yarn Monorepo, onde o frontend vai aproveitar todo o poder do Quasar CLI, e o backend irá aproveitar tudo o que o seu respectivo cliente tenha a oferecer e a ponte entre ambos será um SSR Middleware.

Desta forma, o frontend e o backend irão ser executados no mesmo Nó (Node), porém é importante que o backend não tenha nenhuma dependência adicional para com o frontend, se mantendo completamente desacoplado, desta forma, a qual quer momento poderemos alternar entre ser executado no seu próprio Nó (Node) ou como um simbionte do frontend.

Para este laboratório, estaremos usando o NestJS, mas pode ser usado qual quer framework que possa ser montado sobre o ExpressJS, como por exemplo o FeathersJS.

2 - Yarn Monorepo

Para esta etapa, precisamos certificar que o NodeJS esteja instalado, de preferencia a versão LTS, caso esteja usando a versão Current, pode ser que enfrente problemas inesperados, seja agora ou no futuro.

Caso não o tenha, recomendo que instale através do NVM, segue os links para o NVM Linux/Mac e para o NVM Windows.

Claro, não deixe de instalar todos os command cli que iremos está utilizando:

npm i -g yarn@latest
npm i -g @quasar/cli@latest
npm i -g @nestjs/cli@latest
npm i -g concurrently@latest
Enter fullscreen mode Exit fullscreen mode

Alt Text

E agora crie os seguintes arquivos na raiz do projeto:

./package.json

{
  "private": true,
  "workspaces": {
    "packages": ["backend", "frontend"]
  },
  "scripts": {}
}
Enter fullscreen mode Exit fullscreen mode

./.gitignore

.DS_Store
.thumbs.db
node_modules

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
Enter fullscreen mode Exit fullscreen mode

./.gitmodules

[submodule "backend"]
path = backend
url = git@github.com:${YOUR_USER}/${YOUR_BACKEND_REPO}.git

[submodule "frontend"]
path = frontend
url = git@github.com:${YOUR_USER}/${YOUR_FRONTEND_REPO}.git
Enter fullscreen mode Exit fullscreen mode

Não deixe de modificar o YOUR_USER, YOUR_BACKEND_REPO e o YOUR_FRONTEND_REPO para apontarem para o seu próprio repositório, isto claro, se pretende visionar este projeto.

Alt Text

3 - Backend Project - NestJS

Agora iremos criar o projeto do backend, para tal execute:

nest new backend
Enter fullscreen mode Exit fullscreen mode

Segue as opções selecionadas:

? Which package manager would you ❤️ to use? yarn
Enter fullscreen mode Exit fullscreen mode

Alt Text

Note que temos dois node_modules, um na raiz do monorepo e outro no projeto backend, no node_modules do monorepo é onde é instalado a maioria das nossas dependências.

por fim, adicione alguns scripts ao ./package.json na raiz do monorepo:

{
  "private": true,
  "workspaces": {
     "packages": ["backend", "frontend"]
  },
  "scripts": {
    "backend:dev": "yarn workspace backend build:dev",
    "backend:build": "yarn workspace backend build",
    "backend:start": "yarn workspace backend start",
    "postinstall": "yarn backend:build"
  }
}
Enter fullscreen mode Exit fullscreen mode

Então execute:

yarn backend:start
Enter fullscreen mode Exit fullscreen mode

Alt Text

E acesse http://localhost:3000
Alt Text

4 - Backend Project - OpenAPI

A razão pela qual escolhi o NestJS para este laboratorio, é pela possibilidade de auto documentar a API com pouco ou nenhum esforço adicional. Porém pode usar qualquer outro Framework, o procedimento e os desafios devem ser bem semelhantes.

Caso prefira GraphQL à REST, você pode ignorar esta etapa, e então instalar os pacotes do NestJS para GraphQL.

Mas para tal, precisamos adicionar alguns pacotes:

yarn workspace backend add @nestjs/swagger swagger-ui-express
yarn workspace backend add --dev @types/terser-webpack-plugin
Enter fullscreen mode Exit fullscreen mode

Alt Text

Então modifique o arquivo main.ts em src/backend
./backend/src/main.ts

import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api');
  const config = new DocumentBuilder()
    .setTitle('Quasar Nest example')
    .setDescription('The cats API description')
    .setVersion('1.0')
    .addTag('cats')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api/docs', app, document);
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Por fim, execute o comando yarn backend:start e acesse http://localhost:3000/api/docs:
Alt TextAlt Text

5 - Preparar o Backend para integra-lo ao Frontend

Para esta etapa, iremos precisar criar um script no backend com uma assinatura semelhante ao do SSR Middleware que iremos criar no frontend e iremos mover boa parte da logica presente no main.ts para este novo script.

./backend/src/index.ts

import { Express, Request, Response } from 'express';
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

interface RenderParams {
  req: Request;
  res: Response;
}

interface ConfigureParams {
  app: Express;
  prefix: string;
  render?: (params: RenderParams) => Promise<void>;
}

export default async function bootstrap({
  app: server,
  prefix,
  render,
}: ConfigureParams) {
  const app = await NestFactory.create(AppModule, new ExpressAdapter(server));
  app.setGlobalPrefix(prefix);
  app.useGlobalFilters({
    async catch(exception, host) {
      const ctx = host.switchToHttp();
      const status = exception.getStatus() as number;
      const next = ctx.getNext();
      if (status === 404 && render) {
        const req = ctx.getRequest<Request>();
        const res = ctx.getResponse<Response>();
        await render({ req, res });
      } else {
        next();
      }
    },
  });
  const config = new DocumentBuilder()
    .setTitle('Quasar Nest example')
    .setDescription('The cats API description')
    .setVersion('1.0')
    .addTag('cats')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup(`${prefix}/docs`, app, document);
  return app;
}
Enter fullscreen mode Exit fullscreen mode

E claro, modifique o main.ts:
./backend/src/index.ts

import configure from './index';
import * as express from 'express';

async function bootstrap() {
  const app = express();
  const nest = await configure({ app, prefix: 'api' });
  await nest.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Alt Text

Feito isto, acesse novamente http://localhost:3030/api/docs e veja se está tudo em ordem.

Então, precisamos alterar o package.json em backend, adicionando um script em scripts.

{
  "main": "dist/index.js",
  "scripts": {
    "build:dev": "nest build --watch"
  }
}
Enter fullscreen mode Exit fullscreen mode

Caso esteja a utilizar o Quasar V1, então temos uma incopatibilidade de versões entre o Webpack usado pelo Quasar e o do NestJS, neste caso precisamos configurar o nohoist no package.json > workspaces:

{
  "main": "dist/index.js",
  "scripts": {
    "build:dev": "nest build --watch"
  },
  "workspaces": {
    "nohoist": [
      "*webpack*",
      "*webpack*/**"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

Nós precisamos deste script, pois a configuração do Typescript no frontend é diferente do backend, então o Quasar CLI não será capaz de transpilar do backend, desta forma o frontend irá fazer uso de um arquivo já transpilado (dist/index.js)

precisamos adicionar esta configuração de nohoist ao backend, pois as versões do webpack e dos plugins utilizados pelo Quasar CLI podem ser diferentes das utilizadas pelo NestJS CLI.

por fim, caso revisite o arquivo ./package.json, verá que tem um script de postinstall, ele é necessário para garantir que será feito um build do backend antes de tentar executar o frontend.

6 - Projeto do Frontend - Quasar

Assim como fizemos com o backend, precisamos criar um projeto, para tal, iremos utilizar o quasar cli:

# note que durante a elaboração deste artigo, o Quasar V2 ainda estava em beta, por isto se faz necessário o `-b next`
quasar create frontend -b next
Enter fullscreen mode Exit fullscreen mode

Segue as opções selecionadas:

? Project name (internal usage for dev) frontend
? Project product name (must start with letter if building mobile apps) Quasar App
? Project description A Quasar Framework app
? Author Tobias Mesquita <tobias.mesquita@gmail.com>
? Pick your CSS preprocessor: Sass
? Check the features needed for your project: ESLint (recommended), TypeScript
? Pick a component style: Composition
? Pick an ESLint preset: Prettier
? Continue to install project dependencies after the project has been created? (recommended) yarn
Enter fullscreen mode Exit fullscreen mode

As únicas recomendações que faço aqui, é que use o Yarn e o Prettier

Alt Text

Então, adicione o modo ssr, e o backend como depedencia do frontend:

cd frontend
quasar mode add ssr
cd ..
yarn workspace frontend add --dev @types/compression
yarn workspace frontend add backend@0.0.1
Enter fullscreen mode Exit fullscreen mode

Alt Text

Caso os middlewares sejam criados como .js, você pode transforma-los em arquivos .ts (no momento em que este artigo foi escrito, não havia os templates para Typescript).:

./frontend/src-ssr/middlewares/compression.ts

import compression from 'compression'
import { ssrMiddleware } from 'quasar/wrappers'

export default ssrMiddleware(({ app }) => {
  app.use(
    compression({ threshold: 0 })
  )
})
Enter fullscreen mode Exit fullscreen mode

Por fim, altere o render.js para render.ts e faça que ele se conecte ao backend.

./frontend/src-ssr/middlewares/render.ts

import configure from 'backend'
import { ssrMiddleware } from 'quasar/wrappers'
import { RenderError } from '@quasar/app'

export default ssrMiddleware(async ({ app, render, serve }) => {
  const nest = await configure({
    app,
    prefix: 'api',
    async render ({ req, res }) {
      res.setHeader('Content-Type', 'text/html')

      try {
        const html = await render({ req, res })
        res.send(html)
      } catch (error) {
        const err = error as RenderError
        if (err.url) {
          if (err.code) {
            res.redirect(err.code, err.url)
          } else {
            res.redirect(err.url)
          }
        } else if (err.code === 404) {
          res.status(404).send('404 | Page Not Found')
        } else if (process.env.DEV) {
          serve.error({ err, req, res })
        } else {
          res.status(500).send('500 | Internal Server Error')
        }
      }
    }
  });
  await nest.init()
});
Enter fullscreen mode Exit fullscreen mode

Por fim, modifique o package.json > scripts do frontend e adicione os seguintes scripts:

{
  "scripts": {
    "dev": "quasar dev -m ssr",
    "build": "quasar build -m ssr"
  }
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

E para que possamos testar, modifique o package.json > scripts do monorepo:
./package.json

{
  "private": true,
  "workspaces": {
    "packages": ["backend", "frontend"]
  },
  "scripts": {
    "backend:dev": "yarn workspace backend build:dev",
    "backend:build": "yarn workspace backend build",
    "backend:start": "yarn workspace backend start",
    "frontend:dev": "yarn workspace frontend dev",
    "start": "yarn backend:start",
    "dev": "concurrently \"yarn backend:dev\" \"yarn frontend:dev\"",
    "postinstall": "yarn backend:build"
  }
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

Então execute:

yarn dev
Enter fullscreen mode Exit fullscreen mode

Então acesse http://localhost:8080 para verificar que o frontendestá funcionando, então http://localhost:8080/api/docs para verificar que o backend está em ordem.

Alt TextAlt Text

💖 💪 🙅 🚩
tobymosque
Tobias Mesquita

Posted on April 13, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Monorepo - Quasar V2 + NestJS