· Admin

Laravel ile RESTful API Gelistirme: Best Practices

Laravel ile API gelistiriyorsan, bazi kaliplara uymak projenin omrunu uzatir. Bu yazida REST API gelistirirken uyguladigim best practice'leri, kod ornekleriyle paylasiyorum.

1. Resource Controller Kullan

Laravel'in resource controller'lari REST convention'ina birebir uyuyor. Her seferinde route tanimlamak yerine tek satirda halledersin.

php artisan make:controller Api/PostController --api --model=Post

--api flag'i create ve edit metodlarini atlar (API'de form gostermeye gerek yok).

// routes/api.php
Route::apiResource('posts', PostController::class);

Bu tek satir su route'lari olusturur:

Method URI Action
GET /api/posts index
POST /api/posts store
GET /api/posts/{post} show
PUT/PATCH /api/posts/{post} update
DELETE /api/posts/{post} destroy

Nested Resource'lar

Route::apiResource('posts.comments', CommentController::class);
// GET /api/posts/{post}/comments
// POST /api/posts/{post}/comments
// ...

2. API Resources ile Response Formatlama

Model'i dogrudan dondurmek yerine API Resource kullan. Hangi alanlarin gonderilecegini kontrol edersin.

php artisan make:resource PostResource
php artisan make:resource PostCollection
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'excerpt' => Str::limit($this->body, 150),
            'body' => $this->when($request->routeIs('posts.show'), $this->body),
            'author' => new UserResource($this->whenLoaded('author')),
            'tags' => TagResource::collection($this->whenLoaded('tags')),
            'comments_count' => $this->whenCounted('comments'),
            'is_published' => $this->published_at !== null,
            'published_at' => $this->published_at?->toISOString(),
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

Onemli noktalar:

  • when() ile kosullu alanlar
  • whenLoaded() ile N+1 onleme (iliskiler yuklenmediyse atla)
  • whenCounted() ile count alanlari
  • Tarihler her zaman ISO 8601 formatinda

Collection Resource

// app/Http/Resources/PostCollection.php
class PostCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total_posts' => Post::count(),
                'has_more' => $this->hasMorePages(),
            ],
        ];
    }
}

Controller'da kullanimi:

class PostController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::query()
            ->with(['author', 'tags'])
            ->withCount('comments')
            ->when($request->search, fn ($q, $s) => $q->where('title', 'like', "%{$s}%"))
            ->when($request->tag, fn ($q, $t) => $q->whereHas('tags', fn ($q) => $q->where('slug', $t)))
            ->latest('published_at')
            ->paginate($request->input('per_page', 15));

        return new PostCollection($posts);
    }

    public function show(Post $post)
    {
        $post->load(['author', 'tags', 'comments.author']);

        return new PostResource($post);
    }
}

3. API Versiyonlama

API degisecek, bu kacilmaz. Versiyonlama ile eski client'lari kirmadan yeni ozellikler eklersin.

URL-Based Versioning (Tavsiye Ettigim)

// routes/api.php
Route::prefix('v1')->group(function () {
    Route::apiResource('posts', Api\V1\PostController::class);
});

Route::prefix('v2')->group(function () {
    Route::apiResource('posts', Api\V2\PostController::class);
});

Dizin yapisi:

app/Http/Controllers/Api/
    V1/
        PostController.php
    V2/
        PostController.php
app/Http/Resources/
    V1/
        PostResource.php
    V2/
        PostResource.php

RouteServiceProvider'da Base Path

// bootstrap/app.php (Laravel 11+)
->withRouting(
    api: __DIR__.'/../routes/api.php',
    apiPrefix: 'api',
)

4. Dogru HTTP Status Kodlari

Her response'ta dogru status kodu kullanmak REST'in temel kurali.

class PostController extends Controller
{
    public function store(StorePostRequest $request)
    {
        $post = Post::create($request->validated());

        return new PostResource($post); // 201 Created
    }

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

