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/usersPage 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
- App Router - File-based routing with layouts
- Server Components - Default, fetch data directly
- Client Components - Use
'use client'for interactivity - Server Actions - Call server code from forms
- TypeScript - Type everything for safety
- Tailwind - Utility-first CSS