Qualidade de código no Node.js: ESLint, Prettier, padrões e revisão automatizada

Capítulo 14

Tempo estimado de leitura: 8 minutos

+ Exercício

Por que lint, formatação e padrões importam em um back-end Node.js

Em um back-end com Node.js e TypeScript, “qualidade de código” não é só estética: é reduzir bugs assíncronos, manter consistência entre pessoas do time e tornar mudanças seguras. Para isso, normalmente combinamos três frentes: lint (regras que detectam problemas e más práticas), formatação (padronização automática de estilo) e padrões de escrita (decisões de design e consistência que o time segue).

ESLint é o linter mais usado no ecossistema JS/TS. Ele analisa o código e aponta violações de regras. Prettier é um formatador: ele reescreve o código para um estilo consistente. A boa prática é: Prettier cuida do formato e ESLint cuida de problemas e padrões, evitando conflito entre os dois.

Configuração prática: ESLint + Prettier + TypeScript (passo a passo)

1) Instalar dependências

Instale ESLint, Prettier e os plugins necessários para TypeScript e importações:

npm i -D eslint @eslint/js typescript-eslint eslint-config-prettier eslint-plugin-prettier prettier eslint-plugin-import eslint-import-resolver-typescript

Se você usa Promises intensamente (muito comum em back-end), vale adicionar regras de Promises via plugin:

npm i -D eslint-plugin-promise

2) Criar configuração do ESLint (flat config)

Crie eslint.config.js na raiz do projeto. Abaixo um exemplo focado em back-end TypeScript, com regras para imports, Promises e restrições de any:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

import js from "@eslint/js";import tseslint from "typescript-eslint";import importPlugin from "eslint-plugin-import";import promisePlugin from "eslint-plugin-promise";import prettierPlugin from "eslint-plugin-prettier";import eslintConfigPrettier from "eslint-config-prettier";export default [js.configs.recommended,...tseslint.configs.recommendedTypeChecked,{files:["**/*.ts"],languageOptions:{parserOptions:{project:true,tsconfigRootDir:import.meta.dirname,sourceType:"module"}},plugins:{import:importPlugin,promise:promisePlugin,prettier:prettierPlugin},settings:{"import/resolver":{typescript:{project:true}}},rules:{"prettier/prettier":"error",...eslintConfigPrettier.rules,"@typescript-eslint/no-floating-promises":"error","@typescript-eslint/no-misused-promises":["error",{"checksVoidReturn":false}],"@typescript-eslint/no-explicit-any":"warn","@typescript-eslint/explicit-function-return-type":["warn",{"allowExpressions":true,"allowTypedFunctionExpressions":true}],"@typescript-eslint/consistent-type-imports":["error",{"prefer":"type-imports","fixStyle":"inline-type-imports"}],"import/order":["error",{"groups":["builtin","external","internal","parent","sibling","index","type"],"newlines-between":"always","alphabetize":{"order":"asc","caseInsensitive":true}}],"import/no-duplicates":"error","promise/always-return":"off","promise/catch-or-return":"off"}}];

O que essa configuração entrega:

  • Type-aware lint (com recommendedTypeChecked): habilita regras que dependem do TypeScript entender tipos (ótimo para back-end).
  • no-floating-promises: evita Promises “soltas” (bugs comuns em rotas/serviços assíncronos).
  • no-explicit-any: desencoraja any (mantém contratos claros). Está como warn para permitir adoção gradual.
  • import/order: padroniza organização de imports e melhora legibilidade.
  • Prettier integrado: qualquer divergência de formatação vira erro de lint (útil no CI).

3) Configurar o Prettier

Crie .prettierrc.json:

{"semi":true,"singleQuote":true,"trailingComma":"all","printWidth":100}

E um .prettierignore para evitar formatar artefatos:

distnode_modulescoverage

4) Ajustar o tsconfig para lint type-aware

O ESLint type-aware precisa de um tsconfig que inclua os arquivos analisados. Se seu tsconfig.json não inclui tudo, crie um tsconfig.eslint.json:

{"extends":"./tsconfig.json","include":["src/**/*.ts","test/**/*.ts"],"exclude":["node_modules","dist"]}

E então, no eslint.config.js, troque project:true por project:"./tsconfig.eslint.json".

Scripts para checagens automáticas (lint, format, CI)

No package.json, crie scripts para rodar localmente e em pipelines:

{"scripts":{"lint":"eslint .","lint:fix":"eslint . --fix","format":"prettier . --write","format:check":"prettier . --check","check":"npm run format:check && npm run lint"}}

Como usar no dia a dia:

  • npm run lint: encontra problemas.
  • npm run lint:fix: aplica correções automáticas quando possível (ex.: ordenar imports, remover variáveis não usadas).
  • npm run format: formata tudo.
  • npm run check: ideal para rodar antes de abrir PR.

Opcional: checagem apenas em arquivos alterados (pre-commit)

Para reduzir tempo e aumentar adesão, é comum rodar lint/format só no que mudou. Uma abordagem típica é usar lint-staged + husky:

npm i -D husky lint-staged
npx husky init

No package.json:

{"lint-staged":{"**/*.{ts,js}":["eslint --fix","prettier --write"]}}

No hook .husky/pre-commit:

npx lint-staged

Como interpretar relatórios do ESLint

O ESLint normalmente reporta: arquivo, linha/coluna, severidade e regra. Exemplo:

src/services/user.service.ts:42:13  error  Promises must be awaited  @typescript-eslint/no-floating-promises

