İçeriğe geç
Sedat Demir
Geri dön

TanStack Start ile Type-Safe Full-Stack React Uygulamaları Geliştirme

TanStack Start ile Type-Safe Full-Stack React Uygulamaları Geliştirme

React ekosistemi her geçen gün daha güçlü araçlarla zenginleşiyor. Next.js ve Remix gibi framework'lerin hâkim olduğu full-stack React dünyasında, TanStack Start taze bir soluk olarak karşımıza çıkıyor. Tanner Linsley'nin TanStack ailesiyle (Query, Router, Table, Form) zaten kanıtlanmış olan "type-safety first" felsefesini full-stack bir framework'e taşıyan TanStack Start, 2024'ün en heyecan verici projelerinden biri haline geldi.

Bu yazıda TanStack Start'ın ne olduğunu, neden önemli olduğunu ve nasıl kullanılacağını kapsamlı bir şekilde ele alacağız.


TanStack Start Nedir?

TanStack Start, TanStack Router üzerine inşa edilmiş, full-stack React uygulamaları geliştirmek için tasarlanmış bir meta-framework'tür. Vite üzerinde çalışır ve Nitro (Vinxi) sunucu altyapısını kullanarak hem istemci hem de sunucu tarafında uçtan uca tip güvenliği sunar.

Temel Özellikleri


Neden TanStack Start?

1. Gerçek Anlamda Type-Safety

Next.js'de bir API route yazıp istemciden fetch ile çağırdığınızda, istek ve yanıt tipleri arasında bir "boşluk" oluşur. tRPC bu sorunu çözmek için doğmuştu. TanStack Start ise bu kavramı framework seviyesine entegre ediyor — ek bir kütüphane gerekmeden.

2. TanStack Router'ın Gücü

TanStack Router, React ekosistemindeki en güçlü type-safe router'dır. URL parametreleri, search parametreleri, loader data'ları — her şey TypeScript ile tam olarak tiplenmiştir. TanStack Start bu temeli genişleterek sunucu tarafına taşır.

3. Framework Agnostik Sunucu Altyapısı

Vinxi/Nitro sayesinde tek bir kod tabanıyla birden fazla platforma deploy edebilirsiniz. Serverless, edge, ya da geleneksel Node.js sunucu — seçim sizin.


Proje Kurulumu

TanStack Start ile yeni bir proje oluşturmak oldukça kolaydır:

npx create-tsrouter-app@latest my-app --template file-router --add-ons tanstack-query start
cd my-app
npm install
npm run dev

Alternatif olarak mevcut bir projeye manuel kurulum yapabilirsiniz:

npm install @tanstack/react-start @tanstack/react-router vinxi

Proje yapınız şuna benzer olacaktır:

my-app/
├── app/
│   ├── routes/
│   │   ├── __root.tsx
│   │   ├── index.tsx
│   │   └── posts/
│   │       ├── index.tsx
│   │       └── $postId.tsx
│   ├── client.tsx
│   ├── router.tsx
│   ├── routeTree.gen.ts
│   └── ssr.tsx
├── app.config.ts
├── package.json
└── tsconfig.json

Temel Konfigürasyon

app.config.ts dosyası, TanStack Start'ın kalbidir:

// app.config.ts
import { defineConfig } from '@tanstack/react-start/config'
import tsConfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  vite: {
    plugins: [
      tsConfigPaths({
        projects: ['./tsconfig.json'],
      }),
    ],
  },
})

Router konfigürasyonu:

// app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}

File-Based Routing ile Sayfalar Oluşturma

TanStack Start, dosya sistemi tabanlı routing kullanır. app/routes/ dizinindeki dosyalar otomatik olarak route'lara dönüştürülür.

Root Layout

// app/routes/__root.tsx
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from '@tanstack/react-router'

export const Route = createRootRoute({
  head: () => ({
    meta: [
      { charSet: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { title: 'TanStack Start Uygulamam' },
    ],
  }),
  component: () => (
    <html lang="tr">
      <head>
        <HeadContent />
      </head>
      <body>
        <nav>
          <a href="/">Ana Sayfa</a>
          <a href="/posts">Yazılar</a>
        </nav>
        <Outlet />
        <Scripts />
      </body>
    </html>
  ),
})

Dinamik Route'lar ve Loader'lar

TanStack Start'ın en güçlü yanlarından biri, loader fonksiyonlarının tamamen type-safe olmasıdır:

// app/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { getPost } from '~/server/posts'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    // params.postId otomatik olarak string tipindedir
    const post = await getPost(params.postId)
    return { post }
  },
  component: PostDetail,
})

