Presentation Layer

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:
Presentation Layer Components:
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
Key Patterns:
  • 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:
// 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'
    }
  }
}
Key Features:
  • "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):
// 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>
React Hook Form Integration:
"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:
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/
Benefits:
  • 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:
// āœ… 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>
  )
}
Composition Pattern:
  • 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.
    Presentation Layer | ShipSaaS Documentation | ShipSaaS - Launch your SaaS with AI in days