Guides

Express + Sequelize

Step-by-step guide to configuring Technical Debt Radar for an Express backend with Sequelize ORM and layered architecture.

Express + Sequelize

This guide covers setting up Technical Debt Radar for Express.js applications using Sequelize as the ORM with a layered architecture. Express remains the most widely deployed Node.js framework, and Sequelize is the most mature ORM in the ecosystem. Radar understands both deeply.


Expected Folder Structure

Layered architecture organizes code by technical responsibility rather than business domain:

src/
├── routes/                # Route definitions
│   ├── order.routes.ts
│   ├── user.routes.ts
│   └── index.ts
├── controllers/           # HTTP handlers
│   ├── order.controller.ts
│   └── user.controller.ts
├── middleware/             # Express middleware
│   ├── auth.middleware.ts
│   └── error.middleware.ts
├── services/              # Business logic
│   ├── order.service.ts
│   └── user.service.ts
├── repositories/          # Data access layer
│   ├── order.repository.ts
│   └── user.repository.ts
├── models/                # Sequelize models
│   ├── order.model.ts
│   ├── user.model.ts
│   └── index.ts
├── types/                 # Shared TypeScript types
│   └── index.ts
├── utils/                 # Utility functions
│   └── pagination.ts
└── app.ts                 # Express app setup

The rule: each layer can only call the layer directly below it. Routes call controllers, controllers call services, services call repositories, and repositories call models. No layer skipping.


Configuration Files

radar.yml

stack:
  language: TypeScript
  framework: Express
  orm: Sequelize
  runtime: node

architecture: layered

layers:
  - name: routes
    path: "src/routes/**"
  - name: controllers
    path: "src/controllers/**"
  - name: middleware
    path: "src/middleware/**"
  - name: services
    path: "src/services/**"
  - name: repositories
    path: "src/repositories/**"
  - name: models
    path: "src/models/**"

modules:
  - name: core
    path: "src/**"

data_volumes:
  users: M             # 10K–100K rows
  orders: L            # 100K–1M rows
  products: M
  order_items: L
  sessions: XL         # 1M–50M rows
  audit_logs: XXL      # 50M+ rows

rules.yml

architecture_rules:
  # Enforce top-down dependency flow
  - deny: routes -> services          # Routes must go through controllers
  - deny: routes -> repositories      # Routes cannot access data layer
  - deny: routes -> models
  - deny: controllers -> repositories # Controllers must go through services
  - deny: controllers -> models
  - deny: services -> routes          # No upward dependencies
  - deny: services -> controllers
  - deny: repositories -> routes
  - deny: repositories -> controllers
  - deny: repositories -> services
  - deny: models -> routes
  - deny: models -> controllers
  - deny: models -> services

  # Middleware can access services but not repositories directly
  - deny: middleware -> repositories
  - deny: middleware -> models

runtime_rules:
  block:
    - sync-fs-in-handler
    - sync-crypto
    - sync-compression
    - redos-vulnerable-regex
    - busy-wait-loop
    - unhandled-promise
  warn:
    - unbounded-json-parse
    - large-json-stringify
    - cpu-heavy-loop-in-handler

reliability_rules:
  block:
    - unhandled-promise-rejection
  warn:
    - missing-try-catch
    - external-call-no-timeout
    - empty-catch-block
    - retry-without-backoff

gates:
  block_merge:
    - architecture_violations > 0
    - circular_dependencies_introduced > 0
    - runtime_risk_critical > 0
    - reliability_critical > 0
    - critical_performance_risk > 0
    - debt_delta_score > 15
  warn:
    - complexity_increase > 5
    - coverage_drop > 2%
    - debt_delta_score > 8

Express Handler Scope Detection

Radar detects Express request handlers using multiple patterns:

// Pattern 1: route callback
app.get('/orders', async (req, res) => { /* scoped */ });

// Pattern 2: controller method passed to router
router.post('/orders', orderController.create);

// Pattern 3: express.Router() callback
router.get('/orders/:id', async (req, res, next) => { /* scoped */ });

// Pattern 4: middleware function signature
const authMiddleware = (req: Request, res: Response, next: NextFunction) => { /* scoped */ };

Any synchronous I/O, blocking crypto, or CPU-intensive operation inside these scoped functions triggers a runtime risk violation. The same code in a startup script or CLI utility is not flagged.


Common Violations and Fixes

1. N+1 Query with Sequelize

The most common performance issue in Sequelize applications. Loading related data inside a loop instead of using eager loading.

// VIOLATION: N+1 — one query per order to load items
async getOrdersWithItems(userId: string): Promise<Order[]> {
  const orders = await Order.findAll({ where: { userId } });

  for (const order of orders) {
    order.items = await OrderItem.findAll({    // N queries
      where: { orderId: order.id },
    });
  }

  return orders;
}

Fix: Use Sequelize include for eager loading.

async getOrdersWithItems(userId: string): Promise<Order[]> {
  return Order.findAll({
    where: { userId },
    include: [{
      model: OrderItem,
      as: 'items',
    }],
    limit: 50,
    order: [['createdAt', 'DESC']],
  });
}

2. Missing Pagination on Large Table

// VIOLATION: unbounded query on sessions (XL — 1M+ rows)
async getAllSessions(): Promise<Session[]> {
  return Session.findAll();   // No limit, no where, no pagination
}

Fix: Add limit, offset, and filtering.

async getSessions(page: number, filters: SessionFilters): Promise<PaginatedResult<Session>> {
  const limit = 50;
  const offset = page * limit;

  const { rows, count } = await Session.findAndCountAll({
    where: {
      ...(filters.userId && { userId: filters.userId }),
      ...(filters.active && { expiresAt: { [Op.gt]: new Date() } }),
    },
    limit,
    offset,
    order: [['createdAt', 'DESC']],
  });

  return { data: rows, total: count, page, pageSize: limit };
}

3. Controller Accessing Repository Directly

// VIOLATION: controller → repository (skips service layer)
// src/controllers/order.controller.ts
import { OrderRepository } from '../repositories/order.repository';

export class OrderController {
  constructor(private readonly orderRepo: OrderRepository) {}

  async getOrder(req: Request, res: Response) {
    const order = await this.orderRepo.findById(req.params.id);
    res.json(order);
  }
}

Fix: Route through the service layer.

// src/controllers/order.controller.ts
import { OrderService } from '../services/order.service';

export class OrderController {
  constructor(private readonly orderService: OrderService) {}

  async getOrder(req: Request, res: Response) {
    try {
      const order = await this.orderService.getById(req.params.id);
      res.json(order);
    } catch (error) {
      if (error instanceof NotFoundError) {
        res.status(404).json({ error: error.message });
        return;
      }
      throw error;
    }
  }
}

4. Sync Crypto in Middleware

// VIOLATION: sync crypto blocks event loop
// src/middleware/auth.middleware.ts
import crypto from 'crypto';

export const verifyToken = (req: Request, res: Response, next: NextFunction) => {
  const hash = crypto.pbkdf2Sync(          // blocks event loop
    req.headers.authorization!,
    process.env.SALT!,
    100000,
    64,
    'sha512',
  );
  // ...
};

Fix: Use async crypto.

import crypto from 'crypto';
import { promisify } from 'util';

const pbkdf2 = promisify(crypto.pbkdf2);

export const verifyToken = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const hash = await pbkdf2(
      req.headers.authorization!,
      process.env.SALT!,
      100000,
      64,
      'sha512',
    );
    // ...
  } catch (error) {
    next(error);
  }
};

5. Missing Error Handling in Route Handler

// VIOLATION: no try/catch, errors crash the process
router.post('/orders', async (req, res) => {
  const order = await orderService.create(req.body);
  await emailService.sendConfirmation(order.customerEmail);
  res.status(201).json(order);
});

Fix: Add error handling and use Express error middleware.

router.post('/orders', async (req, res, next) => {
  try {
    const order = await orderService.create(req.body);
    await emailService.sendConfirmation(order.customerEmail);
    res.status(201).json(order);
  } catch (error) {
    next(error);    // Passes to Express error middleware
  }
});

Sequelize-Specific Detection

Radar recognizes Sequelize-specific patterns that other tools miss:

PatternDetectionSeverity
Model.findAll() without limitUnbounded queryVolume-dependent
Model.findAll() in a loopN+1 queryAlways critical
sequelize.query() without LIMITRaw SQL unboundedVolume-dependent
Model.bulkCreate() without updateOnDuplicatePotential duplicatesWarning
Missing include with known relationsPotential N+1Warning
Model.destroy({ where: {} })Table truncationCritical

Quick Start

# 1. Install the CLI
npm i -g @radar/cli

# 2. Initialize config (auto-detects Express + Sequelize + layered)
radar init

# 3. Install the Express layered rule pack
radar pack install express-layered

# 4. Validate configuration
radar validate

# 5. Run your first scan
radar scan .
Technical Debt Radar Documentation