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

Vou mostrar o passo a passo de como realizar todas as configurações que utilizo nos meus projetos com Next.js

Baseado no boilerplate-apps-router do curso React Avançado

Passo a Passo

Meu ambiente de desenvolvimento é o Windows com WSL e VSCode como editor de código. Escrevi esse tutorial sobre como configurar o WSL.

Inicializar o projeto:

Execute os comandos abaixo:

mkdir next-boilerplate
cd next-boilerplate
npm init
git init

Dessa forma, foi criado o arquivo package.json, onde deixei no seguinte molde:

{
  "name": "next-boilerplate",
  "version": "1.0.0",
  "description": "Next.js Boilerplate",
  "repository": "https://github.com/diasjoaovitor/next-boilerplate.git",
  "author": "João Vitor <jvitordiass@outlook.com.br>",
  "license": "MIT"
}

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.

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

Instalar o Next

Instale o next, react e react-dom

npm i next react react-dom

Crie o diretório src/app e adicione os arquivos layout.jsx e page.jsx

export const metadata = {
  title: 'Next Boilerplate',
  description: 'Boilerplate para projetos Next.js'
}

const RootLayout = ({ children }) => {
  return (
    <html lang="pt-br">
      <body>{children}</body>
    </html>
  )
}

export default RootLayout
const Home = () => {
  return <h1>Home</h1>
}

export default Home

Adicione os scripts no arquivo package.json:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

Ignore a pasta .next no arquivo .gitignore

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.

Adicione os scripts:

{
  "scripts": {
    "prettier:check": "prettier --check .",
    "prettier:fix": "prettier --write ."
  }
}

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
}
  • "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.

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.

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, eslint-config-next e o eslint-config-prettier:

npm i -D eslint@^8 eslint-config-next eslint-config-prettier

Devido a uma incompatibilidade com o plugin do next no momento que instalei as dependências, foi instalado o eslint na versão 8. Para mais informações leia essa issue.

Crie o arquivo .eslintrc.json:

{
 "extends": ["next", "prettier"]
}

Adicione o script:

{
  "scripts": {
    "lint": "next lint",
  }
}

Instale o lint-staged e o husky


npm i -D lint-staged husky

Crie o arquivo .lintstagedrc.js:

const path = require('path')

const buildCommand = (filenames) => {
  const files = filenames
    .map((f) => path.relative(process.cwd(), f))
    .join(' --file ')
  return [`npx prettier --write ${files}`, `npx next lint --fix --file ${files}`]
}