        return new PostResource($post); // 200 OK
    }

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

        return response()->noContent(); // 204 No Content
    }
}

Resource'ta 201 dondurmek icin:

// Store metodunda
return (new PostResource($post))
    ->response()
    ->setStatusCode(201);

// veya daha temiz
return PostResource::make($post)
    ->response()
    ->setStatusCode(Response::HTTP_CREATED);

Sik kullanilan status kodlari:

Kod Anlam Kullanim
200 OK Basarili GET, PUT, PATCH
201 Created Basarili POST
204 No Content Basarili DELETE
400 Bad Request Gecersiz istek
401 Unauthorized Auth gerekli
403 Forbidden Yetkisiz erisim
404 Not Found Kaynak bulunamadi
422 Unprocessable Validation hatasi
429 Too Many Requests Rate limit
500 Server Error Sunucu hatasi

5. Hata Yonetimi

Tutarli hata response'lari cok onemli. Tum hatalari ayni formatta dondurmeliyiz.

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->shouldRenderJsonWhen(function (Request $request) {
        return $request->is('api/*') || $request->expectsJson();
    });

    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Kaynak bulunamadi.',
                'error' => 'not_found',
            ], 404);
        }
    });

    $exceptions->render(function (AuthenticationException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Kimlik dogrulama gerekli.',
                'error' => 'unauthenticated',
            ], 401);
        }
    });

    $exceptions->render(function (ValidationException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Girilen veriler gecersiz.',
                'error' => 'validation_failed',
                'errors' => $e->errors(),
            ], 422);
        }
    });
})

6. Form Request ile Validation

Controller'i sismirmek yerine ayri Form Request sinifi kullan.

php artisan make:request StorePostRequest
// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // Policy'de kontrol edeceksen true birak
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'body' => ['required', 'string', 'min:50'],
            'tags' => ['sometimes', 'array', 'max:5'],
            'tags.*' => ['exists:tags,id'],
            'published_at' => ['nullable', 'date', 'after_or_equal:today'],
        ];
    }

    public function messages(): array
    {
        return [
            'title.required' => 'Baslik zorunludur.',
            'body.min' => 'Icerik en az 50 karakter olmalidir.',
            'tags.max' => 'En fazla 5 etiket eklenebilir.',
        ];
    }
}

7. Pagination

API'de her zaman pagination kullan. Tum kayitlari dondurmek hem performans hem guvenlik sorunu.

public function index(Request $request)
{
    $perPage = min($request->input('per_page', 15), 100); // Max 100

    $posts = Post::query()
        ->latest()
        ->paginate($perPage);

    return PostResource::collection($posts);
}

Response otomatik olarak pagination meta bilgisi icerir:

{
    "data": [...],
    "links": {
        "first": "http://api.example.com/posts?page=1",
        "last": "http://api.example.com/posts?page=5",
        "prev": null,
        "next": "http://api.example.com/posts?page=2"
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 5,
        "per_page": 15,
        "to": 15,
        "total": 73
    }
}

Cursor Pagination (Buyuk Veri Setleri Icin)

$posts = Post::query()
    ->orderBy('id')
    ->cursorPaginate(15);

return PostResource::collection($posts);

Cursor pagination offset-based'den cok daha performansli. Ozellikle buyuk tablolarda OFFSET 50000 yerine WHERE id > 50000 kullanir.

8. Rate Limiting

API'ni kotu niyetli kullanimdan koru.

// bootstrap/app.php veya AppServiceProvider
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

// Farkli endpoint'ler icin farkli limitler
RateLimiter::for('auth', function (Request $request) {
    return Limit::perMinute(5)->by($request->ip());
});

RateLimiter::for('upload', function (Request $request) {
    return Limit::perMinute(10)->by($request->user()->id);
});

Route'larda kullanim:

Route::middleware('throttle:auth')->group(function () {
    Route::post('/login', [AuthController::class, 'login']);
    Route::post('/register', [AuthController::class, 'register']);
});

Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::apiResource('posts', PostController::class);
});

