Modern Web Development with Next.js 14: The Complete Guide
Modern Web Development with Next.js 14: The Complete Guide
Next.js 14 introduces groundbreaking features that revolutionize React development. This comprehensive guide will help you master the latest version and build modern web applications.
Getting Started
First, create a new Next.js 14 project:
npx create-next-app@latest my-nextjs-app
cd my-nextjs-app
npm run dev
Key Features in Next.js 14
1. Server Components
Server Components are the default in Next.js 14, offering improved performance and reduced client-side JavaScript:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Page() {
const data = await getData()
return (
<main>
<h1>Server Component Example</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</main>
)
}
2. App Router
The new App Router provides more intuitive routing and layouts:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
{children}
</body>
</html>
)
}
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Welcome to our website!</p>
</div>
)
}
3. Server Actions
Server Actions enable form handling without API routes:
// app/form/page.tsx
export default function FormPage() {
async function handleSubmit(formData: FormData) {
'use server'
const name = formData.get('name')
const email = formData.get('email')
// Process form data on the server
await saveToDatabase({ name, email })
}
return (
<form action={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
)
}
Project: Full-Stack Blog Platform
Let's build a complete blog platform with Next.js 14:
// app/types/index.ts
interface Post {
id: string
title: string
content: string
author: string
createdAt: Date
}
// app/lib/db.ts
import { Prisma } from '@prisma/client'
const prisma = new PrismaClient()
export async function getPosts() {
return prisma.post.findMany({
orderBy: { createdAt: 'desc' }
})
}
export async function createPost(data: Omit<Post, 'id' | 'createdAt'>) {
return prisma.post.create({ data })
}
// app/posts/page.tsx
import { getPosts } from '@/lib/db'
export default async function PostsPage() {
const posts = await getPosts()
return (
<div className="max-w-4xl mx-auto py-8">
<h1 className="text-3xl font-bold mb-8">Blog Posts</h1>
<div className="space-y-8">
{posts.map((post) => (
<article key={post.id} className="border rounded-lg p-6">
<h2 className="text-2xl font-semibold mb-4">{post.title}</h2>
<p className="text-gray-600 mb-4">{post.content}</p>
<div className="text-sm text-gray-500">
By {post.author} • {post.createdAt.toLocaleDateString()}
</div>
</article>
))}
</div>
</div>
)
}
// app/posts/new/page.tsx
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
const content = formData.get('content') as string
const author = formData.get('author') as string
await createPost({ title, content, author })
redirect('/posts')
}
return (
<div className="max-w-2xl mx-auto py-8">
<h1 className="text-3xl font-bold mb-8">Create New Post</h1>
<form action={createPost} className="space-y-6">
<div>
<label htmlFor="title" className="block text-sm font-medium">
Title
</label>
<input
type="text"
name="title"
id="title"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
required
/>
</div>
<div>
<label htmlFor="content" className="block text-sm font-medium">
Content
</label>
<textarea
name="content"
id="content"
rows={5}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
required
/>
</div>
<div>
<label htmlFor="author" className="block text-sm font-medium">
Author
</label>
<input
type="text"
name="author"
id="author"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
required
/>
</div>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
>
Create Post
</button>
</form>
</div>
)
}
Performance Optimization
1. Image Optimization
Next.js provides automatic image optimization:
import Image from 'next/image'
export default function OptimizedImage() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority
className="rounded-lg"
/>
)
}
2. Static Site Generation (SSG)
Generate static pages at build time:
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function PostPage({
params,
}: {
params: { slug: string }
}) {
const post = await getPostBySlug(params.slug)
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
3. Incremental Static Regeneration (ISR)
Update static pages without rebuilding:
// app/products/[id]/page.tsx
export default async function ProductPage({
params,
}: {
params: { id: string }
}) {
const product = await fetch(
`https://api.example.com/products/${params.id}`,
{ next: { revalidate: 3600 } } // Revalidate every hour
).then(res => res.json())
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
</div>
)
}
Advanced Features
1. API Routes
Create API endpoints within Next.js:
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await getPosts()
return NextResponse.json(posts)
}
export async function POST(request: Request) {
const data = await request.json()
const post = await createPost(data)
return NextResponse.json(post, { status: 201 })
}
2. Middleware
Add custom middleware for authentication and more:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}
3. Error Handling
Implement error boundaries and loading states:
// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
<p className="text-red-500 mb-4">{error.message}</p>
<button
onClick={reset}
className="bg-blue-500 text-white px-4 py-2 rounded-md"
>
Try again
</button>
</div>
)
}
// app/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-blue-500" />
</div>
)
}
Best Practices
-
Project Structure
- Use feature-based organization
- Keep components small and focused
- Implement proper TypeScript types
- Follow consistent naming conventions
-
Performance
- Optimize images and fonts
- Implement proper caching
- Use code splitting
- Monitor Core Web Vitals
-
Security
- Validate user input
- Implement proper authentication
- Use HTTPS
- Follow security headers
-
Development Workflow
- Use ESLint and Prettier
- Write unit tests
- Implement CI/CD
- Document code properly
Conclusion
Next.js 14 provides powerful features for modern web development:
- Server Components for better performance
- App Router for intuitive routing
- Server Actions for simplified form handling
- Built-in optimizations for production
Keep exploring the ecosystem and stay updated with the latest features and best practices.