module.exports = {
  '*.{js,jsx,ts,tsx}': [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:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no-install lint-staged

Configurar TypeScript

Instale o typescript e demais dependências:

npm i -D typescript @types/node @types/react @types/react-dom

Crie o arquivo tsconfig.json:

{
  "compilerOptions": {
    "target": "ESNext", // Especifica a versão do ECMAScript de destino para a qual o código TypeScript será compilado.
    "lib": ["dom", "dom.iterable", "esnext"], // Define as bibliotecas de declaração de tipo que serão incluídas na compilação.
    "allowJs": true, // Permite a compilação de arquivos JavaScript junto com arquivos TypeScript.
    "skipLibCheck": true, // Pula a verificação de tipos em todas as declarações de bibliotecas de tipos (*.d.ts).
    "strict": true, // Habilita um conjunto de verificações de tipo mais rigorosas.
    "noEmit": true, // Não emite arquivos de saída (como .js) após a compilação.
    "esModuleInterop": true, // Habilita a interoperabilidade de módulos ES6 com módulos CommonJS/AMD/UMD.
    "module": "esnext", // Define o formato do módulo de saída.
    "moduleResolution": "bundler", // Especifica a estratégia de resolução de módulos.
    "resolveJsonModule": true, // Permite a importação de arquivos .json.
    "isolatedModules": true, // Garante que cada arquivo possa ser compilado de forma isolada.
    "jsx": "preserve", // Preserva as anotações JSX no arquivo de saída.
    "incremental": true, // Habilita a compilação incremental para acelerar as compilações subsequentes.
    "plugins": [
      // Lista de plugins do compilador TypeScript.
      {
        "name": "next" // Especifica o plugin 'next' para ser usado durante a compilação.
      }
    ],
    "paths": {
      // Define um conjunto de entradas de caminho que serão resolvidas durante a compilação.
      "@/*": ["./src/*"] // Permite o uso de '@' como um alias para o diretório './src/'.
    }
  },
  "include": [
    // Especifica os arquivos que devem ser incluídos na compilação.
    "next-env.d.ts", // Arquivo de declaração de tipo específico do Next.js.
    "**/*.ts", // Todos os arquivos TypeScript no projeto.
    "**/*.tsx", // Todos os arquivos TypeScript com JSX no projeto.
    ".next/types/**/*.ts" // Arquivos de tipo específicos do Next.js.
  ],
  "exclude": ["node_modules"] // Exclui a pasta 'node_modules' da compilação.
}

Altere as extensões dos arquivos layout e page para .tsx e defina os tipos:

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Next Boilerplate',
  description: 'Boilerplate para projetos Next.js'
}

const RootLayout = ({
  children
}: Readonly<{
  children: React.ReactNode
}>) => {
  return (
    <html lang="pt-br">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

export default RootLayout

Configurar testes automatizados

Instale as bibliotecas de testes:

npm i -D jest @types/jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event

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

const nextJest = require('next/jest')

const createJestConfig = nextJest({
  dir: '.'
})

const jestConfig = createJestConfig({
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
})

module.exports = jestConfig
import '@testing-library/jest-dom'

Adicione os scripts:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watchAll"
  }
}

Adicione o comando para rodar os testes no arquivo lintstagedrc.json:

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 next lint --fix --file ${files.join(' --file ')}`,
    `npx jest --runInBand --findRelatedTests ${files.join(' ')} --passWithNoTests`
  ]
}

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

Configurar Integração Contínua

Adicione o script "test:ci": "jest --runInBand" no package.json e crie o arquivo ci.yml na pasta .github/workflows:

name: ci
on: [pull_request] # O workflow é acionado quando um pull request é aberto, sincronizado ou reaberto.

jobs:
  build: # Este é o trabalho de construção que será executado.
    runs-on: ubuntu-latest # O trabalho será executado na última versão do Ubuntu disponível.
    steps: # Seguem os passos que serão executados em sequência.
      - name: Checkout Repository
        uses: actions/checkout@v4 # Este passo faz o checkout do seu repositório usando a ação checkout v4.

      - name: Setup Node
        uses: actions/setup-node@v4 # Este passo configura o ambiente Node.js usando a ação setup-node v4.
        with:
          node-version: lts/iron # Define a versão do Node.js para a versão LTS mais recente chamada "Iron".
          cache: 'npm' # Habilita o cache para o gerenciador de pacotes NPM.

      - name: Install dependencies
        run: npm install # Este passo instala as dependências listadas no arquivo package-lock.json.

      - name: Linting
        run: npm run lint # Este passo executa o linting no código para verificar erros de estilo.

      - name: Testing
        run: npm run test:ci # Este passo executa os testes definidos para o projeto.

      - name: Build
        run: npm run build # Este passo constrói o projeto.

Configurar gerador de componentes

Instale o Plop

npm i -D plop

Crie a pasta generators na raiz do projeto e adicione o arquivo plopfile.js:

module.exports = (plop) => {
  plop.setGenerator('component', {
    description: 'Create a component',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'What is your component name?'
      }
    ],
    actions: [
      {
        type: 'add',
        path: '../src/app/components/{{pascalCase name}}/index.tsx',
        templateFile: 'templates/index.tsx.hbs'
      },
      {
        type: 'add',
        path: '../src/app/components/{{pascalCase name}}/{{kebabCase name}}.test.tsx',
        templateFile: 'templates/test.tsx.hbs'
      }
    ]
  })
}

Crie o arquivo .prettierignore e ignore todos templates:

*.hbs

Dentro de generators, crie o templates index.tsx.hbs e test.tsx.hbs:

export const {{pascalCase name}} = () => {
  return (
    <div>{{pascalCase name}}</div>
  )
}
import { render, screen } from '@testing-library/react'
import { {{pascalCase name}} } from '.'

describe('<{{pascalCase name}} />', () => {
  it('should render component', () => {
    render(<{{pascalCase name}} />)
    expect(screen.getByText('{{pascalCase name}}')).toBeInTheDocument()
  })
})

Adicione o script:

{
  "scripts": {
    "generate": "plop --plopfile generators/plopfile.js"
  }
}

Crie um componente:

npm run generate MyComponent

Assim, foi gerado um componente e um teste seguindo o padrão dos templates

Dentro da pasta components, crie o arquivo index.ts e exporte o componente:

export * from './MyComponent'

Essa configuração ajuda a importar todos os componentes exportados nesse arquivo através do alias @/app/components

Para finalizar, altere o componente MyComponent e seu arquivo de teste para:

import { PropsWithChildren } from 'react'

export const MyComponent = ({ children }: PropsWithChildren) => {
  return <div>{children}</div>
}
import { render, screen } from '@testing-library/react'
import { MyComponent } from '.'

describe('<MyComponent />', () => {
  it('should render component', () => {
    render(<MyComponent>MyComponent</MyComponent>)
    expect(screen.getByText('MyComponent')).toBeInTheDocument()
  })
})

Dessa forma, importe o componente no arquivo page.ts na raiz da pasta app e crie o teste home.test.tsx:

import { MyComponent } from '@/app/components'

const Home = () => {
  return <MyComponent>Home</MyComponent>
}

export default Home
import { render, screen } from '@testing-library/react'
import Home from './page'

describe('<MyComponent />', () => {
  it('should render component', () => {
    render(<Home />)
    expect(screen.getByText('Home')).toBeInTheDocument()
  })
})

Execute npm run dev para iniciar a app e npm test para executar os testes.

Considerações finais

Este é um boilerplate genérico para projetos NextJS.

O repositório está disponível no meu perfil do GitHub e está aberto a contribuições.