Organizar um projeto JavaScript vai muito além de simplesmente colocar arquivos em pastas. Uma boa organização é essencial para a escalabilidade, manutenibilidade e colaboração em equipe. Nesta aula, vamos explorar as melhores práticas para estruturar um projeto, desde a divisão de pastas até a configuração de ferramentas e convenções de código.

Vamos começar entendendo a importância de uma estrutura bem definida e como ela impacta diretamente no desenvolvimento. Em seguida, mergulharemos em módulos, que são a base para escrever código limpo e reutilizável. Depois, veremos como configurar ferramentas como linters e formatadores, e finalizaremos com convenções que padronizam o estilo de código.

Estrutura de pastas

Uma estrutura de pastas bem planejada facilita a navegação no projeto, separa responsabilidades e ajuda a evitar dependências circulares. Não existe uma estrutura única que sirva para todos os projetos, mas algumas boas práticas são amplamente adotadas.

Uma abordagem comum é separar o código-fonte (em src), os testes (em test ou __tests__), os arquivos de configuração (na raiz) e os artefatos gerados (como dist ou build). Dentro de src, você pode organizar por funcionalidade ou por tipo de arquivo. A organização por funcionalidade (feature-based) é recomendada para projetos maiores, pois agrupa tudo relacionado a uma funcionalidade em um só lugar.

meu-projeto/
├── src/
│   ├── components/
│   ├── services/
│   ├── utils/
│   ├── pages/
│   └── index.js
├── test/
├── config/
├── public/
├── package.json
└── README.md

Exemplo de estrutura orientada a funcionalidades:

meu-projeto/
├── src/
│   ├── features/
│   │   ├── auth/
│   │   │   ├── AuthService.js
│   │   │   ├── Login.js
│   │   │   └── auth.test.js
│   │   ├── dashboard/
│   │   │   ├── Dashboard.js
│   │   │   └── Dashboard.css
│   │   └── ...
│   ├── shared/
│   │   ├── components/
│   │   └── hooks/
│   └── app.js
└── ...

Independentemente da estrutura escolhida, o importante é ser consistente e documentar a estrutura para a equipe.

Módulos

Módulos são a forma de dividir o código em partes reutilizáveis. No JavaScript moderno, usamos os módulos ES6 (import/export). Eles permitem encapsular funcionalidades, evitar poluição do escopo global e facilitar o teste.

Existem dois tipos principais de exportação: nomeada e padrão. A exportação nomeada permite exportar múltiplos valores, enquanto a padrão exporta um único valor por módulo. A escolha entre elas deve ser consistente no projeto.

// utils/math.js
export function soma(a, b) {
  return a + b;
}

export function multiplica(a, b) {
  return a * b;
}

// main.js
import { soma, multiplica } from './utils/math.js';
console.log(soma(2, 3)); // 5

Para organizar módulos, é comum criar um arquivo index.js em cada pasta para reexportar os módulos internos, facilitando o import de vários arquivos com um único caminho.

// components/index.js
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';

Além disso, é importante evitar dependências circulares, onde dois módulos se importam mutuamente, pois isso pode causar erros difíceis de depurar. Uma boa prática é manter a hierarquia de dependências em uma direção (ex.: camadas de baixo não dependem de camadas de cima).

Configuração

A configuração do projeto envolve ferramentas como o package.json, transpiladores (Babel), empacotadores (Webpack, Vite), linters (ESLint) e formatadores (Prettier). Uma configuração bem definida garante consistência e automatiza tarefas.

O package.json é o coração do projeto: ele contém scripts, dependências e metadados. É essencial definir scripts para tarefas comuns, como dev, build, test e lint.

// package.json
{
  "name": "meu-projeto",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "jest",
    "lint": "eslint ."
  },
  "dependencies": {},
  "devDependencies": {
    "eslint": "^8.0.0",
    "jest": "^29.0.0",
    "vite": "^4.0.0"
  }
}

Para o ESLint, você pode usar um arquivo de configuração .eslintrc.js ou eslint.config.js (dependendo da versão). É importante definir regras claras e integrar com o Prettier para evitar conflitos.

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: ['eslint:recommended', 'prettier'],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  rules: {
    'no-unused-vars': 'warn',
    'no-console': 'off',
  },
};

Além disso, o Prettier pode ser configurado com um arquivo .prettierrc para padronizar a formatação, como uso de aspas simples, ponto e vírgula, etc.

Convenções

Convenções são acordos sobre como escrever código: nomes de variáveis, formatação, estrutura de arquivos, etc. Seguir convenções torna o código mais legível e reduz a fricção entre membros da equipe.

