· Admin

Laravel Wayfinder: Type-Safe Route'lar ve Form'lar

Laravel Wayfinder, PHP controller'larindan otomatik TypeScript kodu ureten resmi bir paket. Inertia.js + Vue veya React kullaniyorsan, route isimlerini string olarak yazmak yerine type-safe fonksiyonlarla cagirabilirsin. Ziggy'nin yerine geldi ve Laravel starter kit'lerinde varsayilan olarak kullaniliyor.

Sorun Ne?

Klasik yontemle Inertia'da route kullanirken:

// Ziggy ile (eski yontem)
import { route } from 'ziggy-js';

router.get(route('posts.show', { post: 1 }));

// veya dogrudan string
router.get('/posts/1');

Sorunlar:

  • Route ismi yanlis yazarsan runtime'da patlar (compile-time hata yok)
  • Route parametrelerini unutabilirsin
  • Controller metodu ne aliyor, ne donduruyor bilmiyorsun
  • Form request'teki validation kurallari frontend'de yok

Wayfinder Nedir?

Wayfinder, PHP kodunu analiz edip TypeScript dosyalari uretiyor. Her controller metodu icin bir fonksiyon, her form request icin bir tip olusturuyor.

Kurulum

composer require laravel/wayfinder
npm install @laravel/wayfinder

Vite Eklentisi

// vite.config.ts
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { wayfinder } from '@laravel/wayfinder/vite';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/js/app.ts'],
            refresh: true,
        }),
        wayfinder(),
    ],
});

TypeScript Uretimi

Vite dev server'i baslattiginda veya build aldiginda Wayfinder otomatik olarak TypeScript dosyalarini uretir:

npm run dev
# veya
npm run build

Uretilen dosyalar resources/js/actions/ dizinine yazilir (varsayilan).

Uretilen Kod Ornekleri

Diyelim su controller'in var:

// app/Http/Controllers/PostController.php
class PostController extends Controller
{
    public function index(): Response
    {
        return Inertia::render('Posts/Index', [
            'posts' => Post::latest()->paginate(15),
        ]);
    }

    public function show(Post $post): Response
    {
        return Inertia::render('Posts/Show', [
            'post' => $post->load('comments'),
        ]);
    }

    public function store(StorePostRequest $request): RedirectResponse
    {
        Post::create($request->validated());

        return redirect()->route('posts.index');
    }

    public function update(UpdatePostRequest $request, Post $post): RedirectResponse
    {
        $post->update($request->validated());

        return redirect()->route('posts.show', $post);
    }

    public function destroy(Post $post): RedirectResponse
    {
        $post->delete();

        return redirect()->route('posts.index');
    }
}

Wayfinder su TypeScript kodunu uretir:

// resources/js/actions/PostController.ts (otomatik uretilmis)
import { route, type RoutableAction } from '@laravel/wayfinder';

export const index: RoutableAction = {
    url: '/posts',
    method: 'get',
};

export const show = (args: { post: number | string }): RoutableAction => ({
    url: `/posts/${args.post}`,
    method: 'get',
});

export const store: RoutableAction = {
    url: '/posts',
    method: 'post',
};

export const update = (args: { post: number | string }): RoutableAction => ({
    url: `/posts/${args.post}`,
    method: 'put',
});

export const destroy = (args: { post: number | string }): RoutableAction => ({
    url: `/posts/${args.post}`,
    method: 'delete',
});

Vue ile Kullanim

Link Olusturma

<script setup lang="ts">
import { index, show } from '@/actions/PostController';
import { Link } from '@inertiajs/vue3';
</script>

<template>
    <!-- Liste sayfasina link -->
    <Link :href="index.url">
        Tum Yazilar
    </Link>

    <!-- Detay sayfasina link -->
    <Link :href="show({ post: post.id }).url">
        {{ post.title }}
    </Link>
</template>

Form Gonderme

<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { store } from '@/actions/PostController';

const form = useForm({
    title: '',
    body: '',
    published: false,
});

function submit() {
    form.submit(store);
    // veya
    form.post(store.url);
}
</script>

<template>
    <form @submit.prevent="submit">
        <input v-model="form.title" type="text" />
        <textarea v-model="form.body"></textarea>
        <label>
            <input v-model="form.published" type="checkbox" />
            Yayinla
        </label>
        <button type="submit" :disabled="form.processing">
            Kaydet
        </button>
    </form>
