Learning Next.js: Two Months of Building in Production

Two months ago, I knew nothing about Next.js beyond the fact that it was "React but better." Coming from a Python background with zero web development experience, I decided to learn Next.js by building something real: my developer blog. Instead of following tutorials, I jumped straight into building Nikolas Dev Journey using Next.js 15.3.1 with the new App Router. In this post, I'll share what I've learned about Next.js through practical examples from my own blog, covering everything from the basics to deployment.

What is Next.js and Why I Chose It

Next.js is a React framework that adds powerful features like server-side rendering, automatic code splitting, and file-based routing. When I was deciding how to build my blog, I researched several options but Next.js stood out for its combination of simplicity and power. Here's why I chose it over other options:

  • React-based: Built on React, which is industry-standard and has huge community support
  • File-based routing: Creating new pages is as simple as adding files to folders
  • Full-stack capabilities: Can handle both frontend and backend in one framework
  • Excellent deployment: Works seamlessly with Vercel (made by the same team)
  • App Router: The new approach makes building complex applications more intuitive
  • Performance: Built-in optimizations for images, fonts, and code splitting

For a beginner like me, Next.js offered the perfect balance,powerful enough to build a professional blog, but simple enough to learn by doing.

Understanding the App Router: My Project Structure

Next.js 13 introduced the App Router, which completely changed how you structure applications. Instead of a pages directory, everything lives in an app directory. Here's how I structured my blog:

My Next.js App Router Structurebash
1nikolas-dev-journey/
2  ├── app/
3  │   ├── layout.js              # Root layout for entire app
4  │   ├── page.js                # Homepage (/)
5  │   ├── globals.css            # Global styles
6  │   ├── about/
7  │   │   └── page.js            # About page (/about)
8  │   ├── blog/
9  │   │   ├── layout.js          # Blog-specific layout
10  │   │   ├── page.js            # Blog listing (/blog)
11  │   │   └── posts/
12  │   │       └── [slug]/
13  │   │           └── page.js    # Individual blog posts
14  │   ├── projects/
15  │   │   └── page.js            # Projects page (/projects)
16  │   ├── contact/
17  │   │   └── page.js            # Contact page (/contact)
18  │   ├── api/
19  │   │   ├── blog/
20  │   │   │   └── route.js       # Blog API endpoint
21  │   │   └── contact/
22  │   │       └── route.js       # Contact form API
23  │   └── components/
24  │       ├── Navbar.js
25  │       ├── Footer.js
26  │       ├── CodeBlock.js
27  │       └── ContactForm.js
28  ├── package.json
29  └── next.config.mjs

The beauty of this structure is that every page.js file automatically becomes a route. Want a new page? Just create a folder with a page.js file inside. This file-based routing eliminated the complexity of manual route configuration.

Building Layouts: The Root Layout Pattern

One of Next.js's most powerful features is its layout system. The root layout wraps your entire application and is where you define global elements like fonts, metadata, and analytics. Heres my root layout:

Root Layout with Global Configurationjavascript
1import { Inter } from 'next/font/google';
2  import './globals.css';
3  import GoogleAnalytics from './components/GoogleAnalytics';
4  import { createMetadata } from './lib/metadata';
5  
6  const inter = Inter({
7    variable: '--font-inter',
8    subsets: ['latin'],
9    display: 'swap',
10  });
11  
12  // Global metadata for the entire site
13  export const metadata = createMetadata('home');
14  
15  export default function RootLayout({ children }) {
16    return (
17      <html lang="en" className={inter.variable}>
18        <body
19          style={{
20            backgroundColor: '#0a0f1c',
21            color: '#e1e5e9',
22            margin: 0,
23            minHeight: '100vh',
24            display: 'flex',
25            flexDirection: 'column',
26          }}
27        >
28          <GoogleAnalytics GA_MEASUREMENT_ID="G-CMEMLMEFFR" />
29          {children}
30          
31          {/* Structured data for SEO */}
32          <script
33            type="application/ld+json"
34            dangerouslySetInnerHTML={{
35              __html: JSON.stringify({
36                "@context": "https://schema.org",
37                "@type": "Person",
38                "name": "Nikolas Stojak",
39                "jobTitle": "Self-taught Software Developer",
40                "url": "https://nikolasdevjourney.com",
41                "sameAs": [
42                  "https://www.linkedin.com/in/nikolas-stojak-335177285/",
43                  "https://github.com/Nikostojak"
44                ]
45              })
46            }}
47          />
48        </body>
49      </html>
50    );
51  }