function PostDetail() {
  const { post } = Route.useLoaderData()
  // post tipi otomatik olarak çıkarılır — herhangi bir cast gerekmez!

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <span>Yazar: {post.author}</span>
    </article>
  )
}

Buradaki Route.useLoaderData() çağrısı, loader fonksiyonunun dönüş tipini otomatik olarak çıkarır. Herhangi bir generic belirtmenize gerek yok.


Server Functions: Full-Stack Sihir

TanStack Start'ın en devrimci özelliği Server Functions'dır. createServerFn ile oluşturduğunuz fonksiyonlar sunucuda çalışır ama istemciden type-safe bir şekilde çağrılabilir.

Temel Server Function

// app/server/posts.ts
import { createServerFn } from '@tanstack/react-start'
import { db } from '~/lib/database'

export const getPosts = createServerFn({ method: 'GET' })
  .handler(async () => {
    const posts = await db.post.findMany({
      orderBy: { createdAt: 'desc' },
    })
    return posts
  })

export const getPost = createServerFn({ method: 'GET' })
  .validator((postId: string) => postId)
  .handler(async ({ data: postId }) => {
    const post = await db.post.findUnique({
      where: { id: postId },
    })

    if (!post) {
      throw new Error('Post bulunamadı')
    }

    return post
  })

Validasyon ile Server Functions

Zod veya Valibot gibi şema kütüphaneleriyle güçlü validasyon ekleyebilirsiniz:

// app/server/posts.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

const CreatePostSchema = z.object({
  title: z.string().min(3, 'Başlık en az 3 karakter olmalı'),
  content: z.string().min(10, 'İçerik en az 10 karakter olmalı'),
  categoryId: z.string().uuid(),
})

export const createPost = createServerFn({ method: 'POST' })
  .validator((data: z.infer<typeof CreatePostSchema>) =>
    CreatePostSchema.parse(data)
  )
  .handler(async ({ data }) => {
    // data artık tamamen type-safe ve validate edilmiş
    const post = await db.post.create({
      data: {
        title: data.title,
        content: data.content,
        categoryId: data.categoryId,
      },
    })
    return post
  })

İstemciden Server Function Çağırma

// app/routes/posts/new.tsx
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
import { createPost } from '~/server/posts'

export const Route = createFileRoute('/posts/new')({
  component: NewPost,
})

function NewPost() {
  const navigate = useNavigate()
  const [error, setError] = useState<string | null>(null)

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)

    try {
      const post = await createPost({
        data: {
          title: formData.get('title') as string,
          content: formData.get('content') as string,
          categoryId: formData.get('categoryId') as string,
        },
      })
      // post tipi otomatik çıkarılır
      navigate({ to: '/posts/$postId', params: { postId: post.id } })
    } catch (err) {
      setError('Yazı oluşturulurken bir hata oluştu')
    }
  }

  return (
    <div>
      <h1>Yeni Yazı Oluştur</h1>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <input name="title" placeholder="Başlık" required />
        <textarea name="content" placeholder="İçerik" required />
        <input name="categoryId" placeholder="Kategori ID" required />
        <button type="submit">Yayınla</button>
      </form>
    </div>
  )
}

TanStack Query Entegrasyonu

TanStack Start, TanStack Query ile mükemmel çalışır. Server function'larınızı Query ile birleştirerek cache, refetch ve optimistic update gibi özelliklerden faydalanabilirsiniz:

// app/routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery, queryOptions } from '@tanstack/react-query'
import { getPosts } from '~/server/posts'

const postsQueryOptions = queryOptions({
  queryKey: ['posts'],
  queryFn: () => getPosts(),
})

export const Route = createFileRoute('/posts/')({
  loader: async ({ context }) => {
    // SSR sırasında veriyi prefetch et
    await context.queryClient.ensureQueryData(postsQueryOptions)
  },
  component: PostList,
})

