Already have an existing project? This guide walks you through migrating to the Scope Rule architecture with real strategies and before/after examples.
Migration Strategy
Migration should be incremental and safe . You don’t need to restructure everything at once.
Phase 1: Audit Current Structure
Identify All Components
Create an inventory of every component, service, hook, and utility: # Find all components
find src -name "*.tsx" -o -name "*.ts" -o -name "*.astro" | sort
# Or use a more sophisticated approach
tree src/components
tree src/features
Create a spreadsheet or document listing:
Component name
Current location
Files that import it
Number of features that use it
Map Feature Boundaries
Identify distinct business features in your application: Example mapping: Current structure: Feature identification:
/components
/auth → Auth feature
/products → Shop feature (products)
/cart → Shop feature (cart)
/profile → Profile feature
/ui → Shared components?
Group related functionality:
Authentication (login, register, password reset)
Shop (products, cart, checkout, wishlist)
Profile (settings, preferences, history)
Dashboard (analytics, reports)
Count Component Usage
For each component, count how many distinct features use it: # Find all imports of a component
grep -r "import.*ProductCard" src/
# Results analysis:
# src/features/shop/products/page.tsx → shop feature
# src/features/shop/cart/page.tsx → shop feature (same feature)
# src/features/wishlist/page.tsx → wishlist feature
# Count: 2 distinct features → Goes in shared/
Multiple uses within the same feature count as 1, not multiple!
Create Migration Plan
Categorize components into three groups: 1. Definitely Local (1 feature) LoginForm → Only in auth/login
ProductFilter → Only in shop/products
CartSummary → Only in shop/cart
2. Definitely Shared (2+ features) Button → Used everywhere
Modal → Used in auth, shop, profile
ProductCard → Used in shop, wishlist, recommendations
3. Uncertain (needs investigation) UserAvatar → Might be used in multiple features
ErrorBoundary → Check actual usage
Phase 2: Create New Structure
Create Feature Directories
Set up your new structure without moving files yet: Angular
Next.js
Astro
React
# Create feature structure
mkdir -p src/app/features/{auth,shop,profile}
mkdir -p src/app/features/shared/{components,services,signals}
# Feature-specific subdirectories
mkdir -p src/app/features/auth/{login,register,components,services}
mkdir -p src/app/features/shop/{products,cart,components,services}
# Create route groups
mkdir -p src/app/{ \( auth \) , \( shop \) , \( dashboard \) }
# Private folders for co-location
mkdir -p src/app/ \( auth \) /{login,register,_components,_hooks,_actions}
mkdir -p src/app/ \( shop \) /{shop,cart,_components,_actions}
# Shared directory
mkdir -p src/shared/{components,hooks,actions}
# Create page structure
mkdir -p src/pages/{blog,shop,about}
# Page-specific components
mkdir -p src/pages/blog/components
mkdir -p src/pages/shop/{components,utils}
# Shared components
mkdir -p src/components/{ui,layout,islands}
# Create feature structure
mkdir -p src/features/{auth,shop,profile}
# Feature-specific subdirectories
mkdir -p src/features/auth/{components,services,hooks}
mkdir -p src/features/shop/{components,services,hooks}
# Shared directory
mkdir -p src/shared/{components,hooks,utils}
Move Definitely Local Components First
Start with components that are definitely used by only 1 feature : # Example: Moving LoginForm (only used in auth/login)
git mv src/components/auth/LoginForm.tsx \
src/app/features/auth/login/components/login-form.tsx
Update imports in the files that use it: // Before
import { LoginForm } from '../../../components/auth/LoginForm' ;
// After (with path alias)
import { LoginForm } from '@features/auth/login/components/login-form' ;
Move Definitely Shared Components
Move components used by 2+ features to shared: # Example: Moving Button (used everywhere)
git mv src/components/ui/Button.tsx \
src/shared/components/ui/button.tsx
Update all imports: // Before
import { Button } from '../../components/ui/Button' ;
// After
import { Button } from '@shared/components/ui/button' ;
Investigate Uncertain Components
For components where usage is unclear:
Run search to find all imports
Count distinct features
Move based on the Scope Rule
Add a comment documenting the decision
/**
* UserAvatar - Shared Component
*
* Used by:
* - Profile feature (profile display)
* - Auth feature (account settings)
* - Dashboard feature (user info widget)
*
* Decision: 3 features → Must be in shared/
* Date: 2024-03-15
*/
export function UserAvatar ({ user } : Props ) {
// Implementation
}
Phase 3: Modernize Framework Patterns
While migrating structure, also update to modern framework patterns.
Angular
Next.js
Astro
React
Migrate to Angular 20 Patterns
Convert to Standalone Components
Before (NgModule): // login.module.ts
import { NgModule } from '@angular/core' ;
import { LoginComponent } from './login.component' ;
@ NgModule ({
declarations: [ LoginComponent ],
imports: [ CommonModule , ReactiveFormsModule ],
})
export class LoginModule {}
After (Standalone): // login.ts
import { Component , ChangeDetectionStrategy } from '@angular/core' ;
import { ReactiveFormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-login' ,
imports: [ ReactiveFormsModule ],
changeDetection: ChangeDetectionStrategy . OnPush ,
templateUrl: './login.html' ,
})
export class LoginComponent {
// Component logic
}
Replace Lifecycle Hooks with Signals
Before (ngOnInit): export class ProductsComponent implements OnInit {
products : Product [] = [];
loading = false ;
ngOnInit () {
this . loading = true ;
this . productService . getProducts (). subscribe ( products => {
this . products = products ;
this . loading = false ;
});
}
}
After (Signals): export class ProductsComponent {
private readonly productService = inject ( ProductService );
readonly loading = signal ( false );
readonly products = signal < Product []>([]);
constructor () {
this . loadProducts ();
}
private async loadProducts () {
this . loading . set ( true );
const data = await this . productService . getProducts ();
this . products . set ( data );
this . loading . set ( false );
}
}
Update to Modern Control Flow
**Before (ngIf, ngFor): < div *ngIf = "loading" > Loading... </ div >
< div *ngIf = "!loading" >
< div *ngFor = "let product of products" >
{{ product.name }}
</ div >
</ div >
After (@if, @for): @if (loading()) {
< div > Loading... </ div >
} @else {
@for (product of products(); track product.id) {
< div > {{ product.name }} </ div >
}
}
Migrate to Next.js 15 App Router
Convert Pages to App Router
Before (Pages Router): // pages/shop/products.tsx
import { GetServerSideProps } from 'next' ;
export default function ProductsPage ({ products }) {
return < div >{ /* UI */ } </ div > ;
}
export const getServerSideProps : GetServerSideProps = async () => {
const products = await fetchProducts ();
return { props: { products } };
};
After (App Router): // app/(shop)/shop/page.tsx
export default async function ShopPage () {
const products = await fetchProducts ();
return < div >{ /* UI */ } </ div > ;
}
Extract Server Actions
Before (API routes + client fetch): // pages/api/cart.ts
export default async function handler ( req , res ) {
if ( req . method === 'POST' ) {
await addToCart ( req . body );
res . status ( 200 ). json ({ success: true });
}
}
// Component
const handleAddToCart = async () => {
await fetch ( '/api/cart' , { method: 'POST' , ... });
};
After (Server Actions): // app/(shop)/_actions/cart-actions.ts
'use server' ;
import 'server-only' ;
export async function addToCart ( productId : string ) {
// Server-side logic
revalidatePath ( '/cart' );
}
// Component
import { addToCart } from '../_actions/cart-actions' ;
const handleAddToCart = async () => {
await addToCart ( productId );
};
Separate Server and Client Components
Before (Everything client-side): 'use client' ;
import { useState } from 'react' ;
export default function ProductPage () {
const [ products , setProducts ] = useState ([]);
useEffect (() => {
fetch ( '/api/products' ). then ( ... );
}, []);
return < div >{ /* UI */ } </ div > ;
}
After (Server Component with Client Island): // page.tsx (Server Component)
import { AddToCart } from './_components/add-to-cart' ;
export default async function ProductPage () {
const products = await fetchProducts (); // Server-side
return (
< div >
{ products . map ( product => (
< div key = {product. id } >
< h2 >{product. name } </ h2 >
< AddToCart productId = {product. id } /> { /* Client island */ }
</ div >
))}
</ div >
);
}
// _components/add-to-cart.tsx (Client Component)
'use client' ;
export function AddToCart ({ productId } : Props ) {
// Interactive logic
}
Migrate to Astro 5 Islands
Convert Static Components
Before (React component for everything): // components/BlogCard.tsx
export function BlogCard ({ title , excerpt } : Props ) {
return (
< article >
< h2 > { title } </ h2 >
< p > { excerpt } </ p >
</ article >
);
}
After (Astro component - static): ---
// pages/blog/components/blog-card.astro
export interface Props {
title : string ;
excerpt : string ;
}
const { title , excerpt } = Astro . props ;
---
< article >
< h2 > { title } </ h2 >
< p > { excerpt } </ p >
</ article >
Result: Zero JavaScript shipped for this component.
Extract Interactive Islands
Before (Everything hydrated): // components/ContactForm.tsx
export function ContactForm () {
const [ formData , setFormData ] = useState ({});
// Form logic
return < form > { /* fields */ } </ form > ;
}
// Used in page
import ContactForm from '../components/ContactForm' ;
< ContactForm />
After (Island with client directive): // components/islands/ContactForm.tsx
export default function ContactForm () {
const [ formData , setFormData ] = useState ({});
// Form logic
return < form > { /* fields */ } </ form > ;
}
// Used in page with client directive
import ContactForm from '@/components/islands/ContactForm' ;
< ContactForm client : load />
Choose the right directive:
client:load - Load immediately
client:idle - Load when browser is idle
client:visible - Load when component is visible
Set Up Content Collections
Before (Manual file reading): const posts = import . meta . glob ( '../content/blog/*.md' );
// Manual processing
After (Type-safe Content Collections): // src/content/config.ts
import { defineCollection , z } from 'astro:content' ;
const blog = defineCollection ({
type: 'content' ,
schema: z . object ({
title: z . string (),
publishDate: z . date (),
tags: z . array ( z . string ()),
}),
});
export const collections = { blog };
// Use in pages
import { getCollection } from 'astro:content' ;
const posts = await getCollection ( 'blog' );
Modernize React Patterns
Implement Container/Presentational Pattern
Before (Mixed concerns): // components/Shop.tsx
export function Shop () {
const [ products , setProducts ] = useState ([]);
const [ loading , setLoading ] = useState ( false );
useEffect (() => {
setLoading ( true );
fetchProducts (). then ( data => {
setProducts ( data );
setLoading ( false );
});
}, []);
return (
< div >
{ loading ? (
< div > Loading ...</ div >
) : (
products . map ( product => (
< div key = {product. id } > {product. name } </ div >
))
)}
</ div >
);
}
After (Separated concerns): // features/shop/shop.tsx (Container)
export function Shop () {
const { products , loading } = useProducts ();
return < ShopView products ={ products } loading ={ loading } />;
}
// features/shop/components/shop-view.tsx (Presentational)
export function ShopView ({ products , loading } : Props ) {
if ( loading ) return < div > Loading ...</ div > ;
return (
< div className = "shop" >
{ products . map ( product => (
< ProductCard key = {product. id } product = { product } />
))}
</ div >
);
}
// features/shop/hooks/use-products.ts (Custom hook)
export function useProducts () {
const [ products , setProducts ] = useState < Product []>([]);
const [ loading , setLoading ] = useState ( false );
useEffect (() => {
setLoading ( true );
fetchProducts (). then ( data => {
setProducts ( data );
setLoading ( false );
});
}, []);
return { products , loading };
}
Extract Custom Hooks
Before (Repeated logic): function ComponentA () {
const [ value , setValue ] = useState (
localStorage . getItem ( 'key' ) || ''
);
useEffect (() => {
localStorage . setItem ( 'key' , value );
}, [ value ]);
}
function ComponentB () {
// Same logic repeated
}
After (Reusable hook): // shared/hooks/use-local-storage.ts
export function useLocalStorage ( key : string , initialValue : string ) {
const [ value , setValue ] = useState (
() => localStorage . getItem ( key ) || initialValue
);
useEffect (() => {
localStorage . setItem ( key , value );
}, [ key , value ]);
return [ value , setValue ] as const ;
}
// Usage
function ComponentA () {
const [ value , setValue ] = useLocalStorage ( 'key' , '' );
}
Complete Migration Example
Let’s walk through a full migration of an e-commerce app:
Before: Everything in One Place
src/
components/
auth/
LoginForm.tsx
RegisterForm.tsx
SocialLogin.tsx
shop/
ProductCard.tsx
ProductList.tsx
ProductFilter.tsx
AddToCart.tsx
cart/
CartItem.tsx
CartSummary.tsx
ui/
Button.tsx
Modal.tsx
Input.tsx
pages/
login.tsx
register.tsx
shop.tsx
cart.tsx
After: Scope Rule Applied (Next.js Example)
src/
app/
(auth)/
login/
page.tsx
_components/
login-form.tsx # Only used in login
register/
page.tsx
_components/
register-form.tsx # Only used in register
_components/
social-login.tsx # Used by login + register (same feature)
_actions/
auth-actions.ts
(shop)/
shop/
page.tsx
_components/
product-list.tsx # Only used in shop page
product-filter.tsx # Only used in shop page
cart/
page.tsx
_components/
cart-item.tsx # Only used in cart
cart-summary.tsx # Only used in cart
_components/
add-to-cart.tsx # Used by shop + cart (same feature)
_actions/
product-actions.ts
cart-actions.ts
shared/ # 2+ features only
components/
ui/
button.tsx # Used everywhere
modal.tsx # Used in auth + shop
input.tsx # Used everywhere
product-card.tsx # Used in shop + wishlist (if added)
What Changed and Why
LoginForm & RegisterForm (Moved to local)
Before: components/auth/LoginForm.tsxAfter: app/(auth)/login/_components/login-form.tsxWhy: Each form is only used in its specific route. Following co-location principle.
SocialLogin (Stays within feature)
Before: components/auth/SocialLogin.tsxAfter: app/(auth)/_components/social-login.tsxWhy: Used by both login and register, but both are part of the same auth feature. Shared within the feature, not globally.
ProductCard (Could go shared)
Initial: app/(shop)/_components/product-card.tsxIf used by wishlist feature later: Move to shared/components/product-card.tsxWhy: Currently only used in shop. If wishlist feature is added and uses it, then it becomes shared (2+ features).
UI Components (Moved to shared)
Before: components/ui/Button.tsxAfter: shared/components/ui/button.tsxWhy: Button, Modal, Input are used across auth, shop, and potentially other features. Clear 2+ usage.
Migration Checklist
Use this checklist to track your migration progress:
Handling Team Migration
Don’t surprise your team with a massive restructure!
Communication Strategy
Share the plan : Show the team the new structure before starting
Migrate incrementally : One feature at a time, not everything at once
Update documentation : Create a guide for the team
Pair on first migrations : Work together on initial moves
Set up linting : Add ESLint rules to enforce new structure
Example Team Guide
# Component Placement Guide
We're migrating to the Scope Rule architecture.
## Quick Decision Tree
1. Is this component used by only 1 feature?
→ Place in that feature's directory
2. Is this component used by 2+ features?
→ Place in `shared/components/`
## Examples
- LoginForm → Only in auth → `app/(auth)/login/_components/`
- Button → Used everywhere → `shared/components/ui/`
- ProductCard → Only in shop (for now) → `app/(shop)/_components/`
## Questions?
Ask in #architecture channel
Troubleshooting Common Issues
Circular dependencies after restructuring
Problem: Components importing each other in circles.Solution:
Identify the circular dependency
Extract shared types to a separate file
Use dependency injection or composition
Consider if one component should be split
Path alias imports not working
Problem: TypeScript can’t resolve @features/* imports.Solution:
Verify tsconfig.json paths configuration
Restart TypeScript server
Check build tool config (Vite, Next.js, etc.)
Ensure paths don’t conflict with node_modules
Tests breaking after migration
Problem: Tests can’t find moved components.Solution:
Update test imports to use new paths
Update mock paths in test setup
Verify test path aliases match app config
Run tests after each move to catch issues early
Uncertainty about component usage
Problem: Not sure how many features use a component.Solution:
Use IDE “Find All References”
Use grep/ripgrep to search imports
Add temporary console.log to see actual usage
When uncertain, start local and move to shared if needed
Next Steps
Best Practices Learn quality checks and patterns for maintaining clean architecture
Component Placement Review the decision framework for placing new components
Use the Agents Let the agents guide your architectural decisions
Examples See real-world examples of the Scope Rule in action
components/auth/SocialLogin.tsxAfter:app/(auth)/_components/social-login.tsxWhy: Used by both login and register, but both are part of the same auth feature. Shared within the feature, not globally.