Why Scopes?
Understanding the value of scopes and when to use them.
The DRY Principle
Don't Repeat Yourself is a fundamental principle in software development. Scopes help you apply this principle to your database queries.
Without Scopes
// In UserController.ts
const users = await userRepo.find({
where: { isActive: true, isVerified: true },
select: ['id', 'email', 'name'],
relations: { posts: true }
});
// In AdminController.ts
const users = await userRepo.find({
where: { isActive: true, isVerified: true },
select: ['id', 'email', 'name'],
relations: { posts: true }
});
// In UserService.ts
const users = await userRepo.find({
where: { isActive: true, isVerified: true },
select: ['id', 'email', 'name'],
relations: { posts: true }
});With Scopes
// Define once
@Scopes<User>({
verified: {
where: { isActive: true, isVerified: true },
select: ['id', 'email', 'name'],
relations: { posts: true }
}
})
// Use everywhere
const users = await userRepo.scope('verified').find();Real-World Benefits
1. Maintenance
Scenario: You need to add a new field to the "verified users" query.
Without Scopes:
- Find all occurrences (grep, search)
- Update each one individually
- Risk missing some
- Test everything
With Scopes:
- Update one scope definition
- All queries automatically updated
- Single point of testing
2. Consistency
Problem: Different developers write similar queries differently.
Without Scopes:
// Developer A
{ where: { isActive: true, isVerified: true } }
// Developer B
{ where: { isVerified: true, isActive: true } }
// Developer C
{ where: { isActive: 1, isVerified: 1 } }With Scopes:
// Everyone uses the same scope
userRepo.scope('verified')3. Readability
Without Scopes:
const result = await userRepo.find({
where: {
isActive: true,
isVerified: true,
role: In(['admin', 'moderator']),
createdAt: MoreThan(lastWeek)
},
select: ['id', 'email', 'name', 'role'],
relations: { posts: true, comments: true },
order: { createdAt: 'DESC' },
take: 10
});With Scopes:
const result = await userRepo
.scope('verified', 'staff', 'recent', 'withActivity')
.find({ take: 10 });Common Use Cases
Soft Deletes
@DefaultScope<Post>({
where: { deletedAt: IsNull() }
})Every query automatically excludes deleted records. Use unscoped() when you need them.
Multi-Tenancy
@Scopes<Data>({
forTenant: (tenantId: number) => ({
where: { tenantId }
})
})Ensure data isolation across tenants.
API Responses
@Scopes<User>({
publicFields: {
select: ['id', 'name', 'avatar']
},
privateFields: {
select: ['id', 'name', 'email', 'phone', 'address']
}
})Consistent field selection for different contexts.
Access Control
@Scopes<Document>({
accessible: (userId: number) => ({
where: [
{ ownerId: userId },
{ sharedWith: { id: userId } }
]
})
})Enforce access control at the query level.
Performance Considerations
No Runtime Overhead
Scopes are resolved at query time, not at runtime:
// This
userRepo.scope('verified').find()
// Becomes this at query time
userRepo.find({ where: { isVerified: true } })No additional database queries, no performance penalty.
Query Optimization
Scopes can actually improve performance:
@Scopes<User>({
minimal: {
select: ['id', 'name'] // Only fetch needed fields
}
})Fetch only what you need, reducing data transfer.
Team Benefits
Onboarding
New developers can understand queries faster:
// What does this query do?
userRepo.scope('verified', 'active', 'withPosts')
// Clear intent, easy to understandCode Reviews
Easier to review:
// Before: Review 20 lines of query options
// After: Review scope name, check scope definition onceTesting
Test scopes once, use everywhere:
describe('User scopes', () => {
it('verified scope filters correctly', async () => {
const users = await userRepo.scope('verified').find();
expect(users.every(u => u.isVerified)).toBe(true);
});
});When NOT to Use Scopes
Scopes aren't always the answer:
One-Off Queries
If a query is truly unique and won't be reused, don't create a scope:
// Don't create a scope for this
const user = await userRepo.findOne({
where: { email: 'specific@email.com', tempToken: 'abc123' }
});Very Simple Queries
For trivial queries, scopes might be overkill:
// Maybe too simple for a scope
const user = await userRepo.findOne({ where: { id: 1 } });Dynamic Complex Logic
If the logic is too dynamic or complex, consider other patterns:
// This might be better as a custom repository method
const results = await customComplexQuery(params);Best Practices
- Name Clearly - Use descriptive scope names
- Keep Focused - Each scope should do one thing well
- Document - Add comments for complex scopes
- Test - Write tests for your scopes
- Review - Regularly review and refactor scopes
Next Steps
- Learn about Default Scopes
- Explore Named Scopes
- Try Function Scopes
- See Real-World Examples