function PostList() {
  const { data: posts } = useSuspenseQuery(postsQueryOptions)

  return (
    <div>
      <h1>Tüm Yazılar</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={`/posts/${post.id}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </div>
  )
}

Search Params ile Type-Safe Filtreleme

TanStack Router'ın en güçlü özelliklerinden biri, search (query) parametrelerinin type-safe olmasıdır:

// app/routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const PostSearchSchema = z.object({
  page: z.number().optional().default(1),
  search: z.string().optional().default(''),
  category: z.enum(['tech', 'life', 'science']).optional(),
  sortBy: z.enum(['date', 'title', 'views']).optional().default('date'),
})

type PostSearch = z.infer<typeof PostSearchSchema>

export const Route = createFileRoute('/posts/')({
  validateSearch: (search) => PostSearchSchema.parse(search),
  component: PostList,
})

function PostList() {
  const { page, search, category, sortBy } = Route.useSearch()
  // Tüm değerler tam olarak tiplenmiştir!

  const navigate = Route.useNavigate()

  return (
    <div>
      <input
        value={search}
        onChange={(e) =>
          navigate({
            search: (prev) => ({ ...prev, search: e.target.value, page: 1 }),
          })
        }
        placeholder="Yazılarda ara..."
      />

      <select
        value={sortBy}
        onChange={(e) =>
          navigate({
            search: (prev) => ({
              ...prev,
              sortBy: e.target.value as PostSearch['sortBy'],
            }),
          })
        }
      >
        <option value="date">Tarihe Göre</option>
        <option value="title">Başlığa Göre</option>
        <option value="views">Görüntülenmeye Göre</option>
      </select>

      <p>Sayfa: {page} | Sıralama: {sortBy}</p>
    </div>
  )
}

Middleware ve Authentication

Server function'lara middleware ekleyerek authentication gibi çapraz işlemleri zarif bir şekilde yönetebilirsiniz:

// app/middleware/auth.ts
import { createMiddleware } from '@tanstack/react-start'
import { getSession } from '~/lib/auth'

export const authMiddleware = createMiddleware().server(
  async ({ next }) => {
    const session = await getSession()

    if (!session?.user) {
      throw new Error('Yetkilendirme hatası: Giriş yapmalısınız')
    }

    return next({ context: { user: session.user } })
  }
)
// app/server/protected.ts
import { createServerFn } from '@tanstack/react-start'
import { authMiddleware } from '~/middleware/auth'

export const getProfile = createServerFn({ method: 'GET' })
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    // context.user type-safe olarak erişilebilir!
    const profile = await db.user.findUnique({
      where: { id: context.user.id },
    })
    return profile
  })

Deploy Seçenekleri

TanStack Start, Vinxi/Nitro altyapısı sayesinde birçok platforma deploy edilebilir:

Platform Preset Komut
Node.js node-server npm run build && node .output/server/index.mjs
Vercel vercel Otomatik algılanır
Netlify netlify Otomatik algılanır
Cloudflare Pages cloudflare-pages wrangler pages deploy
Bun bun bun .output/server/index.mjs

TanStack Start vs Next.js vs Remix

Özellik TanStack Start Next.js Remix
Type-safe routing ✅ Dahili ❌ Manuel ❌ Manuel
Type-safe server functions ✅ Dahili ⚠️ Server Actions (kısıtlı) ⚠️ Actions
Search params type-safety ✅ Dahili
Vite tabanlı ⚠️ Turbopack
Streaming SSR
Olgunluk seviyesi 🟡 Beta 🟢 Stabil 🟢 Stabil

Sonuç

TanStack Start, React ekosisteminde type-safety'yi birinci sınıf vatandaş yapan nadir framework'lerden biri. Server Functions sayesinde ayrı API katmanı yazmaya gerek kalmadan, istemciden sunucuya tam tip güvenliği elde ediyorsunuz. TanStack Router'ın sunduğu type-safe URL parametreleri ve search params desteği, uygulamanızın her katmanında TypeScript'in gücünden tam olarak yararlanmanızı sağlıyor.

Öne çıkan avantajları özetleyecek olursak:

Henüz beta aşamasında olsa da TanStack Start, özellikle TypeScript odaklı takımlar için ciddi bir Next.js alternatifi olmaya aday. Eğer projelerinizde tip güvenliğine büyük önem veriyorsanız, TanStack Start'ı kesinlikle değerlendirmenizi öneriyorum.


Share this post on:

Sonraki Yazı
useEffectEvent Hook: useEffect Dependency Array Sorununa Köklü Çözüm