Visual density
Densidade visual é a quantidade de informação que você empilha em cada bloco de código. Quando muitas linhas se acumulam sem espaço, o olho cansa e você perde o fio do raciocínio. Quando linhas sem relação ficam grudadas, o leitor não sabe onde uma ideia termina e a outra começa. A saída é direta: junte as linhas que contam a mesma pequena história e separe cada história da próxima com uma linha em branco. Este guia mostra como aplicar isso em JavaScript e Node.js, sempre com um exemplo ruim e um bom lado a lado.
Os princípios gerais estão em densidade visual. Aqui eles aparecem adaptados a JavaScript.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| visual density (densidade visual) | Quantidade de informação por bloco de código; o alvo é pouca por bloco e muita por arquivo |
| semantic group (grupo semântico) | Poucas linhas que executam uma etapa coesa, por exemplo validar, calcular ou salvar |
| blank line (linha em branco) | Separa dois grupos; faz o papel que antes cabia a um comentário de seção |
| boundary (limite) | Linha que separa camadas, por exemplo do handler para o service; pede uma linha em branco antes |
| multi-line block (bloco de várias linhas) | Objeto, array ou comando quebrado em várias linhas; pede um respiro depois de si |
| column alignment (alinhamento em colunas) | Espaços extras para alinhar = ou : na vertical; antipadrão, quebra a cada renomeação |
A regra central
A regra que resolve quase tudo: agrupe poucas linhas por vez e separe cada grupo com uma linha em branco. O tamanho natural de um grupo é duas linhas. Três valem quando dividir em duas mais uma deixaria a última linha sozinha. A partir de quatro, quebre em dois grupos de duas.
❌ Ruim: tudo grudado, sem um respiro entre os passos
async function registerUser(input) {
const { name, email } = input;
const exists = await db.users.findByEmail(email);
if (exists) throw new ConflictError("Email taken");
const hash = await hashPassword(input.password);
const user = await db.users.create({ name, email, hash });
const token = generateToken(user.id);
await sendWelcomeEmail(email, token);
return user;
}
✅ Bom: cada fase visível, no máximo duas linhas por grupo
async function registerUser(input) {
const { name, email } = input;
const exists = await userRepository.findByEmail(email);
if (exists) throw new ConflictError("Email taken");
const hash = await hashPassword(input.password);
const user = await userRepository.create({ name, email, hash });
const token = generateToken(user.id);
await sendWelcomeEmail(email, token);
return user;
}
O return fica junto da linha que nomeia o valor
Quando a linha logo acima do return é a const que dá nome ao valor devolvido,
as duas formam uma dupla e ficam juntas, sem linha em branco entre elas. Não
importa quantos passos venham antes. A linha em branco separa essa dupla do que
veio antes; ela nunca entra no meio da dupla.
❌ Ruim: a linha em branco parte a dupla no meio
function mapErrorToStatus(error) {
const status = errorStatusByCode[error.code] ?? 500;
return status;
}
✅ Bom: a `const` e o `return` juntos
function mapErrorToStatus(error) {
const status = errorStatusByCode[error.code] ?? 500;
return status;
}
Quando o return cola na linha acima e quando ganha um respiro
O return só cola na linha imediatamente acima quando essa linha é a const, de
uma única linha, que nomeia o valor devolvido. Em todos os outros casos, deixe
uma linha em branco antes do return:
- a linha acima ocupa várias linhas (um objeto ou comando quebrado em vários pedaços);
- a linha acima só produz um efeito (um
await, uma função que não devolve valor) e não dá nome ao resultado; - o valor devolvido foi criado vários passos antes, sem formar dupla com a linha de cima.
❌ Ruim: a linha em branco separou a `const` do `return` que a devolve
function formatOrderDate(isoString, locale = "pt-BR") {
const parsedDate = new Date(isoString);
const formatter = new Intl.DateTimeFormat(locale, {
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: "America/Sao_Paulo",
});
const formattedDate = formatter.format(parsedDate);
return formattedDate;
}
O formatter ocupa várias linhas e pede um respiro depois de si, mas aqui a
linha em branco foi parar antes do return. formattedDate e
return formattedDate são a dupla que nomeia e devolve o valor: não devem ser
separados.
✅ Bom: o bloco de várias linhas isolado, a `const` e o `return` juntos
function formatOrderDate(isoString, locale = "pt-BR") {
const parsedDate = new Date(isoString);
const formatter = new Intl.DateTimeFormat(locale, {
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: "America/Sao_Paulo",
});
const formattedDate = formatter.format(parsedDate);
return formattedDate;
}
A linha em branco fica depois do formatter, que ocupa várias linhas. A
dupla formattedDate + return formattedDate continua junta.
✅ Bom: return com respiro quando é montado a partir de um objeto de várias linhas
function buildOrderResponse(order, requestId) {
const data = {
id: order.id,
total: order.total,
items: order.items,
};
return { data, requestId };
}
data é um objeto de várias linhas; a linha em branco antes do return separa
esse bloco grande do envelope final.
Exceção: funções de uma linha ficam compactas. O return é o único conteúdo.
function findPendingOrders(userId) {
return orderRepository.findByStatus(userId, "pending");
}
A variável e o if que a valida ficam juntos
Uma variável e o if que a valida logo abaixo formam uma dupla quando o if
cabe em uma linha só (if (...) return;, if (...) throw ...;). Nesse caso, a
linha em branco vem depois da dupla, nunca entre a variável e o seu if.
Quando o if é escrito com chaves { } (mesmo com uma única instrução dentro),
ele vira uma fase à parte: o bloco já tem peso visual próprio. Aí vale a regra de
que todo bloco de várias linhas pede um respiro antes de si. O critério aqui é o
peso visual do bloco na tela.
❌ Ruim: a variável foi separada do `if` que a valida
const order = await fetchOrder(orderId);
if (!order) return;
const invoice = buildInvoice(order);
✅ Bom: `if` de uma linha, junto da variável
const order = await fetchOrder(orderId);
if (!order) return;
const invoice = buildInvoice(order);
✅ Bom: `if` com chaves, fase à parte com um respiro antes
const handler = eventHandlers[eventType];
if (!handler) {
logUnhandledEventType(eventType);
return;
}
const eventPayload = event.data;
✅ Bom: `if` com chaves pede respiro antes mesmo com uma só instrução
const response = await requestFn();
if (response.status !== 429) {
return response;
}
const delayMs = Math.pow(2, attempt) * 1000;
O bloco ocupa três linhas e tem peso visual próprio. Em uma linha só, ficaria junto da variável; com chaves, pede uma linha em branco antes.
Não deixe uma linha sozinha entre espaços
Três declarações simples seguidas (const, let, var) formam um grupo coeso.
Se você quebrar em duas mais uma, a última fica sozinha entre duas linhas em
branco, parecendo esquecida. Mantenha as três juntas. Só divida quando forem
quatro, aí em dois pares.
❌ Ruim: a última linha sozinha entre espaços
const MINIMUM_DRIVING_AGE = 18;
const ORDER_STATUS_APPROVED = 2;
const ONE_DAY_MS = 86_400_000;
✅ Bom: as três juntas
const MINIMUM_DRIVING_AGE = 18;
const ORDER_STATUS_APPROVED = 2;
const ONE_DAY_MS = 86_400_000;
✅ Bom: quatro viram dois pares
const MINIMUM_DRIVING_AGE = 18;
const ORDER_STATUS_APPROVED = 2;
const ONE_DAY_MS = 86_400_000;
const MAX_RETRY_ATTEMPTS = 3;
Duas linhas onde a segunda usa o valor da primeira
Quando a última linha usa o valor recém-criado na linha de cima, as duas formam uma dupla. O respiro natural fica antes da dupla, nunca entre uma linha e o valor de que ela depende.
❌ Ruim: a linha foi separada do valor de que depende
function buildShippingLabel(order) {
const fullName = `${order.customer.firstName} ${order.customer.lastName}`;
const addressLine = `${order.address.street}, ${order.address.number}`;
const cityLine = `${order.address.city} - ${order.address.state}, ${order.address.zipCode}`;
const label = `${fullName}\n${addressLine}\n${cityLine}\nOrder #${order.id}`;
return label;
}
✅ Bom: as duas linhas dependentes juntas
function buildShippingLabel(order) {
const fullName = `${order.customer.firstName} ${order.customer.lastName}`;
const addressLine = `${order.address.street}, ${order.address.number}`;
const cityLine = `${order.address.city} - ${order.address.state}, ${order.address.zipCode}`;
const label = `${fullName}\n${addressLine}\n${cityLine}\nOrder #${order.id}`;
return label;
}
Prepare as partes, depois monte o resultado
Quando você prepara dois ou mais pedaços e depois tem uma linha que junta vários deles (não só o último), trate essa montagem como uma fase à parte, com uma linha em branco antes. É o padrão "preparar as partes, depois montar o resultado". Ele é diferente do caso anterior, em que a última linha depende só da linha logo acima e por isso fica junto dela.
Como decidir rápido:
- A última linha usa só o valor recém-criado acima? É uma dupla dependente: fica junto.
- A última linha costura vários pedaços declarados em linhas diferentes? É a fase de montagem: linha em branco antes.
❌ Ruim: preparação e montagem grudadas como se fossem linhas iguais
function buildDeliveryMessage(user, order) {
const fullName = `${user.firstName} ${user.lastName}`;
const address = `${order.address.street}, ${order.address.city} - ${order.address.state}`;
const deliveryMessage = `Olá ${fullName}, seu pedido #${order.id} foi confirmado e será entregue em ${address} em até ${order.deliveryDays} dias úteis.`;
return deliveryMessage;
}
deliveryMessage usa fullName, address, order.id e order.deliveryDays ao
mesmo tempo. Não é uma dupla com address: é a fase de montagem. Grudada como se
as três linhas fossem iguais, as fases somem.
✅ Bom: pedaços em uma dupla, montagem à parte, `return` junto do valor
function buildDeliveryMessage(user, order) {
const fullName = `${user.firstName} ${user.lastName}`;
const address = `${order.address.street}, ${order.address.city} - ${order.address.state}`;
const deliveryMessage = `Olá ${fullName}, seu pedido #${order.id} foi confirmado e será entregue em ${address} em até ${order.deliveryDays} dias úteis.`;
return deliveryMessage;
}
Duas fases ficam visíveis: preparar os pedaços, depois montar e devolver.
✅ Bom: contraste, a última linha depende só da anterior
function buildOrderSlug(order) {
const normalizedTitle = order.title.toLowerCase().replace(/\s+/g, "-");
const slug = `${order.id}-${normalizedTitle}`;
return slug;
}
slug depende só de normalizedTitle, a linha logo acima. As duas ficam
juntas, e o return continua junto de slug.
Dentro de laços e condições curtas
Em laços (while, for) e condições curtas, duas linhas mais uma continua sendo
a divisão natural quando as linhas não são todas do mesmo tipo.
❌ Ruim: três linhas de tipos diferentes grudadas
while (attempt < maxAttempts) {
const connection = connectToDatabase();
if (connection.isReady) break;
attempt++;
}
✅ Bom: variável e `if` juntos, o incremento separado
while (attempt < maxAttempts) {
const connection = connectToDatabase();
if (connection.isReady) break;
attempt++;
}
Deixe cada fase do método visível
Métodos com vários passos (buscar, transformar, salvar, responder) devem deixar cada passo visível. Uma linha em branco entre eles marca onde um termina e o outro começa, ainda mais quando os passos cruzam um limite entre camadas, por exemplo do handler para o service.
❌ Ruim: todos os passos grudados, sem separação visual
async function createUserHandler(req, res) {
const sanitized = sanitizeCreateUser(req.body);
const input = createUserSchema.parse(sanitized);
await createUser(input);
const body = { id: input.id };
res.status(201).json(body);
}
✅ Bom: cada passo visível
async function createUserHandler(request, response) {
const sanitized = sanitizeCreateUser(request.body);
const input = createUserSchema.parse(sanitized);
await createUser(input);
const body = { id: input.id };
response.status(201).json(body);
}
No teste, a verificação é uma fase separada
No teste, a linha que verifica o resultado (expect) é uma fase própria. A linha
em branco antes dela separa o que está sendo verificado de como você
preparou o cenário.
❌ Ruim: `expect` grudado na preparação, as fases somem
it("applies percentage discount to order price", () => {
const order = { price: 100, discountPercentage: 10 };
const actualOrder = applyDiscount(order);
const expectedPrice = 90;
expect(actualOrder.price).toBe(expectedPrice);
});
✅ Bom: `expect` separado, a verificação como fase própria
it("applies percentage discount to order price", () => {
const order = { price: 100, discountPercentage: 10 };
const actualOrder = applyDiscount(order);
const expectedPrice = 90;
expect(actualOrder.price).toBe(expectedPrice);
});
Depois de um bloco de várias linhas, deixe um respiro
Quando um objeto, um array ou um comando quebra em várias linhas, esse bloco já ocupa um espaço visual próprio. Deixe uma linha em branco depois dele para separá-lo do próximo passo. Sem esse respiro, o leitor não vê onde o bloco termina e o próximo começa.
❌ Ruim: objeto de várias linhas grudado no próximo comando
async function createSession(user) {
const claims = {
sub: user.id,
email: user.email,
roles: user.roles,
issuedAt: Date.now(),
};
const token = await signJwt(claims);
return token;
}
✅ Bom: linha em branco depois do objeto separa o bloco
async function createSession(user) {
const claims = {
sub: user.id,
email: user.email,
roles: user.roles,
issuedAt: Date.now(),
};
const token = await signJwt(claims);
return token;
}
Dois if seguidos com chaves pedem uma linha entre eles
Dois if seguidos, cada um com um bloco de várias linhas entre chaves, formam
uma parede: o olho não distingue onde um bloco termina e o outro começa. Sempre
coloque uma linha em branco entre eles.
Exceção: os if de saída rápida, com uma linha só (if (!input) throw ...),
são do mesmo tipo e ficam juntos, como qualquer grupo de linhas iguais.
❌ Ruim: dois blocos com chaves grudados
function processOrder(order) {
if (order.status === "pending") {
notifyCustomer(order);
scheduleReview(order);
}
if (order.total > 1_000) {
flagForAudit(order);
notifyManager(order);
}
}
✅ Bom: linha em branco entre os blocos
function processOrder(order) {
if (order.status === "pending") {
notifyCustomer(order);
scheduleReview(order);
}
if (order.total > 1_000) {
flagForAudit(order);
notifyManager(order);
}
}
✅ Bom: saídas rápidas de uma linha ficam juntas
function validateInput(input) {
if (!input) throw new ValidationError("Input required");
if (!input.email) throw new ValidationError("Email required");
if (!input.password) throw new ValidationError("Password required");
return input;
}
Não alinhe o código em colunas
Não use espaços extras para alinhar =, : ou valores na vertical. Use sempre um
espaço só. O alinhamento artificial quebra assim que você renomeia qualquer
coisa, gera um diff cheio de ruído (mudanças que não importam) e treina o olho a
procurar colunas que somem na primeira refatoração.
❌ Ruim: espaços extras alinhando colunas
const userName = "alice";
const userEmail = "alice@example.com";
const userRole = "admin";
const lastLoginAt = new Date();
✅ Bom: um espaço só, sem preenchimento
const userName = "alice";
const userEmail = "alice@example.com";
const userRole = "admin";
const lastLoginAt = new Date();
Textos longos montados em uma linha
Um texto longo grudado dentro de um return esconde os pedaços que o compõem.
Separe cada pedaço em uma variável com nome antes de montar o resultado. Em
JavaScript esse texto costuma ser uma template string (o texto entre crases
que aceita valores no meio com ${...}).
❌ Ruim: texto enorme em uma linha, sem nome nos pedaços
function buildDeliveryMessage(user, order) {
return `Olá ${user.firstName} ${user.lastName}, seu pedido #${order.id} foi confirmado e será entregue no endereço ${order.address.street}, ${order.address.city} - ${order.address.state} em até ${order.deliveryDays} dias úteis.`;
}
✅ Bom: pedaços com nome, texto final limpo
function buildDeliveryMessage(user, order) {
const fullName = `${user.firstName} ${user.lastName}`;
const address = `${order.address.street}, ${order.address.city} - ${order.address.state}`;
const deliveryMessage = `Olá ${fullName}, seu pedido #${order.id} foi confirmado e será entregue em ${address} em até ${order.deliveryDays} dias úteis.`;
return deliveryMessage;
}
DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.