9. Filtreleme ve Siralama

Esnek filtreleme icin query builder pattern'i:

// app/Filters/PostFilter.php
class PostFilter
{
    public function __construct(
        protected Request $request
    ) {}

    public function apply(Builder $query): Builder
    {
        return $query
            ->when($this->request->search, function ($q, $search) {
                $q->where('title', 'like', "%{$search}%");
            })
            ->when($this->request->status, function ($q, $status) {
                match ($status) {
                    'published' => $q->whereNotNull('published_at'),
                    'draft' => $q->whereNull('published_at'),
                    default => $q,
                };
            })
            ->when($this->request->author_id, function ($q, $authorId) {
                $q->where('author_id', $authorId);
            })
            ->when($this->request->sort, function ($q, $sort) {
                $direction = $this->request->input('direction', 'desc');
                $allowed = ['title', 'created_at', 'published_at'];

                if (in_array($sort, $allowed)) {
                    $q->orderBy($sort, $direction === 'asc' ? 'asc' : 'desc');
                }
            }, function ($q) {
                $q->latest();
            });
    }
}

Controller'da:

public function index(Request $request)
{
    $filter = new PostFilter($request);

    $posts = $filter->apply(Post::query())
        ->with(['author', 'tags'])
        ->paginate(15);

    return PostResource::collection($posts);
}

Ornek istek: GET /api/posts?search=laravel&status=published&sort=title&direction=asc&page=2

10. CORS Ayarlari

// config/cors.php
return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => [
        env('FRONTEND_URL', 'http://localhost:3000'),
    ],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true, // Sanctum SPA icin sart
];

Production'da allowed_origins icin * kullanma. Sadece frontend domain'ini yaz.

11. API Dokumantasyonu

API dokumantasyonu icin Scribe veya Scramble kullanabilirsin.

Laravel Scramble (Otomatik OpenAPI)

composer require dedoc/scramble

Scramble, kodunu analiz edip otomatik OpenAPI dokumantasyonu olusturur. Ekstra annotation yazmana gerek yok.

/docs/api adresinden erisirsin. PHPDoc eklemek istersen:

/**
 * Yazilari listele.
 *
 * Filtreleme, siralama ve sayfalama destekler.
 *
 * @queryParam search string Baslikta arama. Example: laravel
 * @queryParam status string Durum filtresi (published, draft). Example: published
 * @queryParam per_page int Sayfa basi kayit (max 100). Example: 15
 */
public function index(Request $request)
{
    // ...
}

12. Ornek Proje Yapisi

app/
    Http/
        Controllers/
            Api/
                V1/
                    AuthController.php
                    PostController.php
                    CommentController.php
                    UserController.php
        Middleware/
            ForceJsonResponse.php
        Requests/
            StorePostRequest.php
            UpdatePostRequest.php
        Resources/
            V1/
                PostResource.php
                PostCollection.php
                UserResource.php
                CommentResource.php
    Filters/
        PostFilter.php
    Models/
        Post.php
routes/
    api.php
    api_v1.php

JSON Response Middleware

Tum API isteklerinin JSON dondurmesini garanti altina al:

// app/Http/Middleware/ForceJsonResponse.php
class ForceJsonResponse
{
    public function handle(Request $request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        return $next($request);
    }
}
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->api(prepend: [
        ForceJsonResponse::class,
    ]);
})

Sonuc

RESTful API gelistirmek zor degil ama disiplin istiyor. Dogru HTTP status kodlari, tutarli response formati, guvenlik onlemleri ve iyi dokumantasyon -- bunlarin hepsi projenin basarisini dogrudan etkiliyor.

Bu yazidaki pratikleri uyguladiginda, hem frontend ekibi seninle calismayi sever hem de API'ni kullanan ucuncu partiler daha az sorun yasarlar. Kucuk basla, ihtiyac duydukca buyut -- ama basta dogru temeli at.