Next.js Advanced Patterns: Modern Web Uygulamaları İçin En İyi Pratikler

Yunus Emre Güzel
29 Ocak 202520 dkReact
Next.js Advanced Patterns: Modern Web Uygulamaları İçin En İyi Pratikler

Next.js, React tabanlı web uygulamaları geliştirmek için en popüler framework'lerden biri. Bu yazıda, Next.js'in ileri seviye özelliklerini, performans optimizasyonlarını ve modern web uygulamaları için en iyi pratikleri detaylıca inceleyeceğiz.

Next.js 14'ün Getirdiği Yenilikler

Next.js 14, birçok yeni özellik ve iyileştirme ile geldi. Öne çıkan yenilikler:

  • Partial Prerendering (Preview)
  • Server Actions (Stable)
  • Metadata API İyileştirmeleri
  • Turbopack Geliştirmeleri
  • Image Component Optimizasyonları

Partial Prerendering

Partial Prerendering, statik ve dinamik içeriği aynı sayfada birleştirmenize olanak tanır:

// app/page.tsx
export default async function Page() {
  return (
    <main>
      {/* Statik içerik - Build time'da oluşturulur */}
      <header>
        <h1>Ürün Kataloğu</h1>
        <StaticFilters />
      </header>

      {/* Dinamik içerik - Runtime'da yüklenir */}
      <Suspense fallback={<ProductsSkeleton />}>
        <Products />
      </Suspense>
    </main>
  );
}

// components/Products.tsx
async function Products() {
  const products = await fetchProducts();
  
  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Server Actions ve Form Handling

Server Actions, form işlemlerini ve veri mutasyonlarını güvenli bir şekilde yönetmenizi sağlar:

// actions/product.ts
'use server';

import { revalidatePath } from 'next/cache';
import { z } from 'zod';

const productSchema = z.object({
  name: z.string().min(3),
  price: z.number().positive(),
  description: z.string().min(10),
  categoryId: z.string().uuid()
});

export async function createProduct(formData: FormData) {
  const rawData = {
    name: formData.get('name'),
    price: Number(formData.get('price')),
    description: formData.get('description'),
    categoryId: formData.get('categoryId')
  };

  try {
    // Veri validasyonu
    const validatedData = productSchema.parse(rawData);

    // Veritabanına kaydet
    const product = await prisma.product.create({
      data: validatedData
    });

    // Cache'i temizle
    revalidatePath('/products');

    return { success: true, product };
  } catch (error) {
    if (error instanceof z.ZodError) {
      return {
        success: false,
        errors: error.errors
      };
    }

    return {
      success: false,
      error: 'Bir hata oluştu'
    };
  }
}

// components/ProductForm.tsx
'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { createProduct } from '@/actions/product';

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button
      type="submit"
      disabled={pending}
      className="btn btn-primary"
    >
      {pending ? 'Kaydediliyor...' : 'Kaydet'}
    </button>
  );
}

export function ProductForm() {
  const [state, formAction] = useFormState(createProduct, {
    success: false,
    errors: []
  });

  return (
    <form action={formAction} className="space-y-4">
      <div>
        <label htmlFor="name">Ürün Adı</label>
        <input
          type="text"
          id="name"
          name="name"
          className="input"
          required
        />
        {state.errors?.name && (
          <p className="text-red-500">{state.errors.name}</p>
        )}
      </div>

      <div>
        <label htmlFor="price">Fiyat</label>
        <input
          type="number"
          id="price"
          name="price"
          className="input"
          required
        />
      </div>

      <div>
        <label htmlFor="description">Açıklama</label>
        <textarea
          id="description"
          name="description"
          className="textarea"
          required
        />
      </div>

      <SubmitButton />
    </form>
  );
}

Route Handlers ve API Endpoints

Next.js 14'te API route'ları daha esnek ve type-safe bir şekilde tanımlanabilir:

// app/api/products/route.ts
import { NextRequest } from 'next/server';
import { z } from 'zod';

const querySchema = z.object({
  category: z.string().optional(),
  sort: z.enum(['price_asc', 'price_desc', 'name_asc', 'name_desc']).optional(),
  page: z.coerce.number().min(1).default(1),
  limit: z.coerce.number().min(1).max(100).default(20)
});

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    
    // Query parametrelerini validate et
    const query = querySchema.parse({
      category: searchParams.get('category'),
      sort: searchParams.get('sort'),
      page: searchParams.get('page'),
      limit: searchParams.get('limit')
    });

    // Veritabanı sorgusu
    const products = await prisma.product.findMany({
      where: {
        ...(query.category && {
          categoryId: query.category
        })
      },
      orderBy: {
        ...(query.sort === 'price_asc' && { price: 'asc' }),
        ...(query.sort === 'price_desc' && { price: 'desc' }),
        ...(query.sort === 'name_asc' && { name: 'asc' }),
        ...(query.sort === 'name_desc' && { name: 'desc' })
      },
      skip: (query.page - 1) * query.limit,
      take: query.limit
    });

    // Toplam sayfa sayısını hesapla
    const total = await prisma.product.count({
      where: {
        ...(query.category && {
          categoryId: query.category
        })
      }
    });

    return Response.json({
      data: products,
      pagination: {
        total,
        pages: Math.ceil(total / query.limit),
        current: query.page
      }
    });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json(
        { error: 'Invalid query parameters', details: error.errors },
        { status: 400 }
      );
    }

    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Metadata ve SEO Optimizasyonu

Next.js'in Metadata API'si ile SEO optimizasyonunu kolayca yapabilirsiniz:

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: {
    default: 'Site Adı',
    template: '%s | Site Adı'
  },
  description: 'Site açıklaması',
  openGraph: {
    type: 'website',
    locale: 'tr_TR',
    url: 'https://example.com',
    siteName: 'Site Adı'
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
  verification: {
    google: 'google-site-verification-code',
    yandex: 'yandex-verification-code'
  }
};

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug);
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      type: 'article',
      title: post.title,
      description: post.excerpt,
      publishedTime: post.publishDate,
      authors: [post.author],
      images: [
        {
          url: post.coverImage,
          width: 1200,
          height: 630,
          alt: post.title
        }
      ]
    }
  };
}

Performans Optimizasyonları

1. Image Optimizasyonu

// components/OptimizedImage.tsx
import Image from 'next/image';
import { getPlaiceholder } from 'plaiceholder';

interface Props {
  src: string;
  alt: string;
  width: number;
  height: number;
}

async function OptimizedImage({ src, alt, width, height }: Props) {
  // Blur placeholder oluştur
  const { base64 } = await getPlaiceholder(src);

  return (
    <div className="relative aspect-video">
      <Image
        src={src}
        alt={alt}
        width={width}
        height={height}
        placeholder="blur"
        blurDataURL={base64}
        className="object-cover"
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        priority={false}
        loading="lazy"
      />
    </div>
  );
}

2. Font Optimizasyonu

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
  preload: true,
  fallback: ['system-ui', 'sans-serif']
});

export default function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="tr" className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

3. Bundle Size Optimizasyonu

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  poweredByHeader: false,
  compress: true,
  
  // Bundle analyzer
  webpack: (config, { isServer }) => {
    if (process.env.ANALYZE) {
      const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
      config.plugins.push(
        new BundleAnalyzerPlugin({
          analyzerMode: 'server',
          analyzerPort: isServer ? 8888 : 8889,
          openAnalyzer: true
        })
      );
    }
    return config;
  },
  
  // Image optimizasyonu
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    domains: ['images.unsplash.com'],
    minimumCacheTTL: 60
  }
};

module.exports = nextConfig;

Error Handling ve Loading States

1. Error Boundaries

// app/error.tsx
'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h2 className="text-2xl font-bold mb-4">
        Bir şeyler yanlış gitti!
      </h2>
      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-500 text-white rounded"
      >
        Tekrar Dene
      </button>
    </div>
  );
}

2. Loading States

// 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>
  );
}

// components/ProductList.tsx
import { Suspense } from 'react';

export default function ProductList() {
  return (
    <Suspense 
      fallback={
        <div className="grid grid-cols-3 gap-4">
          {Array.from({ length: 6 }).map((_, i) => (
            <ProductCardSkeleton key={i} />
          ))}
        </div>
      }
    >
      <Products />
    </Suspense>
  );
}

Middleware ve Authentication

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(request: NextRequest) {
  const token = await getToken({ req: request });
  
  // Auth gerektiren route'ları kontrol et
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('from', request.nextUrl.pathname);
      return NextResponse.redirect(loginUrl);
    }
  }

  // Rate limiting
  if (request.nextUrl.pathname.startsWith('/api')) {
    const ip = request.ip ?? '127.0.0.1';
    const rateLimit = await checkRateLimit(ip);
    
    if (!rateLimit.success) {
      return new NextResponse(
        JSON.stringify({ error: 'Too many requests' }),
        {
          status: 429,
          headers: {
            'Content-Type': 'application/json',
            'X-RateLimit-Limit': rateLimit.limit.toString(),
            'X-RateLimit-Remaining': rateLimit.remaining.toString(),
            'X-RateLimit-Reset': rateLimit.reset.toString()
          }
        }
      );
    }
  }

  return NextResponse.next();
}

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

Internationalization (i18n)

// middleware.ts
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

let locales = ['tr', 'en'];
let defaultLocale = 'tr';

function getLocale(request: NextRequest): string {
  const negotiatorHeaders: Record<string, string> = {};
  request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));

  const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
  const locale = match(languages, locales, defaultLocale);
  
  return locale;
}

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  const pathnameIsMissingLocale = locales.every(
    locale => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );

  if (pathnameIsMissingLocale) {
    const locale = getLocale(request);
    return NextResponse.redirect(
      new URL(`/${locale}${pathname}`, request.url)
    );
  }
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)'
  ]
};

// messages/tr.json
{
  "common": {
    "welcome": "Hoş Geldiniz",
    "login": "Giriş Yap",
    "register": "Kayıt Ol"
  }
}

// app/[lang]/layout.tsx
import { getDictionary } from '@/lib/dictionaries';

export default async function Layout({
  children,
  params: { lang }
}: {
  children: React.ReactNode;
  params: { lang: string };
}) {
  const dictionary = await getDictionary(lang);

  return (
    <html lang={lang}>
      <body>
        {children}
      </body>
    </html>
  );
}

Testing

1. Unit Testing

// __tests__/components/ProductCard.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ProductCard from '@/components/ProductCard';

describe('ProductCard', () => {
  const mockProduct = {
    id: '1',
    name: 'Test Product',
    price: 100,
    image: '/test.jpg'
  };

  it('renders product information correctly', () => {
    render(<ProductCard product={mockProduct} />);
    
    expect(screen.getByText(mockProduct.name)).toBeInTheDocument();
    expect(screen.getByText(`${mockProduct.price} TL`)).toBeInTheDocument();
  });

  it('calls onAddToCart when add button is clicked', async () => {
    const onAddToCart = jest.fn();
    render(
      <ProductCard 
        product={mockProduct}
        onAddToCart={onAddToCart}
      />
    );
    
    await userEvent.click(screen.getByRole('button', { name: /sepete ekle/i }));
    expect(onAddToCart).toHaveBeenCalledWith(mockProduct.id);
  });
});

2. Integration Testing

// __tests__/pages/products.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import ProductsPage from '@/app/products/page';
import { createMockRouter } from '@/test-utils/createMockRouter';

jest.mock('next/navigation', () => ({
  useRouter: () => createMockRouter({}),
  useSearchParams: () => new URLSearchParams()
}));

