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-typescriptSe você usa Promises intensamente (muito comum em back-end), vale adicionar regras de Promises via plugin:
npm i -D eslint-plugin-promise2) 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:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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: desencorajaany(mantém contratos claros). Está comowarnpara 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_modulescoverage4) 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-stagednpx husky initNo package.json:
{"lint-staged":{"**/*.{ts,js}":["eslint --fix","prettier --write"]}}No hook .husky/pre-commit:
npx lint-stagedComo 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-promisesLeitura 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
nulle 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
ifencadeado 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 checkpassou localmente e no CI. - Sem Promises soltas: não há violações de
@typescript-eslint/no-floating-promises; uso devoidapenas quando intencional. - Tipos claros: sem
anynovo (ou, se inevitável, justificado e isolado); preferiuunknown+ validação quando entrada é externa. - Imports organizados: ordem consistente, sem duplicatas,
import typequando 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.:
AppErrorcomcode); não hátry/catchque 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.