Project foundation

Note

Essa estrutura reflete como costumo iniciar projetos Node.js. Os exemplos são referências conceituais: podem não cobrir todos os detalhes de implementação e, conforme as tecnologias evoluem, alguns podem ficar desatualizados. O que importa é o princípio: entry point como índice, configuração centralizada, módulos por domínio.

Três decisões definem a fundação de um projeto Node.js, e o time convive com elas por anos: onde a configuração é lida, como os módulos se dividem por domínio, e o que o entry point (arquivo por onde a aplicação começa a rodar) faz quando o processo sobe. Esta página mostra a forma que mantém as três respostas visíveis no código. Editor, linter e gerenciador de pacotes ficam alinhados antes da primeira linha de domínio, porque acertar isso depois custa um diff que toca o repositório inteiro.

Conceitos fundamentais

ConceitoO que é
Entry point (ponto de entrada)Arquivo inicial que carrega configuração, registra rotas e sobe o servidor
Handler (função que atende a rota)Função que recebe a requisição e devolve a resposta
Middleware (função que roda antes do handler)Função que intercepta a requisição antes ou depois do handler
Token (bilhete de acesso)Texto que o cliente apresenta a cada requisição para provar quem é, como um crachá / credencial
JWT (JSON Web Token · token assinado que identifica o usuário)Credencial que o cliente envia a cada requisição; o servidor confere a assinatura em vez de guardar sessão
SQL (Structured Query Language · Linguagem de Consulta Estruturada)Linguagem de consulta do banco relacional; usada via driver ou ORM

Preparar o editor antes do primeiro arquivo

Duas ferramentas resolvem o atrito de formatação que aparece na primeira revisão de código em dupla:

  • EditorConfig: indentação, charset, trailing whitespace
  • ESLint + Prettier: linting e formatação de código
npm init @eslint/config
npm install --save-dev prettier
Note

Biome é uma alternativa moderna que substitui ESLint + Prettier em um único binário: mais rápido e sem conflito de configuração entre as duas ferramentas.

O arquivo que sobe o servidor é um índice, não um depósito

server.js declara a intenção e delega o resto. Quem abre o arquivo vê o que o projeto tem, não como cada parte funciona: lê a configuração, monta a aplicação, escuta a porta. Quando esse mesmo arquivo acumula rota, autenticação e conexão de banco, ele vira o lugar onde toda mudança passa, e duas pessoas nunca conseguem mexer no projeto sem conflito.

❌ Ruim: server.js como depósito de toda a configuração
import express from "express";
import jwt from "jsonwebtoken";
import rateLimit from "express-rate-limit";

const app = express();

app.use(express.json());

app.use(
  rateLimit({
    windowMs: 60 * 1000,
    max: 100,
  }),
);

app.use((req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "Unauthorized" });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET); // process.env solto
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
});

app.get("/users/:id", async (req, res) => {
  const user = await db.query("SELECT * FROM users WHERE id = $1", [
    req.params.id,
  ]);
  res.json(user.rows[0]);
});

app.post("/orders", async (req, res) => {
  const order = await db.query("INSERT INTO orders ...", [req.body]);
  res.status(201).json(order.rows[0]);
});

app.listen(process.env.PORT || 3000);
✅ Bom: server.js como índice, configuração delegada
import { config } from "./config.js";
import { createApp } from "./app.js";

const app = createApp(config);
app.listen(config.port);

Cada domínio registra as próprias rotas

Quem cuida de pedidos é o módulo de pedidos. Ele registra as rotas de /orders, cria o serviço de que precisa e recebe a fatia da configuração que lhe diz respeito. O app.js não conhece SQL, não conhece JWT, não valida corpo de requisição: ele chama quem conhece, e a lista dessas chamadas é o mapa do sistema. Um arquivo central de rotas parece organizado no primeiro mês e vira lista de conflitos de merge no terceiro, porque todo domínio novo edita a mesma linha.

❌ Ruim: app.js conhece SQL, validação e regras de negócio
// app.js
import express from "express";
import pg from "pg";
import jwt from "jsonwebtoken";

const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const app = express();

app.use(express.json());

app.get("/api/orders", async (req, res) => {
  const token = req.headers.authorization?.split(" ")[1];
  const { userId } = jwt.verify(token, process.env.JWT_SECRET);

  const { rows } = await db.query("SELECT * FROM orders WHERE user_id = $1", [
    userId,
  ]);
  res.json(rows);
});

app.post("/api/orders", async (req, res) => {
  if (!req.body.productId || !req.body.quantity) {
    return res.status(400).json({ error: "productId and quantity required" });
  }
  const { rows } = await db.query("INSERT INTO orders ...", [req.body]);
  res.status(201).json(rows[0]);
});
❌ Ruim: rotas definidas fora do domínio, em arquivo centralizado
// routes.js: arquivo monolítico de rotas
import {
  listOrders,
  getOrder,
  createOrder,
} from "./features/orders/order.endpoints.js";
import { listUsers, getUser } from "./features/users/user.endpoints.js";

