Setup inicial + Express
Tags: node, braziliandevs, typescript, tutorial
Published: 2021-01-10 03:14:46

Olá, sou Vitor Delfino e como muitos de vocês que cairam aqui, sou um dev.

Faz um tempo que tenho a vontade de começar a escrever alguns posts, mas nunca tomava essa iniciativa. Que em 2021 seja um pouco diferente.

Bora lá !

Após diversos projetos criados, tanto profissionalmente quanto para aprendizado, cheguei a uma estrutura interessante de projetos com Node e decidi compartilha-lo em alguns posts.

O que eu costumo utilizar:

- Node.js + Typescript
- Typeorm
- Jest

Com apenas isso, já é possível desenvolver muita coisa. 
Maaaas, para deixar o projeto de exemplo um pouco mais profissional, algo parecido com o que encontrariamos em uma empresa, vou escrever um exemplo mais completo, com testes mocados, documentação com swagger, separação das variáveis por ambiente e utilizar recursos externos com Docker (mongoDb, Redis, Rabbit, etc...) que é algo que sempre senti falta nos tutorias.

Neste primeiro post, irei montar toda a estrutura das pastas e configurar o ESLint, Commitlint, tsconfig.json e algumas outras coisinhas.

# Iniciando o projeto

Vamos lá, primeiramente criamos a pasta e inicializamos o projeto.

Eu costumo utilizar yarn.

```bash
mkdir example
cd example
yarn init -y
code .
```

E depois instalamos algumas dependências.

## Plugins e mais plugins

Primeiramente instalo o eslint e o inicio com as seguintes opções.

```bash
yarn add eslint -D
yarn eslint --init
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2bvrnirzjm2tig3wsa6m.png)

Como o eslint instala os pacotes utilizando npm, eu simplemente apago o arquivo package-lock.json e rodo o comando yarn.

E para incrementar as nossas regras, instalamos mais alguns plugins.

```
yarn add eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import-helpers eslint-plugin-prettier prettier typescript -D
```

E alteramos o eslint.json com as seguintes configurações.

```json
{
    "env": {
        "es2021": true,
        "node": true
    },
    "extends": [
        "airbnb-base",
        "plugin:@typescript-eslint/recommended",
        "prettier",
        "prettier/@typescript-eslint"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": 12,
        "sourceType": "module"
    },
    "plugins": [
        "@typescript-eslint",
        "prettier",
        "eslint-plugin-import-helpers"
    ],
    "rules": {},
    "settings": {
        "import/resolver": {
            "typescript": {
                "directory": "./tsconfig.json"
            },
            "node": {
                "paths": [
                    "src"
                ],
                "extensions": [
                    ".ts"
                ]
            }
        }
    }
}
```

## Configurando alguns arquivos

Próximo passo, configuramos o `tscofig.json`, `.prettierrc` e `.editorconfig`

`.preetierrc`
```json
{
  "singleQuote": true,
  "trailingComma": "es5",
  "prettier/prettier": [
    "error",
    {
      "endOfLine": "auto"
    }
  ]
}
```

`tsconfig.json`
```json
{
  "compilerOptions": {
    "target": "es2017",
    "lib": [
      "es2019.array"
    ],
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "removeComments": true,
    "strict": true,
    "sourceMap": true,
    "allowJs": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "resolveJsonModule": true,
    "forceConsistentCasingInFileNames": true,
    "suppressImplicitAnyIndexErrors": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@apps/*": [
        "./src/apps/*"
      ],
      "@config/*": [
        "./src/config/*"
      ],
      "@helper/*": [
        "./src/helper/*"
      ],
      "@middlewares/*": [
        "./src/middlewares/*"
      ],
      "@tools/*": [
        "./src/tools/*"
      ]
    },
    "typeRoots": [
      "./src/@types",
      "node_modules/@types"
    ]
  },
  "include": [
    "./src/**/*",
    ".vscode/@types"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "logs",
    "coverage"
  ]
}
```
Por enquanto, ignore o campo paths, irei explicar um pouco pra frente.

[`.editorconfig`](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) Veja mais detalhes sobre esse cara [aqui](https://editorconfig.org/)
```
root = true

