📝 nextjs
11 min read

Building Modern Web Apps with Next.js 15: Complete Guide

Master Next.js 15's groundbreaking features including the App Router, Server Components, Streaming, and Performance optimizations. Build lightning-fast web applications with the latest React patterns.

Topics covered:

#nextjs
#react
#web-development
#performance
#server-components
Lazar Kapsarov - Full Stack Developer

Lazar Kapsarov

✨ Full Stack Developer

Building high-performing SaaS, e-commerce & landing pages with Next.js, React & TypeScript. Helping businesses create digital experiences that convert.

Available for new projects
Building Modern Web Apps with Next.js 15: Complete Guide - Technical illustration

Building a Modern Portfolio with Next.js 15

A portfolio is more than a gallery. It’s your sales engine.

The truth is… most developers treat their portfolio like a photo album. Pretty screens, endless scrolling, but slow. And slow kills trust. Clients don’t wait. They judge you in seconds. Your code, your process, your results — all compressed into that first load. That’s why I build portfolios with Next.js 15. Architecture that scales. Routing that flows. Server Components that cut the fat. And performance baked in from the start. This post breaks it down.


Architecture That Scales

Let’s start with the foundation. A modern portfolio should feel like a product, not a side-project. That means treating it with the same discipline I give SaaS dashboards or e-commerce flows.

Here’s my stack:

  • Next.js 15 App Router → clean separation of routes, layouts, and nested UI. Why does this matter? Because portfolios grow. You start with three projects, then ten, then twenty-five. If your foundation is shaky, each new addition makes it slower and more challenging to maintain.
    • React 19 + Server Components → less JavaScript in the browser, more speed.
    • TypeScript 5 → predictable, safe, no runtime surprises.
    • Tailwind CSS + shadcn/ui → design tokens, polished components, fast iteration.
    • Framer Motion + GSAP → tasteful motion that supports, not slows.

Routing with the App Router

Speed. Clarity. Momentum.


Advanced Routing with the App Router

The old pages/ system worked, but it was blunt. App Router is surgical.

What you get:

  1. Nested layouts → keep headers, footers, and sidebars consistent.
  2. Route groups → structure sections (work, blog, about) without messy URLs.
  3. Loading & error states baked right in → smoother UX, no blank screens.

Imagine a potential client clicking from Projects → Case Study → Blog. Every step feels instant. No clunky reloads. No broken flow.

Even better? Search engines love the clean structure. SEO is about clarity as much as keywords.

Server Components in Action

This makes it simple to scale your portfolio as you add new projects.

Performance Benefits

And Core Web Vitals love it.

Performance Considerations

Now let’s talk speed.

A slow portfolio is a silent killer. It makes you look outdated, even if your skills aren’t. Here are the non-negotiables I aim for:

  • LCP < 2.5s → hero content must load fast. I use next/image with AVIF/WebP, lazy-load below the fold.
  • INP < 200ms → interactions should feel instant. That means clean focus states, minimal client JS, and no bloat libraries.
  • CLS < 0.1 → no layout shifts. Font subsetting, stable containers, and consistent image ratios. Supporting practices:
  • Font strategy: local fonts, subset weights, display: swap.
  • Code strategy: dynamic imports, tree-shaking, no heavy libs.
  • Caching strategy: ISR (Incremental Static Regeneration) and smart edge caching on Vercel.
  • Monitoring: WebPageTest + Lighthouse + Vercel Analytics.

The result? Portfolios that hit 90+ Lighthouse scores on key pages.

Why Performance Matters for a Portfolio

And that’s not vanity. It’s trust.

Why This Matters for a Portfolio

Clients don’t just want to see that you can code. They want to feel that you can deliver.

A modern portfolio does three things:

  1. Loads in seconds → proves discipline and attention to detail.
  2. Feels interactive → shows you care about users, not just visuals.
  3. Highlights results → moves the conversation from “nice design” to “business impact”. Here’s proof:
  • A SaaS onboarding flow I rebuilt cut Interaction Latency by 50% and boosted activation rates by 11 percentage points.
  • An e-commerce checkout I optimised cut cart abandonment by 16 percentage points.
  • A lead-gen landing page lifted qualified enquiries by 45%.

Final Word

That’s what clients see in a portfolio. Not just screens — but outcomes.

Wrap-Up

Building with Next.js 15 isn’t just about using the latest shiny tool. It’s about proving you can ship like a pro.

