The Presentation Layer handles all user interface concerns using Next.js App Router patterns. It follows React Server Components by default, uses Client Components only when necessary, and handles mutations through Server Actions.
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:
DAL Caching - 'use cache' in the DAL function, per read
React Server Components (Default):
typescript
// ✅ Server Component (No "use client")export default async function UserProfile({ userId }: { userId: string }) { const user = await getUserByIdDal(userId) // Direct server data access return ( <div className="profile"> <h1>{user.name}</h1> <UserActions userId={user.id} /> {/* Can include client components */} </div> )}
Client Components (When Needed):
typescript
"use client" // Only when needed for interactivityimport { useState } from 'react'export function UserActions({ userId }: { userId: string }) { const [isLoading, setIsLoading] = useState(false) // Interactive functionality that needs browser APIs return ( <button onClick={() => setIsLoading(true)}> {isLoading ? 'Loading...' : 'Update Profile'} </button> )}
Server Validation: Double validation (client + server)
Forms and Validation
The boilerplate uses a dual validation strategy for optimal user experience:
Client-Side Validation (UX):
typescript
// components/features/auth/auth-form-validation.tsimport { 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>
React Hook Form Integration:
typescript
"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> )}
Server-Side Validation (Security):
typescript
// Service layer validation (different schema for server rules)import { z } from 'zod'export const updateUserServiceSchema = z.object({ name: z.string().min(2).max(100), email: z.string().email().refine(async (email) => { // Check if email is already taken const existingUser = await getUserByEmailDao(email) return !existingUser }, "Email already exists"), bio: z.string().max(500).optional()})
Complete Validation Flow:
1. 📝 User types in form
2. ✅ Client validation (React Hook Form + Zod)
3. 📤 Form submitted to Server Action
4. 🛡️ Server validation (Service layer + Zod)
5. 🔐 Authorization check (CASL permissions)
6. 💾 Database operation
7. 🔄 Cache revalidation
8. ✨ UI update
Presentation layer ready! Your UI follows React Server Components best practices with optimal performance, clear data flow, and proper separation of concerns.