Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Principal-level product development and architecture skill for modern professional web platforms. Covers advanced frontend engineering, Next.js App Router patterns, React component architecture, React Hook Form, state management, system design, Firebase auth/session, and long-term platform stewardship. Use for any core or peripheral module, architecture decision, or technical authority question.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 512% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 453% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 874% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 565% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 802% | 0% |
Bu skill, modern kurumsal yazılım platformlarında principal mühendis standartlarıyla analiz, tasarım, implementasyon ve evrim döngüsünü tanımlar. Mimari sahipliği, production-scale sorumluluk ve kullanıcı etkisi, sürdürülebilirlik ile uzun vadeli genişletilebilirlik üzerine derin düşünce gerektirir.
Runtime: Node.js 22.x
Framework: Next.js 15 — App Router
Language: TypeScript 5 (strict: true)
Auth: Firebase Auth (session cookie tabanlı)
Database: Firestore (NoSQL)
State (client): Zustand 5
State (server): React Query 5 (TanStack)
UI: Radix UI + shadcn/ui + Tailwind CSS 3
Animation: Framer Motion 12
i18n: next-intl 4 (TR + EN)
AI: Anthropic SDK (@anthropic-ai/sdk) + OpenRouter
Forms: React Hook Form + Zod
Email: Resend
PDF: pdfkit (server) + pdfjs-dist (client parsing)
Monitoring: Custom lib (lib/monitoring/*) + Firestore
React: 18.x (React 19 geçiş notları §10'da)src/
app/ — Next.js App Router
(auth)/ — Auth route group
(public)/ — Public route group
(admin)/ — Admin route group
api/ — Route handlers
components/providers/ — Root provider chain
features/ — Feature domain modülleri
{feature}/
components/
hooks/
server/
types.ts
lib/
api.ts — Client transport (canonical)
server-api.ts — Server transport (canonical)
auth/session.ts — Shared session core
auth/client-session.ts
admin/auth.ts
server/auth.ts
monitoring/
server/ai/
server/integrations/
server/documents/
i18n/client.ts — next-intl client hook
store/ — Zustand store'larıuseState, useEffect, event handler, Zustand, Browser API var mı?
→ 'use client'
Sadece veri getirip render mı ediyor?
→ Server Component (varsayılan)
Her ikisi de gerekli mi?
→ Server parent + Client child olarak ayırKural: Client Component'ler component ağacında mümkün olduğunca derinde olmalı.
| Dosya | Amaç | | --------------- | --------------------------------- | | page.tsx | Route UI | | layout.tsx | Paylaşılan layout | | loading.tsx | Loading state (Suspense boundary) | | error.tsx | Error boundary | | not-found.tsx | 404 sayfası |
| Route Pattern | Kullanım | | -------------- | -------------------- | | (group) | URL'siz organizasyon | | @slot | Parallel routes | | (.)intercept | Modal overlay |
| Pattern | Kullanım | | --------------- | ------------------------ | | Default | Static (build'de cache) | | revalidate: N | ISR (zaman bazlı) | | no-store | Dynamic (her request) | | React Query | Client-side server state |
| Katman | Kontrol | | ------------ | -------------------- | | Request | fetch options | | Data | revalidate / tags | | Programmatic | revalidatePath/Tag |
typescript'use server' async function submitAction(formData: FormData) { await saveToDatabase(formData) revalidatePath('/') }
Form submission, data mutation, revalidation trigger için kullan. Her zaman input validate et.
| ❌ Yapma | ✅ Yap | | --------------------- | -------------------- | | Her yere use client | Server varsayılan | | Client'ta fetch | Server'da fetch | | Loading state atlama | loading.tsx kullan | | Error boundary atlama | error.tsx kullan | | Büyük client bundle | Dynamic import |
children propPascalCase component, use* hookcond ? <A/> : <B/>) — && yerinetypescriptexport function Card({ children, variant = 'default' }: CardProps) { return <div className={`card card--${variant}`}>{children}</div> } export function CardHeader({ children }: { children: React.ReactNode }) { return <div className='card-header'>{children}</div> } export function CardBody({ children }: { children: React.ReactNode }) { return <div className='card-body'>{children}</div> }
typescriptconst TabsContext = createContext<TabsContextValue | undefined>(undefined) export function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) { const [activeTab, setActiveTab] = useState(defaultTab) return <TabsContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabsContext.Provider> } export function Tab({ id, children }: { id: string; children: React.ReactNode }) { const ctx = useContext(TabsContext) if (!ctx) throw new Error('Tab must be used within Tabs') return ( <button className={ctx.activeTab === id ? 'active' : ''} onClick={() => ctx.setActiveTab(id)}> {children} </button> ) }
Not: Radix primitifleri zaten compound pattern kullanır. Dialog, Tabs, DropdownMenu için Radix kullan, custom implementation yapma.
typescriptexport function useToggle(initial = false): [boolean, () => void] { const [value, setValue] = useState(initial) const toggle = useCallback(() => setValue((v) => !v), []) return [value, toggle] } export function useDebounce<T>(value: T, delay: number): T { const [debounced, setDebounced] = useState(value) useEffect(() => { const id = setTimeout(() => setDebounced(value), delay) return () => clearTimeout(id) }, [value, delay]) return debounced }
React 18'de (mevcut proje) manuel memoization hâlâ geçerlidir:
typescriptconst sorted = useMemo(() => items.sort((a, b) => b.score - a.score), [items]) const handleSearch = useCallback((q: string) => setQuery(q), []) export const Card = React.memo<CardProps>(({ item }) => <div>{item.name}</div>)
Kural: Ölç, sonra optimize et — spekülatif memoization yasak.
typescriptconst HeavyComponent = lazy(() => import('./HeavyComponent')) <Suspense fallback={<Skeleton />}> <HeavyComponent /> </Suspense>
typescriptimport { useVirtualizer } from '@tanstack/react-virtual' const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 72, overscan: 5, })
react-error-boundary kütüphanesi kullan — class-based custom boundary yazma:
typescriptimport { ErrorBoundary } from 'react-error-boundary' <ErrorBoundary fallback={<ErrorFallback />}> <Feature /> </ErrorBoundary>
typescriptimport { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' const schema = z.object({ name: z.string().min(1, 'Ad zorunlu'), email: z.string().email('Geçersiz email'), role: z.enum(['admin', 'user']), }) type FormValues = z.infer<typeof schema> const form = useForm<FormValues>({ resolver: zodResolver(schema), defaultValues: { name: '', email: '', role: 'user' }, })
field.onChange vs setValue| Yöntem | Kullanım | | ---------------- | -------------------------------------------- | | field.onChange | Kullanıcı etkileşimi (onClick, onSelect) | | setValue | Programatik güncelleme (useEffect, dış veri) |
typescriptconst { field } = useController({ name: 'status', control }) const { setValue } = useFormContext() useEffect(() => { if (externalData) setValue('status', externalData.default) }, [externalData, setValue]) const handleUserSelect = (value: string) => field.onChange(value)
Sonsuz döngü tuzağı — asla yapma:
typescriptconst { field } = useController({ name: 'status' }) useEffect(() => { field.onChange(defaultValue) }, [field, defaultValue])
field her render'da yenilenir → effect tetikler → onChange → re-render → sonsuz döngü.
register vs useControllerNative input → register yeterli:
typescript<input {...register('name')} />
Third-party component (Radix Select, custom picker) → useController:
typescriptfunction RoleSelect({ control }: { control: Control<FormValues> }) { const { field, fieldState } = useController({ name: 'role', control }) return ( <Select onValueChange={field.onChange} value={field.value}> {fieldState.error && <span>{fieldState.error.message}</span>} </Select> ) }
typescriptconst { fields, append, remove } = useFieldArray({ control, name: 'items' }) {fields.map((field, index) => ( <div key={field.id}> <input {...register(`items.${index}.value`)} /> <button type='button' onClick={() => remove(index)}>Sil</button> </div> ))}
typescriptconst onSubmit = async (data: FormValues) => { try { await api.post('/endpoint', data) } catch { form.setError('root', { message: 'İşlem başarısız oldu. Tekrar deneyin.' }) } }
watch() parametresiz çağırma — watch('field') kullandefaultValues her zaman tanımla — tüm alanlar içinform.trigger(['field1', 'field2']) ile adım doğrulamaform.reset(newValues) — manuel setValue çağrıları değilstore/use-auth-store.ts — Auth state
store/use-subscription-store.ts — Subscription
store/use-app-config-store.ts — App configZustand yalnızca gerçek client state için: auth, subscription, app config, UI state (modal, drawer).
Kural: Server'dan gelen veri Zustand'a yazılmaz — React Query cache'inde kalır.
typescriptconst { data, isLoading, error } = useQuery({ queryKey: ['cv', userId], queryFn: () => api.get(`/cv/${userId}`), })
Kural: API response'larını Zustand'a kopyalama. Cross-contamination mimari borç yaratır.
typescriptimport { api } from '@/lib/api' const data = await api.get('/endpoint') import { serverApi } from '@/lib/server-api' const data = await serverApi.get('/endpoint') import { monitoredFetch } from '@/lib/monitoring/fetch'
Kural: fetch doğrudan kullanma.
lib/auth/session.ts — Tek kaynak: session doğrulama
lib/server/auth.ts — API route protection
lib/admin/auth.ts — Admin route protectionKural: Session doğrulama mantığını route handler'a yazma.
typescriptexport async function POST(request: Request) { const session = await requireAuth(request) // 1. Auth const body = await parseBody(request, schema) // 2. Validation const result = await featureService(body) // 3. Business logic return NextResponse.json(result) // 4. Response }
features/ats/server/
features/cv/server/
features/chat/server/
features/cover-letter/server/
features/calculator/server/
features/profile/server/
features/subscription/server/Shared altyapı: lib/server/ai/, lib/server/integrations/, lib/server/documents/
Kural: Feature servisleri god-service pattern'ına düşürülmez.
typescriptimport { useTranslation } from '@/i18n/client' const { t } = useTranslation() import { getTranslations } from 'next-intl/server' const t = await getTranslations('namespace')
npm run locales:check CI'da çalışır — parity zorunluCaching: React Query cache, Next.js ISR
Streaming: Suspense boundary ile progressive rendering
Code Split: Route-level (otomatik) + manuel lazy()
Memoization: useMemo/useCallback — gerçek sorun varsa
Bundle: next.config.mjs'teki optimizePackageImports listesi
Images: next/image — priority above-fold, blur placeholderPDF notu: pdfjs-dist, pdfkit, html2canvas+jspdf — dördüncü strateji ekleme.
requireAuth() ilk satırdatypescripttype ApiResponse<T> = { data: T; error?: string } const schema = z.object({ title: z.string().min(1) }) type FormData = z.infer<typeof schema>
any yasak — unknown + type guard kullan@ts-ignore yasak — @ts-expect-error + açıklamanoUncheckedIndexedAccess kapalı olsa bile)Proje şu an React 18. React 19'da gelecek değişiklikler:
| Mevcut (React 18) | React 19 | | ---------------------------- | ------------------------------------- | | forwardRef() wrapper | ref direkt prop | | Manuel useMemo/useCallback | React Compiler otomatik optimize eder | | Async useEffect pattern | use() hook ile promise okuma | | useReducer + action | useActionState |
Hazırlık kuralları:
forwardRef sarmalayıcılarını şimdi kaldırma — acil değiluseMemo/useCallback kaldırma — React Compiler aktif olmadan performansı bozaruse() hook React 18'de yok — geçiş sonrası kullanılabilirnpm run type-check geçmeden PR açılmaznpx eslint src temiz olmalınode scripts/check-locale-parity.mjs geçmeli.env güvenlik açığı — API key'ler repo'da. Rotate + Vercel env.reactStrictMode: false — Side effect tespiti devre dışı.ignoreDuringBuilds: true.no-explicit-any: warn — error'a çevrilmeli.Proje OPENROUTER_API_KEY kullanıyor. lib/server/ai/ altındaki shared AI infra üzerinden erişilmeli.
typescriptimport OpenAI from 'openai' const client = new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY, }) const completion = await client.chat.completions.create({ model: 'anthropic/claude-sonnet-4-5', messages: [{ role: 'user', content: prompt }], })
typescriptconst completion = await client.chat.completions.create({ model: 'anthropic/claude-sonnet-4-5', extra_body: { models: ['openai/gpt-4o', 'google/gemini-2.0-flash-exp:free'], }, messages: [{ role: 'user', content: prompt }], })
fetch doğrudan kullanma — lib/server/ai/ wrapper'ları kullan@anthropic-ai/sdk) ve OpenRouter aynı anda kullanılıyor — yeni AI feature için hangisini seçeceğini lib/server/ai/ dosyalarına bakarak belirlelib/monitoring/ ile telemetry'ye yazılmalımax_tokens her çağrıda set et — maliyet kontrolü içintypescriptclass AppError extends Error { constructor( message: string, public code: string, public statusCode: number = 500, public details?: Record<string, unknown>, ) { super(message) this.name = this.constructor.name } } class ValidationError extends AppError { constructor(message: string, details?: Record<string, unknown>) { super(message, 'VALIDATION_ERROR', 400, details) } } class NotFoundError extends AppError { constructor(resource: string, id: string) { super(`${resource} bulunamadı`, 'NOT_FOUND', 404, { resource, id }) } }
typescripttype Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E } function Ok<T>(value: T): Result<T, never> { return { ok: true, value } } function Err<E>(error: E): Result<never, E> { return { ok: false, error } }
catch (error) bloğu asla boş bırakma — log veya rethrowtry/catch zorunlu — stack trace client'a sızmasınlogger.error sadece gerçek hatalar için — validation errors logger.warnany ile catch etme: catch (error: unknown) + type guardtypescriptexport async function POST(request: Request) { try { const session = await requireAuth(request) const body = await parseBody(request, schema) const result = await featureService(body) return NextResponse.json(result) } catch (error) { if (error instanceof ValidationError) { return NextResponse.json({ error: error.message }, { status: 400 }) } if (error instanceof NotFoundError) { return NextResponse.json({ error: error.message }, { status: 404 }) } console.error('[API Error]', error) return NextResponse.json({ error: 'İşlem gerçekleştirilemedi' }, { status: 500 }) } }
Layer 1: Firestore kuralları → Firebase emulator ile test
Layer 2: API Route'ları → Jest + supertest
Layer 3: Component'ler → React Testing Library
Layer 4: E2E → PlaywrightAI çağrıları içeren feature'lar için:
typescriptjest.mock('@/lib/server/ai/anthropic', () => ({ generateCompletion: jest.fn().mockResolvedValue({ content: 'Mock AI yanıtı', usage: { input_tokens: 10, output_tokens: 20 }, }), }))
Mock'ları lib/server/ai/ wrapper seviyesinde yap — SDK seviyesinde değil. Bu sayede transport değişse bile test geçerli kalır.
typescriptdescribe('CvService', () => { it('kullanıcının CV verilerini döndürür', async () => { const mockUserId = 'test-user-123' const result = await cvService.getUserCvs(mockUserId) expect(result).toBeDefined() expect(Array.isArray(result)).toBe(true) }) it('yetkisiz erişimde hata fırlatır', async () => { await expect(cvService.getUserCvs('')).rejects.toThrow() }) })
bashnpm run type-check # tsc + locale parity npx eslint src # lint # jest --passWithNoTests # unit tests
Herhangi bir bug veya beklenmedik davranışta şu sırayı takip et:
git diff, yeni dependencyfeatures/{feature}/
components/ → UI Layer (Radix, shadcn)
hooks/ → Application Layer (React Query, Zustand)
server/ → Domain + Infrastructure (Firebase, AI)
types.ts → Domain Entities
lib/server/
ai/ → Shared AI infrastructure
integrations/ → External services
documents/ → PDF/document processingtypescript// PORT (interface) — lib/server/ai/types.ts interface AiCompletionPort { complete(prompt: string, options: CompletionOptions): Promise<CompletionResult> } // ADAPTER (implementation) — lib/server/ai/anthropic.ts class AnthropicAdapter implements AiCompletionPort { async complete(prompt: string, options: CompletionOptions) { return this.client.messages.create({ ... }) } } // ADAPTER (test) — lib/server/ai/__mocks__/anthropic.ts class MockAiAdapter implements AiCompletionPort { async complete() { return { content: 'mock response', usage: { ... } } } }
✅ CvGenerator, AtsScorer, ChatSessionManager, SalaryCalculator
❌ utils.ts, helpers.ts, common.ts, shared.ts, misc.tsHer modülün tek, açık bir sorumluluğu olmalı. Generic isimler yasak.
Her Server Action ve Route Handler'da auth kontrolü ilk satırda olmalı:
typescript'use server' export async function submitCv(formData: FormData) { const session = await requireAuth() // ← İLK SATIR, geçilmez const body = await parseBody(formData, schema) return cvService.create(session.userId, body) }
typescript// ✅ Doğru const cookieStore = await cookies() const headersList = await headers() const { id } = await params // ❌ Yanlış (Next.js 15'te hata verir) const cookieStore = cookies() const { id } = params
typescriptexport const config = { matcher: [ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', '/(api|trpc)(.*)', ], }
Protected route'lar matcher'da tanımlı olmalı. Auth mantığı middleware'den çıkarılmamalı.
Proje Zod kullanıyor (zod: ^3.25.20). v4'e geçildiğinde breaking change'ler:
typescript// ❌ Zod 3 (ESKİ) z.string().email() z.string().uuid() z.string().url() z.string().nonempty() z.object({ name: z.string() }).required_error('Zorunlu') // ✅ Zod 4 (YENİ) z.email() z.uuid() z.url() z.string().min(1) z.object({ name: z.string() }, { error: 'Zorunlu' })
typescript// ❌ v3: 'message' parametresi z.string().min(8, { message: 'En az 8 karakter' }) // ✅ v4: 'error' parametresi z.string().min(8, { error: 'En az 8 karakter' }) // v4 custom error map const schema = z.string({ error: (issue) => { if (issue.code === 'too_small') return 'Çok kısa' return 'Geçersiz değer' }, })
typescriptconst schema = z.object({ email: z.string().email('Geçersiz email'), password: z.string().min(8, 'En az 8 karakter'), role: z.enum(['admin', 'user']), }) type FormValues = z.infer<typeof schema>
typescript// app/dashboard/layout.tsx export default function DashboardLayout({ children, analytics, team, }: { children: React.ReactNode analytics: React.ReactNode team: React.ReactNode }) { return ( <div className='dashboard-grid'> <main>{children}</main> <aside>{analytics}</aside> <aside>{team}</aside> </div> ) } // app/dashboard/@analytics/page.tsx export default async function AnalyticsSlot() { const stats = await getAnalytics() return <AnalyticsChart data={stats} /> } // app/dashboard/@analytics/loading.tsx export default function AnalyticsLoading() { return <ChartSkeleton /> }
app/
├── @modal/
│ ├── (.)photos/[id]/page.tsx ← Intercept
│ └── default.tsx
├── photos/
│ └── [id]/page.tsx ← Full page
└── layout.tsxtypescript// app/@modal/(.)photos/[id]/page.tsx export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const photo = await getPhoto(id) return ( <Modal> <PhotoDetail photo={photo} /> </Modal> ) }
typescriptexport default async function ProductPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const product = await getProduct(id) // Blocking — hemen render return ( <div> <ProductHeader product={product} /> <Suspense fallback={<ReviewsSkeleton />}> <Reviews productId={id} /> {/* Yavaş API — stream */} </Suspense> <Suspense fallback={<RecommendationsSkeleton />}> <Recommendations productId={id} /> {/* ML tabanlı — stream */} </Suspense> </div> ) }
typescript// ✅ Next.js 15 — async searchParams export default async function Page({ searchParams, }: { searchParams: Promise<{ category?: string; page?: string }> }) { const params = await searchParams return <ProductList category={params.category} /> }
typescriptexport async function generateStaticParams() { const products = await db.product.findMany({ select: { slug: true } }) return products.map((p) => ({ slug: p.slug })) }
KESİNLİKLE: Failing test olmadan production kodu yazmaKod yazdıktan sonra test yazmak, testin doğru çalışıp çalışmadığını kanıtlamaz.
1. RED → Failing test yaz (tek davranış)
2. VERIFY → Testin beklenen nedenle fail ettiğini gör
3. GREEN → Testi geçirecek minimum kod yaz
4. VERIFY → Testin geçtiğini ve diğerlerinin kırılmadığını doğrula
5. REFACTOR → Temizle (test geçerken)
6. TEKRARtypescript// 1. Önce bug'ı reproduce eden test yaz it('boş email kabul etmez', async () => { const result = await submitForm({ email: '' }) expect(result.error).toBe('Email zorunlu') }) // 2. Testi çalıştır — fail etmeli // 3. Minimum fix yaz function submitForm(data: FormData) { if (!data.email?.trim()) { return { error: 'Email zorunlu' } } } // 4. Testi çalıştır — pass etmeli
❌ "Bir dene bakalım" yaklaşımı
❌ Test olmadan "manuel doğrularım"
❌ Birden fazla değişiklik aynı anda
❌ Root cause anlamadan fix önerisitypescript// ❌ Sequential — waterfall const user = await fetchUser(id) const orders = await fetchOrders(user.id) const profile = await fetchProfile(user.id) // ✅ Parallel — Promise.all const [user, orders] = await Promise.all([fetchUser(id), fetchOrders(id)])
typescript// ❌ Barrel import — tüm bundle'ı çeker import { Button, Input, Modal } from '@/components' // ✅ Direct import — tree-shaking çalışır import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input'
typescript// ❌ Ağır component her zaman yüklü import { HeavyChart } from './HeavyChart' // ✅ Dynamic import — lazy loading const HeavyChart = dynamic(() => import('./HeavyChart'), { ssr: false })
typescript// ❌ State'e subscribe ama sadece callback'te kullanıyor const count = useStore((s) => s.count) // Her değişimde re-render const increment = () => useStore.setState((s) => ({ count: s.count + 1 })) // ✅ Sadece action, state değil const increment = useStore((s) => s.increment) // Stabil referans
typescript// ❌ Her render'da yeniden oluşturuluyor export function Nav() { const LINKS = [{ href: '/cv', label: 'CV' }, { href: '/ats', label: 'ATS' }] return <nav>{LINKS.map(...)}</nav> } // ✅ Component dışında — bir kez oluşturuluyor const LINKS = [{ href: '/cv', label: 'CV' }, { href: '/ats', label: 'ATS' }] export function Nav() { return <nav>{LINKS.map(...)}</nav> }
typescript// ❌ id yokken fetch yapıyor const { data } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) }) // ✅ enabled flag ile koşullu const { data } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id), enabled: !!id, staleTime: 5 * 60 * 1000, })
typescript// store/slices/createCvSlice.ts import { StateCreator } from 'zustand' export interface CvSlice { cvList: Cv[] selectedCvId: string | null selectCv: (id: string) => void clearSelection: () => void } export const createCvSlice: StateCreator<CvSlice & AtsSlice, [], [], CvSlice> = (set) => ({ cvList: [], selectedCvId: null, selectCv: (id) => set({ selectedCvId: id }), clearSelection: () => set({ selectedCvId: null }), }) // store/index.ts import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { createCvSlice, CvSlice } from './slices/createCvSlice' import { createAtsSlice, AtsSlice } from './slices/createAtsSlice' type StoreState = CvSlice & AtsSlice export const useStore = create<StoreState>()( devtools( (...args) => ({ ...createCvSlice(...args), ...createAtsSlice(...args), }), { name: 'talent-architect-store' }, ), )
typescript// ❌ Tüm store'a subscribe — her değişimde re-render const { cvList, selectedCvId, selectCv } = useStore() // ✅ Sadece gerekli slice'a subscribe const cvList = useStore((s) => s.cvList) const selectCv = useStore((s) => s.selectCv)
typescript// Zustand: UI state (client-only) const useUIStore = create<UIState>((set) => ({ sidebarOpen: true, activeModal: null, toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })), })) // React Query: Server state function Dashboard() { const { sidebarOpen } = useUIStore() const { data: cvs } = useQuery({ queryKey: ['cvs', userId], queryFn: () => api.get(`/cv/${userId}`), }) // cvs asla Zustand'a kopyalanmaz }
typescript// ❌ Kontrol edilemez hale gelir <Button isPrimary isLarge isLoading hasIcon isDisabled showBadge /> // ❌ Her yeni variant yeni boolean ekler
typescript// ✅ Açık variant'lar — API okunabilir <PrimaryButton size='lg' isLoading icon={<Spinner />} /> <GhostButton size='sm' /> <DestructiveButton />
typescript// ✅ Context ile state paylaşımı <Accordion> <Accordion.Item id='cv'> <Accordion.Trigger>CV Yönetimi</Accordion.Trigger> <Accordion.Content> <CvList /> </Accordion.Content> </Accordion.Item> </Accordion>
typescript// ❌ renderX prop pattern <Card renderHeader={() => <h2>Başlık</h2>} renderFooter={() => <Button>Kaydet</Button>} /> // ✅ children/slot composition <Card> <Card.Header>Başlık</Card.Header> <Card.Body><CvContent /></Card.Body> <Card.Footer><Button>Kaydet</Button></Card.Footer> </Card>
typescript// ❌ React 18 — forwardRef zorunluydu const Input = forwardRef<HTMLInputElement, InputProps>( ({ label, ...props }, ref) => ( <input ref={ref} {...props} /> ) ) // ✅ React 19 — ref artık normal prop function Input({ label, ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) { return <input ref={ref} {...props} /> }
typescript// Provider state yönetimini kapsüller — dışarıya interface açar interface SubscriptionContext { state: SubscriptionState actions: { upgrade: (plan: Plan) => Promise<void> cancel: () => Promise<void> } meta: { isLoading: boolean; error: string | null } } // Context yalnızca interface'i bilir, implementasyonu değil const SubscriptionCtx = createContext<SubscriptionContext | null>(null) export function SubscriptionProvider({ children }: { children: React.ReactNode }) { // Zustand, useState, React Query — provider içinde kapsüllü const store = useSubscriptionStore() return ( <SubscriptionCtx.Provider value={store}> {children} </SubscriptionCtx.Provider> ) }
typescript// ❌ Her render'da yeniden oluşturuluyor function NavMenu() { const ITEMS = [ { href: '/cv', label: 'CV Builder' }, { href: '/ats', label: 'ATS Analizi' }, ] return <nav>{ITEMS.map(item => <NavLink key={item.href} {...item} />)}</nav> } // ✅ Component dışında — bir kez oluşturuluyor const NAV_ITEMS = [ { href: '/cv', label: 'CV Builder' }, { href: '/ats', label: 'ATS Analizi' }, ] function NavMenu() { return <nav>{NAV_ITEMS.map(item => <NavLink key={item.href} {...item} />)}</nav> }
typescript// ❌ && ile sıfır render sorunu {count && <Badge>{count}</Badge>} // count=0 ise "0" render eder // ✅ Ternary her zaman güvenli {count ? <Badge>{count}</Badge> : null} // ✅ Boolean explicit cast {!!count && <Badge>{count}</Badge>}
AI endpoint'lerinde çok fazla eş zamanlı istek UI'ı dondurabilir.
typescript// ❌ Sınırsız concurrent request — UI donabilir const analyses = await Promise.all(cvIds.map((id) => analyzeCV(id))) // ✅ Batched execution — max 3 concurrent async function executeBatched<T>( tasks: Array<() => Promise<T>>, concurrency = 3, ): Promise<Array<PromiseSettledResult<T>>> { const results: Array<PromiseSettledResult<T>> = [] for (let i = 0; i < tasks.length; i += concurrency) { const batch = tasks.slice(i, i + concurrency) const batchResults = await Promise.allSettled(batch.map((task) => task())) results.push(...batchResults) } return results } // Kullanım — CV batch analizi const tasks = cvIds.map((id) => () => analyzeCV(id)) const results = await executeBatched(tasks, 3) const successful = results .filter((r) => r.status === 'fulfilled') .map((r) => (r as PromiseFulfilledResult<CvAnalysis>).value)
typescript// Ağır işlem bitmeden UI'ı güncelleme function CvAnalysisButton({ cvId }: { cvId: string }) { const [isPending, startTransition] = useTransition() const handleAnalyze = () => { startTransition(async () => { await analyzeCV(cvId) // Yavaş AI çağrısı await refetchAnalysis() // Cache yenile }) } return ( <Button onClick={handleAnalyze} disabled={isPending}> {isPending ? 'Analiz ediliyor...' : 'ATS Analizi'} </Button> ) }
1. Code smell'leri tespit et:
- 50+ satır fonksiyon → bölümlere ayır
- 3+ nesting seviyesi → early return
- Tekrar eden kod blokları → extract function
- Generic isimler (utils, helper, misc) → domain specific isim ver
- Tek sorumluluktan fazlası → separate concerns
2. Risk değerlendirmesi:
- Bu kod test edilmiş mi? (değişiklik güvenli mi?)
- Kaç yer tarafından kullanılıyor?
- Breaking change yaratır mı?Faz 1: Davranışı koru
→ Önce test yaz (mevcut davranışı belgele)
→ Sonra refactor et
→ Test geçiyor mu? ✓
Faz 2: Temizle
→ İsimleri düzelt
→ Extract function/component
→ Remove duplication
Faz 3: Yeniden test et
→ Tüm testler geçiyor mu?
→ TypeScript error var mı?
→ i18n key'leri kontrol ettypescript// ❌ Generic naming — kim çağırıyor? export function handleData(input: any) { ... } export function processItem(item: unknown) { ... } // ✅ Domain-specific naming export function parseCvUploadResponse(raw: unknown): CvDocument { ... } export function calculateAtsScore(cv: CvDocument, job: JobDescription): AtsResult { ... }
typescript// ❌ Feature logic lib/server/ altında // lib/server/services/cvHelper.ts // ✅ Feature domain'inde // features/cv/server/cv.service.ts
typescript// Primitive obsession'ı önle — aynı tipte farklı domain değerleri karışmasın type Brand<K, T> = K & { __brand: T } type UserId = Brand<string, 'UserId'> type CvId = Brand<string, 'CvId'> type AtsScore = Brand<number, 'AtsScore'> // Yanlış parametre sırasını compile-time yakala function analyzeCV(cvId: CvId, userId: UserId): Promise<AtsScore> { ... } // Oluşturma const cvId = 'abc123' as CvId const score = 87 as AtsScore
typescript// Result type — exception yerine explicit error handling type Result<T, E = Error> = { success: true; data: T } | { success: false; error: E } async function parseCv(file: File): Promise<Result<CvDocument, 'INVALID_FORMAT' | 'TOO_LARGE'>> { if (file.size > MAX_SIZE) return { success: false, error: 'TOO_LARGE' } // ... return { success: true, data: parsedCv } } // Kullanım const result = await parseCv(file) if (!result.success) { // result.error'ın tipi: 'INVALID_FORMAT' | 'TOO_LARGE' handleError(result.error) } else { // result.data'nın tipi: CvDocument displayCv(result.data) }
typescript// ReturnType utility — var olan fonksiyonun dönüş tipini çıkar type CvAnalysisResult = Awaited<ReturnType<typeof analyzeCV>> // Infer ile derinlemesine çıkarım type UnwrapPromise<T> = T extends Promise<infer U> ? U : T type ElementType<T> = T extends (infer U)[] ? U : never // Template literal types type EventName = 'cv' | 'ats' | 'chat' type EventHandler = `on${Capitalize<EventName>}Change` // → 'onCvChange' | 'onAtsChange' | 'onChatChange'
typescript// Tüm property'leri optional yap (Partial dahili yapar bunu) type PartialCv = Partial<CvDocument> // Tüm property'leri readonly yap type ReadonlyCv = Readonly<CvDocument> // String property'leri filtrele type StringFields<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] } // StringFields<{ id: string; score: number; name: string }> // → { id: string; name: string } // Getter metodları üret type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }
json{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": false, "noImplicitOverride": true, "exactOptionalPropertyTypes": false, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "incremental": true } }
typescript// "The inferred type of X cannot be named" // → Tipi explicit export et export type CvAnalysis = Awaited<ReturnType<typeof analyzeCv>> // "Excessive stack depth comparing types" // → Type intersection yerine interface extends kullan // ❌ type Combined = TypeA & TypeB & TypeC // ✅ interface Combined extends TypeA, TypeB, TypeC {} // "Type instantiation is excessively deep" // → Recursion'ı sınırla type NestedArray<T, D extends number = 5> = D extends 0 ? T : T | NestedArray<T, [-1, 0, 1, 2, 3, 4][D]>[] // "Cannot find module" hataları // → tsconfig'de path alias ve baseUrl doğru mu? // → import style projeyle tutarlı mı? (@/ prefix)
typescripttype CvEvents = { 'cv:uploaded': { cvId: CvId; userId: UserId } 'cv:analyzed': { cvId: CvId; score: AtsScore } 'cv:error': { cvId: CvId; error: string } } class TypedEventEmitter<T extends Record<string, unknown>> { private listeners: { [K in keyof T]?: Array<(data: T[K]) => void> } = {} on<K extends keyof T>(event: K, cb: (data: T[K]) => void) { ;(this.listeners[event] ??= []).push(cb) } emit<K extends keyof T>(event: K, data: T[K]) { this.listeners[event]?.forEach((cb) => cb(data)) } } const cvEmitter = new TypedEventEmitter<CvEvents>() cvEmitter.on('cv:analyzed', ({ cvId, score }) => { // Tipler otomatik: cvId: CvId, score: AtsScore })
typescript// Mouse function handleClick(e: React.MouseEvent<HTMLButtonElement>) { e.currentTarget.disabled = true } // Form submit function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() const formData = new FormData(e.currentTarget) const email = formData.get('email') as string } // Input change function handleChange(e: React.ChangeEvent<HTMLInputElement>) { setValue(e.target.value) } // Keyboard function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { if (e.key === 'Enter') e.currentTarget.blur() } // Textarea function handleTextarea(e: React.ChangeEvent<HTMLTextAreaElement>) { setDescription(e.target.value) }
typescript// Tablo — keyof T ile column key type safety type Column<T> = { key: keyof T header: string render?: (value: T[keyof T], item: T) => React.ReactNode } type TableProps<T extends { id: string | number }> = { data: T[] columns: Column<T>[] } function Table<T extends { id: string | number }>({ data, columns }: TableProps<T>) { return ( <table> <thead> <tr>{columns.map(col => <th key={String(col.key)}>{col.header}</th>)}</tr> </thead> <tbody> {data.map(item => ( <tr key={item.id}> {columns.map(col => ( <td key={String(col.key)}> {col.render ? col.render(item[col.key], item) : String(item[col.key])} </td> ))} </tr> ))} </tbody> </table> ) } // Kullanım — T otomatik CvDocument'a infer edilir <Table<CvDocument> data={cvList} columns={[ { key: 'title', header: 'Başlık' }, { key: 'score', header: 'ATS Skoru', render: (v) => <ScoreBadge score={v as number} /> }, ]} />
typescript// Primitive tip — inference yeterli const [count, setCount] = useState(0) // Union/null — explicit gerekli const [user, setUser] = useState<User | null>(null) const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') // DOM ref — null başlangıç değeri zorunlu const inputRef = useRef<HTMLInputElement>(null) // inputRef.current?.focus() ← null check gerekli // Mutable değer — null değil const counterRef = useRef<number>(0) // counterRef.current++ ← doğrudan erişim
typescript// as const olmadan tip [boolean, () => void] yerine (boolean | (() => void))[] function useToggle(initial = false): [boolean, () => void] { const [value, setValue] = useState(initial) const toggle = useCallback(() => setValue((v) => !v), []) return [value, toggle] as const } // Kullanım const [isOpen, toggleOpen] = useToggle()
typescripttype CvState = { cvList: CvDocument[] selected: CvId | null status: 'idle' | 'loading' | 'error' } type CvAction = | { type: 'SET_LIST'; payload: CvDocument[] } | { type: 'SELECT'; payload: CvId } | { type: 'CLEAR_SELECTION' } | { type: 'SET_STATUS'; payload: CvState['status'] } function cvReducer(state: CvState, action: CvAction): CvState { switch (action.type) { case 'SET_LIST': return { ...state, cvList: action.payload } case 'SELECT': return { ...state, selected: action.payload } case 'CLEAR_SELECTION': return { ...state, selected: null } case 'SET_STATUS': return { ...state, status: action.payload } default: { // Exhaustive check — yeni action eklenirse compile error const _: never = action return state } } }
typescript// ✅ Zustand 5'te birden fazla alan için useShallow kullan import { useShallow } from 'zustand/react/shallow' function UserInfo() { const { name, email } = useUserStore( useShallow(state => ({ name: state.name, email: state.email })) ) return <div>{name} - {email}</div> } // ❌ Tüm store'u al — her state değişiminde re-render const store = useUserStore()
typescriptimport { create } from 'zustand' import { persist } from 'zustand/middleware' interface SettingsStore { theme: 'light' | 'dark' language: string setTheme: (theme: 'light' | 'dark') => void setLanguage: (language: string) => void } const useSettingsStore = create<SettingsStore>()( persist( (set) => ({ theme: 'light', language: 'tr', setTheme: (theme) => set({ theme }), setLanguage: (language) => set({ language }), }), { name: 'ta-settings-storage', // localStorage key }, ), )
typescriptimport { immer } from 'zustand/middleware/immer' interface CvStore { cvList: CvDocument[] addCv: (cv: CvDocument) => void updateCvTitle: (id: string, title: string) => void } const useCvStore = create<CvStore>()( immer((set) => ({ cvList: [], addCv: (cv) => set((state) => { state.cvList.push(cv) }), updateCvTitle: (id, title) => set((state) => { const cv = state.cvList.find((c) => c.id === id) if (cv) cv.title = title }), })), )
typescriptimport { devtools, persist } from 'zustand/middleware' const useStore = create<Store>()( devtools( persist( (set) => ({ /* store definition */ }), { name: 'ta-store' }, ), { name: 'TalentArchitectStore' }, ), )
typescript// Component dışında store'a doğrudan erişim const { user, setUser } = useAuthStore.getState() // Değişiklikleri dinle const unsubscribe = useAuthStore.subscribe((state) => console.log('Auth state changed:', state.user?.email)) // Cleanup unsubscribe()
typescript// ✅ getMock{X} factory fonksiyonları — DRY test data interface CvDocument { id: string userId: string title: string score: number createdAt: Date } function getMockCvDocument(overrides?: Partial<CvDocument>): CvDocument { return { id: 'cv-test-123', userId: 'user-test-456', title: 'Test CV', score: 75, createdAt: new Date('2024-01-01'), ...overrides, } } // Kullanım it('should display ATS score', () => { const cv = getMockCvDocument({ score: 90 }) render(<CvCard cv={cv} />) expect(screen.getByText('90')).toBeInTheDocument() })
typescriptimport { ComponentProps } from 'react' function getMockCvCardProps(overrides?: Partial<ComponentProps<typeof CvCard>>) { return { cv: getMockCvDocument(), onAnalyze: jest.fn(), onDelete: jest.fn(), isLoading: false, ...overrides, } }
typescript// AI servislerini wrapper seviyesinde mock'la jest.mock('@/lib/server/ai/anthropic', () => ({ generateCompletion: jest.fn().mockResolvedValue({ content: 'Mock AI yanıtı', usage: { input_tokens: 10, output_tokens: 20 }, }), })) // Test içinde değişken davranış const mockGenerate = jest.requireMock('@/lib/server/ai/anthropic').generateCompletion mockGenerate.mockResolvedValueOnce({ content: 'Hata durumu yanıtı' })
typescriptdescribe('CvAnalysisService', () => { beforeEach(() => { jest.clearAllMocks() }) describe('analyzeAts()', () => { it('should return score when CV and job description provided', async () => { // Arrange const cv = getMockCvDocument() const jobDesc = 'Senior TypeScript Developer...' // Act const result = await analyzeAts(cv, jobDesc) // Assert expect(result.score).toBeGreaterThan(0) expect(result.score).toBeLessThanOrEqual(100) }) it('should throw when CV is empty', async () => { const emptyCv = getMockCvDocument({ title: '' }) await expect(analyzeAts(emptyCv, 'job')).rejects.toThrow('CV içeriği boş') }) }) })
typescript// ❌ Mock'un çağrıldığını test etmek — gerçek davranışı test etmiyor expect(mockFetchCv).toHaveBeenCalled() // ✅ Gerçek çıktıyı test et expect(screen.getByText('CV başarıyla yüklendi')).toBeInTheDocument()
typescript// ❌ Barrel import — tüm bundle'ı çeker import { Button, Input, Modal, Card } from '@/components' // ✅ Direct import — tree-shaking import { Button } from '@/components/ui/button' // ❌ Her zaman yüklü import { HeavyChart } from './HeavyChart' // ✅ Lazy load — conditional const HeavyChart = dynamic(() => import('./HeavyChart'), { ssr: false })
typescript// ✅ Functional setState — stale closure önler setCount((prev) => prev + 1) // ✅ Lazy state init — pahalı hesaplama bir kez çalışır const [state, setState] = useState(() => expensiveInitialState()) // ✅ useTransition — non-urgent update için const [isPending, startTransition] = useTransition() startTransition(() => setFilteredResults(heavy())) // ✅ Module-level cache — her render'da yeniden hesaplamayı önle const cache = new Map<string, AtsResult>() function getCachedAtsScore(key: string) { if (cache.has(key)) return cache.get(key)! const result = computeExpensiveScore(key) cache.set(key, result) return result }
typescript// ❌ Sequential — waterfall oluşturur const cv = await fetchCv(id) const jobs = await fetchJobs(cv.userId) const stats = await fetchStats(cv.userId) // ✅ Parallel — Promise.all ile const [cv, jobs, stats] = await Promise.all([fetchCv(id), fetchJobs(userId), fetchStats(userId)])
typescript// ❌ Tekrarlanan array lookup — O(n) her seferinde const match = cvList.find((cv) => cv.id === targetId) // ✅ Map ile O(1) lookup const cvMap = new Map(cvList.map((cv) => [cv.id, cv])) const match = cvMap.get(targetId) // ✅ Immutable sort const sorted = cvList.toSorted((a, b) => b.score - a.score) // toSorted ile mutasyon yok
typescript// ✅ Sıralama: external → internal → relative import { z } from 'zod' // external import { NextResponse } from 'next/server' // external framework import { prisma } from '@/lib/db' // internal absolute import { requireAuth } from '@/lib/server/auth' // internal absolute import { CvDocument } from '../types' // relative // ✅ Type-only import — tree-shaking için import type { CvDocument } from './types' import type { Metadata } from 'next'
typescript// ❌ Default export — barrel export ile uyumlu değil export default function CvCard() { ... } // ✅ Named export — her zaman export function CvCard() { ... } export type { CvCardProps }
any — unknown + Type Guard Kullantypescript// ❌ any function parseApiResponse(data: any) { return data.score } // ✅ unknown + type guard function parseApiResponse(data: unknown): number { if ( typeof data === 'object' && data !== null && 'score' in data && typeof (data as { score: unknown }).score === 'number' ) { return (data as { score: number }).score } throw new Error('Invalid API response shape') }
satisfies ile Tip Doğrulamatypescript// ✅ satisfies — tipi kontrol et, literal inference'ı koru const apiConfig = { baseUrl: 'https://api.talent-architect.com', timeout: 5000, retries: 3, } satisfies Record<string, string | number> // apiConfig.baseUrl tipi: string (değil string | number)
typescript// ✅ Type-safe query key factory — Talent Architect pattern export const cvKeys = { all: ['cvs'] as const, lists: () => [...cvKeys.all, 'list'] as const, list: (filters: CvFilters) => [...cvKeys.lists(), filters] as const, details: () => [...cvKeys.all, 'detail'] as const, detail: (id: string) => [...cvKeys.details(), id] as const, } // Kullanım const { data } = useQuery({ queryKey: cvKeys.detail(cvId), queryFn: () => api.get(`/cv/${cvId}`), enabled: !!cvId, }) // Invalidation queryClient.invalidateQueries({ queryKey: cvKeys.lists() })
typescript// ✅ Typed, stacktrace korumalı error class export class CvServiceError extends Error { constructor( message: string, public readonly code: string, public readonly statusCode: number = 500, ) { super(message) this.name = 'CvServiceError' Error.captureStackTrace(this, this.constructor) } } // Kullanım throw new CvServiceError('CV bulunamadı', 'CV_NOT_FOUND', 404)
typescript// Object destructuring ile default function createCvConfig({ title = 'Yeni CV', template = 'modern', language = 'tr', ...rest }: Partial<CvConfig> = {}) { return { title, template, language, ...rest } } // Array destructuring const [first, second, ...remaining] = cvList const [primarySkill, ...otherSkills] = skills
typescript// ❌ Verbose null check zinciri const score = user && user.cv && user.cv.atsScore ? user.cv.atsScore : 0 // ✅ Modern — kısa ve güvenli const score = user?.cv?.atsScore ?? 0 // ✅ Method call ile const firstName = session?.user?.name?.split(' ').at(0) ?? 'Kullanıcı'
typescript// Tek geçişte filter + map + sort (chaining) const topCvs = cvList .filter((cv) => cv.score >= 70) .map((cv) => ({ ...cv, grade: cv.score >= 90 ? 'A' : 'B' })) .toSorted((a, b) => b.score - a.score) .slice(0, 5) // Object.groupBy ile gruplama (ES2024) const grouped = Object.groupBy(cvList, (cv) => cv.score >= 90 ? 'excellent' : cv.score >= 70 ? 'good' : 'needs-improvement', )
typescript// Object spread — nested update const updatedCv = { ...cv, metadata: { ...cv.metadata, updatedAt: new Date(), version: cv.metadata.version + 1, }, } // Array immutable operations const withNew = [...cvList, newCv] // ekle const withoutDeleted = cvList.filter((cv) => cv.id !== id) // sil const updated = cvList.map((cv) => (cv.id === id ? newCv : cv)) // güncelle
typescript// ✅ Promise.allSettled — kısmi hata toleransı const results = await Promise.allSettled([ analyzeAts(cv, jobDesc), generateCoverLetter(cv), fetchCompanyInfo(companyName), ]) const successful = results .filter((r): r is PromiseFulfilledResult<unknown> => r.status === 'fulfilled') .map((r) => r.value) const failed = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map((r) => r.reason) // ✅ Race condition önleme let latestRequestId = 0 async function searchJobs(query: string) { const requestId = ++latestRequestId const results = await fetchJobs(query) if (requestId !== latestRequestId) return // Eski request — yoksay setResults(results) }
typescript// WeakMap — DOM element veya nesneyle ilişkilendirilmiş veri const cvMetaCache = new WeakMap<CvDocument, CvMeta>() function getCvMeta(cv: CvDocument): CvMeta { if (cvMetaCache.has(cv)) return cvMetaCache.get(cv)! const meta = computeMeta(cv) cvMetaCache.set(cv, meta) return meta } // cv nesnesi garbage collect edilince cache otomatik temizlenir
Failing test sayısının fazla olduğu durumlarda sistematik yaklaşım:
bashnpm test -- --passWithNoTests 2>&1 | tee /tmp/test-failures.txt
Öncelik sırası:
bash# Hata tiplerini say grep -E "Error:|Cannot find" /tmp/test-failures.txt | sort | uniq -c | sort -rn
Her grup için:
1. Root cause'u belirle — git diff ile son değişiklikleri kontrol et
2. Minimum değişiklik yap — tek bir konuya odaklan
3. Sadece o grubun testlerini çalıştır
→ npm test -- --testPathPattern="specific-file"
→ jest -k "pattern"
4. Grup tamamen geçince → bir sonraki gruba geçtypescript// ❌ Birden fazla değişikliği aynı anda uygulama // → Hangi değişikliğin hangi testi geçirdiği belirsizleşir // ✅ Tek seferde bir grup — verify — sonraki
Tüm gruplar geçtikten sonra:
bashnpm test # Tam suite — regresyon yok mu kontrol et
bash# Vercel'de üç environment seviyesi vercel env add ANTHROPIC_API_KEY production vercel env add ANTHROPIC_API_KEY preview vercel env add ANTHROPIC_API_KEY development # ASLA client-side'a sızdırma ANTHROPIC_API_KEY=... # ✅ Sunucu-only NEXT_PUBLIC_SITE_URL=... # ✅ Public değer — istemci görebilir NEXT_PUBLIC_API_KEY=... # ❌ Secret kesinlikle NEXT_PUBLIC_ olmamalı
typescript// Edge Runtime — Tercih koşulları: // - Middleware (auth check, redirect, A/B test) // - Basit response transformation // - Coğrafi routing export const runtime = 'edge' // middleware.ts içinde // Serverless (Node.js) — Tercih koşulları: // - Firebase Admin SDK (Node.js gerektiriyor) // - pdfkit, pdfjs-dist // - Anthropic SDK // - Ağır CPU işlemleri // Talent Architect'te API route'lar için varsayılan: Serverless
typescript// next.config.mjs — Talent Architect mevcut konfigürasyon üzerine const nextConfig = { // Bundle analizi için (dev'de) // ANALYZE=true npm run build → bundle boyutunu gör // Server external packages — bundle'a dahil etme serverExternalPackages: ['pdfkit', 'pdfjs-dist'], // Optimized imports (mevcut listede olanlar) experimental: { optimizePackageImports: ['lucide-react', 'framer-motion', '@radix-ui/react-dialog'], }, }
| Risk | Seviye | Çözüm | | --------------------------------- | ------ | ---------------------------------------------------- | | NEXT_PUBLIC_ prefix'li secret | KRİTİK | Yalnızca gerçekten public değerler için kullan | | Preview deploy → production DB | YÜKSEK | Her environment için ayrı Firebase project | | Serverless function boyutu >50MB | YÜKSEK | serverExternalPackages ile bundle dışında tut | | Edge runtime'da Node.js API | YÜKSEK | Firebase Admin, pdfkit → Serverless kullan | | Function timeout (varsayılan 10s) | ORTA | AI çağrıları için maxDuration artır (Pro: 300s) | | Build'de env var okuma zamanı | ORTA | NEXT_PUBLIC_ build'de, diğerleri runtime'da okunur |
typescript// AI route'larda timeout artırma export const maxDuration = 60 // saniye — Pro plan: 300s
typescript// Ortama göre farklı davranış const isProduction = process.env.VERCEL_ENV === 'production' const isPreview = process.env.VERCEL_ENV === 'preview' // Preview'da monitoring'i hafiflet if (!isProduction) { console.log('Preview mode — monitoring disabled') }
typescript// ❌ Cached data varken spinner gösterir — gereksiz flash if (loading) return <Spinner /> // ✅ Yalnızca data yokken loading göster const { data, isLoading, error, refetch } = useQuery(...) if (error) return <ErrorState error={error} onRetry={refetch} /> if (isLoading && !data) return <CvListSkeleton /> if (!data?.items.length) return <EmptyState /> return <CvList items={data.items} />
Hata var mı?
→ Evet: Error state + retry butonu
→ Hayır: devam
Loading VE data yok mu?
→ Evet: Skeleton veya spinner
→ Hayır: devam
Data var mı?
→ Evet, dolu: İçeriği göster
→ Evet, boş: Empty state
→ Hayır: Loading (fallback)| Skeleton Kullan | Spinner Kullan | | ---------------------------------- | ------------------------------ | | Bilinen içerik şekli (liste, kart) | Bilinmeyen içerik şekli | | İlk sayfa yüklemesi | Modal aksiyonları | | CV listesi, profil kartı | Buton submit, inline operasyon |
typescriptinterface ErrorStateProps { error: Error onRetry?: () => void title?: string } function ErrorState({ error, onRetry, title }: ErrorStateProps) { return ( <div role='alert' className='flex flex-col items-center gap-3 py-8'> <AlertCircle className='text-destructive' size={32} /> <p className='font-medium'>{title ?? 'Bir şeyler ters gitti'}</p> <p className='text-sm text-muted-foreground'>{error.message}</p> {onRetry && ( <Button variant='outline' onClick={onRetry}> Tekrar Dene </Button> )} </div> ) }
typescript// Her liste için zorunlu — hiçbir zaman atlanmaz function CvList({ items }: { items: CvDocument[] }) { if (!items.length) { return ( <EmptyState icon={<FileText size={40} className='text-muted-foreground' />} title='Henüz CV yok' description='İlk CV\'ni oluşturmak için aşağıdaki butona tıkla.' action={{ label: 'CV Oluştur', onClick: handleCreate }} /> ) } return <ul>{items.map(cv => <CvCard key={cv.id} cv={cv} />)}</ul> }
typescript// ❌ Çift submit riski — kullanıcı birden fazla kez tıklayabilir <Button onClick={handleSubmit}>Analizi Başlat</Button> // ✅ Loading sırasında disabled + görsel geri bildirim <Button onClick={handleSubmit} disabled={isSubmitting} > {isSubmitting ? ( <><Loader2 className='animate-spin mr-2' size={16} /> Analiz ediliyor...</> ) : ( 'Analizi Başlat' )} </Button>
typescript// React Query ile optimistic update const mutation = useMutation({ mutationFn: (cvId: string) => api.delete(`/cv/${cvId}`), onMutate: async (cvId) => { await queryClient.cancelQueries({ queryKey: cvKeys.lists() }) const previousCvs = queryClient.getQueryData(cvKeys.lists()) // UI'ı anında güncelle queryClient.setQueryData(cvKeys.lists(), (old: CvDocument[]) => old.filter((cv) => cv.id !== cvId)) return { previousCvs } }, onError: (_err, _cvId, context) => { // Hata durumunda geri al queryClient.setQueryData(cvKeys.lists(), context?.previousCvs) toast.error('CV silinemedi. Tekrar deneyin.') }, onSettled: () => { queryClient.invalidateQueries({ queryKey: cvKeys.lists() }) }, })
typescript// ❌ Kullanıcı hiçbir şey görmez onError: (error) => { console.log(error) } // ✅ Her zaman kullanıcıya bildir onError: (error) => { console.error('[CvDelete]', error) toast.error('CV silinemedi. Lütfen tekrar deneyin.') }
Other measured skills in the registry, with their headline benchmark lift.