Cursor defaults to tightly coupled code with direct imports instead of dependency injection. By adding IoC rules to .cursorrules, referencing your DI container setup with @file, and prompting Cursor to generate constructor-injected classes, you get testable, loosely coupled code that works with any IoC container like InversifyJS or tsyringe.
Generating dependency-injection-friendly code with Cursor
Cursor tends to generate classes with hardcoded dependencies (new UserRepository() inside a service). This makes testing difficult and couples your code tightly. This tutorial shows how to configure Cursor to generate classes that depend on interfaces, accept dependencies through constructors, and integrate with IoC containers like InversifyJS or tsyringe.
Prerequisites
- Cursor installed with a TypeScript project
- A DI container installed (InversifyJS, tsyringe, or similar)
- Understanding of interfaces and constructor injection
- Familiarity with Cmd+L and Cmd+K in Cursor
Step-by-step guide
Add dependency injection rules to .cursor/rules
Add dependency injection rules to .cursor/rules
Create rules that tell Cursor to always use constructor injection and interface-based dependencies. This changes the default pattern from tight coupling to dependency inversion.
1---2description: Dependency injection rules3globs: "src/services/**/*.ts,src/repositories/**/*.ts"4alwaysApply: true5---67## Dependency Injection Rules8- ALWAYS depend on interfaces, NEVER on concrete classes9- ALWAYS accept dependencies through the constructor10- NEVER use 'new' to instantiate dependencies inside a class11- Define interfaces in src/types/interfaces.ts12- Use @injectable() decorator from tsyringe13- Use @inject() for constructor parameters14- Name interfaces with I prefix: IUserRepository, IOrderService15- Export interfaces separately from implementations16- Every class must be testable with mocked dependenciesExpected result: Cursor generates classes with constructor injection and interface-based dependencies.
Generate interfaces for your service layer
Generate interfaces for your service layer
Ask Cursor to create the interfaces first, then the implementations. This establishes the contract before the code, making the dependency graph explicit.
1// Cursor Chat prompt (Cmd+L):2// Create interfaces for a UserService and UserRepository3// in src/types/interfaces.ts. UserRepository should have4// findById, findAll, create, update, delete methods.5// UserService should have getUser, listUsers, createUser,6// updateUser. All methods return Promises with typed results.78export interface IUserRepository {9 findById(id: string): Promise<User | null>;10 findAll(page: number, limit: number): Promise<User[]>;11 create(data: CreateUserDto): Promise<User>;12 update(id: string, data: UpdateUserDto): Promise<User>;13 delete(id: string): Promise<void>;14}1516export interface IUserService {17 getUser(id: string): Promise<User>;18 listUsers(page: number, limit: number): Promise<User[]>;19 createUser(data: CreateUserDto): Promise<User>;20 updateUser(id: string, data: UpdateUserDto): Promise<User>;21}Expected result: Clean interface definitions that serve as contracts for implementations.
Generate an injectable service class
Generate an injectable service class
Ask Cursor to generate a service that implements the interface and accepts its dependencies through the constructor. Reference the interfaces file so Cursor knows the exact method signatures.
1// Cursor Chat prompt (Cmd+L):2// @src/types/interfaces.ts Generate a UserService class3// that implements IUserService. It depends on IUserRepository4// through constructor injection. Use @injectable() and5// @inject() decorators from tsyringe. Add error handling.67import { injectable, inject } from 'tsyringe';8import type { IUserService, IUserRepository } from '@/types/interfaces';910@injectable()11export class UserService implements IUserService {12 constructor(13 @inject('IUserRepository')14 private userRepository: IUserRepository15 ) {}1617 async getUser(id: string): Promise<User> {18 const user = await this.userRepository.findById(id);19 if (!user) throw new Error(`User not found: ${id}`);20 return user;21 }2223 async listUsers(page: number, limit: number): Promise<User[]> {24 return this.userRepository.findAll(page, limit);25 }2627 async createUser(data: CreateUserDto): Promise<User> {28 return this.userRepository.create(data);29 }3031 async updateUser(id: string, data: UpdateUserDto): Promise<User> {32 return this.userRepository.update(id, data);33 }34}Pro tip: Reference the interface file with @file in every prompt. This ensures Cursor implements the exact method signatures defined in the contract.
Expected result: An injectable service class that depends on an interface, not a concrete repository.
Generate the DI container configuration
Generate the DI container configuration
Ask Cursor to create the container setup that binds interfaces to implementations. This is where concrete classes are wired together.
1// Cursor Chat prompt (Cmd+L):2// @src/types/interfaces.ts @src/services/UserService.ts3// @src/repositories/UserRepository.ts4// Generate a tsyringe container configuration at5// src/container.ts that registers:6// IUserRepository -> UserRepository7// IUserService -> UserService89import 'reflect-metadata';10import { container } from 'tsyringe';11import { UserRepository } from './repositories/UserRepository';12import { UserService } from './services/UserService';1314container.register('IUserRepository', { useClass: UserRepository });15container.register('IUserService', { useClass: UserService });1617export { container };Expected result: A DI container that maps interfaces to implementations, ready for use across the application.
Generate a test with mocked dependencies
Generate a test with mocked dependencies
Verify the DI pattern works by asking Cursor to generate a unit test that mocks the repository. This proves the service is properly decoupled from its dependencies.
1// Cursor Chat prompt (Cmd+L):2// @src/services/UserService.ts @src/types/interfaces.ts3// Generate a unit test for UserService using Vitest.4// Mock IUserRepository with vi.fn() implementations.5// Test getUser with found and not-found scenarios.6// Do NOT import the real UserRepository.78import { describe, it, expect, vi } from 'vitest';9import { UserService } from './UserService';10import type { IUserRepository } from '@/types/interfaces';1112const mockRepo: IUserRepository = {13 findById: vi.fn(),14 findAll: vi.fn(),15 create: vi.fn(),16 update: vi.fn(),17 delete: vi.fn(),18};1920describe('UserService', () => {21 const service = new UserService(mockRepo);2223 it('returns user when found', async () => {24 vi.mocked(mockRepo.findById).mockResolvedValue({ id: '1', name: 'Test' });25 const user = await service.getUser('1');26 expect(user.name).toBe('Test');27 });2829 it('throws when user not found', async () => {30 vi.mocked(mockRepo.findById).mockResolvedValue(null);31 await expect(service.getUser('999')).rejects.toThrow('User not found');32 });33});Expected result: Tests pass with mocked dependencies, proving the service is properly decoupled.
Complete working example
1import { injectable, inject } from 'tsyringe';2import type {3 IUserService,4 IUserRepository,5} from '@/types/interfaces';6import type { User, CreateUserDto, UpdateUserDto } from '@/types/user';78@injectable()9export class UserService implements IUserService {10 constructor(11 @inject('IUserRepository')12 private readonly userRepository: IUserRepository13 ) {}1415 async getUser(id: string): Promise<User> {16 if (!id) throw new Error('User ID is required');17 const user = await this.userRepository.findById(id);18 if (!user) {19 throw new Error(`User not found: ${id}`);20 }21 return user;22 }2324 async listUsers(page = 1, limit = 20): Promise<User[]> {25 if (page < 1) throw new Error('Page must be >= 1');26 if (limit < 1 || limit > 100) throw new Error('Limit must be 1-100');27 return this.userRepository.findAll(page, limit);28 }2930 async createUser(data: CreateUserDto): Promise<User> {31 if (!data.email) throw new Error('Email is required');32 return this.userRepository.create(data);33 }3435 async updateUser(id: string, data: UpdateUserDto): Promise<User> {36 const existing = await this.userRepository.findById(id);37 if (!existing) throw new Error(`User not found: ${id}`);38 return this.userRepository.update(id, data);39 }4041 async deleteUser(id: string): Promise<void> {42 const existing = await this.userRepository.findById(id);43 if (!existing) throw new Error(`User not found: ${id}`);44 return this.userRepository.delete(id);45 }46}Common mistakes when generating dependency-injection-friendly code with Cursor
Why it's a problem: Cursor using 'new Repository()' inside service classes
How to avoid: Add 'NEVER use new to instantiate dependencies inside a class' to .cursorrules. Require constructor injection for all dependencies.
Why it's a problem: Forgetting to define interfaces before implementations
How to avoid: Always generate interfaces first, then reference them with @file when generating implementations.
Why it's a problem: Importing concrete classes in test files
How to avoid: In your test generation prompt, explicitly say 'Do NOT import the real repository. Use mocked implementations of the interface.'
Best practices
- Define interfaces before implementations and reference them in all Cursor prompts
- Add DI rules to .cursorrules requiring constructor injection and interface-based dependencies
- Place all interfaces in src/types/interfaces.ts for easy @file referencing
- Generate tests alongside services to verify decoupling immediately
- Use the @inject() decorator with string tokens for container resolution
- Keep the container configuration in a single file for easy auditing
- Mark constructor parameters as private readonly for immutability
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Create a dependency-injection-friendly UserService in TypeScript using tsyringe. The service depends on IUserRepository (interface) through constructor injection. Include @injectable and @inject decorators. Implement getUser, listUsers, createUser, updateUser methods with error handling. Then generate the container configuration and a unit test with mocked repository.
In Cursor Chat (Cmd+L): @src/types/interfaces.ts @.cursor/rules/dependency-injection.mdc Generate a UserService that implements IUserService. Accept IUserRepository through constructor injection with @inject('IUserRepository'). Add input validation and error handling. Follow our DI rules.
Frequently asked questions
Which DI container should I use with Cursor?
tsyringe is the simplest and works well with Cursor. InversifyJS has more features but more boilerplate. NestJS has built-in DI. Specify your choice in .cursorrules so Cursor uses the correct decorators.
Does Cursor understand the @injectable decorator?
Yes. Cursor's training data includes tsyringe, InversifyJS, and NestJS DI patterns. Specify which container you use in your rules and Cursor will apply the correct decorators.
Can I use DI without a container?
Yes. Manual constructor injection (passing dependencies in the constructor call) works without any container. Ask Cursor to generate a factory function instead of container configuration.
How do I handle DI in React components?
Use React Context to provide services to components. Ask Cursor to generate a ServiceProvider component that resolves services from the DI container and passes them through Context.
Will DI patterns increase my bundle size?
The runtime overhead is minimal. tsyringe adds about 3KB gzipped. The reflect-metadata polyfill adds about 5KB. For frontend code where bundle size matters, consider manual injection without a container.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation