Agent Skill
2/7/2026

nestjs-repository-pattern

NestJS Repository Pattern architecture and best practices for api-app. Use this skill when implementing data access layers, separating business logic from database queries, or refactoring services to use repositories. Applies to NestJS backend modules using Prisma ORM.

J
just
0GitHub Stars
1Views
npx skills add Just-DX/maintenance-management-system

SKILL.md

Namenestjs-repository-pattern
DescriptionNestJS Repository Pattern architecture and best practices for api-app. Use this skill when implementing data access layers, separating business logic from database queries, or refactoring services to use repositories. Applies to NestJS backend modules using Prisma ORM.

name: nestjs-repository-pattern description: NestJS Repository Pattern architecture and best practices for api-app. Use this skill when implementing data access layers, separating business logic from database queries, or refactoring services to use repositories. Applies to NestJS backend modules using Prisma ORM. license: MIT metadata: author: maintenance-management-system version: '1.0.0' organization: JustMT date: February 2026 abstract: Clean architecture guidelines for implementing repository pattern in NestJS applications. Defines clear separation between data access (repositories), business logic (services), and HTTP concerns (controllers). Includes ownership rules, naming conventions, transaction support, and error handling patterns.

NestJS Repository Pattern Best Practices

Clean architecture guidelines for implementing repository pattern in NestJS applications with Prisma ORM.

When to Apply

Reference these guidelines when:

  • Creating new NestJS modules with database access
  • Refactoring services that contain direct Prisma/DB calls
  • Implementing data access layers
  • Writing queries for User, Asset, Site, or other entities
  • Setting up transaction support
  • Organizing code for testability and maintainability

Core Principles

1. Separation of Concerns (CRITICAL)

Rule: All Prisma/DB queries MUST live in repository classes, not in services or controllers.

// ❌ INCORRECT: Direct Prisma call in service
@Injectable()
export class AssetService {
  async findById(id: string) {
    return await prisma.asset.findFirst({ where: { id } })
  }
}

// ✅ CORRECT: Repository handles data access
@Injectable()
export class AssetsRepository {
  async findById(id: string, siteId: string, select?: Prisma.AssetSelect) {
    return prisma.asset.findFirst({ where: { id, siteId }, select })
  }
}

@Injectable()
export class AssetService {
  constructor(private readonly assetsRepo: AssetsRepository) {}

  async findById(id: string, siteId: string) {
    const asset = await this.assetsRepo.findById(id, siteId, this.select)
    if (!asset) throw new NotFoundException('Asset not found')
    return asset
  }
}

Why: Enables testing, reusability, and clear architectural boundaries.

2. Repository Ownership (CRITICAL)

Rule: Each entity gets ONE repository. The repository is owned by the module that manages that entity as its "main table."

Entity Ownership Map:

EntityRepositoryModuleLocation
UserUsersRepositoryusersmodules/users/repositories/users.repository.ts
AssetAssetsRepositoryassetsmodules/assets/repositories/assets.repository.ts
SiteSitesRepositorysitemodules/site/repositories/sites.repository.ts

Cross-Module Usage:

// ✅ CORRECT: Auth module imports users module to access UsersRepository
@Module({
  imports: [UsersModule], // Import the module
  providers: [AuthService],
})
export class AuthModule {}

@Injectable()
export class AuthService {
  constructor(
    private readonly usersRepo: UsersRepository // Inject from imported module
  ) {}
}

Anti-Pattern:

// ❌ INCORRECT: Duplicating user queries in auth.repository.ts
// If you need user queries, use UsersRepository from users module

3. Repository Content (HIGH)

Rule: Repositories contain ONLY data access code. No business logic, no HTTP exceptions.

// ✅ CORRECT: Repository returns null, no exceptions
@Injectable()
export class UsersRepository {
  async findById(id: string): Promise<User | null> {
    return prisma.user.findUnique({ where: { id } })
    // Returns null if not found - let service decide what to do
  }
}

// ✅ CORRECT: Service adds business logic and HTTP exceptions
@Injectable()
export class UserService {
  async getUser(id: string): Promise<UserDto> {
    const user = await this.usersRepo.findById(id)
    if (!user) {
      throw new NotFoundException('User not found') // HTTP exception
    }
    if (!user.isActive) {
      throw new ForbiddenException('User is inactive') // Business rule
    }
    return this.transformToDto(user) // Business logic
  }
}

4. Method Naming Conventions (HIGH)

Rule: Use consistent prefixes that signal behavior.

Repository Methods:

  • find* - Returns null if not found
  • create - Creates and returns record
  • update - Updates and returns record
  • softDelete / delete - Deletes record
  • count - Returns count
@Injectable()
export class UsersRepository {
  // Returns null if not found
  async findById(id: string): Promise<User | null>
  async findByEmail(email: string): Promise<User | null>

  // Returns created/updated record
  async create(data: CreateData): Promise<User>
  async update(id: string, data: UpdateData): Promise<User>

  // Soft delete
  async softDelete(id: string): Promise<User>
}

Service Methods:

  • find* - May return null or throw (business decision)
  • get* - Expected to exist, throws if not found
  • create / update / remove - Operations with validation
@Injectable()
export class UserService {
  // Throws NotFoundException if not found
  async getUser(id: string): Promise<UserDto>

  // Returns null or record
  async findUserByEmail(email: string): Promise<UserDto | null>
}

5. Transaction Support (HIGH)

Rule: All repository methods MUST accept optional transaction parameter.

// ✅ CORRECT: Repository accepts optional transaction
@Injectable()
export class UsersRepository {
  async findById(
    id: string,
    tx?: Prisma.TransactionClient // Optional transaction
  ): Promise<User | null> {
    const db = tx ?? prisma // Use transaction if provided, else prisma
    return db.user.findUnique({ where: { id } })
  }

  async upsertUserProfile(payload: CreateUserData, tx?: Prisma.TransactionClient) {
    const db = tx ?? prisma

    // If already in transaction, execute directly
    if (tx) {
      return tx.user.upsert({
        /* ... */
      })
    }

    // Otherwise create new transaction
    return prisma.$transaction(async (innerTx) => {
      return innerTx.user.upsert({
        /* ... */
      })
    })
  }
}

// ✅ CORRECT: Service coordinates transactions
@Injectable()
export class UserService {
  async complexOperation(userId: string) {
    return prisma.$transaction(async (tx) => {
      const user = await this.usersRepo.findById(userId, tx)
      const sites = await this.sitesRepo.findByUser(userId, tx)
      // All operations use same transaction
      return { user, sites }
    })
  }
}

6. Error Handling (MEDIUM)

Rule: Repositories return null or throw Prisma errors. Services transform to HTTP exceptions.

// ✅ CORRECT: Repository doesn't catch errors
@Injectable()
export class AssetsRepository {
  async create(data: CreateAssetData) {
    return prisma.asset.create({ data }) // Prisma errors bubble up
  }
}

// ✅ CORRECT: Service catches and transforms errors
@Injectable()
export class AssetService {
  async create(dto: CreateAssetDto) {
    try {
      return await this.assetsRepo.create({
        name: dto.name.trim(), // Business logic
        code: dto.code.trim(),
        /* ... */
      })
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError) {
        if (error.code === 'P2002') {
          throw new ConflictException('Asset already exists') // HTTP exception
        }
      }
      throw error
    }
  }
}

7. Module Organization (MEDIUM)

Rule: Follow consistent folder structure for repositories.

modules/
├── users/
│   ├── users.module.ts           # Exports UsersRepository
│   └── repositories/
│       └── users.repository.ts   # All User queries
├── assets/
│   ├── assets.module.ts          # Provides AssetsRepository
│   ├── controllers/
│   │   └── asset.controller.ts
│   ├── services/
│   │   └── asset.service.ts      # Uses AssetsRepository
│   └── repositories/
│       └── assets.repository.ts  # All Asset queries
└── site/
    ├── site.module.ts
    ├── controllers/
    ├── services/
    └── repositories/
        └── sites.repository.ts

8. Dependency Injection (MEDIUM)

Rule: Repositories are provided at module level and injected into services.

