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:
| Pattern | Detection | Severity |
|---|---|---|
Model.findAll() without limit | Unbounded query | Volume-dependent |
Model.findAll() in a loop | N+1 query | Always critical |
sequelize.query() without LIMIT | Raw SQL unbounded | Volume-dependent |
Model.bulkCreate() without updateOnDuplicate | Potential duplicates | Warning |
Missing include with known relations | Potential N+1 | Warning |
Model.destroy({ where: {} }) | Table truncation | Critical |
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 .