Monorepo - Quasar V2 + NestJS
Tags: vue, monorepo, quasarframework, nestjs
Published: 2021-04-13 15:09:07

#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](https://github.com/nvm-sh/nvm) e para o [NVM Windows](https://github.com/coreybutler/nvm-windows).

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

```bash
npm i -g yarn@latest
npm i -g @quasar/cli@latest
npm i -g @nestjs/cli@latest
npm i -g concurrently@latest
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zzg14n5gsb0edd89mone.png)

E agora crie os seguintes arquivos na raiz do projeto:

**./package.json**
```json
{
  "private": true,
  "workspaces": {
    "packages": ["backend", "frontend"]
  },
  "scripts": {}
}
```
**./.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
```
**./.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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/908q1u7cgcr8almllrq4.png)

#3 - Backend Project - NestJS

Agora iremos criar o projeto do backend, para tal execute:
```bash
nest new backend
```

Segue as opções selecionadas:
```html
? Which package manager would you ❤️ to use? yarn
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2bkwllgijztkrrhg50ij.png)

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`:

```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",
    "postinstall": "yarn backend:build"
  }
}
```

Então execute:
```bash
yarn backend:start
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bc7swvz4934l3pt0ougx.png)

E acesse http://localhost:3000
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ebelwwa3o6r4dye3sd2k.png)

#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:
```bash
yarn workspace backend add @nestjs/swagger swagger-ui-express
yarn workspace backend add --dev @types/terser-webpack-plugin
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xzge1yjqfbmea9nl5v0z.png)

Então modifique o arquivo `main.ts` em `src/backend`
**./backend/src/main.ts**
```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();
```

Por fim, execute o comando `yarn backend:start` e acesse `http://localhost:3000/api/docs`:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ql56g1bx4km45nykk1wi.png)![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1n394w2ujmxccovex6tb.png)

#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**
```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;
}
```

E claro, modifique o `main.ts`:
**./backend/src/index.ts**
```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();
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oisvp3n3qmxys3qu9mqo.png)

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

```json
{
  "main": "dist/index.js",
  "scripts": {
    "build:dev": "nest build --watch"
  }
}
```

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*/**"
    ]
  }
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yksal51lej1ok3uhc40g.png)

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:
```bash
# 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
```
Segue as opções selecionadas:
```bash
? 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
```
As únicas recomendações que faço aqui, é que use o `Yarn` e o `Prettier`

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y4twabgz8lsnuubbjkvr.png)

Então, adicione o modo `ssr`, e o backend como depedencia do frontend:
```bash
cd frontend
quasar mode add ssr
cd ..
yarn workspace frontend add --dev @types/compression
yarn workspace frontend add backend@0.0.1
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yewilius4jhm861ft4tr.png)

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**
```ts
import compression from 'compression'
import { ssrMiddleware } from 'quasar/wrappers'

export default ssrMiddleware(({ app }) => {
  app.use(
    compression({ threshold: 0 })
  )
})
```

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

**./frontend/src-ssr/middlewares/render.ts**
```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()
});
```

Por fim, modifique o `package.json > scripts` do `frontend` e adicione os seguintes scripts:
```json
{
  "scripts": {
    "dev": "quasar dev -m ssr",
    "build": "quasar build -m ssr"
  }
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ombzg8hzmpwhtlcz90c.png)

E para que possamos testar, modifique o `package.json > scripts` do monorepo:
**./package.json**
```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"
  }
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0enc3l23ewjc0jml2vtr.png)

Então execute:
```
yarn dev
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/442eonzlax0pc8dsw38w.png)![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xyc0hr1yr6ez4gzfw6f0.png)