Project foundation

Note

A fundação de um projeto Node.js define três decisões estruturantes: onde fica a configuração, como módulos se organizam por domínio, e como o entry point orquestra o boot da aplicação. Editor, linter e gerenciador de pacotes ficam alinhados antes da primeira linha de domínio.

Conceitos fundamentais

ConceitoO que é
Entry point (ponto de entrada)Arquivo inicial que carrega configuração, registra rotas e sobe o servidor
Middleware (componente de pipeline)Função que intercepta a requisição antes ou depois do handler
JWT (JSON Web Token, Token Web em JSON)Token assinado usado para autenticação stateless
SQL (Structured Query Language, Linguagem de Consulta Estruturada)Linguagem de consulta do banco relacional; usada via driver ou ORM

Ambiente

Antes de iniciar, configure o editor:

  • 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.

Entry point enxuto

server.js declara intenção, não implementa. Toda configuração é delegada para módulos. O arquivo serve como índice do projeto: o leitor vê o que existe, não como funciona.

❌ Ruim: server.js como dumping ground de 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);

Módulos por domínio

Cada domínio registra suas próprias rotas e dependências. app.js não conhece SQL, JWT ou validação: apenas chama quem conhece. Os módulos ficam co-localizados com o domínio que representam.

❌ 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);
  };
}

Configuração centralizada

config.js é o único ponto de leitura de variáveis de ambiente. Nenhum módulo acessa process.env diretamente: apenas importa a seção que precisa.

❌ 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
  // ...
}

Middleware pipeline

A ordem do middleware (componente de pipeline) é determinística e importa. Registrar autenticação após roteamento não protege as rotas.

express.json()     → parseia o 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));
}

Estrutura de arquivos

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)

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.