Leitura prática:

  • error: deve quebrar o build/CI. Corrija antes do merge.
  • warn: não quebra, mas indica dívida técnica ou adoção gradual (ex.: no-explicit-any).
  • Nome da regra: procure a documentação para entender o porquê e exemplos de correção.

Exemplo: corrigindo no-floating-promises

Problema (Promise solta):

export async function handle(): Promise<void> {sendEmail();}

Correções possíveis:

  • Aguardar (quando o fluxo depende disso):
export async function handle(): Promise<void> {await sendEmail();}
  • Disparar em background de forma explícita (quando é intencional):
export async function handle(): Promise<void> {void sendEmail();}

O void deixa claro para o leitor e para o linter que você está descartando o resultado de propósito.

Exemplo: reduzindo uso de any com tipos mínimos

Problema:

function parse(body: any) {return body.userId;}

Melhorias:

type Body = { userId: string };function parse(body: Body): string {return body.userId;}

Quando o formato é desconhecido (ex.: entrada externa), prefira unknown e valide:

function parse(body: unknown): string {if (typeof body !== 'object' || body === null) throw new Error('Invalid body');if (!('userId' in body)) throw new Error('Missing userId');return String((body as { userId: unknown }).userId);}

Padrões de escrita para back-end: consistência que o lint não cobre sozinho

Tratamento consistente de erros

Defina um padrão para representar erros de domínio e erros inesperados. Um caminho comum é ter uma classe de erro com code e status (ou equivalente), e garantir que camadas internas não “engulam” exceções.

Exemplo de erro de aplicação:

export class AppError extends Error {constructor(message: string, public readonly status: number, public readonly code: string) {super(message);}}

Uso consistente em regras de negócio:

if (!user) {throw new AppError('User not found', 404, 'USER_NOT_FOUND');}

Padrão recomendado:

  • Erros esperados: lançe AppError (ou similar) com código estável.
  • Erros inesperados: deixe propagar e trate em um middleware/handler central.
  • Evite retornar null e também lançar erro para o mesmo caso em pontos diferentes; escolha um contrato por método.

Retorno explícito e contratos claros

Em back-end, funções “meio implícitas” geram bugs. Prefira retornos explícitos e tipos que expressem intenção.

Exemplo (evitar retorno implícito):

async function getUserName(id: string) {const user = await repo.findById(id);if (!user) return;return user.name;}

Melhor:

async function getUserName(id: string): Promise<string> {const user = await repo.findById(id);if (!user) throw new AppError('User not found', 404, 'USER_NOT_FOUND');return user.name;}

Se a ausência for parte do contrato, expresse no tipo:

async function getUserName(id: string): Promise<string | null> {const user = await repo.findById(id);return user ? user.name : null;}

Organização de imports

Além de import/order, defina convenções:

  • Imports de tipos com import type (evita dependências em runtime e melhora legibilidade).
  • Evite caminhos relativos longos; use aliases do TypeScript quando fizer sentido (e configure o resolver do ESLint).
  • Não misture imports de bibliotecas externas com internos sem linha em branco.

Exemplo com import type:

import type { Request, Response } from 'express';import { AppError } from '../errors/app-error.js';import { createUser } from '../services/create-user.js';

Limites de complexidade e legibilidade

Complexidade alta dificulta manutenção. Você pode impor limites com regras do ESLint e também com padrões de refatoração.

Adicione regras de complexidade no eslint.config.js:

rules: {"complexity": ["warn", 10],"max-depth": ["warn", 4],"max-params": ["warn", 4]}

Como agir quando estourar:

  • Quebre funções grandes em funções menores com nomes descritivos.
  • Troque if encadeado por early returns.
  • Extraia validações para helpers puros (fáceis de testar).

Checklist de qualidade para Pull Requests (legibilidade e manutenção)

  • Lint e format: npm run check passou localmente e no CI.
  • Sem Promises soltas: não há violações de @typescript-eslint/no-floating-promises; uso de void apenas quando intencional.
  • Tipos claros: sem any novo (ou, se inevitável, justificado e isolado); preferiu unknown + validação quando entrada é externa.
  • Imports organizados: ordem consistente, sem duplicatas, import type quando aplicável.
  • Contratos explícitos: funções assíncronas com retornos e tipos coerentes; ausência de retorno é expressa no tipo (null/undefined) ou tratada como erro.
  • Erros consistentes: erros esperados usam um padrão (ex.: AppError com code); não há try/catch que engole exceção sem ação.
  • Complexidade sob controle: funções não excedem limites de complexidade/profundidade; lógica complexa foi extraída e nomeada.
  • Legibilidade: nomes de variáveis e funções comunicam intenção; não há comentários redundantes explicando “o óbvio”, apenas decisões.
  • Manutenção: mudanças evitam duplicação; trechos repetidos viraram funções/utilitários; não há “gambiarras” sem ticket/justificativa.

Agora responda o exercício sobre o conteúdo:

Em um back-end Node.js com TypeScript, qual prática ajuda a evitar conflito entre ferramentas e aumentar a qualidade do código ao combinar ESLint e Prettier?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A recomendação é separar responsabilidades: o Prettier padroniza o formato, enquanto o ESLint detecta problemas e aplica padrões. Integrar os dois evita conflitos e faz o estilo inconsistente virar falha no lint/CI.

Próximo capitúlo

Testabilidade no Node.js com Express e TypeScript: unidades, integração e contratos

Arrow Right Icon
Capa do Ebook gratuito Node.js Essencial: Construindo um Back-end com Express e TypeScript
88%

Node.js Essencial: Construindo um Back-end com Express e TypeScript

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.