← Back to Blog
Getting Started with Next.js 14

Getting Started with Next.js 14

After 27 years of building web applications, I've seen frameworks come and go. Next.js, however, has fundamentally changed how I approach building modern web applications. The introduction of the App Router in Next.js 13+ represents one of the most significant paradigm shifts I've experienced since transitioning from server-side rendering to single-page applications over a decade ago.

Having built this personal website and numerous production applications with Next.js, I want to share practical insights that go beyond the basics and help you avoid common pitfalls.

Why Next.js?

Next.js has become my go-to framework for several compelling reasons, backed by real production experience:

  • Server Components: Render components on the server for better performance and reduced JavaScript bundle sizes
  • File-based Routing: Intuitive routing based on your file structure—no more route configuration files
  • Built-in Optimization: Automatic image, font, and script optimization that actually works
  • TypeScript Support: First-class TypeScript integration with excellent type inference
  • Developer Experience: Fast refresh, helpful error messages, and a great development workflow

The App Router, introduced in Next.js 13, has fundamentally changed the framework's architecture, moving from a purely client-centric model to a more balanced approach that leverages both server and client capabilities.

Setting Up Your First Project

Getting started is straightforward:

npx create-next-app@latest my-app

During setup, you'll be prompted with several options. Here's what I recommend for most projects:

  • TypeScript: Yes (always)
  • ESLint: Yes
  • Tailwind CSS: Yes (unless you have strong opinions about CSS-in-JS)
  • App Router: Yes (the future of Next.js)
  • Import alias: Yes (@/*)

This creates a solid foundation with TypeScript, ESLint, and the modern App Router structure.

Understanding the App Router Architecture

The App Router represents a fundamental shift in how Next.js applications are structured. Unlike the Pages Router, which was purely client-side focused, the App Router embraces a hybrid model.

Server Components vs Client Components

This is perhaps the most important concept to understand. By default, all components in the app directory are Server Components. This means they:

  • Run only on the server
  • Can directly access backend resources (databases, file systems)
  • Don't add to the client JavaScript bundle
  • Can't use browser APIs or React hooks like useState or useEffect

Here's a practical example from a blog listing page:

// app/blog/page.tsx - Server Component (default)
import { getPosts } from '@/lib/posts'
 
export default async function BlogPage() {
  // This runs on the server - can directly access the database
  const posts = await getPosts()
 
  return (
    <div className="grid gap-8">
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.description}</p>
          <time>{post.date}</time>
        </article>
      ))}
    </div>
  )
}

Client Components, marked with 'use client', are needed when you require:

  • Interactive features (onClick, onChange, etc.)
  • React hooks (useState, useEffect, useContext)
  • Browser APIs (localStorage, window, etc.)
  • Third-party libraries that depend on browser APIs
// app/components/theme-toggle.tsx - Client Component
'use client'
 
import { useState, useEffect } from 'react'
 
export function ThemeToggle() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
 
  useEffect(() => {
    const stored = localStorage.getItem('theme')
    if (stored) setTheme(stored as 'light' | 'dark')
  }, [])
 
  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light'
    setTheme(newTheme)
    localStorage.setItem('theme', newTheme)
    document.documentElement.classList.toggle('dark')
  }
 
  return (
    <button onClick={toggleTheme} className="px-4 py-2 rounded">
      {theme === 'light' ? '🌙' : '☀️'}
    </button>
  )
}

Common Pitfall: Don't make everything a Client Component just because you're used to it. Start with Server Components and only add 'use client' when you need interactivity. This keeps your bundle size small and improves performance.

Data Fetching Patterns

Next.js 14 introduces several powerful data fetching patterns that I've found incredibly useful in production.

Server Component Data Fetching

Server Components can be async and fetch data directly:

// app/projects/[id]/page.tsx
import { getProject } from '@/lib/projects'
import { notFound } from 'next/navigation'
 
export default async function ProjectPage({
  params,
}: {
  params: { id: string }
}) {
  const project = await getProject(params.id)
 
  if (!project) {
    notFound() // Shows 404 page
  }
 
  return (
    <div>
      <h1>{project.title}</h1>
      <p>{project.description}</p>
    </div>
  )
}

Parallel Data Fetching

One powerful pattern is fetching multiple data sources in parallel:

// app/dashboard/page.tsx
export default async function DashboardPage() {
  // These fetch in parallel
  const [user, stats, activities] = await Promise.all([
    getUser(),
    getStats(),
    getRecentActivities(),
  ])
 
  return (
    <div className="space-y-8">
      <UserProfile user={user} />
      <Statistics stats={stats} />
      <ActivityFeed activities={activities} />
    </div>
  )
}

Streaming with Suspense

For slower data fetching, you can stream content to the client:

// app/blog/page.tsx
import { Suspense } from 'react'
 
function PostList() {
  // This might be slow
  const posts = await getPosts()
  return <div>{/* render posts */}</div>
}
 
export default function BlogPage() {
  return (
    <div>
      <h1>Blog</h1>
      <Suspense fallback={<div>Loading posts...</div>}>
        <PostList />
      </Suspense>
    </div>
  )
}

Routing Deep Dive

The file-based routing in Next.js is intuitive once you understand the conventions.

Dynamic Routes

Create dynamic segments using square brackets:

app/
  blog/
    [slug]/
      page.tsx  # /blog/my-post
  projects/
    [id]/
      page.tsx  # /projects/123
// app/blog/[slug]/page.tsx
export default async function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  const post = await getPostBySlug(params.slug)
  return <article>{/* render post */}</article>
}
 
// Generate static paths at build time
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

Route Groups

Route groups let you organize routes without affecting the URL structure:

app/
  (marketing)/
    about/
      page.tsx    # /about
    contact/
      page.tsx    # /contact
  (app)/
    dashboard/
      page.tsx    # /dashboard
    settings/
      page.tsx    # /settings

Each group can have its own layout:

// app/(marketing)/layout.tsx
export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <>
      <MarketingHeader />
      {children}
      <MarketingFooter />
    </>
  )
}

Parallel Routes

This advanced pattern lets you render multiple pages in the same layout:

app/
  dashboard/
    @analytics/
      page.tsx
    @team/
      page.tsx
    layout.tsx
    page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-2 gap-4">
      <div className="col-span-2">{children}</div>
      <div>{analytics}</div>
      <div>{team}</div>
    </div>
  )
}

Loading States and Error Handling

Next.js provides built-in conventions for loading and error states.

Loading UI

Create a loading.tsx file to show while the page loads:

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="space-y-4">
      <div className="h-8 bg-gray-200 rounded animate-pulse" />
      <div className="h-4 bg-gray-200 rounded animate-pulse w-3/4" />
      <div className="h-4 bg-gray-200 rounded animate-pulse w-1/2" />
    </div>
  )
}

Error Boundaries

Create an error.tsx file to handle errors gracefully:

// app/blog/error.tsx
'use client' // Error components must be Client Components
 
import { useEffect } from 'react'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log to error reporting service
    console.error(error)
  }, [error])
 
  return (
    <div className="text-center py-12">
      <h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-500 text-white rounded"
      >
        Try again
      </button>
    </div>
  )
}

Metadata and SEO

Next.js 14 provides excellent SEO support through the Metadata API.

Static Metadata

// app/about/page.tsx
import { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'About Jason Cochran',
  description: 'Learn about my 27 years of experience in software development',
  openGraph: {
    title: 'About Jason Cochran',
    description: 'Learn about my 27 years of experience in software development',
    images: ['/og-image.jpg'],
  },
}
 
export default function AboutPage() {
  return <div>{/* content */}</div>
}

Dynamic Metadata

For dynamic pages, use generateMetadata:

// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPostBySlug(params.slug)
 
  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: 'article',
      publishedTime: post.date,
      authors: ['Jason Cochran'],
    },
  }
}

Performance Optimization

Image Optimization

Next.js's Image component is fantastic for performance:

import Image from 'next/image'
 
export function ProjectCard({ project }) {
  return (
    <div>
      <Image
        src={project.image}
        alt={project.title}
        width={800}
        height={600}
        placeholder="blur"
        blurDataURL={project.blurDataURL}
        className="rounded-lg"
      />
      <h3>{project.title}</h3>
    </div>
  )
}

Font Optimization

Use next/font for optimized font loading:

// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google'
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
})
 
const jetbrainsMono = JetBrains_Mono({
  subsets: ['latin'],
  variable: '--font-mono',
})
 
export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}

Static Generation

Use generateStaticParams to pre-render pages at build time:

export async function generateStaticParams() {
  const posts = await getAllPosts()
 
  return posts.map(post => ({
    slug: post.slug,
  }))
}

Real-World Example: Building This Website

When building my personal website with Next.js 14, I used these patterns:

  1. Server Components for all static pages (about, projects)
  2. Client Components only for interactive features (theme toggle, contact form)
  3. Dynamic routes for blog posts and project details
  4. Route groups to separate marketing and app sections
  5. Metadata API for comprehensive SEO
  6. Static generation for all blog posts at build time

This resulted in:

  • Excellent Lighthouse scores (95+ across all metrics)
  • Fast Time to First Byte (TTFB)
  • Small JavaScript bundle size
  • Great SEO performance

Common Pitfalls to Avoid

  1. Making everything a Client Component: Start with Server Components and only use Client Components when needed
  2. Not using Suspense boundaries: This causes entire pages to wait for slow data
  3. Ignoring loading states: Users need feedback while content loads
  4. Not leveraging static generation: Pre-render what you can at build time
  5. Overusing client-side data fetching: Fetch on the server when possible

Conclusion

Next.js 14 with the App Router represents a mature, production-ready framework that fundamentally improves how we build web applications. The combination of Server Components, streaming, and excellent developer experience makes it my framework of choice for new projects.

The key is understanding when to use Server Components versus Client Components, leveraging static generation where possible, and providing excellent loading and error states. Master these patterns, and you'll build fast, maintainable applications that scale.

After 27 years of web development, I can confidently say that Next.js is one of the most significant advances in the React ecosystem. Start with these patterns, and you'll be well on your way to building production-grade applications.

Share this article

Help others discover this content


Jason Cochran

Jason Cochran

Sofware Engineer | Cloud Consultant | Founder at Strataga

27 years of experience building enterprise software for oil & gas operators and startups. Specializing in SCADA systems, field data solutions, and AI-powered rapid development. Based in Midland, TX serving the Permian Basin.