REST API Best Practices¶
RESTful API tasarımı, tutarlı ve kullanılabilir API’ler oluşturmayı sağlar; yanlış tasarım kafa karıştırıcı ve bakımı zor API’lere yol açar.
1. Fiil Bazlı URL Kullanmak¶
❌ Yanlış Kullanım: URL’de fiil kullanmak.
app.MapGet("/api/getProducts", GetAll);
app.MapPost("/api/createProduct", Create);
app.MapPost("/api/deleteProduct/{id}", Delete);
app.MapPost("/api/updateProduct", Update);
✅ İdeal Kullanım: İsim bazlı URL ve HTTP metod ile eylemi belirtin.
var products = app.MapGroup("/api/products");
products.MapGet("/", GetAll); // GET /api/products
products.MapGet("/{id}", GetById); // GET /api/products/5
products.MapPost("/", Create); // POST /api/products
products.MapPut("/{id}", Update); // PUT /api/products/5
products.MapDelete("/{id}", Delete); // DELETE /api/products/5
2. Tutarsız HTTP Status Code Döndürmek¶
❌ Yanlış Kullanım: Her durumda 200 OK döndürmek.
app.MapPost("/api/orders", async (CreateOrderDto dto, IOrderService service) =>
{
try
{
var order = await service.CreateAsync(dto);
return Results.Ok(new { success = true, data = order });
}
catch (Exception ex)
{
return Results.Ok(new { success = false, error = ex.Message }); // Hata da 200!
}
});
✅ İdeal Kullanım: Duruma uygun HTTP status code döndürün.
app.MapPost("/api/orders", async (CreateOrderDto dto, IOrderService service) =>
{
var order = await service.CreateAsync(dto);
return TypedResults.Created($"/api/orders/{order.Id}", order); // 201 Created
});
app.MapGet("/api/orders/{id}", async (int id, IOrderService service) =>
{
var order = await service.GetByIdAsync(id);
return order is not null
? TypedResults.Ok(order) // 200 OK
: TypedResults.NotFound(); // 404 Not Found
});
app.MapDelete("/api/orders/{id}", async (int id, IOrderService service) =>
{
await service.DeleteAsync(id);
return TypedResults.NoContent(); // 204 No Content
});
3. Pagination Yapmamak¶
❌ Yanlış Kullanım: Tüm kayıtları tek seferde döndürmek.
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync(); // 100.000 kayıt döner
});
✅ İdeal Kullanım: Sayfalama ile sınırlı veri döndürün.
public record PagedResponse<T>(List<T> Items, int TotalCount, int Page, int PageSize)
{
public bool HasNextPage => Page * PageSize < TotalCount;
public bool HasPreviousPage => Page > 1;
}
app.MapGet("/api/products", async (
int page = 1,
int pageSize = 20,
string? search = null,
IProductService service = default!) =>
{
pageSize = Math.Min(pageSize, 100); // Maksimum sayfa boyutu
var result = await service.GetPagedAsync(page, pageSize, search);
return TypedResults.Ok(result);
});
4. Problem Details Kullanmamak¶
❌ Yanlış Kullanım: Tutarsız hata formatı döndürmek.
return Results.BadRequest("Geçersiz istek");
return Results.BadRequest(new { error = "Stok yetersiz" });
return Results.BadRequest(new { message = "Hata", code = 1001 });
// Her endpoint farklı hata formatı
✅ İdeal Kullanım: RFC 7807 Problem Details standardını kullanın.
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier;
};
});
// Kullanım
app.MapPost("/api/orders", async (CreateOrderDto dto, IOrderService service) =>
{
var result = await service.CreateAsync(dto);
return result.Match(
success => TypedResults.Created($"/api/orders/{success.Id}", success),
error => TypedResults.Problem(
title: "Sipariş oluşturulamadı",
detail: error.Message,
statusCode: 422));
});
5. API Versioning Yapmamak¶
❌ Yanlış Kullanım: Breaking change yaparak mevcut istemcileri kırmak.
// v1: { "name": "Ali", "surname": "Yılmaz" }
// v2: { "fullName": "Ali Yılmaz" }
// Eski istemciler bozulur
✅ İdeal Kullanım: URL versioning ile geriye uyumluluk sağlayın.
var v1 = app.MapGroup("/api/v1/users");
v1.MapGet("/", async (IUserService service) =>
{
var users = await service.GetAllAsync();
return users.Select(u => new { u.Name, u.Surname }); // v1 format
});
var v2 = app.MapGroup("/api/v2/users");
v2.MapGet("/", async (IUserService service) =>
{
var users = await service.GetAllAsync();
return users.Select(u => new { FullName = $"{u.Name} {u.Surname}" }); // v2 format
});