platform-docs/development/verify-circular-deps-pattern.md

4.4 KiB

Circular Dependency Verification Pattern

Problem

ESM circular dependencies in TypeScript projects (especially NestJS with TypeORM) fail at runtime, not during:

  • tsc --noEmit (typecheck)
  • nest build (compilation)
  • Even successful builds can crash on startup

Traditional approaches are too slow or unsafe:

  • Starting the full app (./run up) takes 3+ minutes
  • Running node dist/main.js starts servers, connects to DBs (side effects)

Solution

Fast, safe verification that catches circular deps in ~5 seconds:

  1. Build the project
  2. Import AppModule (triggers decorators without bootstrapping)
  3. No servers started, no DB connections, no side effects

Implementation

1. Create verification script

Create scripts/verify-circular-deps.mjs in your NestJS project:

#!/usr/bin/env node
/**
 * Verify Circular Dependencies
 *
 * Safely checks for circular dependency issues by importing the AppModule
 * without bootstrapping the application (no server start, no DB connections).
 *
 * Usage: node scripts/verify-circular-deps.mjs
 */

import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const distPath = join(__dirname, '..', 'dist');

// Prevent application from actually starting
process.env.NODE_ENV = 'test';
process.env.SKIP_BOOTSTRAP = 'true';

console.log('🔍 Checking for circular dependencies...\n');

try {
  // Import AppModule - this triggers all decorators without starting the app
  const appModulePath = join(distPath, 'app.module.js');
  await import(appModulePath);

  console.log('✅ No circular dependency issues detected');
  console.log('   All modules and entities loaded successfully\n');
  process.exit(0);
} catch (error) {
  console.error('❌ Circular dependency detected!\n');
  console.error('Error:', error.message);
  console.error('\nStack trace:');
  console.error(error.stack);
  console.error('\n💡 Hint: Look for entities with bidirectional relations.');
  console.error('   Use string references in decorators: @ManyToOne(\'EntityName\', ...)\n');
  process.exit(1);
}

2. Add to package.json

{
  "scripts": {
    "verify": "pnpm build && node scripts/verify-circular-deps.mjs"
  }
}

3. Usage

# During development
pnpm verify

# In CI/CD (add to your pipeline)
pnpm verify || exit 1

Common Causes & Fixes

TypeORM Bidirectional Relations

Problem:

// user.entity.ts
import { Post } from './post.entity'
@OneToMany(() => Post, post => post.author)
posts!: Post[]

// post.entity.ts
import { User } from './user.entity'
@ManyToOne(() => User, user => user.posts)
author!: User

Solution: Use string references

// user.entity.ts
@OneToMany('Post', post => post.author)
posts!: any[]

// post.entity.ts
@ManyToOne('User', user => user.posts)
author!: any

NestJS Module Circular Imports

Problem:

// users.module.ts
import { PostsModule } from './posts.module'

// posts.module.ts
import { UsersModule } from './users.module'

Solution: Use forwardRef()

// users.module.ts
@Module({
  imports: [forwardRef(() => PostsModule)]
})

// posts.module.ts
@Module({
  imports: [forwardRef(() => UsersModule)]
})

Performance

Method Time Safe Catches Issue
tsc --noEmit ~2s (type-level only)
pnpm build ~3s (just transpiles)
pnpm verify ~5s (runtime check)
./run up ~3m (but too slow)
node dist/main.js ~10s (side effects!)

Integration

Local Development

Add to pre-commit hook or run before pushing:

pnpm verify && git push

CI/CD (Forgejo Actions)

- name: Verify no circular dependencies
  run: pnpm verify

Pre-deployment Check

#!/bin/bash
# deploy.sh
pnpm verify || {
  echo "❌ Circular dependency detected. Fix before deploying."
  exit 1
}
pnpm build
# ... continue deployment

Rollout Plan

Apply this pattern to all NestJS backend services:

  • landing/backend-api
  • marketplace/backend-api
  • sso/api
  • webmap/router
  • Other NestJS services...

References

  • Original issue: landing-api circular deps between entities
  • Detection time: 3 minutes (via ./run up) → 5 seconds (via pnpm verify)
  • Date added: 2026-01-21