⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
⚡
Next.js

Next.js 14 App Router Complete Guide

12/22/2024
20 min read
Next.jsReactApp RouterServer ComponentsFull Stack

Next.js 14 App Router Complete Guide

Next.js 14 ile birlikte App Router artık stable durumda. Bu yeni routing sistemi ile neler yapabileceğimizi inceleyelim.

Proje Yapısı

src/
  app/
    layout.tsx          # Root layout
    page.tsx           # Ana sayfa
    loading.tsx        # Loading UI
    error.tsx          # Error UI
    not-found.tsx      # 404 page
    globals.css        # Global styles
    
    blog/
      page.tsx         # /blog
      loading.tsx      # Blog loading
      [slug]/
        page.tsx       # /blog/[slug]
        loading.tsx    # Post loading
        
    dashboard/
      layout.tsx       # Dashboard layout
      page.tsx         # /dashboard
      settings/
        page.tsx       # /dashboard/settings
        
    api/
      posts/
        route.ts       # API endpoints

Layouts

Root Layout

// app/layout.tsx
import './globals.css'

export const metadata = {
  title: 'My Blog',
  description: 'A modern blog built with Next.js 14',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="tr">
      <body>
        <nav>
          <h1>My Blog</h1>
        </nav>
        <main>{children}</main>
        <footer>© 2024 My Blog</footer>
      </body>
    </html>
  )
}

Nested Layouts

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard-layout">
      <aside className="sidebar">
        <nav>
          <a href="/dashboard">Dashboard</a>
          <a href="/dashboard/settings">Settings</a>
          <a href="/dashboard/profile">Profile</a>
        </nav>
      </aside>
      <div className="content">
        {children}
      </div>
    </div>
  )
}

Server Components

// app/blog/page.tsx
interface Post {
  id: string;
  title: string;
  content: string;
  createdAt: string;
}

async function getPosts(): Promise<Post[]> {
  // Bu server'da çalışır, client'a API call gerekmez
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // 60 saniye cache
  });
  
  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }
  
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <div>
      <h1>Blog Posts</h1>
      <div className="posts-grid">
        {posts.map(post => (
          <article key={post.id} className="post-card">
            <h2>{post.title}</h2>
            <p>{post.content.substring(0, 150)}...</p>
            <time>{new Date(post.createdAt).toLocaleDateString()}</time>
          </article>
        ))}
      </div>
    </div>
  );
}

Dynamic Routes

// app/blog/[slug]/page.tsx
interface PageProps {
  params: {
    slug: string;
  };
}

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 } // 1 saat cache
  });
  
  if (!res.ok) {
    throw new Error('Post not found');
  }
  
  return res.json();
}

export async function generateMetadata({ params }: PageProps) {
  const post = await getPost(params.slug);
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  };
}

export default async function PostPage({ params }: PageProps) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{new Date(post.createdAt).toLocaleDateString()}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
  
  return posts.map((post: Post) => ({
    slug: post.slug,
  }));
}

Loading States

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="loading-container">
      <div className="skeleton-grid">
        {Array.from({ length: 6 }).map((_, i) => (
          <div key={i} className="skeleton-card">
            <div className="skeleton-title"></div>
            <div className="skeleton-text"></div>
            <div className="skeleton-text"></div>
          </div>
        ))}
      </div>
    </div>
  );
}

Error Handling

// app/blog/error.tsx
'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>
  );
}

API Routes

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET() {
  try {
    const posts = await getPosts(); // Your data fetching logic
    return NextResponse.json(posts);
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal Server Error' },
      { status: 500 }
    );
  }
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const post = await createPost(body);
    return NextResponse.json(post, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: 'Bad Request' },
      { status: 400 }
    );
  }
}
// app/api/posts/[id]/route.ts
interface RouteParams {
  params: {
    id: string;
  };
}

export async function GET(
  request: NextRequest,
  { params }: RouteParams
) {
  try {
    const post = await getPost(params.id);
    if (!post) {
      return NextResponse.json(
        { error: 'Post not found' },
        { status: 404 }
      );
    }
    return NextResponse.json(post);
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal Server Error' },
      { status: 500 }
    );
  }
}

Client Components

// app/components/LikeButton.tsx
'use client';

import { useState } from 'react';

interface LikeButtonProps {
  postId: string;
  initialLikes: number;
}

export default function LikeButton({ postId, initialLikes }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialLikes);
  const [isLiked, setIsLiked] = useState(false);

  const handleLike = async () => {
    try {
      const response = await fetch(`/api/posts/${postId}/like`, {
        method: 'POST',
      });
      
      if (response.ok) {
        setLikes(prev => isLiked ? prev - 1 : prev + 1);
        setIsLiked(!isLiked);
      }
    } catch (error) {
      console.error('Failed to like post:', error);
    }
  };

  return (
    <button 
      onClick={handleLike}
      className={`like-button ${isLiked ? 'liked' : ''}`}
    >
      ❤️ {likes}
    </button>
  );
}

Middleware

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Auth check
  const token = request.cookies.get('auth-token')?.value;
  
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }
  
  // Rate limiting
  const ip = request.ip ?? '127.0.0.1';
  // Implement rate limiting logic here
  
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Data Fetching Patterns

Parallel Data Fetching

async function getPostWithComments(slug: string) {
  // Paralel olarak çalışır
  const [post, comments] = await Promise.all([
    getPost(slug),
    getComments(slug)
  ]);
  
  return { post, comments };
}

Sequential Data Fetching

async function getUserPosts(userId: string) {
  const user = await getUser(userId);
  // User bilgisi gerektiği için sıralı
  const posts = await getPosts(user.id);
  
  return { user, posts };
}

Deployment

Vercel Deploy

npm i -g vercel
vercel

Docker

FROM node:18-alpine AS base

# Dependencies
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Builder
FROM base AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build

# Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]

Sonuç

Next.js 14 App Router ile modern, performanslı ve SEO dostu web uygulamaları geliştirebilirsiniz. Server Components sayesinde daha az JavaScript bundle'ı, daha hızlı sayfa yüklemeleri ve daha iyi kullanıcı deneyimi elde edebilirsiniz.