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.
