DEV Community

loading...
Cover image for Setup inicial + Express

Setup inicial + Express

Vitor Silva Delfino
Updated on ・10 min read

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.

mkdir example
cd example
yarn init -y
code .
Enter fullscreen mode Exit fullscreen mode

E depois instalamos algumas dependências.

Plugins e mais plugins

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

yarn add eslint -D
yarn eslint --init
Enter fullscreen mode Exit fullscreen mode

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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

{
    "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"
                ]
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Configurando alguns arquivos

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

.preetierrc

{
  "singleQuote": true,
  "trailingComma": "es5",
  "prettier/prettier": [
    "error",
    {
      "endOfLine": "auto"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

tsconfig.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"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Por enquanto, ignore o campo paths, irei explicar um pouco pra frente.

.editorconfig Veja mais detalhes sobre esse cara aqui

root = true

[*]
end_of_line = lf
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
Enter fullscreen mode Exit fullscreen mode

E até o momento temos a seguinte estrutura:

Alt Text

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.

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
Enter fullscreen mode Exit fullscreen mode

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

/src/config/index.ts

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,
}
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

adicione as regras no campo rules

eslintrc.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
        }
      }
    ]
  },
Enter fullscreen mode Exit fullscreen mode

Vamos escrever agora as configurações do winston

Esse vai ser nosso primeiro middleware.

src/middlwares/logger.ts

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),
};
Enter fullscreen mode Exit fullscreen mode

E agora algumas configurações do express

src/app.ts

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();
Enter fullscreen mode Exit fullscreen mode

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

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

Alt Text

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

declare namespace Express {
  interface Request {
    id: string;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

tsconfig.json

...
"include": [
    "./src/**/*",
    ".vscode/@types"
  ],
Enter fullscreen mode Exit fullscreen mode

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

src/server.ts

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 });
});
Enter fullscreen mode Exit fullscreen mode

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.

Vamos aos downloads

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
Enter fullscreen mode Exit fullscreen mode

E mais algumas configurações...

babel.config.js

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'],
};
Enter fullscreen mode Exit fullscreen mode

Vamos adicionar o script de start

package.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"
}
...
Enter fullscreen mode Exit fullscreen mode

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

yarn start:dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

src/routes.ts

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;
Enter fullscreen mode Exit fullscreen mode

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

src/app.ts

...

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();

Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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 que configuramos no arquivo src/app.ts

E como ficou a visão da request no navegador

Alt Text

Primeiro commit

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

Vamos usar o commitlint e o commitzen, 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:

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
Enter fullscreen mode Exit fullscreen mode

Crie o arquivo commitlint.config.js

module.exports = {
  extends: ['@commitlint/config-conventional'],
  'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS',
};
Enter fullscreen mode Exit fullscreen mode

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

"config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
Enter fullscreen mode Exit fullscreen mode

adicionamos também alguns scripts e a configuração do husky

{
  "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"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

E podemos fazer o primeiro commit.

Vamos inicializar o git e configurar .gitignore.

git init
Enter fullscreen mode Exit fullscreen mode

.gitignore

node_modules
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

Leia mais sobre a convenção de mensagens aqui

Para facilitar escrever o commit, instalamos a lib commitzen

Vamos testa-la:

Alt Text

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

No próximo post, iremos configurar Typeorm para conectar com o banco de dados, e escrever o primeiro CRUD.

Discussion (0)