[*]
end_of_line = lf
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
```
E até o momento temos a seguinte estrutura:

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

Geralmente o start de projetos é realmente chato, mas com o tempo isso vai ficando mais automático, e a gente acaba aproveitando muitas configurações de projetos anteriores.

Então vamos para um pouco de código.

## Lets code!

Começamos instalando o necessário para a configuração do express e para o start da api.

Algumas libs que vamos utilizar.

- [express-async-errors](https://www.npmjs.com/package/express-async-errors)
- [express-handler-errors](https://www.npmjs.com/package/express-handler-errors) Essa eu mesmo criei, para lidar de uma forma mais simplificada com os erros retornados pela API.
- [express-request-id](https://www.npmjs.com/package/express-request-id) para adicionar um uuid na request
- [continuation-local-storage](https://www.npmjs.com/package/continuation-local-storage) para capturar o id da request nos serviços e nos logs
- [cors](https://www.npmjs.com/package/cors)
- [dotenv](https://www.npmjs.com/package/dotenv) para separação das environments
- [morgan-body](https://www.npmjs.com/package/morgan-body) para logar o conteúdo das requests
- [winston](https://www.npmjs.com/package/winston) para logs 

```bash
yarn add express cors dotenv continuation-local-storage express-async-errors express-handler-errors express-request-id morgan-body winston  && yarn add @types/express @types/cors @types/dotenv @types/node @types/winston @types/continuation-local-storage
```

O primeiro arquivo que eu começo escrevendo, é o arquivo que configura todas as variáveis de ambiente utilizada pela aplicação.

Fica na seguinte estrutura: `/src/config/index.ts`

E é aqui que utilizamos a lib [dotenv](https://www.npmjs.com/package/dotenv)

`/src/config/index.ts`
```typescript
import { config } from 'dotenv';

/*
 * Aqui estamos dizendo para o dotenv
 * onde ele deve buscar as variáveis de ambiente
 * NODE_ENV será o stage da nossa aplicação [dev, qa, prod, local, etc...]
 */
const envfile = `.env.${process.env.NODE_ENV}`;
const envdir = process.cwd();

config({ path: `${envdir}/${envfile}` });

export const server = {
  port: process.env.PORT,
  env: process.env.NODE_ENV,
}
```

Até aqui, já podemos observar que o eslint está reclamando de algumas coisas, então vamos atualizar as configs.

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

adicione as regras no campo rules

`eslintrc.json`
```json
"rules": {
    "prettier/prettier": "error",
    "global-require": "off",
    "no-new": "off",
    "no-console": "off",
    "import/prefer-default-export": "off",
    "import/extensions": [
      "error",
      "ignorePackages",
      {
        "js": "never",
        "ts": "never"
      }
    ],
    "import-helpers/order-imports": [
      "warn",
      {
        "newlinesBetween": "always", // new line between groups
        "groups": [
          "module",
          "/^@config/",
          "/^@apps/",
          "/^@services/",
          "/^@helper/",
          "/^@/",
          [
            "parent",
            "sibling",
            "index"
          ]
        ],
        "alphabetize": {
          "order": "asc",
          "ignoreCase": true
        }
      }
    ]
  },
```

Vamos escrever agora as configurações do [winston](https://www.npmjs.com/package/winston)

Esse vai ser nosso primeiro middleware.

`src/middlwares/logger.ts`
```typescript
import { getNamespace } from 'continuation-local-storage';
import winston from 'winston';

const options = {
  console: {
    level: 'info',
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    ),
    prettyPrint: true,
    colorize: process.stdout.isTTY,
  },
};

const logger = winston.createLogger({
  transports: [new winston.transports.Console(options.console)],
  exitOnError: false,
});

const formatMessage = (message: string) => {
  // É aqui que resgatamos o id da requisição que será armazenado em um storage
  const namespace = getNamespace('request');
  const id = namespace && namespace.get('id');
  return id ? `[${id}] ${message}` : message;
};

/**
 * aqui devolvemos os níveis do log
 * formatando a mensagem com o id da requisição caso exista
 */
export default {
  log: (message: string): winston.Logger => logger.info(message),
  info: (message: string, obj?: any): winston.Logger =>
    logger.info(formatMessage(message), obj),
  error: (message: string, obj?: any): winston.Logger =>
    logger.error(formatMessage(message), obj),
  warn: (message: string, obj?: any): winston.Logger =>
    logger.warn(formatMessage(message), obj),
  debug: (message: string, obj?: any): winston.Logger =>
    logger.debug(formatMessage(message), obj),
  silly: (message: string, obj?: any): winston.Logger =>
    logger.silly(formatMessage(message), obj),
};
```

E agora algumas configurações do express

`src/app.ts`
```typescript
import { Namespace, createNamespace } from 'continuation-local-storage';
import cors from 'cors';
import express, {
  Application,
  NextFunction,
  Request,
  RequestHandler,
  Response,
} from 'express';
import { ErrorHandler } from 'express-handler-errors';
import morgan from 'morgan-body';

import logger from '@middlewares/logger';