</template>

Method Spoofing

PUT ve DELETE istekleri HTML form'larinda desteklenmiyor. Inertia bunu otomatik hallediyor ama Wayfinder ile daha da temiz:

<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { update, destroy } from '@/actions/PostController';

const props = defineProps<{
    post: { id: number; title: string; body: string };
}>();

const form = useForm({
    title: props.post.title,
    body: props.post.body,
});

function submitUpdate() {
    // method: 'put' otomatik olarak eklenir
    form.submit(update({ post: props.post.id }));
}

function submitDelete() {
    if (confirm('Silmek istediginize emin misiniz?')) {
        // method: 'delete' otomatik
        form.submit(destroy({ post: props.post.id }));
    }
}
</script>

React ile Kullanim

import { useForm, Link } from '@inertiajs/react';
import { index, show, store } from '@/actions/PostController';

function PostCreate() {
    const { data, setData, submit, processing, errors } = useForm({
        title: '',
        body: '',
    });

    function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        submit(store);
    }

    return (
        <form onSubmit={handleSubmit}>
            <input
                value={data.title}
                onChange={e => setData('title', e.target.value)}
            />
            {errors.title && <span>{errors.title}</span>}

            <textarea
                value={data.body}
                onChange={e => setData('body', e.target.value)}
            />
            {errors.body && <span>{errors.body}</span>}

            <button type="submit" disabled={processing}>
                Kaydet
            </button>
        </form>
    );
}

Form Request Tip Uretimi

Wayfinder, Form Request siniflarindan TypeScript tipleri de uretebilir:

// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'body' => ['required', 'string', 'min:50'],
            'category_id' => ['required', 'exists:categories,id'],
            'tags' => ['sometimes', 'array'],
            'tags.*' => ['exists:tags,id'],
            'published_at' => ['nullable', 'date'],
        ];
    }
}

Uretilen tip:

// Otomatik uretilmis
export interface StorePostData {
    title: string;
    body: string;
    category_id: number;
    tags?: number[];
    published_at?: string | null;
}

Artik form verisini tip-guvenli sekilde kullanabilirsin:

<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { store } from '@/actions/PostController';
import type { StorePostData } from '@/types/requests';

const form = useForm<StorePostData>({
    title: '',
    body: '',
    category_id: 0,
    tags: [],
    published_at: null,
});
</script>

Model Tipleri

Wayfinder, Eloquent modellerinden de TypeScript interface'leri uretebilir:

// app/Models/Post.php
class Post extends Model
{
    protected $casts = [
        'published_at' => 'datetime',
        'is_featured' => 'boolean',
        'metadata' => 'array',
        'view_count' => 'integer',
    ];
}

Uretilen:

export interface Post {
    id: number;
    title: string;
    body: string;
    slug: string;
    published_at: string | null;
    is_featured: boolean;
    metadata: Record<string, unknown>;
    view_count: number;
    created_at: string;
    updated_at: string;
}

PHP Enum Destegi

PHP 8.1 enum'lari da TypeScript'e cevriliyor:

// app/Enums/PostStatus.php
enum PostStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}

Uretilen:

export enum PostStatus {
    Draft = 'draft',
    Published = 'published',
    Archived = 'archived',
}

Vue'da kullanim:

<script setup lang="ts">
import { PostStatus } from '@/types/enums';

const statusOptions = Object.entries(PostStatus).map(([key, value]) => ({
    label: key,
    value: value,
}));
</script>

<template>
    <select v-model="form.status">
        <option v-for="opt in statusOptions" :key="opt.value" :value="opt.value">
            {{ opt.label }}
        </option>
    </select>
</template>

Ziggy ile Karsilastirma

Ozellik Ziggy Wayfinder
Route generation Runtime (JS objesi) Build-time (TypeScript)
Type safety Yok (string-based) Tam
Bundle size Route listesi client'a gider Sadece kullanilan route'lar
Form request tipleri Yok Var
Model tipleri Yok Var
Enum destegi Yok Var
Method spoofing Manuel Otomatik
Inertia entegrasyonu Ekstra is gerekir Native
Tree-shaking Zor Kolay
Starter kit'lerde Eski kit'ler Yeni kit'ler (2025+)

