Comprehensive step-by-step migration guide for transitioning the Wedding RSVP application from legacy architecture to modern feature-based structure.
Reference: CONST-P1 (Modular Architecture), CONST-P11 (Version Control Excellence), CONST-P12 (Deployment & Production Readiness)
This migration guide provides detailed instructions for migrating the Wedding RSVP application from its original structure to a modern, feature-based architecture with enhanced security, performance, and maintainability.
rsvps.json
)src/
├── components/ # Mixed UI components
├── pages/ # Legacy page structure
├── styles/ # Basic CSS
└── utils/ # Utility functions
src/
├── app/ # Next.js App Router
│ ├── admin/ # Admin dashboard pages
│ ├── api/ # API routes
│ │ ├── auth/ # Authentication endpoints
│ │ └── rsvp/ # RSVP endpoints
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── features/ # Feature-based modules
│ ├── auth/ # Authentication feature
│ │ ├── components/ # Auth UI components
│ │ ├── hooks/ # Auth-specific hooks
│ │ ├── services/ # Auth API operations
│ │ ├── types/ # Auth-specific types
│ │ └── index.ts # Clean exports
│ ├── rsvp/ # RSVP feature
│ │ ├── components/ # RSVP form components
│ │ ├── hooks/ # RSVP data management hooks
│ │ ├── services/ # RSVP API operations
│ │ ├── types/ # RSVP-specific types
│ │ └── index.ts
│ ├── admin/ # Admin feature
│ │ ├── components/ # Admin UI components
│ │ ├── hooks/ # Admin data management hooks
│ │ ├── services/ # Admin API operations
│ │ ├── types/ # Admin-specific types
│ │ └── index.ts
│ └── content/ # Wedding Content Feature
│ ├── components/ # Story, moments, schedule components
│ ├── hooks/ # Content management hooks
│ ├── services/ # Content API operations
│ ├── types/ # Content types and schemas
│ └── index.ts
├── shared/ # Cross-cutting concerns
│ ├── components/ # Reusable UI (Button, Card, Input)
│ ├── hooks/ # Shared React hooks
│ ├── utils/ # Utility functions
│ ├── types/ # Shared type definitions
│ └── constants/ # Application constants
└── lib/ # Core libraries
├── services/ # Base service classes
├── security/ # Security utilities
└── validation/ # Validation schemas
# From project root
mkdir -p src/features/{auth,rsvp,admin,content}/{components,hooks,services,types}
mkdir -p src/shared/{components,hooks,utils,types,constants}
mkdir -p src/lib/{services,security,validation}
mv src/components/LoginForm.tsx src/features/auth/components/ mv src/components/AuthGuard.tsx src/features/auth/components/
mv src/components/RSVPForm.tsx src/features/rsvp/components/ mv src/components/RSVPStatus.tsx src/features/rsvp/components/
mv src/components/Button.tsx src/shared/components/ mv src/components/Card.tsx src/shared/components/ mv src/components/Input.tsx src/shared/components/
3. **Update Import Paths**
```typescript
// Before
import { LoginForm } from '../components/LoginForm';
import { Button } from '../components/Button';
// After
import { LoginForm } from '@/features/auth';
import { Button } from '@/shared/components';
// src/features/rsvp/index.ts export { RSVPForm } from ‘./components/RSVPForm’; export { useRSVP } from ‘./hooks/useRSVP’; export { RSVPService } from ‘./services/rsvp-service’; export type { RSVPFormData, RSVPResponse } from ‘./types’;
### 1.2 Design System Implementation
#### Tailwind Configuration Migration
```javascript
// tailwind.config.js - Updated wedding theme
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {
colors: {
// Wedding color palette
primary: {
50: '#fdf7f0',
100: '#faeee1',
// ... full color scale
900: '#7c2d12',
},
secondary: {
// Complementary colors
},
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
serif: ['Playfair Display', 'serif'],
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
};
// src/shared/components/Button.tsx
interface ButtonProps {
variant: 'primary' | 'secondary' | 'outline' | 'ghost';
size: 'sm' | 'md' | 'lg';
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({ variant, size, children, ...props }) => {
const baseClasses = 'font-medium rounded-lg transition-colors';
const variants = {
primary: 'bg-primary-600 text-white hover:bg-primary-700',
secondary: 'bg-secondary-600 text-white hover:bg-secondary-700',
outline: 'border border-primary-600 text-primary-600 hover:bg-primary-50',
ghost: 'text-primary-600 hover:bg-primary-50',
};
return (
<button
className={cn(baseClasses, variants[variant], sizes[size])}
{...props}
>
{children}
</button>
);
};
// src/shared/types/rsvp.ts
import { z } from 'zod';
export const CreateRSVPSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
email: z.string().email('Invalid email format').max(255),
phone: z.string().optional(),
attendance: z.enum(['yes', 'no', 'maybe']),
plusOne: z.boolean().default(false),
plusOneName: z.string().optional(),
dietaryRestrictions: z.string().max(500).optional(),
message: z.string().max(1000).optional(),
});
export type CreateRSVPRequest = z.infer<typeof CreateRSVPSchema>;
// tsconfig.json - Strict mode enabled
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
// src/features/auth/services/auth-service.ts
export class AuthService {
private static readonly ADMIN_CREDENTIALS = {
username: 'admin',
password: process.env.ADMIN_PASSWORD || 'wedding2025',
};
static async login(credentials: LoginRequest): Promise<AuthResponse> {
// Input validation
const { username, password } = LoginSchema.parse(credentials);
// Security: Rate limiting implemented at API level
if (username === this.ADMIN_CREDENTIALS.username &&
password === this.ADMIN_CREDENTIALS.password) {
const token = jwt.sign(
{ username, role: 'admin' },
process.env.JWT_SECRET || 'dev-secret',
{ expiresIn: '24h' }
);
return { success: true, token, user: { username, role: 'admin' } };
}
throw new Error('Invalid credentials');
}
}
// src/features/auth/components/LoginForm.tsx
import { useAuth } from '../hooks/useAuth';
import { Button } from '@/shared/components';
export const LoginForm: React.FC = () => {
const { login, loading, error } = useAuth();
// Component implementation with proper error handling
// and loading states
};
// src/features/rsvp/services/rsvp-service.ts
export class RSVPService {
private static readonly DATA_FILE = path.join(process.cwd(), 'rsvps.json');
static async create(data: CreateRSVPRequest): Promise<RSVP> {
// Validation
const validData = CreateRSVPSchema.parse(data);
// Security: Input sanitization
const sanitizedData = this.sanitizeInput(validData);
// Read existing data
const rsvps = await this.readRSVPs();
// Check for duplicates
const existingRSVP = rsvps.find(rsvp => rsvp.email === sanitizedData.email);
if (existingRSVP) {
throw new Error('RSVP already exists for this email');
}
// Create new RSVP
const newRSVP: RSVP = {
id: crypto.randomUUID(),
...sanitizedData,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// Save to file
rsvps.push(newRSVP);
await this.writeRSVPs(rsvps);
return newRSVP;
}
}
// src/features/admin/components/AdminDashboard.tsx
export const AdminDashboard: React.FC = () => {
const { rsvps, loading, error } = useRSVPList();
const [activeSection, setActiveSection] = useState<SectionKey>('rsvps');
// Optimized with useMemo and useCallback
const handleSectionChange = useCallback((section: SectionKey) => {
setActiveSection(section);
}, []);
const sectionContent = useMemo(() => {
switch (activeSection) {
case 'rsvps':
return <RSVPManagement rsvps={rsvps} />;
case 'events':
return <EventManagement />;
case 'content':
return <ContentManagement />;
default:
return null;
}
}, [activeSection, rsvps]);
return (
<div className="admin-dashboard">
{/* Dashboard implementation */}
</div>
);
};
// src/lib/security/headers.ts
export const securityHeaders = {
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
};
// src/lib/security/rate-limit.ts
const rateLimits = new Map<string, { count: number; resetTime: number }>();
export function checkRateLimit(identifier: string, maxRequests: number, windowMs: number): boolean {
const now = Date.now();
const limit = rateLimits.get(identifier);
if (!limit || now > limit.resetTime) {
rateLimits.set(identifier, { count: 1, resetTime: now + windowMs });
return true;
}
if (limit.count >= maxRequests) {
return false;
}
limit.count++;
return true;
}
// src/lib/security/sanitization.ts
export function sanitizeString(input: string): string {
return input
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+="[^"]*"/gi, '')
.replace(/on\w+='[^']*'/gi, '')
.trim();
}
// src/app/api/auth/login/route.ts
export async function POST(request: Request) {
// Security headers
const headers = new Headers(securityHeaders);
try {
// Rate limiting
const clientIP = getClientIP(request);
if (!checkRateLimit(`auth:${clientIP}`, 5, 15 * 60 * 1000)) {
return NextResponse.json(
{ error: 'Too many login attempts. Please try again later.' },
{ status: 429, headers }
);
}
// Input validation and sanitization
const body = await request.json();
const credentials = LoginSchema.parse(body);
// Authentication
const result = await AuthService.login(credentials);
return NextResponse.json(result, { headers });
} catch (error) {
return NextResponse.json(
{ error: 'Authentication failed' },
{ status: 401, headers }
);
}
}
{
"rsvps": [
{
"id": "uuid",
"name": "string",
"email": "string",
"attendance": "yes|no|maybe",
"createdAt": "ISO string",
"updatedAt": "ISO string"
}
]
}
// src/lib/data/data-access.ts
export abstract class DataAccess {
abstract create<T>(data: T): Promise<T>;
abstract findById<T>(id: string): Promise<T | null>;
abstract findMany<T>(filter?: object): Promise<T[]>;
abstract update<T>(id: string, data: Partial<T>): Promise<T>;
abstract delete(id: string): Promise<void>;
}
// JSON implementation
export class JSONDataAccess extends DataAccess {
// Current JSON file operations
}
// Future SQLite implementation
export class SQLiteDataAccess extends DataAccess {
// SQLite operations
}
// scripts/migrate-json-to-sqlite.ts
export async function migrateJSONToSQLite() {
console.log('Starting JSON to SQLite migration...');
// 1. Read existing JSON data
const jsonData = await fs.readFile('rsvps.json', 'utf-8');
const rsvps = JSON.parse(jsonData);
// 2. Initialize SQLite database
const db = new Database('wedding-rsvp.db');
// 3. Create tables
await db.exec(`
CREATE TABLE IF NOT EXISTS rsvps (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
phone TEXT,
attendance TEXT NOT NULL CHECK (attendance IN ('yes', 'no', 'maybe')),
plus_one BOOLEAN DEFAULT FALSE,
plus_one_name TEXT,
dietary_restrictions TEXT,
message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
// 4. Migrate data
for (const rsvp of rsvps.rsvps || []) {
await db.run(`
INSERT INTO rsvps (id, name, email, phone, attendance, plus_one, plus_one_name, dietary_restrictions, message, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
rsvp.id,
rsvp.name,
rsvp.email,
rsvp.phone || null,
rsvp.attendance,
rsvp.plusOne || false,
rsvp.plusOneName || null,
rsvp.dietaryRestrictions || null,
rsvp.message || null,
rsvp.createdAt,
rsvp.updatedAt,
]);
}
// 5. Create backup
await fs.copyFile('rsvps.json', `rsvps.json.backup.${Date.now()}`);
console.log(`Migration complete. Migrated ${rsvps.rsvps?.length || 0} RSVPs.`);
}
// src/lib/database/config.ts
export const databaseConfig = {
development: {
type: 'json',
file: 'rsvps.json',
},
production: {
type: 'sqlite',
file: 'wedding-rsvp.db',
// Future PostgreSQL config
// host: process.env.DB_HOST,
// port: parseInt(process.env.DB_PORT || '5432'),
// database: process.env.DB_NAME,
// username: process.env.DB_USER,
// password: process.env.DB_PASSWORD,
},
};
# .env.example
# Authentication
ADMIN_PASSWORD=your_secure_admin_password
JWT_SECRET=your_jwt_secret_key_minimum_32_characters
# Database (for future SQLite/PostgreSQL)
DATABASE_URL=file:./wedding-rsvp.db
# DATABASE_URL=postgresql://username:password@localhost:5432/wedding_rsvp
# Email (for future email integration)
EMAIL_FROM=noreply@yourweddingdomain.com
RESEND_API_KEY=your_resend_api_key
# Analytics (optional)
VERCEL_ANALYTICS_ID=your_analytics_id
# Security
ALLOWED_ORIGINS=https://yourweddingdomain.com,https://www.yourweddingdomain.com
npm run build
npm audit
# 1. Build verification
npm run build
npm run start
# 2. Environment setup
cp .env.example .env.local
# Edit .env.local with production values
# 3. Security verification
npm audit --audit-level moderate
# 4. Deploy to Vercel
npx vercel --prod
# 5. Post-deployment verification
curl -I https://your-domain.com
curl -X POST https://your-domain.com/api/rsvp -d '{"test": "security"}'
// Monitor key metrics
const monitoringChecks = {
'Response Time': 'Average < 2 seconds',
'Error Rate': '< 1% of requests',
'Security Headers': 'All headers present',
'Rate Limiting': 'Blocks excessive requests',
'RSVP Submissions': 'Successful storage',
'Admin Access': 'Secure login working',
};
# 1. Stop current deployment
git checkout main
# 2. Restore from backup
cp rsvps.json.backup rsvps.json
# 3. Redeploy previous version
npm run build
npm run start
# 4. Verify functionality
curl https://your-domain.com/api/rsvp
# 1. Stop application
pkill -f "npm run start"
# 2. Restore JSON backup
cp rsvps.json.backup.{timestamp} rsvps.json
# 3. Revert database configuration
git checkout HEAD~1 -- src/lib/database/
# 4. Restart with JSON storage
npm run start
// scripts/data-recovery.ts
export async function recoverData() {
// 1. Check for JSON backup files
const backupFiles = await fs.readdir('.').then(files =>
files.filter(f => f.startsWith('rsvps.json.backup.'))
);
// 2. Find most recent backup
const latestBackup = backupFiles.sort().reverse()[0];
// 3. Validate backup data
const backupData = JSON.parse(await fs.readFile(latestBackup, 'utf-8'));
// 4. Restore from backup
await fs.copyFile(latestBackup, 'rsvps.json');
console.log(`Data recovered from ${latestBackup}`);
}
# Unit tests
npm run test
# Integration tests
npm run test:integration
# E2E tests
npm run test:e2e
# Performance tests
npm run test:performance
RSVP Functionality
Admin Dashboard
Security Testing
const performanceTargets = {
'Page Load Time': '< 3 seconds',
'First Contentful Paint': '< 1.5 seconds',
'Largest Contentful Paint': '< 2.5 seconds',
'Cumulative Layout Shift': '< 0.1',
'First Input Delay': '< 100ms',
'Bundle Size': '< 150kB gzipped',
};
# Install Lighthouse
npm install -g lighthouse
# Run audit
lighthouse https://your-domain.com --output html --output-path ./lighthouse-report.html
# Target scores
# Performance: > 90
# Accessibility: > 90
# Best Practices: > 90
# SEO: > 90
# Clear TypeScript cache
rm -rf .next/cache
rm -rf node_modules/.cache
# Reinstall dependencies
npm ci
# Check TypeScript version
npx tsc --version
# Check for missing environment variables
npm run build 2>&1 | grep -i "environment"
# Verify all imports
npm run build 2>&1 | grep -i "module not found"
# Check bundle analysis
npm run build && npm run analyze
// Debug security headers
const debugHeaders = (request: Request) => {
console.log('Request headers:', Object.fromEntries(request.headers.entries()));
console.log('Origin:', request.headers.get('origin'));
console.log('Referer:', request.headers.get('referer'));
};
refactor_log.md
for detailed troubleshootingSECURITY.md
for security policiesplan.md
for optimization strategiesTechnical Excellence
Business Requirements
This migration guide is a living document. Update it as needed during the migration process to reflect any changes or lessons learned.