[Tutorial] - Como criar um boilerplate para projetos Node.js

Depois de já ter configurado o Next.js Boilerplate e o Vite Boilerplate, agora vamos criar um boilerplate para API's em Node.js

Passo a Passo

Inicializar o Projeto

Acesse o diretório desejado e execute os comandos abaixo:

mkdir node-boilerplate
cd node-boilerplate
npm init -y
git init

Dessa forma, foi criado a pasta do projeto, inicializado o git e criado o arquivo package.json, onde vamos deixar no seguinte molde:

{
  "name": "node-boilerplate",
  "version": "1.0.0",
  "description": "Boilerplate para projetos Node.js",
  "repository": "https://github.com/diasjoaovitor/node-boilerplate.git",
  "author": "João Vitor <jvitordiass@outlook.com.br>",
  "license": "MIT",
  "main": "src/app.ts",
  "keywords": [
    "node-boilerplate",
    "eslint",
    "commit-linter",
    "prettier",
    "jest",
    "typescript"
  ]
}

Especificar a versão do Node:

Crie o arquivo .nvmrc e adicione a versão a ser utilizada:

lts/iron

É obrigatório deixar uma linha em branco ao final do arquivo para que essa configuração funcione corretamente. Dessa forma, ao acessar o diretório do projeto e executar o comando nvm use, o NVM carrega automaticamente a versão especificada no arquivo.

Ensino como instalar o NVM e versões especificas do Node no artigo Como configurar um ambiente de desenvolvimento com o WSL

Configurar o commit linter

Instale o git-commit-msg-linter:

npm i -D git-commit-msg-linter

Essa biblioteca verifica se a mensagem de um commit contém um prefixo semântico como feat, fix, refactor e demais convenções.

Crie o arquivo .gitignore e adicione a pasta node_modules

Configurar o TypeScript

npm i -D typescript ts-node-dev @types/node

Crie o arquivo tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "target": "ESNext",
    "module": "CommonJS",
    "skipLibCheck": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "outDir": "dist",
    "removeComments": true,
    "forceConsistentCasingInFileNames": true,
    "sourceMap": true,
    "baseUrl": "src"
  },
  "include": ["src"]
}

Estrutura do arquivo tsconfig.json:

  1. composite: true: Habilita o modo de projeto composto. Isso permite que o TypeScript otimize a compilação em projetos com dependências entre subprojetos. Por exemplo, se você tem um projeto com vários subdiretórios, o modo composto permite que o TypeScript recompile apenas os arquivos afetados em vez de todo o projeto.

  2. target: "ESNext": Define o alvo da transpilação para uma versão mais recente do JavaScript, conhecida como ESNext.

  3. module: "CommonJS": Especifica que o sistema de módulos usado será o CommonJS, que é o padrão para o Node.js. O CommonJS permite a utilização de require e module.exports para módulos.

  4. skipLibCheck: true: Faz com que o compilador ignore a checagem de tipos nas definições de bibliotecas (.d.ts).

  5. moduleResolution: "node": Define a estratégia de resolução de módulos como node, o que significa que o TypeScript usará o mesmo mecanismo de resolução de módulos que o Node.js.

  6. allowSyntheticDefaultImports: true: Permite que você importe módulos que não têm uma exportação padrão.

  7. esModuleInterop: true: Habilita a interoperabilidade completa entre os módulos ES e CommonJS, corrigindo problemas com importações padrão e nomes de exportação.

  8. strict: true: Ativa um conjunto de opções de verificação estrita de tipos no TypeScript.

  9. outDir: "dist": Define o diretório de saída onde os arquivos JavaScript compilados serão colocados. Neste caso, todos os arquivos transpilados serão colocados no diretório dist.

  10. removeComments: true: Remove os comentários dos arquivos .ts ao transpilá-los para .js, o que resulta em arquivos mais leves.

  11. forceConsistentCasingInFileNames: true: Impede que o compilador permita importações que usam diferentes capitalizações em nomes de arquivos.

  12. sourceMap: true: Gera arquivos que mapeiam o código transpilado para o código original TypeScript.

  13. baseUrl: "src": Define o diretório base para resolução de módulos relativos.

  14. include: ["src"]: Especifica que apenas os arquivos incluídos em src devem ser incluídos no processo de compilação.