class App {
  public readonly app: Application;

  private readonly session: Namespace;

  constructor() {
    this.app = express();
    this.session = createNamespace('request'); // é aqui que vamos armazenar o id da request
    this.middlewares();
    this.errorHandle();
  }

  /**
   * Aqui nos configuramos os middlewares
   */
  private middlewares(): void {
    this.app.use(express.json());
    this.app.use(cors());
    const reqId = require('express-request-id'); // essa lib não tem tipagem
    this.app.use(reqId());
    const attachContext: RequestHandler = (
      _: Request,
      __: Response,
      next: NextFunction
    ) => {
      this.session.run(() => next());
    };

    const setRequestId: RequestHandler = (
      req: Request,
      _: Response,
      next: NextFunction
    ) => {
      this.session.set('id', req.id);
      next();
    };
    // Toda vez que chegar um request, nós armazenamos o id dela em um storage
    this.app.use(attachContext, setRequestId);
    morgan(this.app, {
      noColors: true,
      prettify: false,
      logReqUserAgent: false,
      stream: {
        write: (msg: string) => logger.info(msg) as any,
      },
    });
  }

  /**
   * Aqui é a configuração da lib para tratar os error
   */
  private errorHandle(): void {
    this.app.use(
      (err: Error, _: Request, res: Response, next: NextFunction) => {
        new ErrorHandler().handle(err, res, next, logger as any);
      }
    );
  }
}

export default new App();
```

Por enquanto o arquivo fica assim, ainda adicionaremos algumas outras configurações.

E se repararmos no import de nossas dependências, conseguimos buscar as pastas utilizando @, é por conta da configuração dos paths no tsconfig.json
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/316epmnvmew9p2soj0bj.png)

Mas ainda há um problema de tipagem nas configurações do express.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8toygj456oetw5sszbmf.png)

Por padrão, a interface Request do express não conhece o campo id que adicionamos nela. Então vamos sobrescrever essa interface.

`/src/@types/express/index.d.ts`
```typescript
declare namespace Express {
  interface Request {
    id: string;
  }
}
```

No nosso `tsconfig.json` nós adicionamos o path /src/@types como um caminho para definição de tipos

`tsconfig.json`
```json
...
"include": [
    "./src/**/*",
    ".vscode/@types"
  ],
```

Agora vamos configurar o arquivo que irá iniciar a nossa aplicação.

`src/server.ts`
```typescript
import { server } from '@config/index';

import logger from '@middlewares/logger';

import express from './app';

express.app.listen(server.port, () => {
  logger.info('Server running', { port: server.port, mode: server.env });
});
```

Nossa aplicação está quase pronta para ser iniciada, mas como estamos utilizando typescript, precisamos transpilar todos os nossos arquivos, só assim o Node conseguirá entender o que queremos que ele execute.

E também tem um outro ponto, quando o transpilador encontrar um import nomeado com @middlewares/logger por exemplo, ele precisa entender exatamente onde buscar o arquivo.

Então utilizaremos mais duas bibliotecas para lidar com o build e transpile da aplicação.

- [babel e seus plugins](https://babeljs.io/)
- [ts-node-dev](https://www.npmjs.com/package/ts-node-dev)

Vamos aos downloads

```bash
yarn add @babel/cli @babel/core @babel/node @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env @babel/preset-typescript babel-eslint babel-plugin-module-resolver babel-plugin-transform-typescript-metadata ts-node-dev tsconfig-paths
```

## E mais algumas configurações...

`babel.config.js`
```javascript
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
  ],
  plugins: [
    'babel-plugin-transform-typescript-metadata',
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ['@babel/plugin-proposal-class-properties', { loose: true }],
    [
      'module-resolver',
      {
        // aqui que ensinamos ele onde buscar os imports
       // e também ja podemos ter uma ideia de como irá ficar nossa estrutura de pastas
        alias: {
          '@apps': './src/apps',
          '@config': './src/config',
          '@helper': './src/helper',
          '@middlewares': './src/middlewares',
          '@shared': './src/shared',
          '@tools': './src/tools',
          '@services': './src/services',
          '@utils': './src/utils',
        },
      },
    ],
  ],
  ignore: ['**/*.spec.ts'],
};
```

Vamos adicionar o script de start

`package.json`
```json
...
"scripts": {
    "start:dev": "NODE_ENV=dev ts-node-dev -r tsconfig-paths/register --respawn --transpile-only --ignore-watch node_modules --no-notify src/server.ts"
}
...
```

Nesse momento, a aplicação já até pode ser iniciada utilizando o comando

```bash
yarn start:dev
```

Porém, nós não configuramos a porta que ficará escutando as requisições e também nenhuma rota.

Vamos lá!
Primeiro, nossa única variável de ambiente até o momento

`.env.dev`
```
PORT=3000
```

Agora, vamos configurar um arquivo de rotas e conecta-lo ao express

`src/routes.ts`
```typescript
import { Request, Response, Router } from 'express';

