Layer Responsibility: The Presentation Layer manages UI rendering, user interactions, and data presentation. It never contains business logic or direct database access.
Perfect for: Building fast, interactive UIs with optimal server/client rendering balance and efficient data fetching.
Presentation Layer Architecture
The layer is organized with clear separation between server and client code:Presentation Layer Components:Key Patterns:
src/app/ # š Next.js App Router
āāā (public)/ # Public routes
āāā (auth)/ # Authentication routes
āāā (app)/ # Protected user routes
āāā (admin)/ # Admin routes
āāā dal/ # š Data Access Layer (DAL)
ā āāā user-dal.ts # Cached data access
ā āāā subscription-dal.ts
āāā api/ # API routes (if needed)
src/components/ # šØ UI Components
āāā ui/ # Base UI components
āāā features/ # Feature-specific components
ā āāā auth/
ā āāā subscription/
āāā forms/ # Form components
- RSC First - Server Components by default
- Selective Client - "use client" only when needed
- Server Actions - All mutations through server
- DAL Caching - React cache for performance
Server Actions for Mutations
All data mutations go through Server Actions, providing security and consistency:Server Action Pattern:Key Features:
// src/app/(app)/account/actions.ts
"use server"
import { revalidatePath } from 'next/cache'
import { updateUserService } from '@/services/facades/user-service-facade'
export async function updateUserAction(formData: FormData) {
// Extract form data
const userData = {
id: formData.get('id') as string,
name: formData.get('name') as string,
email: formData.get('email') as string
}
try {
// Call service layer (handles validation + authorization)
const result = await updateUserService(userData)
// Revalidate cached data
revalidatePath('/account')
revalidatePath(`/users/${userData.id}`)
return { success: true, data: result }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}- "use server" directive for server execution
- Service layer integration for business logic
- Cache invalidation with
revalidatePath - Error handling with proper error types
Forms and Validation
The boilerplate uses a dual validation strategy for optimal user experience:Client-Side Validation (UX):React Hook Form Integration:
// components/features/auth/auth-form-validation.ts
import { z } from 'zod'
export const updateUserSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email format"),
bio: z.string().max(500, "Bio must be under 500 characters").optional()
})
export type UpdateUserInput = z.infer<typeof updateUserSchema>"use client"
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { updateUserSchema, type UpdateUserInput } from './auth-form-validation'
export function UserForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<UpdateUserInput>({
resolver: zodResolver(updateUserSchema)
})
const onSubmit = async (data: UpdateUserInput) => {
// Submit to server action
const result = await updateUserAction(data)
if (!result.success) {
// Handle server errors
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
{errors.name && <span>{errors.name.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Updating...' : 'Update'}
</button>
</form>
)
}Route Organization
Next.js App Router provides powerful routing with route groups:Route Groups:Benefits:
app/
āāā (public)/ # š Public routes (no auth)
ā āāā page.tsx # Homepage
ā āāā about/
ā āāā contact/
āāā (auth)/ # š Authentication routes
ā āāā layout.tsx # Auth-specific layout
ā āāā actions.ts # Auth actions
ā āāā signin/
ā āāā signup/
āāā (app)/ # š¤ Protected user routes
ā āāā middleware.ts # Route protection
ā āāā layout.tsx # App layout with navigation
ā āāā account/
ā ā āāā actions.ts
ā ā āāā page.tsx
ā ā āāā subscription/
ā āāā dashboard/
āāā (admin)/ # š Admin routes
ā āāā layout.tsx # Admin layout
ā āāā middleware.ts # Admin protection
ā āāā users/
ā āāā analytics/
āāā api/ # š API routes (when needed)
āāā webhooks/
- Layout Sharing: Each group has its own layout
- Middleware: Different protection per group
- Organization: Clear separation by user type
- SEO: Clean URLs without group names
Performance Optimizations
The Presentation Layer implements several performance optimizations:ā”
Server-First Rendering
⢠RSC by default for faster initial loads
⢠Selective hydration only where needed
⢠Server-side data fetching reduces client requests
⢠Streaming for progressive page loading
š¾
Caching Strategy
⢠React cache in DAL layer
⢠Request deduplication automatic
⢠Route revalidation with Server Actions
⢠Static generation where possible
šÆ
Code Splitting
⢠Route-based splitting automatic
⢠Component lazy loading with
dynamic()⢠Client components loaded only when needed
⢠Feature-based organization for better splitting
š
Data Loading
⢠Parallel data fetching in RSC
⢠Waterfall elimination with proper async/await
⢠Error boundaries for graceful failures
⢠Loading UI with Suspense boundaries
Best Practices
Component Organization:Composition Pattern:
// ā
Server Component (Default)
// No "use client" directive needed
export default async function UserDashboard() {
const user = await getUserByIdDal(userId) // Server data access
return (
<div>
<UserProfile user={user} />
<UserActions userId={user.id} /> {/* Client component */}
</div>
)
}
// ā
Client Component (When Needed)
"use client"
export function UserActions({ userId }: { userId: string }) {
const [isLoading, setIsLoading] = useState(false)
return (
<button onClick={() => handleAction(userId)}>
Action
</button>
)
}- Server components handle data fetching
- Client components handle interactivity
- Compose together for optimal performance
- Pass data down from server to client components
Presentation layer ready! Your UI follows React Server Components best practices with optimal performance, clear data flow, and proper separation of concerns.