Algumas convenções comuns em JavaScript:

  • Use camelCase para variáveis e funções: minhaVariavel, minhaFuncao().
  • Use PascalCase para classes e componentes: MinhaClasse, MeuComponente.
  • Use UPPER_CASE para constantes imutáveis: MAX_LIMIT.
  • Prefira const para valores que não mudam e let apenas quando necessário.
  • Use aspas simples ou duplas de forma consistente (o Prettier pode forçar uma delas).
  • Adote o padrão de nomenclatura para arquivos: kebab-case ou camelCase (ex.: meu-componente.js ou meuComponente.js). Escolha um e siga.

Além disso, é recomendável adotar um style guide, como o do Airbnb ou do Standard, para ter regras mais detalhadas. Ferramentas como o ESLint ajudam a aplicar essas regras automaticamente.

Também é importante definir convenções para commits, como o Conventional Commits, que padroniza as mensagens de commit e facilita a geração de changelogs.

// Exemplo de commit semântico
feat: adiciona página de login
fix: corrige bug no cálculo de frete
docs: atualiza README

Boas práticas e observações finais

Além dos tópicos acima, algumas boas práticas ajudam a manter o projeto organizado:

  • Mantenha as dependências atualizadas e remova as não utilizadas.
  • Use um gerenciador de pacotes com lockfile (como package-lock.json) para garantir consistência entre ambientes.
  • Documente a estrutura e as decisões de arquitetura em um README.md ou em um arquivo docs/.
  • Automatize tarefas com hooks de pre-commit (ex.: lint-staged) para rodar linters e testes antes de cada commit.

Organizar um projeto é um processo contínuo; revise e ajuste a estrutura conforme o projeto evolui.

Referências

Exercícios

  1. Crie uma estrutura de pastas para um projeto de aplicativo de tarefas (to-do list), incluindo pastas para componentes, serviços, utilitários e testes. Explique a função de cada pasta.

    ✓ Resposta: Uma possível estrutura:
    meu-app/
    ├── src/
    │   ├── components/
    │   │   ├── TaskForm.js
    │   │   └── TaskList.js
    │   ├── services/
    │   │   └── taskService.js
    │   ├── utils/
    │   │   └── dateUtils.js
    │   ├── styles/
    │   │   └── main.css
    │   └── index.js
    ├── test/
    │   ├── components/
    │   └── services/
    ├── package.json
    └── README.md
    
    A pasta components guarda os componentes de UI, services contém a lógica de negócio e comunicação com APIs, utils tem funções auxiliares, styles CSS, test contém os testes, e a raiz tem arquivos de configuração.
  2. Converta o seguinte código para usar módulos ES6, separando a função saudacao em um arquivo próprio e importando-a no arquivo principal. Mostre os arquivos resultantes.

    function saudacao(nome) {
      return 'Olá, ' + nome + '!';
    }
    console.log(saudacao('Maria'));
    

    ✓ Resposta: Arquivo saudacao.js:
    export function saudacao(nome) {
      return `Olá, ${nome}!`;
    }
    
    Arquivo principal main.js:
    import { saudacao } from './saudacao.js';
    console.log(saudacao('Maria'));
    
  3. Escreva um arquivo package.json para um projeto que usa Vite, ESLint e Jest. Inclua scripts para desenvolvimento, build, teste e lint.

    ✓ Resposta:
    {
      "name": "meu-projeto",
      "version": "1.0.0",
      "scripts": {
        "dev": "vite",
        "build": "vite build",
        "test": "jest",
        "lint": "eslint ."
      },
      "dependencies": {},
      "devDependencies": {
        "vite": "^4.0.0",
        "eslint": "^8.0.0",
        "jest": "^29.0.0"
      }
    }
    
  4. Explique a diferença entre exportação nomeada e exportação padrão, e dê um exemplo de quando usar cada uma.

    ✓ Resposta: A exportação nomeada permite exportar múltiplos valores de um módulo, e o import deve usar os mesmos nomes. A exportação padrão exporta um único valor, e no import podemos escolher qualquer nome. Use exportação nomeada para utilitários ou múltiplas funções; use exportação padrão para uma classe ou função principal de um módulo (ex.: componente React).
  5. Escreva uma configuração básica do ESLint que estenda o estilo recomendado e desative a regra no-console. Explique por que essa configuração é útil.

    ✓ Resposta:
    // .eslintrc.js
    module.exports = {
      env: {
        browser: true,
        es2021: true,
      },
      extends: ['eslint:recommended'],
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
      },
      rules: {
        'no-console': 'off',
      },
    };
    
    Essa configuração aplica as regras recomendadas do ESLint (que ajudam a evitar erros comuns) e desliga a regra que proíbe o uso de console.log, permitindo logs durante o desenvolvimento.