czay.dev/academy
Bölüm 017 dk

Modern stack kurulumu — Next.js 16, TypeScript, Tailwind v4

Next.js 16 App Router, TypeScript, Tailwind v4 ve shadcn/ui ile production-ready bir proje iskelesini kurarız. Folder structure, env discipline ve code style discipline gibi "ileride pişman olmayacağın" kararları başta veririz.

Yeni bir Next.js projesi başlatmak pnpm create next-app ile 5 dakikalık iş. Ama ilk haftadan sonra fark edersin: dosyalar dağılmış, env'ler karışmış, lint kuralları yarım, deploy zamanı her şey patlıyor.

Bu bölümde projeyi sıfırdan kuracağız ama "production'a giderken yarım yola dönmek istemeyeceğin" kararları önden alacağız. Sonraki 7 bölümün tüm temeli bu bölümün üstüne inşa olacak.

Hangi sürümlerle çalışıyoruz?

Bu eğitim Next.js 16, React 19, TypeScript 5, Tailwind v4 üzerine yazıldı. Sürüm numaraları önemli — Next.js 13+ App Router, 14'te server actions kararlı, 15'te async params/searchParams, 16'da daha hızlı build

  • daha iyi caching. Eğitim boyunca bu sürümlere özgü pattern'leri kullanacağız.
node --version  # ≥ 20.0.0 olmalı
pnpm --version  # ≥ 8.0.0 olmalı

npm veya yarn da çalışır ama bu eğitimde pnpm kullanacağız — disk + hız avantajı, monorepo desteği daha iyi.

Projeyi başlatma

pnpm create next-app@latest myapp --typescript --tailwind --app \
  --eslint --use-pnpm --src-dir=false --import-alias="@/*"
cd myapp

Flag'ler ne yapıyor?

  • --typescript — TS başlat (zaten kararımız bu)
  • --tailwind — Tailwind v4 + PostCSS hazır
  • --app — App Router (Pages Router'ı geçtik, modern Next.js bu)
  • --eslint — ESLint config + flat config
  • --src-dir=falsesrc/ klasörü kullanmayalım, root'tan import daha temiz
  • --import-alias="@/*"@/components/foo gibi import'lar için baseUrl

Kurulum sonrası pnpm dev ile localhost:3000'de "Welcome to Next.js" sayfası açılır. Bu noktada sadece "next-app boilerplate" var — şimdi production-ready hale getireceğiz.

Folder structure — ne nereye?

Boilerplate'in default'u tek bir app/ klasörü. Gerçek projelerde dosya sayısı arttıkça mantıksal gruplar lazım. Önerdiğim yapı:

myapp/
├── app/                       # Next.js routes (page.tsx, layout.tsx, route.ts)
│   ├── (main)/               # Layout group — public sayfalar
│   ├── (auth)/               # Layout group — auth sayfaları (signin/signup)
│   ├── api/                  # API routes (webhook, sse, OAuth)
│   └── layout.tsx            # Root layout
├── components/                # Reusable UI components
│   ├── ui/                   # shadcn/ui primitives (Button, Dialog, vs.)
│   ├── layout/               # Header, Footer, Sidebar
│   └── feature/              # Domain-specific (UserCard, OrderRow)
├── lib/                       # Server-side utilities
│   ├── actions/              # Server actions (form handlers)
│   ├── queries/              # DB query helpers (read-only, type-safe)
│   ├── auth.ts               # Auth instance export
│   └── utils.ts              # cn(), formatDate(), vb.
├── db/                        # Drizzle schema + client + migrations
│   ├── schema.ts
│   ├── client.ts
│   └── migrations/
├── content/                   # MDX/Markdown content (eğer varsa)
├── public/                    # Statik dosyalar
└── ...

Neden bu yapı?

  • app/ sadece route'lar — page, layout, route handler. Component logic'i buraya yazma; route'tan import et.
  • components/ui/ shadcn'in primitives'i. Dokunma, sürüm güncellemelerini manuel yap. (shadcn npx shadcn add button ile bu klasöre kopyalar.)
  • lib/actions/ ve lib/queries/ ayrımı kritik: action'lar yazar, queries okur. Server actions formdan tetiklenir, DB'ye INSERT/UPDATE yapar. Queries sayfa render'ında DB'den okur. Karıştırma — action içinden query çağırabilirsin ama query içinden action çağrılmaz.
  • db/ sadece şema + client. İçerikle ilgili logic lib/queries/'de.

pnpm dev çalıştırırken bu klasörleri elle yarat:

mkdir -p components/ui components/layout components/feature \
         lib/actions lib/queries db

Code style discipline — başta sıkı, sonra rahat

Production projede format + lint disiplini baştan sıkı kurulmalı. 3 ay sonra kodu okurken "ben mi yazdım bunu" deme — Prettier + ESLint

  • TypeScript strict mode bunu garanti eder.

Prettier

pnpm add -D prettier

.prettierrc (root):

{
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "tabWidth": 2,
  "printWidth": 80,
  "plugins": ["prettier-plugin-tailwindcss"]
}

prettier-plugin-tailwindcss Tailwind class'larını otomatik sıralar (flex items-center gap-2 p-4 gibi tutarlı sıra). Manuel yazıma sırasına güvenme — plugin yapsın.

pnpm add -D prettier-plugin-tailwindcss

ESLint — Next.js + TypeScript strict

Boilerplate'in eslint.config.mjs'ini biraz sıkılaştıralım:

import next from "eslint-config-next";
 
export default [
  ...next,
  {
    rules: {
      "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
      "@typescript-eslint/no-explicit-any": "error",
      "react-hooks/exhaustive-deps": "error",
      "import/order": [
        "error",
        {
          groups: ["builtin", "external", "internal", "parent", "sibling"],
          "newlines-between": "always",
        },
      ],
    },
  },
];

no-explicit-any özellikle önemli — any kullanmak type system'in sebebini ortadan kaldırır. Lazımsa unknown + type narrowing yap.

TypeScript strict

tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
    // ... diğer default'lar
  }
}