export function registerRoutes(app, orderService, userService) {
  app.get("/api/orders", listOrders(orderService));
  app.get("/api/orders/:id", getOrder(orderService));

  app.post("/api/orders", createOrder(orderService));

  app.get("/api/users", listUsers(userService));
  app.get("/api/users/:id", getUser(userService));
  // domínios diferentes no mesmo arquivo: cresce sem controle
}
✅ Bom: ponto de entrada agrega os módulos
// app.js
import express from "express";
import { applyMiddleware } from "./middleware.js";
import { registerUsers } from "./features/users/users.module.js";
import { registerOrders } from "./features/orders/orders.module.js";

export function createApp(config) {
  const app = express();
  applyMiddleware(app, config);

  registerUsers(app, config);
  registerOrders(app, config);

  return app;
}
✅ Bom: domínio de Orders dono das suas rotas
// features/orders/orders.module.js
import { createOrderService } from "./order.service.js";
import { findAll, findById, create } from "./order.endpoints.js";

export function registerOrders(app, config) {
  const orderService = createOrderService(config.database);

  app.get("/api/orders", findAll(orderService));
  app.get("/api/orders/:id", findById(orderService));

  app.post("/api/orders", create(orderService));
}
// features/orders/order.endpoints.js
export function findAll(orderService) {
  return async (request, response) => {
    const orders = await orderService.findAll();
    response.json(orders);
  };
}

export function findById(orderService) {
  return async (request, response) => {
    const order = await orderService.findById(request.params.id);
    response.json(order);
  };
}

export function create(orderService) {
  return async (request, response) => {
    const order = await orderService.create(request.body);
    response.status(201).json(order);
  };
}

Uma única porta de entrada para as variáveis de ambiente

config.jsprocess.env e mais ninguém lê. Cada módulo recebe por parâmetro a seção que lhe cabe: o de pedidos recebe config.database, o de autenticação recebe config.auth. O ganho aparece no teste, onde passar um objeto qualquer substitui a configuração real, e aparece no dia em que uma variável muda de nome: o ajuste acontece em um arquivo só, sem varrer o repositório atrás de cada process.env.

❌ Ruim: process.env espalhado em todo lugar
// auth/auth.middleware.js
const secret = process.env.JWT_SECRET; // leitura direta

// db/db.client.js
const url = process.env.DATABASE_URL; // leitura direta

// server.js
const port = process.env.PORT || 3000; // leitura direta
✅ Bom: config.js como único ponto de entrada de env vars
// config.js
export const config = {
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    url: process.env.DATABASE_URL,
  },
  auth: {
    secret: process.env.JWT_SECRET,
    audience: process.env.JWT_AUDIENCE,
  },
  rateLimit: {
    windowMs: 60 * 1000,
    max: 100,
  },
};
// features/orders/orders.module.js
export function registerOrders(app, config) {
  const orderService = createOrderService(config.database); // recebe a seção
  // ...
}

A ordem do pipeline decide o que fica protegido

O Express executa cada middleware (função que roda antes do handler) na ordem em que ele foi registrado, sem exceção. Registrar a autenticação depois das rotas deixa as rotas abertas: quando o pedido chega, o handler já respondeu, e o middleware de autenticação nunca é alcançado. A sequência abaixo é a que protege:

express.json()     → faz parse do body antes de qualquer handler
rateLimit          → rejeita cedo, antes de autenticação e I/O
cors               → cabeçalhos CORS antes de autenticação
authenticate       → resolve a identidade
rotas              → handlers recebem o usuário já autenticado no contexto
❌ Ruim: authenticate depois das rotas
app.use(express.json());
app.use(cors());

app.get("/api/orders", findAll(orderService)); // rota sem proteção
app.post("/api/orders", create(orderService)); // rota sem proteção

app.use(authenticate(config.auth)); // tarde demais
✅ Bom: ordem correta do pipeline
// middleware.js
import cors from "cors";
import rateLimit from "express-rate-limit";
import express from "express";
import { authenticate } from "./auth/auth.middleware.js";

export function applyMiddleware(app, config) {
  app.use(express.json());
  app.use(rateLimit(config.rateLimit));

  app.use(cors());
  app.use(authenticate(config.auth));
}

Onde cada arquivo mora

A árvore abaixo é o desenho completo: quatro arquivos na raiz de src/ para subir e configurar, uma pasta por domínio em features/, e o que conversa com o mundo externo (banco, autenticação) isolado em infra/.

src/
├── server.js
├── app.js
├── config.js
├── middleware.js
├── features/
│   ├── orders/
│   │   ├── orders.module.jsregisterOrders()
│   │   ├── order.endpoints.js
│   │   └── order.service.js
│   └── users/
│       ├── users.module.jsregisterUsers()
│       ├── user.endpoints.js
│       └── user.service.js
└── infra/
    ├── database.client.jscreateDatabaseClient(config.database)
    └── auth.middleware.jsauthenticate(config.auth)

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.