describe('ProductsPage', () => {
  it('renders products and pagination', async () => {
    render(<ProductsPage />);
    
    // Loading state kontrolü
    expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument();
    
    // Ürünlerin yüklenmesini bekle
    await waitFor(() => {
      expect(screen.getAllByTestId('product-card')).toHaveLength(20);
    });
    
    // Pagination kontrolü
    expect(screen.getByRole('navigation')).toBeInTheDocument();
  });

  it('handles filter changes', async () => {
    const { rerender } = render(<ProductsPage />);
    
    // Filtre değişikliği
    rerender(<ProductsPage searchParams={{ category: 'electronics' }} />);
    
    await waitFor(() => {
      expect(screen.getByText('Electronics')).toBeInTheDocument();
    });
  });
});

3. E2E Testing

// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
    await page.login(); // Custom helper
  });

  test('completes checkout process', async ({ page }) => {
    // Ürün sepete ekleme
    await page.click('[data-testid="product-card"]');
    await page.click('button:has-text("Sepete Ekle")');
    
    // Sepete gitme
    await page.click('[data-testid="cart-icon"]');
    
    // Ödeme adımları
    await page.click('button:has-text("Ödemeye Geç")');
    await page.fill('[name="cardNumber"]', '4242424242424242');
    await page.fill('[name="expiry"]', '12/25');
    await page.fill('[name="cvc"]', '123');
    
    await page.click('button:has-text("Ödemeyi Tamamla")');
    
    // Başarılı ödeme kontrolü
    await expect(page.locator('.success-message')).toBeVisible();
    await expect(page).toHaveURL(/\/success/);
  });
});

Production Deployment

1. Docker Yapılandırması

# Dockerfile
FROM node:18-alpine AS base

# Dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn 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 /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

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

2. CI/CD Pipeline

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: yarn install --frozen-lockfile
        
      - name: Run tests
        run: yarn test
        
      - name: Build
        run: yarn build
        
      - name: Deploy to production
        uses: digitalocean/app-action@main
        with:
          app-name: my-next-app
          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}

Monitoring ve Analytics

1. Performance Monitoring

// lib/monitoring.ts
export function reportWebVitals(metric: any) {
  const body = {
    ...metric,
    page: window.location.pathname,
    userAgent: window.navigator.userAgent
  };

  const blob = new Blob([JSON.stringify(body)], {
    type: 'application/json'
  });

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', blob);
  } else {
    fetch('/api/vitals', {
      body: blob,
      method: 'POST',
      keepalive: true
    });
  }
}

// app/api/vitals/route.ts
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';

export async function POST(request: NextRequest) {
  const metric = await request.json();
  
  await prisma.webVital.create({
    data: {
      name: metric.name,
      value: metric.value,
      page: metric.page,
      userAgent: metric.userAgent
    }
  });

  return new Response(null, { status: 202 });
}

2. Error Tracking

// lib/error-tracking.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
  environment: process.env.NODE_ENV
});

export function captureError(error: Error, context?: any) {
  Sentry.withScope(scope => {
    if (context) {
      scope.setExtras(context);
    }
    Sentry.captureException(error);
  });
}

// app/error.tsx
'use client';

import { useEffect } from 'react';
import { captureError } from '@/lib/error-tracking';

export default function Error({
  error,
  reset
}: {
  error: Error;
  reset: () => void;
}) {
  useEffect(() => {
    captureError(error);
  }, [error]);

  return (
    // ...
  );
}

Sonuç

Next.js 14, modern web uygulamaları geliştirmek için güçlü özellikler ve araçlar sunar. Bu yazıda incelediğimiz konular:

  • Server Components ve Partial Prerendering
  • Server Actions ve Form Handling
  • Route Handlers ve API Endpoints
  • Metadata ve SEO Optimizasyonu
  • Performans Optimizasyonları
  • Error Handling ve Loading States
  • Middleware ve Authentication
  • Internationalization
  • Testing Stratejileri
  • Production Deployment
  • Monitoring ve Analytics

Bu özellikleri ve best practice'leri kullanarak, yüksek performanslı, ölçeklenebilir ve maintainable Next.js uygulamaları geliştirebilirsiniz.

Kaynaklar