// ✅ CORRECT: Module setup
@Module({
  imports: [ListModule, AuthModule],
  controllers: [AssetController],
  providers: [AssetService, AssetsRepository], // Provide repository
  exports: [AssetService, AssetsRepository], // Export for other modules
})
export class AssetsModule {}

// ✅ CORRECT: Service injection
@Injectable()
export class AssetService {
  constructor(
    private readonly assetsRepo: AssetsRepository // Inject repository
  ) {}
}

Common Patterns

Pattern 1: Simple CRUD

// Repository
@Injectable()
export class AssetsRepository {
  async findById(id: string, siteId: string, select?: Prisma.AssetSelect) {
    return prisma.asset.findFirst({ where: { id, siteId, isDeleted: false }, select })
  }

  async create(data: CreateAssetData, select?: Prisma.AssetSelect) {
    return prisma.asset.create({ data, select })
  }

  async update(id: string, data: Prisma.AssetUpdateInput, select?: Prisma.AssetSelect) {
    return prisma.asset.update({ where: { id }, data, select })
  }

  async softDelete(id: string) {
    return prisma.asset.update({ where: { id }, data: { isDeleted: true } })
  }
}

// Service
@Injectable()
export class AssetService {
  constructor(private readonly assetsRepo: AssetsRepository) {}

  async findById(siteId: string, id: string): Promise<AssetDto> {
    const asset = await this.assetsRepo.findById(id, siteId, this.select)
    if (!asset) throw new NotFoundException('Asset not found')
    return asset
  }

  async create(siteId: string, dto: CreateAssetDto): Promise<AssetDto> {
    try {
      return await this.assetsRepo.create(
        {
          siteId,
          name: dto.name.trim(),
          code: dto.code.trim(),
        },
        this.select
      )
    } catch (error) {
      this.handlePrismaError(error)
    }
  }
}

Pattern 2: Complex Queries with Relations

// Repository
@Injectable()
export class UsersRepository {
  async findUserRolesByIdAndSite(
    userId: string,
    siteId: string,
    tenantId: string,
    appScopes: AppScope[]
  ) {
    return prisma.userRole.findMany({
      where: {
        userId,
        siteId,
        role: { siteId, appScope: { in: appScopes } },
      },
      select: {
        role: {
          select: {
            id: true,
            code: true,
            name: true,
            rolePermissions: {
              where: { permission: { tenantId, appScope: { in: appScopes } } },
              select: { permission: { select: { code: true } } },
            },
          },
        },
      },
    })
  }
}

Pattern 3: Assertion Helper

// Service helper method
private async assertExists(siteId: string, id: string): Promise<void> {
  const asset = await this.assetsRepo.findById(id, siteId, { id: true })
  if (!asset) {
    throw new NotFoundException('Asset not found')
  }
}

// Usage
async update(siteId: string, id: string, dto: UpdateAssetDto) {
  await this.assertExists(siteId, id) // Throws if not found
  return this.assetsRepo.update(id, { /* ... */ })
}

When NOT to Use Repository

  • read-only utilities: Simple helper functions that don't need injection
  • One-off scripts: Migration scripts or seeders (can use Prisma directly)
  • ListService delegate: List/pagination queries can pass Prisma delegate to ListService

Migration Checklist

When refactoring existing code to use repositories:

  1. ✅ Create repository file in repositories/ folder
  2. ✅ Move all Prisma queries from service to repository
  3. ✅ Add transaction support (tx?: Prisma.TransactionClient) to all methods
  4. ✅ Make repositories return null for not-found cases
  5. ✅ Update service to inject repository
  6. ✅ Add HTTP exceptions in service layer
  7. ✅ Update module to provide/export repository
  8. ✅ Verify no direct Prisma calls remain in service
  9. ✅ Check cross-module dependencies (import correct module)
  10. ✅ Run tests and check for compilation errors

Related Documentation

  • See apps/api-app/REFACTORING_SUMMARY.md for implementation details
  • See apps/api-app/ARCHITECTURE.md for module structure
  • See docs/ARCHITECTURE.md for overall system architecture
Skills Info
Original Name:nestjs-repository-patternAuthor:just