noUncheckedIndexedAccess runtime hata kaynağını kapatır:

const arr = [1, 2, 3];
const x = arr[5];           // x: number (yanlış — undefined olabilir!)
                            // strict + noUncheckedIndexedAccess ile:
                            // x: number | undefined ✓

Junior'ın en çok düştüğü hata burada — TS sana yanlış güven veriyor. Flag açınca compiler tüm "sınır dışı erişim" potansiyelini işaretler.

Environment variables — .env discipline

Production'da secret leak'lerinin %80'i env discipline'ı zayıf projelerden çıkar. Bu konuyu baştan oturtalım.

4 dosya pattern'i

.env                      # Default değerler — git'e commit edilebilir (örn PORT=3000)
.env.local                # Lokal secret'lar — GIT'e ASLA commit etme
.env.example              # Template — git'e commit, gerçek değer yok
.env.production           # Prod-only override — git'e commit etme

.gitignore zaten .env* ignore ediyor, sadece .env*.example exception:

.env*
!.env*.example

Convention'lar

  • Server-side secret → prefixsiz: DATABASE_URL, BETTER_AUTH_SECRET
  • Client-side publicNEXT_PUBLIC_*: NEXT_PUBLIC_SITE_URL
  • NEXT_PUBLIC_ ile başlayan her şey bundle'a embed edilir — secret buraya koyma, lobi'de duyurmuş gibi olur.

Type-safe env — Zod + parse

Env'lerin runtime'da tek noktadan validate edilmesi en sağlıklısı:

// lib/env.ts
import { z } from "zod";
 
const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  BETTER_AUTH_SECRET: z.string().min(32),
  GITHUB_CLIENT_ID: z.string().optional(),
  GITHUB_CLIENT_SECRET: z.string().optional(),
  RESEND_API_KEY: z.string().optional(),
  NEXT_PUBLIC_SITE_URL: z.string().url(),
});
 
export const env = envSchema.parse(process.env);

lib/env.ts'i import ettiğin anda env validate edilir. Eksik bir DATABASE_URL ile uygulama açılmadan crash eder — production'da yanlış env'le 30 dakika debugging yerine 5 saniyede patlayan açık hata.

pnpm add zod kurulumu sonrası bu modülü import ettiğin her yerde type-safe env access:

import { env } from "@/lib/env";
console.log(env.DATABASE_URL);  // string ✓
console.log(env.UNKNOWN);       // type error ✓

shadcn/ui kurulumu

UI primitive'leri için shadcn/ui öneririm. Headless component library değil — kendi component'lerini sana kopyalar. Bu sayede tema değiştirebilir, custom feature ekleyebilir, üst sürüme geçtiğinde breaking change yaşamazsın.

pnpm dlx shadcn@latest init

Init sırasında:

  • Style → Default
  • Base color → Zinc (sade) veya kendi rengin
  • CSS variables → Yes (Tailwind v4 ile uyumlu)

İlk component'leri ekle:

pnpm dlx shadcn@latest add button input dialog dropdown-menu

Bunlar components/ui/ altına gelir. Sonradan istediğin kadar customize edersin — Button'a yeni variant eklemek için doğrudan dosyayı düzenle.

İlk component'i yazalım

Az önce kurduğumuz Button'u kullanan basit bir component:

// components/feature/welcome-card.tsx
import { Button } from "@/components/ui/button";
 
export function WelcomeCard({ name }: { name: string }) {
  return (
    <div className="rounded-2xl border border-border bg-card p-6">
      <h2 className="text-xl font-semibold tracking-tight">
        Hoş geldin, {name}
      </h2>
      <p className="mt-2 text-sm text-muted-foreground">
        Hazır olduğunda başlayalım.
      </p>
      <Button className="mt-4">Devam et</Button>
    </div>
  );
}

Önemli noktalar:

  • import { Button } from "@/components/ui/button"@/ alias'ı baseUrl'den (./) gelir, dosya derinliği fark etmez
  • text-muted-foreground — shadcn'in CSS variable'ı (theme-aware)
  • tracking-tight — Tailwind'in letter-spacing utility'si
  • Function name PascalCase, file name kebab-case — Next.js convention

Bu bölümün özeti

Bu bölümde:

  1. Next.js 16 + TS + Tailwind v4 + shadcn projesini kurduk
  2. Folder structure'ını scale-friendly kurguladık
  3. Code style (Prettier + ESLint + TS strict) discipline'i baştan oturttuk
  4. Env discipline + Zod-validated lib/env.ts ile type-safe env access kurduk
  5. shadcn/ui ile UI primitive'leri ekledik

Sonraki bölümde Postgres + Drizzle ORM ile type-safe database kuracağız. Schema design, migration workflow, indexler ve N+1 önleme — junior'ın genelde sonraki yıl öğrendiği şeyleri şimdi kuracağız ki ileride refactor zorunluluğu olmasın.

Takıldığın yer mi var?

Sor, birlikte çözelim.

community.czay.dev — Türkçe yazılım topluluğumuzda eğitimde takıldığın konuları sorabilir, başka geliştiricilerin deneyimlerinden faydalanabilirsin. Hızlı cevap, doğru yer.