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 alanlarwhenLoaded()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.