Fast prototypes. Clean builds. Performance that speaks louder than claims.

Your portfolio is your runway. Get it wrong, and you stall. You can get it right, and you launch.

Advanced App Router Features:

  • 📁 Co-located Layouts: Define layouts alongside routes for better organization
  • 🔄 Nested Routing: Natural hierarchy that mirrors your UI structure
  • Loading & Error States: Built-in loading.tsx and error.tsx support
  • 📂 Route Groups: Organize routes with () without affecting URL paths
  • 🎯 Parallel Routes: Load multiple pages simultaneously with @folder syntax
  • 🔀 Intercepting Routes: Capture routes for modals and overlays
  • 🛡️ Route Handlers: API routes with full middleware support

� Server Components: Zero JavaScript by Default

Server Components revolutionize performance by rendering on the server:

Code
// app/products/page.tsx - Server Component (default)
async function ProductsPage() {
  // This runs on the server - no client bundle impact!
  const products = await fetch('https://api.example.com/products')
    .then(res => res.json())

  return (
    <div className="products-grid">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

export default ProductsPage

Performance Impact:

  • =� Smaller Bundle Size: Server logic stays on the server
  • Faster Initial Load: No JavaScript needed for static content
  • = Enhanced Security: Sensitive operations remain server-side
  • =� Better SEO: Fully rendered HTML for search engines

<� Client Components: Interactive When Needed

Use Client Components selectively for interactivity:

Code
// components/SearchBar.tsx - Interactive component
'use client'

import { useState } from 'react'

export function SearchBar() {
  const [query, setQuery] = useState('')

  return (
    <div className="search-container">
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search products..."
        className="search-input"
      />
      <SearchResults query={query} />
    </div>
  )
}

Advanced Data Fetching Patterns

Parallel Data Loading

Fetch multiple data sources simultaneously:

Code
// app/dashboard/page.tsx - Parallel data fetching
async function DashboardPage() {
  // These requests happen in parallel!
  const [user, analytics, notifications] = await Promise.all([
    getUser(),
    getAnalytics(),
    getNotifications()
  ])

  return (
    <div className="dashboard">
      <UserProfile user={user} />
      <AnalyticsChart data={analytics} />
    </div>
  )
}

Streaming with Suspense

Improve perceived performance with streaming:

Code
// app/dashboard/page.tsx - Streaming data
import { Suspense } from 'react'

export default function DashboardPage() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {/* This loads immediately */}
      <QuickStats />

      {/* This streams in when ready */}
      <Suspense fallback={<AnalyticsLoading />}>
        <AnalyticsChart />
      </Suspense>

      <Suspense fallback={<RecentOrdersLoading />}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}

// This component streams when data is ready
async function AnalyticsChart() {
  const data = await getAnalyticsData() // This can be slow
  return <Chart data={data} />
}

Performance Optimizations

Image Optimization

Next.js 15's Image component delivers exceptional performance:

Code
import Image from 'next/image'

export function ProductGrid({ products }) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <div key={product.id} className="product-card">
          <Image
            src={product.image}
            alt={product.name}
            width={300}
            height={200}
            placeholder="blur"
            blurDataURL="data:image/jpeg;base64,..."
            sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
            priority={product.featured}
          />
          <h3>{product.name}</h3>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  )
}

Image Features:

  • =� Automatic WebP/AVIF: Modern formats for optimal compression
  • =� Responsive Images: Serve appropriate sizes for each device
  • Lazy Loading: Load images as they enter the viewport
  • <� Blur Placeholders: Smooth loading experience

Font Optimization

Eliminate layout shift with optimized fonts:

Code
// app/layout.tsx - Font optimization
import { Inter, Playfair_Display } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
})

