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.