This layout runs on every page and handles global concerns like font loading, analytics, and SEO structured data. Next.js automatically optimizes the Google Font loading, which significantly improves performance compared to manual font loading.

Client vs Server Components: When to Use Which

One of the biggest learning curves in Next.js is understanding when to use server components (default) versus client components (marked with 'use client'). Here's what I learned through building my blog:

Server Components (Default)

Server components run on the server and are great for static content and SEO. Most of my blog post pages are server components:

Server Component Example - Blog Postjavascript
1// No 'use client' directive = Server Component
2  export const metadata = {
3    title: 'My Blog Post | Nikolas Dev Journey',
4    description: 'Description of my blog post for SEO',
5  };
6  
7  import Navbar from '../../../components/Navbar';
8  import Footer from '../../../components/Footer';
9  import Container from '../../../components/Container';
10  
11  export default function BlogPost() {
12    return (
13      <main className="page-layout">
14        <Navbar />
15        <Container>
16          <article className="blog-post-section">
17            <h1 className="blog-post-title">
18              My Blog Post Title
19            </h1>
20            <div className="blog-post-content">
21              <p>Static content that gets rendered on the server...</p>
22            </div>
23          </article>
24        </Container>
25        <Footer />
26      </main>
27    );
28  }

Client Components for Interactivity

Client components are needed for interactivity, state management, and browser APIs. My navbar with mobile menu is a perfect example:

Client Component Example - Interactive Navbarjavascript
1'use client';
2  import { useState, useEffect, useRef } from 'react';
3  import Link from 'next/link';
4  
5  export default function Navbar() {
6    const [isMenuOpen, setIsMenuOpen] = useState(false);
7    const menuRef = useRef(null);
8  
9    useEffect(() => {
10      const handleClickOutside = (event) => {
11        if (menuRef.current && !menuRef.current.contains(event.target)) {
12          setIsMenuOpen(false);
13        }
14      };
15  
16      if (isMenuOpen) {
17        document.addEventListener('mousedown', handleClickOutside);
18        document.body.classList.add('menu-open');
19      } else {
20        document.body.classList.remove('menu-open');
21      }
22  
23      return () => {
24        document.removeEventListener('mousedown', handleClickOutside);
25        document.body.classList.remove('menu-open');
26      };
27    }, [isMenuOpen]);
28  
29    return (
30      <nav className="homepage-nav">
31        <div className="nav-content">
32          <Link href="/" className="logo">
33            Nikolas<span className="logo-highlight">.Dev</span>
34          </Link>
35  
36          <button
37            className={`hamburger ${isMenuOpen ? 'open' : ''}`}
38            onClick={() => setIsMenuOpen(!isMenuOpen)}
39          >
40            {/* Hamburger icon */}
41          </button>
42  
43          <div className={`navbar-menu ${isMenuOpen ? 'open' : ''}`} ref={menuRef}>
44            {['Blog', 'Projects', 'About', 'Contact'].map((page) => (
45              <Link key={page} href={`/${page.toLowerCase()}`}>
46                {page}
47              </Link>
48            ))}
49          </div>
50        </div>
51      </nav>
52    );
53  }

The rule I follow: if it needs state, event handlers, or browser APIs, use 'use client'. If it's just rendering static content, keep it as a server component for better performance.

API Routes: Building a Backend in Next.js

Next.js isn't just a frontend framework,it can handle your backend too. I use API routes for my blog data and contact form. Here's how my blog API works:

API Route Example - Blog Posts Endpointjavascript
1export async function GET() {
2    try {
3      const posts = [
4        {
5          title: "Learning Next.js: Two Months of Building in Production",
6          slug: "learning-nextjs-two-months",
7          date: "2025-06-04",
8          excerpt: "My practical journey learning Next.js by building a real developer blog.",
9          category: "Next.js",
10          tags: ["Next.js", "React", "Web Development"],
11          isFeatured: true
12        },
13        {
14          title: "From Zero to Functional: My First Month with JavaScript",
15          slug: "my-javascript-journey",
16          date: "2025-05-24",
17          excerpt: "How I learned JavaScript fundamentals by building my blog.",
18          category: "JavaScript",
19          tags: ["JavaScript", "Learning Journey"],
20          isFeatured: false
21        },
22        // More posts...
23      ];
24  
25      return new Response(JSON.stringify(posts), {
26        status: 200,
27        headers: {
28          "Content-Type": "application/json",
29          "Cache-Control": "no-store"
30        }
31      });
32    } catch (error) {
33      console.error("Error in /api/blog:", error);
34      return new Response(
35        JSON.stringify({ error: "Internal Server Error" }), 
36        { status: 500 }
37      );
38    }
39  }

This API endpoint can be called from anywhere in my app with fetch('/api/blog'). Next.js handles all the server setup,I just focus on writing the logic.

Data Fetching: From Static to Dynamic

Next.js offers multiple ways to fetch data. I started with static data in my components, then moved to API calls for dynamic content. Here's how I fetch blog posts on my homepage:

Client-Side Data Fetching with Error Handlingjavascript
1'use client';
2  import { useState, useEffect } from 'react';
3  
4  export default function HomePage() {
5    const [posts, setPosts] = useState([]);
6    const [isLoading, setIsLoading] = useState(true);
7    const [error, setError] = useState(null);
8  
9    useEffect(() => {
10      async function fetchPosts() {
11        try {
12          setIsLoading(true);
13          setError(null);
14          const res = await fetch('/api/blog', { cache: 'no-store' });
15          
16          if (!res.ok) {
17            throw new Error(`Failed to fetch posts: ${res.status}`);
18          }
19          
20          const data = await res.json();
21          if (!Array.isArray(data)) {
22            throw new Error('Expected an array of posts');
23          }
24          setPosts(data);
25        } catch (err) {
26          console.error('Error fetching posts:', err);
27          setError(err.message);
28        } finally {
29          setIsLoading(false);
30        }
31      }
32      
33      fetchPosts();
34    }, []);
35  
36    // Find featured and recent posts
37    const featuredPost = posts.find(post => post?.isFeatured);
38    const recentPosts = posts
39      .filter(post => !post?.isFeatured)
40      .sort((a, b) => new Date(b?.date || 0) - new Date(a?.date || 0))
41      .slice(0, 5);
42  
43    if (isLoading) return <div>Loading posts...</div>;
44    if (error) return <div>Error: {error}</div>;
45  
46    return (
47      <main>
48        {/* Render posts */}
49      </main>
50    );
51  }

Image and Font Optimization

Next.js automatically optimizes images and fonts, which significantly improves performance. Here's how I handle fonts in my project:

Font Optimization with next/fontjavascript
1import { Inter } from 'next/font/google';
2  
3  const inter = Inter({
4    variable: '--font-inter',
5    subsets: ['latin'],
6    display: 'swap', // Improves loading performance
7  });
8  
9  export default function RootLayout({ children }) {
10    return (
11      <html lang="en" className={inter.variable}>
12        <body>
13          {children}
14        </body>
15      </html>
16    );
17  }

Next.js downloads the font files, self-hosts them, and eliminates external network requests. This removes layout shift and improves Core Web Vitals automatically.

Metadata and SEO

Next.js makes SEO straightforward with its metadata API. I created a reusable metadata system for my blog:

Reusable Metadata Systemjavascript
1export const createMetadata = (page, customData = {}) => {
2    const baseMetadata = {
3      title: 'Nikolas Dev Journey',
4      description: 'Follow my journey from philosophy to software development.',
5      keywords: 'developer, python, javascript, web development',
6      authors: [{ name: 'Nikolas Stojak' }],
7      
8      openGraph: {
9        type: 'website',
10        locale: 'en_US',
11        url: 'https://nikolasdevjourney.com',
12        siteName: 'Nikolas Dev Journey',
13        images: [{
14          url: '/og-image.jpg',
15          width: 1200,
16          height: 630,
17        }]
18      },
19      
20      twitter: {
21        card: 'summary_large_image',
22        creator: '@NikolasStojak'
23      },
24    };
25  
26    const pageSpecific = {
27      home: {
28        title: 'Nikolas Dev Journey | From Philosophy to Code',
29      },
30      blog: {
31        title: 'Developer Blog | Nikolas Dev Journey',
32      },
33      // More page-specific metadata...
34    };
35  
36    return {
37      ...baseMetadata,
38      ...pageSpecific[page],
39      ...customData
40    };
41  };