Bundle Size Farki

Ziggy tum route listesini JavaScript bundle'ina ekler. 100 route'un varsa o 100 route'un hepsi client'a gider. Wayfinder ise sadece import ettigin route'lari bundle'a ekler (tree-shaking).

// Ziggy - Tum route'lar yuklenir
import { route } from 'ziggy-js';
route('posts.show', { post: 1 }); // Tum route listesi bellekte

// Wayfinder - Sadece PostController yuklenir
import { show } from '@/actions/PostController';
show({ post: 1 }); // Sadece bu action bellekte

Konfigurasyon

// config/wayfinder.php
return [
    // TypeScript dosyalarinin uretilecegi dizin
    'output' => resource_path('js/actions'),

    // Hangi controller'lar dahil edilecek
    'include' => [
        'App\\Http\\Controllers\\*',
    ],

    // Hangi controller'lar haric tutulacak
    'exclude' => [
        'App\\Http\\Controllers\\Admin\\*', // Admin route'larini haric tut
    ],

    // Model tip uretimi
    'models' => true,

    // Enum tip uretimi
    'enums' => true,

    // Form request tip uretimi
    'form_requests' => true,
];

Ziggy'den Wayfinder'a Gecis

1. Paketleri Degistir

# Ziggy'yi kaldir
composer remove tightenco/ziggy
npm uninstall ziggy-js

# Wayfinder'i yukle
composer require laravel/wayfinder
npm install @laravel/wayfinder

2. Vite Config'i Guncelle

// Ziggy eklentisini cikar, Wayfinder ekle
import { wayfinder } from '@laravel/wayfinder/vite';

export default defineConfig({
    plugins: [
        laravel({ ... }),
        wayfinder(),
    ],
});

3. Import'lari Guncelle

// Eskisi (Ziggy)
import { route } from 'ziggy-js';
const url = route('posts.show', { post: 1 });

// Yenisi (Wayfinder)
import { show } from '@/actions/PostController';
const action = show({ post: 1 });
// action.url = '/posts/1'
// action.method = 'get'

4. Link'leri Guncelle

<!-- Eskisi -->
<Link :href="route('posts.show', { post: post.id })">

<!-- Yenisi -->
<Link :href="show({ post: post.id }).url">

5. Form'lari Guncelle

<!-- Eskisi -->
<script setup>
const form = useForm({ ... });
form.post(route('posts.store'));
</script>

<!-- Yenisi -->
<script setup>
import { store } from '@/actions/PostController';
const form = useForm({ ... });
form.submit(store);
</script>

Pratik Ipuclari

1. IDE Otomatik Tamamlama

TypeScript dosyalari uretildigi icin IDE'de tam otomatik tamamlama calisir. PostController. yazdiginda tum action'lari gorebilirsin.

2. Refactoring Guvenligi

Controller'da bir metodu yeniden adlandirdigin veya sildigin an, TypeScript tarafinda hata alirsin. Bu, kotu route referanslarini deploy'dan once yakalar.

3. Nested Route'lar

// routes/web.php
Route::apiResource('posts.comments', CommentController::class);
// Uretilen
import { store } from '@/actions/CommentController';

// /posts/5/comments'a POST
form.submit(store({ post: 5 }));

4. Route Parametreleri ve Query String

import { index } from '@/actions/PostController';

// Sadece URL
const url = index.url; // '/posts'

// Query string ekle
const urlWithQuery = `${index.url}?page=2&search=laravel`;

Sonuc

Wayfinder, Inertia.js kullanan Laravel projelerinde buyuk bir iyilestirme. String-based route isimleri yerine type-safe fonksiyonlar, otomatik method spoofing, form request tipleri ve model tipleri -- hepsi compile-time'da garanti altinda.

Eger yeni bir Inertia projesi basliyorsan, Wayfinder ile basla. Mevcut Ziggy projeni de kademeli olarak gecis yapabilirsin -- ikisi bir arada da calisabilir gecis surecinde.

TypeScript kullaniyorsan ve Laravel backend'in varsa, Wayfinder'i denemeni kesinlikle tavsiye ederim. O "route ismi yanlis mi yazdim acaba" endisesinden kurtulmak buyuk bir rahatlama.