Variáveis de Ambiente

As informações sensíveis devem ser atribuídas em arquivos .env, dessa forma vamos instalar o dotenv:

npm i dotenv

Crie um arquivo .env e um arquivo .env.example. Esse segundo arquivo serve para indicar quais variáveis de ambiente devem ser atribuídas, pois o arquivo .env não deve ser subir no repositório, dessa forma, vamos especificar essa condição no arquivo .gitignore.

.env:

MY_SECRET_KEY=12345678

.env.example:

MY_SECRET_KEY=

.gitignore:

.env

Finalmente, vamos criar o arquivo src/app.ts e atribuir a configuração:

import dotenv from 'dotenv'

dotenv.config()

console.log(process.env.MY_SECRET_KEY)

Configurar Alias

Instale o module-alias:

npm i module-alias
npm i -D @types/module-alias

Crie o arquivo src/config/alias-config.ts:

import { addAlias } from 'module-alias'
import { resolve } from 'path'

const env = process.env.NODE_ENV

const basePath = env === 'production' ? 'dist/src' : 'src'

addAlias('@', resolve(basePath))

Adicione o path no tsconfig.json

{
  "compilerOptions": {
    "paths": {
      "@/*": ["*"]
    }
  }
}

Agora vamos criar os arquivos sum.ts e index.ts no diretório src/utils:

export const sum = (a: number, b: number) => a + b
export * from './sum'

Importe a configuração no arquivo app.ts, dessa forma é possível importar a função sum com o atalho @/utils:

import './config/alias-config'
import dotenv from 'dotenv'
import { sum } from '@/utils'

dotenv.config()

console.log(process.env.PORT)
console.log(sum(1, 2))

Configurar o Jest

Instale as bibliotecas:

npm i -D jest ts-jest @types/jest

Crie os arquivos jest.config.ts e jest.setup.ts:

export default {
  preset: 'ts-jest',
  testRegex: '((\\.|/*.)(test))\\.ts?$',
  modulePaths: ['<rootDir>/src/'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
}
beforeAll(async () => {
  jest.spyOn(console, 'error').mockImplementation(() => {})
})

afterAll(async () => {
  jest.restoreAllMocks()
})

Crie o arquivos src/utils/sum.ts e src/tests/sum.test.ts:

export const sum = (a: number, b: number) => a + b
import { sum } from '.'

describe('Sum', () => {
  it('Sum a and b', () => {
    const result = sum(2, 3)
    expect(result).toBe(5)
  })
})

Configurar as regras do editor e do código

Instale o módulo e a extensão do prettier:

npm i -D prettier

Crie o arquivo .prettierrc.json:

{
  "trailingComma": "none",
  "semi": false,
  "singleQuote": true
}
  • "trailingComma": "none": Isso indica que o Prettier não deve adicionar vírgulas ao final de listas, objetos ou parâmetros de função quando eles são formatados em várias linhas.
  • "semi": false: Isso significa que o Prettier não deve adicionar ponto e vírgula ao final de cada instrução.
  • "singleQuote": true: Isso especifica que o Prettier deve usar aspas simples em vez de aspas duplas, sempre que possível.

Defina o prettier como o formatador padrão do VSCode em Default Formatter e habilite a opção Format On Save

Crie a pasta .vscode e inclua o arquivo settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "files.autoSave": "off",
  "git.autofetch": true,
  "typescript.tsdk": "node_modules/typescript/lib"
}
  • "editor.formatOnSave": true - Esta opção faz com que o VSCode formate automaticamente o código quando você salva um arquivo. Isso ajuda a manter o código limpo e consistente com as regras de formatação definidas.
  • "editor.defaultFormatter": "esbenp.prettier-vscode" - Define o Prettier como o formatador de código padrão. O Prettier é uma ferramenta popular que suporta muitas linguagens e estilos de codificação.
  • "files.autoSave": "off" - Desativa o salvamento automático de arquivos. Com essa configuração, os arquivos não serão salvos automaticamente após um período ou quando o foco é alterado; você precisará salvar manualmente suas alterações.
  • "git.autofetch": true - Quando habilitado, o VSCode buscará automaticamente as alterações mais recentes do seu repositório Git periodicamente. Isso é útil para manter seu repositório local atualizado com as alterações remotas.
  • "typescript.tsdk": "node_modules/typescript/lib": Garante que o VSCode use a versão do TypeScript instalada nas dependências do projeto ao invés da versão global.

