• ShipSaaSDocumentation
  • Introduction
  • Getting Started
    • Installation
    • Environment Setup
    • Database Setup
    • Development Guide
  • Database
    • Repository Pattern
    • Drizzle Queries
    • Migration Workflow
    • Drizzle Studio
    • Best Practices
  • File Management
    • File Upload Component
    • Server Actions
    • Storage Configuration
  • Authentication Methods
    • Credentials Authentication
    • Magic Link Authentication
    • Two-Factor Authentication (2FA)
    • Social Authentication
    • API Keys & Bearer Tokens
    • Organizations (Multi-tenant)
  • Authorization & Permissions
    • Roles & Hierarchy
    • Route Protection
    • API Protection
    • CASL Permissions
  • Email & Notifications
    • Resend Setup
    • React Email Templates
    • Notification System
    • Email Workflows
    • Customization & Extension
  • Stripe & Payments
    • Stripe Setup
    • Better Auth Integration
    • Checkout Methods
    • Subscription Management
    • Admin Dashboard
    • Webhook Events
  • Architecture & Development
    • Presentation Layer
    • Service Layer
    • Persistence Layer
    • Helpers Architecture
    • Project Structure
    • Development Workflow
  • Testing
    • General Testing
    • Unit Testing
    • UI Component Testing
    • Repository Testing
    • Service Testing
    • End-to-End Testing
  • Deployment
    • Vercel Deployment
    • GitHub Actions CI/CD
    • Database Backup & Restore
    • Database Migrations
    • Troubleshooting Deployment
  • AI Agents Integration
  • Blog Management
    • Blog Setup
    • MDX Blog
    • Database Blog
  • Async Tasks
    • Setup
    • Email Workflows
    • Payment Processing
    • Maintenance Jobs
  1. Documentation
  2. Architecture & Development
  3. Presentation Layer
Buydocschangelogsdemocommunity

Presentation Layer

PreviousArchitecture & DevelopmentNextService Layer
On this page
Presentation Layer ArchitectureServer Actions for MutationsForms and ValidationRoute OrganizationPerformance OptimizationsBest Practices

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 - '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 interactivity

import { 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>
  )
}

When to Use "use client":

  • State management (useState, useReducer)
  • Event handlers (onClick, onChange)
  • Browser APIs (localStorage, navigator)
  • Third-party libraries requiring client-side execution

DAL (Data Access Layer):

typescript
// src/app/dal/user-dal.ts
import { cache } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
import { getUserByIdService } from '@/services/facades/user-service-facade'

// ✅ Public read: cached across requests, invalidated by tag.
//    cache() from React still deduplicates within one request.
export const getUserByIdDal = cache(async (id: string) => {
  'use cache'
  cacheLife('hours')
  cacheTag(`user:${id}`)

  const userModel = await getUserByIdService(id)
  return transformToUserDTO(userModel) // Transform to domain type
})

// ✅ Per-user read: no cache at all — the page streams it behind <Suspense>
export const getUserSubscriptionsDal = cache(async (userId: string) => {
  const subscriptions = await getSubscriptionsByUserIdService(userId)
  return transformToSubscriptionDTOs(subscriptions)
})

Benefits:

  • Caching: 'use cache' across requests, cache() within one request
  • Type Safety: Transforms DB models to domain types
  • Performance: Server-side data fetching
  • Reusability: Shared across multiple components

Server Actions for Mutations

All data mutations go through Server Actions, providing security and consistency:

Server Action Pattern:

typescript
// 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

In Forms:

typescript
// Client Component Form
"use client"

import { useActionState } from 'react'
import { updateUserAction } from './actions'

export function UserForm({ user }: { user: User }) {
  const [state, formAction] = useActionState(updateUserAction, null)

  return (
    <form action={formAction}>
      <input name="id" type="hidden" value={user.id} />
      <input name="name" defaultValue={user.name} required />
      <input name="email" defaultValue={user.email} type="email" required />

      <button type="submit">
        {state?.success ? 'Updated!' : 'Update Profile'}
      </button>

      {state?.error && (
        <p className="text-red-500">{state.error}</p>
      )}
    </form>
  )
}

Benefits:

  • Progressive Enhancement: Works without JavaScript
  • Real-time Feedback: Loading and error states
  • Type Safety: TypeScript validation
  • 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.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:

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

Route-based Actions:

app/
├── (auth)/
│   ├── actions.ts          # Shared auth actions
│   ├── signin/
│   │   └── page.tsx
│   └── signup/
│       └── page.tsx
├── (app)/
│   ├── account/
│   │   ├── actions.ts      # Account-specific actions
│   │   └── page.tsx
│   └── dashboard/
│       ├── actions.ts      # Dashboard actions
│       └── page.tsx

Action Organization:

  • Route-level: actions.ts per route/feature
  • Shared: Common actions in parent directories
  • Naming: Descriptive action names (updateUserAction)
  • Error Handling: Consistent error response format

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

Layout Composition:

typescript
// app/layout.tsx - Root layout
export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html>
      <body>
        <AuthProvider>
          {children} {/* Route group layouts render here */}
        </AuthProvider>
      </body>
    </html>
  )
}

// app/(app)/layout.tsx - Protected app layout
export default function AppLayout({ children }: { children: ReactNode }) {
  return (
    <div className="app-layout">
      <Navigation />
      <main>{children}</main> {/* Page content */}
      <Footer />
    </div>
  )
}

// app/(admin)/layout.tsx - Admin layout
export default function AdminLayout({ children }: { children: ReactNode }) {
  return (
    <div className="admin-layout">
      <AdminSidebar />
      <div className="admin-content">
        {children} {/* Admin pages */}
      </div>
    </div>
  )
}

Middleware Protection:

typescript
// middleware.ts - Route protection
import { NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  // Protected app routes
  if (pathname.startsWith('/app')) {
    const session = await getSession()
    if (!session) {
      return NextResponse.redirect(new URL('/signin', request.url))
    }
  }

  // Admin routes
  if (pathname.startsWith('/admin')) {
    const session = await getSession()
    if (!session || !session.user.isAdmin) {
      return NextResponse.redirect(new URL('/unauthorized', request.url))
    }
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}

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

• 'use cache' in DAL layer, per read
• Request deduplication with React cache()
• Tag invalidation with updateTag in 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:

typescript
// ✅ 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

DAL Usage:

typescript
// ✅ Good: Use DAL for all data access
export default async function UserPage({ userId }: { userId: string }) {
  const user = await getUserByIdDal(userId)
  const subscriptions = await getUserSubscriptionsDal(userId)

  return <UserDisplay user={user} subscriptions={subscriptions} />
}

// ❌ Bad: Direct service calls from components
export default async function UserPage({ userId }: { userId: string }) {
  const user = await getUserByIdService(userId) // Skip DAL
  return <UserDisplay user={user} />
}

// ❌ Bad: Direct database access
export default async function UserPage({ userId }: { userId: string }) {
  const user = await db.query.users.findFirst() // Skip layers
  return <UserDisplay user={user} />
}

Error Boundaries:

typescript
// app/error.tsx - Route-level error boundary
"use client"

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  )
}

// app/loading.tsx - Loading UI
export default function Loading() {
  return <div>Loading...</div>
}

// app/not-found.tsx - 404 handling
export default function NotFound() {
  return <div>Page not found</div>
}

Presentation layer ready! Your UI follows React Server Components best practices with optimal performance, clear data flow, and proper separation of concerns.