Deployment with Vercel

Deploying Next.js apps is incredibly smooth with Vercel. Here's my deployment setup:

My Deployment Processbash
1# 1. Connect GitHub repository to Vercel
2  # 2. Configure build settings (automatic for Next.js)
3  # 3. Set environment variables in Vercel dashboard
4  # 4. Every git push triggers automatic deployment
5  
6  # Build command (automatic):
7  npm run build
8  
9  # Output directory (automatic):
10  .next
11  
12  # Environment variables I set:
13  RESEND_API_KEY=*******
14  GA_MEASUREMENT_ID=G-CMEMLMEFFR
15  
16  # Custom domain setup:
17  # 1. Add domain in Vercel dashboard
18  # 2. Update DNS records at domain provider
19  # 3. Automatic SSL certificate generation

Vercel handles everything automatically,builds, deployments, SSL certificates, and global CDN distribution. The integration is so seamless that I often forget I'm running a production website.

Performance Insights: What I Learned

After two months of building with Next.js, here are the performance insights I've gained:

  • Automatic code splitting: Next.js only loads the JavaScript needed for each page
  • Image optimization: Next/Image component automatically optimizes and lazy-loads images
  • Font optimization: Google Fonts are self-hosted and preloaded
  • Static generation: Pages are pre-generated at build time when possible
  • Bundle analysis: Built-in tools help identify large dependencies
  • Core Web Vitals: Next.js is optimized for Google's performance metrics

Challenges I Faced and How I Solved Them

Learning Next.js wasn't without challenges. Here are the main ones I encountered:

1. Server vs Client Component Confusion

Problem: I initially marked everything as 'use client' because I didn't understand the difference.
Solution: I learned to start with server components by default and only add 'use client' when I need interactivity.

2. Hydration Errors

Problem: My components would render differently on server and client, causing hydration mismatches.
Solution: I ensured server and client rendered identical HTML, especially for dynamic content.

3. API Route Organization

Problem: I didn't know how to structure API routes properly.
Solution: I learned that each API route needs a route.js file with named exports like GET, POST.

What I'd Do Differently

If I started over today, here's what I'd change:

  • Start with TypeScript: The type safety would have prevented many bugs
  • Use a CSS framework: Tailwind CSS would have sped up styling significantly
  • Implement testing early: Jest and Testing Library integration would catch regressions
  • Set up error boundaries: Better error handling for production
  • Use a database: Move beyond static API data to a real database like PostgreSQL

Next Steps in My Next.js Journey

After two months with Next.js, I'm excited to explore more advanced features:

  • Server Actions: Handling form submissions without API routes
  • Streaming: Progressive page loading with Suspense
  • Middleware: Request-level logic for authentication and redirects
  • ISR (Incremental Static Regeneration): Updating static content without rebuilds
  • Advanced caching: Optimizing data fetching with Next.js cache strategies

Why I Recommend Next.js for Beginners

After building a real production website with Next.js, I'd recommend it to other beginners for these reasons:

  • Gentle learning curve: You can start simple and add complexity gradually
  • Excellent documentation: The Next.js docs are clear and comprehensive
  • Full-stack capabilities: Learn both frontend and backend in one framework
  • Industry standard: Used by companies like Netflix, TikTok, and Airbnb
  • Great ecosystem: Huge community and third-party library support
  • Deployment simplicity: Vercel makes going to production effortless
"Next.js taught me that you don't need to master every concept before building something real. Start with the basics, build your project, and learn advanced features as you need them. Two months later, I have both a production website and solid Next.js skills."

💬 Comments & Discussion

Share your thoughts, ask questions, or discuss this post. Comments are powered by GitHub Discussions.