Crie o arquivo .editorconfig:

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
  • root = true: Esta configuração sinaliza que este é o arquivo de configuração principal e que o EditorConfig não deve procurar por outros arquivos de configuração nas pastas acima.

  • [*]: Este é um padrão de correspondência que se aplica a todos os arquivos no projeto.

  • indent_style = space: Define que o estilo de indentação deve ser feito com espaços em vez de tabulações.

  • indent_size = 2: Especifica que o tamanho da indentação deve ser de dois espaços.

  • end_of_line = lf: Indica que o final de linha deve ser formatado usando LF (Line Feed), que é o padrão para sistemas Unix e macOS.

  • charset = utf-8: Define que o conjunto de caracteres do arquivo deve ser UTF-8.

  • trim_trailing_whitespace = true: Quando verdadeiro, remove automaticamente qualquer espaço em branco no final das linhas ao salvar o arquivo.

  • insert_final_newline = true: Garante que haja uma nova linha no final do arquivo ao salvar.

Configurar Linting

O linting é o processo de aplicar regras a uma base de código e destacar padrões ou códigos problemáticos que não aderem a determinadas diretrizes de estilo. ESLint permite que os desenvolvedores descubram problemas com seu código sem a necessidade de executá-lo.

Instale o eslint, @typescript-eslint/eslint-plugin, @typescript-eslint/parser e o eslint-config-prettier:

npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier

Crie o arquivo .eslintrc.cjs:

module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs', 'node_modules'],
  parser: '@typescript-eslint/parser'
}

Automatizar Scripts Pré-Commit

Instale o lint-staged e o husky


npm i -D lint-staged husky

Crie o arquivo .lintstagedrc.cjs:

const path = require('path')

const buildCommand = (filenames) => {
  const files = filenames.map((f) => path.relative(process.cwd(), f))
  return [
    `npx prettier --write ${files.join(' --file ')}`,
    `npx eslint --fix ${files.join(' ')} --report-unused-disable-directives --max-warnings 0`,
    `npx jest --runInBand --findRelatedTests ${filenames.join(' ')} --passWithNoTests`
  ]
}

module.exports = {
  '*.{js,ts}': [buildCommand]
}

Inicie as configurações do husky:

npx husky init

Dessa forma, foi criado o script prepare no arquivo package.json e a pasta .husky, onde vamos alterar o conteúdo do arquivo pre-commit para:

npx --no-install lint-staged

Configurar Scripts:

Adicione os scripts no arquivos package.json:

{
  "scripts": {
    "dev": "ts-node-dev --respawn --transpile-only src/app.ts",
    "start": "NODE_ENV=production node dist/src/app.js",
    "build": "tsc",
    "test": "jest --runInBand",
    "test:watch": "jest --runInBand --watchAll",
    "prettier:check": "prettier --check .",
    "prettier:fix": "prettier --write .",
    "lint": "eslint . --ext js,ts --report-unused-disable-directives --max-warnings 0",
    "prepare": "husky"
  }
}

Configurar Integração Contínua

Crie o arquivo .github/workflows/ci.yml:

name: ci

on: [pull_request, push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 'lts/iron'
          cache: 'npm'

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ${{ runner.tool_cache }}/npm
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install dependencies
        run: npm ci

      - name: Linting
        run: npm run lint

      - name: Testing
        run: npm run test

      - name: Clean build folder
        run: rm -rf dist

      - name: Build
        run: npm run build

Considerações Finais

Ao final desses passos, temos um boilerplate genérico para projetos Node.js e que possui as principais ferramentas de desenvolvimento para manter o código seguro e organizado.

O repositório está disponível no meu perfil do GitHub.