Skip to content

Module 2: Frontend with Next.js

Next.js 14 with the App Router is the standard for modern React applications.

App Router Fundamentals

File-Based Routing

app/
├── page.tsx              → /
├── about/
│   └── page.tsx          → /about
├── blog/
│   ├── page.tsx          → /blog
│   └── [slug]/
│       └── page.tsx      → /blog/my-post
├── dashboard/
│   ├── layout.tsx        → Shared layout
│   ├── page.tsx          → /dashboard
│   └── settings/
│       └── page.tsx      → /dashboard/settings
└── api/
    └── users/
        └── route.ts      → /api/users

Page Component

tsx
// app/page.tsx
export default function HomePage() {
  return (
    <main>
      <h1>Welcome to My SaaS</h1>
    </main>
  )
}

Layout Component

tsx
// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'My SaaS',
  description: 'The best SaaS product',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav>{/* Navigation */}</nav>
        {children}
        <footer>{/* Footer */}</footer>
      </body>
    </html>
  )
}

Server vs Client Components

Server Components (Default)

tsx
// app/users/page.tsx
// This is a Server Component by default

import { db } from '@/lib/db'

export default async function UsersPage() {
  // This runs on the server - safe to access DB directly
  const users = await db.user.findMany()

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Server Component Benefits:

  • Direct database access
  • Keep secrets on server
  • Smaller client bundle
  • Better SEO (HTML sent to client)

Client Components

tsx
// components/counter.tsx
'use client' // This directive makes it a Client Component

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

Use Client Components When:

  • Using useState, useEffect, useContext
  • Event handlers (onClick, onChange)
  • Browser APIs (window, localStorage)
  • Third-party libraries that need browser

Mixing Server and Client

tsx
// app/dashboard/page.tsx (Server Component)
import { db } from '@/lib/db'
import { InteractiveChart } from '@/components/chart'

export default async function DashboardPage() {
  // Fetch data on server
  const stats = await db.stats.findMany()

  // Pass to client component
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Server Component renders static parts */}
      <p>Total users: {stats.totalUsers}</p>

      {/* Client Component handles interactivity */}
      <InteractiveChart data={stats.chartData} />
    </div>
  )
}

Data Fetching

Server-Side Fetching

tsx
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Cache for 1 hour
  })
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Loading States

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

Error Handling

tsx
// app/posts/error.tsx
'use client'

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

Server Actions

Server Actions let you run server code from client components:

tsx
// app/actions.ts
'use server'

import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  await db.post.create({
    data: { title, content }
  })

  revalidatePath('/posts')
}
tsx
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Create Post</button>
    </form>
  )
}

TypeScript Setup

Type-Safe Props

tsx
// components/user-card.tsx
interface User {
  id: string
  name: string
  email: string
  avatar?: string
}

interface UserCardProps {
  user: User
  showEmail?: boolean
}

export function UserCard({ user, showEmail = false }: UserCardProps) {
  return (
    <div>
      <h3>{user.name}</h3>
      {showEmail && <p>{user.email}</p>}
    </div>
  )
}

Type-Safe API Routes

tsx
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

interface CreateUserBody {
  name: string
  email: string
}

export async function POST(request: NextRequest) {
  const body: CreateUserBody = await request.json()

  // Validate
  if (!body.name || !body.email) {
    return NextResponse.json(
      { error: 'Name and email required' },
      { status: 400 }
    )
  }

  // Create user...
  return NextResponse.json({ success: true })
}

Tailwind CSS Patterns

Responsive Design

tsx
<div className="
  grid
  grid-cols-1      /* Mobile: 1 column */
  md:grid-cols-2   /* Tablet: 2 columns */
  lg:grid-cols-3   /* Desktop: 3 columns */
  gap-4
">
  {items.map(item => (
    <Card key={item.id} />
  ))}
</div>

Common Patterns

tsx
// Centered container
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">

// Flex between
<div className="flex items-center justify-between">

// Card
<div className="bg-white rounded-lg shadow p-6">

// Button
<button className="
  bg-blue-600 hover:bg-blue-700
  text-white font-medium
  px-4 py-2 rounded-md
  transition-colors
">

// Input
<input className="
  w-full px-3 py-2
  border border-gray-300 rounded-md
  focus:outline-none focus:ring-2 focus:ring-blue-500
"/>

Common Patterns

Protected Routes

tsx
// app/dashboard/layout.tsx
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const session = await auth()

  if (!session) {
    redirect('/login')
  }

  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  )
}

Route Groups

app/
├── (marketing)/        # Group for marketing pages
│   ├── layout.tsx      # Marketing layout
│   ├── page.tsx        # / (home)
│   ├── about/
│   └── pricing/
├── (dashboard)/        # Group for app pages
│   ├── layout.tsx      # Dashboard layout (with sidebar)
│   ├── dashboard/
│   └── settings/
└── (auth)/             # Group for auth pages
    ├── layout.tsx      # Minimal layout
    ├── login/
    └── signup/

Summary

  1. App Router - File-based routing with layouts
  2. Server Components - Default, fetch data directly
  3. Client Components - Use 'use client' for interactivity
  4. Server Actions - Call server code from forms
  5. TypeScript - Type everything for safety
  6. Tailwind - Utility-first CSS

Next: Module 3: UI Components →