const playfair = Playfair_Display({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-playfair',
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${playfair.variable}`}>
      <body className="font-sans">
        {children}
      </body>
    </html>
  )
}

Advanced Routing Patterns

Dynamic Routes with Type Safety

Code
// app/products/[category]/[slug]/page.tsx
interface ProductPageProps {
  params: {
    category: string
    slug: string
  }
  searchParams: {
    color?: string
    size?: string
  }
}

export default function ProductPage({ params, searchParams }: ProductPageProps) {
  const { category, slug } = params
  const { color, size } = searchParams

  return (
    <div>
      <h1>Product: {slug}</h1>
      <p>Category: {category}</p>
      {color && <p>Color: {color}</p>}
      {size && <p>Size: {size}</p>}
    </div>
  )
}

// Generate static params for better performance
export async function generateStaticParams() {
  const products = await getProducts()

  return products.map(product => ({
    category: product.category,
    slug: product.slug,
  }))
}

Route Groups and Layouts

Organize your application structure:

Code
app/
�� (marketing)/
   �� layout.tsx
   �� page.tsx
   �� about/
       �� page.tsx
�� (shop)/
   �� layout.tsx
   �� products/
      �� page.tsx
   �� cart/
       �� page.tsx
�� (auth)/
    �� login/
       �� page.tsx
    �� register/
        �� page.tsx

State Management in Next.js 15

Server State vs Client State

Code
// Server State - Fetch once, cache efficiently
async function ProductList() {
  const products = await getProducts() // Server-side

  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

// Client State - Interactive state management
'use client'
function ShoppingCart() {
  const [items, setItems] = useLocalStorage('cart', [])
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        Cart ({items.length})
      </button>
      {isOpen && <CartDropdown items={items} />}
    </div>
  )
}

Deployment and Performance

Edge Runtime

Deploy functions to the edge for global performance:

Code
// app/api/edge/route.ts
export const runtime = "edge";

export async function GET() {
  return new Response("Hello from the edge!", {
    headers: {
      "content-type": "text/plain",
    },
  });
}

Analytics and Monitoring

Track Core Web Vitals:

Code
// app/layout.tsx - Web vitals tracking
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  )
}

Best Practices for Production

1. Component Architecture

  • Use Server Components by default
  • Add 'use client' only when needed
  • Compose Client and Server Components effectively

2. Performance First

  • Implement streaming for slow data
  • Use Suspense boundaries strategically
  • Optimize images and fonts

3. SEO Optimization

Code
// app/products/[slug]/page.tsx - Dynamic metadata
export async function generateMetadata({ params }) {
  const product = await getProduct(params.slug);

  return {
    title: `${product.name} - Your Store`,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      images: [product.image],
    },
  };
}

4. Error Handling

Code
// app/error.tsx - Global error boundary
'use client'

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

Real-World Example: E-commerce App

Here's how all these concepts come together in a production app:

Code
// app/shop/products/page.tsx - Complete example
import { Suspense } from 'react'
import { ProductGrid } from '@/components/ProductGrid'
import { CategoryFilter } from '@/components/CategoryFilter'
import { SearchBar } from '@/components/SearchBar'

interface ProductsPageProps {
  searchParams: {
    category?: string
    search?: string
    page?: string
  }
}

export default function ProductsPage({ searchParams }: ProductsPageProps) {
  return (
    <div className="products-page">
      <div className="filters-section">
        <CategoryFilter />
        <SearchBar />
      </div>

      <Suspense fallback={<ProductGridLoading />}>
        <ProductGrid searchParams={searchParams} />
      </Suspense>
    </div>
  )
}

// This component streams in when data is ready
async function ProductGrid({ searchParams }) {
  const products = await getProducts({
    category: searchParams.category,
    search: searchParams.search,
    page: Number(searchParams.page) || 1,
  })

  if (products.length === 0) {
    return <EmptyState />
  }

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

Migration Guide

From Pages Router to App Router

Code
// Old: pages/products/[id].js
export async function getServerSideProps({ params }) {
  const product = await getProduct(params.id)
  return { props: { product } }
}

export default function Product({ product }) {
  return <div>{product.name}</div>
}

// New: app/products/[id]/page.tsx
export default async function Product({ params }) {
  const product = await getProduct(params.id)
  return <div>{product.name}</div>
}

Conclusion

Next.js 15 represents the future of React development, offering unprecedented performance, developer experience, and scalability. By embracing Server Components, the App Router, and modern data fetching patterns, you can build applications that are:

  • Lightning Fast - Server Components eliminate unnecessary JavaScript
  • <� SEO Optimized - Server-side rendering with streaming capabilities
  • =' Developer Friendly - Intuitive patterns and excellent tooling
  • =� Infinitely Scalable - Edge runtime and optimized deployments

Start building your next project with Next.js 15 and experience the future of web development today!

Resources


Ready to build something amazing? Start your Next.js 15 project today and join the thousands of developers creating the next generation of web applications. Ready to build something amazing? Start your Next.js 15 project today and join the thousands of developers creating the next generation of web applications.