import logger from '@middlewares/logger';

const route = Router();

route.get('/hello', (_: Request, res: Response) => {
  logger.info(`Request recebida`);
  res.json({ message: 'Hello World' });
});

export default route;
```

criaremos um método para plugar as rotas e chamaremos no construtor

`src/app.ts`
```typescript
...

class App {
  public readonly app: Application;

  private readonly session: Namespace;

  constructor() {
    this.app = express();
    this.session = createNamespace('request'); // é aqui que vamos armazenar o id da request
    this.middlewares();
    // chamada do método
    // primeiro configuramos as rotas e depois o error handler
    this.routes();
    this.errorHandle();
    
  }

  ...

 /**
   * configuração de rota
   */
  private routes(): void {
    this.app.use('/api', routes);
  }
}

export default new App();

```

Agora se subirmos a api e chamarmos no navegador http://localhost:3000/api/hello, teremos o seguinte resultado

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

O que podemos notar no log do console:
- o endoint que foi chamado `Request: GET /api/hello at Sat Jan 09 2021 17:21:53 GMT-030`
- o log que adicionamos `Request recebida`
- o que a nossa api devolveu `Response Body:{"message":"Hello World"}`
- o id da requisição do inicio de toda linha logada `fc410867-6fb3-4637-b771-7334c2f12781`

O responsável por logar as informações do request é o [Morgan](https://www.npmjs.com/package/morgan) que configuramos no arquivo `src/app.ts`

E como ficou a visão da request no navegador

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

## Primeiro commit

Agora já estamos prontos para realizar o primeiro commit, mas antes disso, vamos escrever mais uma configuração.

Vamos usar o [commitlint](https://commitlint.js.org/#/) e o [commitzen](https://www.npmjs.com/package/commitizen), para desde de o início todos os commits já seguir um padrão

Também vamos utilizar o husky para executar alguns scripts antes de cada commit. Por exemplo, no futuro antes de cada commit, vamos rodar a switch de testes para garantir que nada suba quebrado para o repositório.

Execute os camandos:

```bash
npx commitizen init cz-conventional-changelog --save-dev --save-exact

npm install --save-dev @commitlint/{cli,config-conventional} 

yarn add -D husky

rm -rf node_modules

yarn
```

Crie o arquivo `commitlint.config.js`
```javascript
module.exports = {
  extends: ['@commitlint/config-conventional'],
  'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS',
};
```

Veremos que no arquivo `package.json` terá agora uma configuração nova.

```json
"config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
```
adicionamos também alguns scripts e a configuração do husky

```json
{
  "name": "example",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start:dev": "NODE_ENV=dev ts-node-dev -r tsconfig-paths/register --respawn --transpile-only --ignore-watch node_modules --no-notify src/server.ts",
    "commit": "git-cz" // um script para abrir uma interface de commit
  },
  "devDependencies": {
    ...
  },
  "dependencies": {
    ...
  },
  // configuração do husk
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
}

```

E podemos fazer o primeiro commit.

Vamos inicializar o git e configurar `.gitignore`.

```bash
git init
```

`.gitignore`
```.gitignore
node_modules
```

Note que se tentarmos escrever uma mensagem sem padrão no commit, receberemos um erro.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1f52hrwj2bfxmd7oo5i1.png)

Leia mais sobre a convenção de mensagens [aqui](https://www.conventionalcommits.org/en/v1.0.0/)

Para facilitar escrever o commit, instalamos a lib [commitzen](https://www.npmjs.com/package/commitizen)

Vamos testa-la:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7owmz6n6h641o2bwuk03.png)

A lib nos oferece uma CLI no terminal, e fica bem mais fácil seguir o padrao de mensagens.

## Considerações finais

Sei que esse primeiro post ficou realmente grande, mas espero ter conseguido detalhar bem o passo a passo da criação de uma api, um pouco mais robusta, com mais configurações, algo mais parecido com a vida real.

Até o momento, nossa estrutura de pastas está assim.

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

No próximo post, iremos configurar [Typeorm](https://typeorm.io/#/) para conectar com o banco de dados, e escrever o primeiro CRUD.

- Meu [github](https://github.com/vitordelfino)
- Meu [portfólio](https://github.com/vitordelfino)