Cursor frequently introduces global variables, singletons, and module-level mutable state that break modular design and make testing difficult. By adding architecture rules to .cursorrules that forbid global state and mandate dependency injection, you prevent Cursor from generating tightly coupled code that becomes unmaintainable as your project grows.
Preventing bad architecture from Cursor
Global variables are the fastest way to share state, which is why Cursor reaches for them. But they create hidden dependencies, make tests unpredictable, and cause bugs when multiple parts of the code modify the same state. This tutorial configures Cursor to use proper module patterns instead.
Prerequisites
- Cursor installed with a project open
- Code that may contain global state patterns
- Understanding of module exports and imports
- Familiarity with Cursor Chat (Cmd+L) and Cmd+K
Step-by-step guide
Add anti-global rules to .cursor/rules
Add anti-global rules to .cursor/rules
Create rules that explicitly forbid global state patterns and redirect Cursor toward modular alternatives.
1---2description: Architecture rules - no global state3globs: "src/**/*.ts,src/**/*.tsx"4alwaysApply: true5---67## Global State Rules8- NEVER use global variables or module-level mutable state9- NEVER use the singleton pattern with module-level instances10- NEVER store state in file-scoped 'let' variables11- ALWAYS pass dependencies through function parameters or constructors12- ALWAYS use React Context or Zustand for shared UI state13- For configuration: use an imported config module (readonly)14- For caching: use a dedicated cache service passed via DI15- For logging: accept a logger parameter, do not import a global logger1617## Allowed Module-Level Declarations18- const with primitive values or frozen objects19- Type definitions and interfaces20- Pure functions with no side effects21- Readonly configuration objectsExpected result: Cursor avoids global state and uses dependency injection or module parameters instead.
Audit existing code for global state
Audit existing code for global state
Use Cursor to find all global state patterns in your codebase. This identifies the scope of the problem before you start fixing.
1// Cursor Chat prompt (Cmd+L, Ask mode):2// @codebase Find all instances of global state in src/:3// 1. Module-level 'let' or 'var' declarations4// 2. Singleton patterns (getInstance())5// 3. Mutable objects assigned at module scope6// 4. Global event emitters or bus instances7// 5. window.* or globalThis.* assignments8// List each file, the pattern found, and suggest a modular alternative.Expected result: A comprehensive list of global state patterns with suggested refactoring approaches.
Refactor a global singleton to dependency injection
Refactor a global singleton to dependency injection
Select a singleton pattern and use Cmd+K to refactor it into a proper dependency-injected class. Reference your architecture rules so Cursor follows the modular pattern.
1// BEFORE (global singleton):2let instance: DatabasePool | null = null;3export function getPool(): DatabasePool {4 if (!instance) {5 instance = new DatabasePool(process.env.DATABASE_URL);6 }7 return instance;8}910// Select the code, press Cmd+K:11// Refactor this global singleton into a factory function12// that creates a pool instance. The instance should be13// created once in the app entry point and passed to14// services via constructor injection.1516// AFTER:17export function createPool(url: string): DatabasePool {18 return new DatabasePool(url);19}2021// In app entry point:22const pool = createPool(config.databaseUrl);23const userService = new UserService(pool);Expected result: Singleton refactored to a factory function with explicit dependency passing.
Replace global logger with injected logger
Replace global logger with injected logger
Global loggers are the most common anti-pattern. Refactor them to accept a logger parameter so each module can have its own logger context.
1// BEFORE:2import { logger } from './globalLogger';3export function processOrder(order: Order) {4 logger.info('Processing order', { orderId: order.id });5}67// Cmd+K prompt:8// Refactor to accept a logger parameter instead of9// importing a global logger. Use an ILogger interface.1011// AFTER:12import type { ILogger } from '@/types/interfaces';13export function processOrder(order: Order, logger: ILogger) {14 logger.info('Processing order', { orderId: order.id });15}Expected result: Logger passed as a parameter, making the function testable with a mock logger.
Verify the refactoring with tests
Verify the refactoring with tests
Generate tests that prove the refactored code works without global state. The key test is that each function can be called with different dependencies.
1// Cursor Chat prompt (Cmd+L):2// @src/services/orderService.ts Generate a test that:3// 1. Creates a mock logger4// 2. Calls processOrder with the mock5// 3. Verifies the mock logger was called correctly6// 4. Proves no global state is accessed78import { processOrder } from './orderService';9import type { ILogger } from '@/types/interfaces';1011const mockLogger: ILogger = {12 info: vi.fn(),13 error: vi.fn(),14 warn: vi.fn(),15};1617it('uses the injected logger', () => {18 processOrder({ id: '123' } as Order, mockLogger);19 expect(mockLogger.info).toHaveBeenCalledWith(20 'Processing order', { orderId: '123' }21 );22});Expected result: Tests pass using only injected dependencies, proving no global state is needed.
Complete working example
1---2description: Modular architecture rules - no global state3globs: "src/**/*.{ts,tsx,js,jsx}"4alwaysApply: true5---67## Forbidden Patterns8- `let` or `var` at module/file scope (mutable globals)9- Singleton pattern with `getInstance()` or module-level instances10- `window.*` or `globalThis.*` assignments11- Global event buses without explicit subscription management12- Module-level `Map`, `Set`, or `Array` used as shared state1314## Required Patterns15- Pass dependencies through constructor or function parameters16- Create instances in the application entry point (composition root)17- Use factory functions instead of singletons18- Use React Context or state management libraries for UI state19- Use readonly config objects (created once, never mutated)2021## Module-Level Allowed22- `const` with immutable primitive values23- `const` with Object.freeze() for config objects24- Type/interface/enum definitions25- Pure function declarations26- Re-exports from barrel index files2728## How to Share State29| Need | Solution |30|------|----------|31| Database pool | Factory function, inject via constructor |32| Logger | Accept ILogger parameter |33| Configuration | Readonly config module |34| Cache | Cache service, inject via constructor |35| UI state | React Context or Zustand store |36| Event handling | Typed EventEmitter, inject via constructor |Common mistakes when preventing bad architecture from Cursor
Why it's a problem: Cursor creating module-level cache Maps
How to avoid: Add 'NEVER create module-level Map, Set, or Array for state' to .cursorrules. Use a cache service passed via DI.
Why it's a problem: Cursor importing a global logger directly
How to avoid: Add 'Accept a logger parameter, do not import a global logger' to your rules.
Why it's a problem: Cursor generating singleton getInstance() patterns
How to avoid: Forbid singletons in .cursorrules. Use factory functions and create instances at the composition root.
Best practices
- Forbid global mutable state explicitly in .cursor/rules
- Pass all dependencies through function parameters or constructors
- Create all instances at the application entry point (composition root)
- Use factory functions instead of singleton patterns
- Accept ILogger interfaces as parameters instead of importing global loggers
- Use Object.freeze() for configuration objects at module scope
- Test every function with injected dependencies to verify no global state leaks
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Create architecture rules for a TypeScript project that forbid global variables, singletons, module-level mutable state, and global event buses. Provide allowed alternatives for each: factory functions for singletons, constructor injection for dependencies, readonly config for settings, and React Context for UI state. Include a comparison table.
In Cursor Chat (Cmd+L): @codebase @.cursor/rules/architecture.mdc Find all global state patterns in src/. For each global variable, singleton, or module-level mutable state, suggest a refactoring to use dependency injection or factory functions instead.
Frequently asked questions
Are all module-level variables bad?
No. Immutable constants (const MAX_RETRIES = 3), frozen config objects, type definitions, and pure functions are fine at module level. Only mutable state (let, unfrozen objects used as caches) is problematic.
How do I share a database connection pool without a global?
Create the pool in your app entry point and pass it to services through their constructors. This is the composition root pattern.
Is React Context considered global state?
React Context is scoped to the provider tree, not truly global. It is the recommended way to share state in React. Just avoid putting everything in a single context.
What about environment variables (process.env)?
Access process.env in a single config module and export a readonly object. All other modules import from the config module, never from process.env directly.
Can I use Zustand stores without them being global?
Zustand stores are technically module-level singletons, but they are designed for this purpose with proper subscription management. They are an acceptable exception to the no-singleton rule for UI state.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation