API Test Araçları Haritası: curl, .http Dosyaları, Postman, Bruno ve OpenAPI UI'ları Arasında Seçim
Aracı seçerken asıl soru, istek tanımlarının nerede durduğu: repo'da yaşayan .http dosyaları ve Bruno collection'ları API koduyla aynı pull request'te değişir, bulut collection'ı ise geride kalır. Bookstore.Api'ye login ve JWT ekleyip ortak senaryoyu curl ile koşuyor, OpenAPI dokümanına Bearer şemasını yazdırıyor, Scalar'ın yerini, Postman'in Mart 2026'dan beri tek kullanıcılı ücretsiz planını ve Bruno'nun sürümlerini karşılaştırıyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (ASP.NET Core 10, yerleşik OpenAPI 3.1) ve Scalar.AspNetCore 2.21 ile derlenip denenmiştir; komutlar curl 8.x ile çalıştırılmıştır. Postman ve Bruno'nun plan ve fiyat bilgileri 14 Kasım 2026'da resmi sayfalarından alınmıştır.
Önce Tahmin Et
Ekipte üç kişi aynı API'yi deniyor. Emre her sabah terminalde aynı dört curl komutunu history'den bulup çalıştırıyor. Ayşe Postman'de güzel bir collection kurmuş, login isteğinin script'i token'ı otomatik olarak environment'a yazıyor. Hakan'ın bilgisayarında ise sadece tarayıcıda açık bir Scalar sekmesi var. Bir gün POST /api/orders isteğinin body'sine yeni bir zorunlu alan ekleniyor ve pull request'e giriyor.
Soru: Bu değişiklik code review'da üç kişiden hangisinin aracına yansır? Hangisinin denemesi ertesi sabah sessizce 400 dönmeye başlar? Ayşe collection'ını ekiple paylaşmak isterse, Mart 2026'dan beri Postman'in ücretsiz planında bunu yapabilir mi? Cevaplar yazının ortasında; ama ipucu şu: aracı seçerken asıl soru "hangisi daha güzel" değil, "istek tanımları nerede duruyor" sorusu.
Dört Ayrı İhtiyaç: Keşif, Tekrar, Paylaşım, Otomasyon
"API'yi test etmek" dediğimiz şey aslında dört farklı iş. Hepsine tek bir araç seçmeye çalışınca ya basit işler ağırlaşıyor ya da ciddi işler el emeğine kalıyor.
- Keşif: Bir endpoint'i ilk kez görüyorsun, ne döndüğünü merak ediyorsun. Hız önemli, kalıcılık önemli değil. Scalar gibi bir OpenAPI arayüzü ya da tek satırlık bir curl yeter.
- Tekrar eden manuel kontrol: Her gün aynı akışı elle çalıştırıyorsun: login, sepet, sipariş. Burada isteklerin bir dosyada durması, token'ın bir istekten ötekine kendiliğinden geçmesi gerekiyor.
.httpdosyaları, Postman ve Bruno bu iş için var. - Paylaşım: Aynı istekleri ekip arkadaşın, frontend geliştiricisi ya da QA (kalite güvence, yani test ekibi) da kullanacak. İstek tanımı API değişince güncellenmeli ve bu güncellemeyi herkes görmeli.
- Otomasyon: Aynı akış CI'da her commit'te koşsun, bozulunca build kırmızıya dönsün. Bu noktada artık bir CLI ve makinenin okuyabileceği bir rapor (JUnit XML gibi) gerekiyor.
Son madde önemli bir sınır çiziyor. CI'da koşan ve assertion içeren bir collection artık bir test suite'idir, ama asıl integration test'lerin yerini almaz. ASP.NET Core Web API serisinin "ASP.NET Core Web API'de Integration Test: WebApplicationFactory ile Uçtan Uca Test" yazısında anlattığımız testler aynı process içinde, gerçek veritabanıyla ve debugger'la çalışır; ilerideki Integration ve E2E (uçtan uca) Test serisi de Testcontainers ve Playwright tarafını anlatacak. Bu serinin araçları geliştirirken elle ve yarı otomatik deneme içindir. Yük testi Performance ve Load Testing serisinde, güvenlik testi API Güvenliği (OWASP API Top 10) serisinin "API Güvenliğini Sürekli Test Etmek ve İzlemek" yazısında.
Ortak Senaryo: Bookstore.Api'de Login'den Siparişe
Serinin her yazısında aynı API'ye, aynı akışla istek atacağız. API, sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Bookstore.Api projesi. Referans uygulamada katalog, sepet ve sipariş endpoint'leri vardı ama authentication yoktu. Bu seri için bir login endpoint'i ekledim; Development ortamında iki demo kullanıcıyla JWT (JSON Web Token; imzalı ve içinde kullanıcı bilgisi taşıyan token) veriyor. Kullanıcılar appsettings.Development.json'da duruyor:
{
"Auth": {
"DemoUsers": [
{ "Email": "ada@example.com", "Password": "Bookstore-Demo-1", "Name": "Ada Lovelace", "Role": "Customer" },
{ "Email": "grace@example.com", "Password": "Bookstore-Demo-2", "Name": "Grace Hopper", "Role": "StoreStaff" }
]
}
}
Bu şifreler yalnız yerel geliştirme içindir ve bilerek açık duruyor; gerçek kullanıcılar Identity module'ünden gelir. JWT'yi imzalayan anahtar ise dosyada yok: Development'ta API her açılışta rastgele 32 byte'lık bir anahtar üretiyor, başka ortamda Auth:SigningKey verilmezse açılmıyor. Yan etkisi şu: API'yi yeniden başlattığında eski token'lar geçersiz olur, araçlarında yeniden login olman gerekir. Token üretme ve doğrulamanın ayrıntısı Web API serisinin "ASP.NET Core Web API'de JWT ile Authentication: Token Üretmek ve Doğrulamak" yazısında.
Login handler'ının kalbi şu kadar:
// POST /api/auth/login { "email": "...", "password": "..." } → bearer token
private static Results<Ok<TokenResponse>, ProblemHttpResult> Login(
LoginRequest request, IOptions<AuthOptions> options, JwtSigningKey signingKey, TimeProvider clock)
{
AuthOptions auth = options.Value;
DemoUser? user = auth.DemoUsers.FirstOrDefault(u =>
string.Equals(u.Email, request.Email, StringComparison.OrdinalIgnoreCase));
if (user is null || !CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(user.Password), Encoding.UTF8.GetBytes(request.Password)))
{
return TypedResults.Problem("Invalid email or password.", statusCode: StatusCodes.Status401Unauthorized);
}
// ... SecurityTokenDescriptor ile 15 dakikalık token
}
Sepet ve sipariş endpoint'leri artık RequireAuthorization() ile korunuyor; katalog herkese açık. Bir de küçük ama önemli bir kural ekledim: GET /api/orders/{id} müşteriye yalnız kendi siparişini gösteriyor, StoreStaff rolü hepsini görüyor. Bu olmasa her araçla deneyeceğimiz akış, API Güvenliği serisinin anlattığı BOLA (Broken Object Level Authorization; başkasının kaydına ID'yi değiştirerek erişebilmek) açığını her gün çalıştırıyor olurdu.
Senaryonun adımları:
| Adım | İstek | Token | Sonraki adıma ne taşınır |
|---|---|---|---|
| 1 | POST /api/auth/login | Hayır | accessToken |
| 2 | GET /api/books?page=1 | Hayır | Bir kitabın isbn'i |
| 3 | POST /api/carts | Evet | Sepetin id'si |
| 4 | PUT /api/carts/{id}/items/{isbn} | Evet | Sepetin toplamı |
| 5 | POST /api/orders | Evet | Location header'ındaki sipariş adresi |
| 6 | GET /api/orders/{id} | Evet | Sipariş durumu |
Son sütun, araçları ayıran şeyin tam kendisi. Bir aracın "request chaining" yeteneği, bir isteğin cevabındaki değeri (token, ID, header) bir sonraki isteğe ne kadar zahmetsiz taşıdığıdır. Aynı akışı bash'te curl ve jq ile elle bağladığımda çıktı şöyle oldu:
$ TOKEN=$(curl -s $B/api/auth/login --json '{"email":"ada@example.com","password":"Bookstore-Demo-1"}' | jq -r .accessToken)
$ curl -s "$B/api/books?page=1&pageSize=3" | jq -c '.items[] | {isbn, title, price}'
{"isbn":"9780451524935","title":"1984","price":279.00}
{"isbn":"9780143035008","title":"Anna Karenina","price":429.00}
{"isbn":"9780060850524","title":"Brave New World","price":299.00}
$ CART=$(curl -s -X POST $B/api/carts -H "Authorization: Bearer $TOKEN" | jq -r .id)
$ curl -s -X PUT $B/api/carts/$CART/items/9780261102217 -H "Authorization: Bearer $TOKEN" --json '{"quantity":2}' | jq -c '{total, items: [.items[].title]}'
{"total":659.80,"items":["The Hobbit"]}
$ curl -s -D - -o /dev/null $B/api/orders -H "Authorization: Bearer $TOKEN" \
--json "{\"cartId\":\"$CART\",\"customerName\":\"Ada Lovelace\",\"customerEmail\":\"ada@example.com\"}" | grep -iE '^(HTTP|location)'
HTTP/1.1 201 Created
Location: http://localhost:5080/api/orders/1
$ curl -s $B/api/orders/1 -H "Authorization: Bearer $TOKEN" | jq -c '{id, status, total}'
{"id":1,"status":"Placed","total":659.80}
($B, http://localhost:5080.) Çalışıyor ama kırılgan: değişkenler shell oturumunda yaşıyor, terminali kapatınca akış kayboluyor, ekip arkadaşın bu komutları senin history'nden göremiyor. Bir sonraki yazıda curl'ü script'e dönüştürüp bu sorunların bir kısmını çözeceğiz; geri kalanı için diğer araçlar var.
Her Aracın Girişi: /openapi/v1.json
Bütün araçların ortak bir besin kaynağı var: API'nin kendi ürettiği OpenAPI dokümanı. ASP.NET Core 10 bunu AddOpenApi() ve MapOpenApi() ile varsayılan olarak OpenAPI 3.1 formatında üretiyor (ayrıntısı Web API serisinin ".NET 10 Yenilikleri: ASP.NET Core'da AddValidation, OpenAPI 3.1 ve Server-Sent Events" yazısında). Dokümana token'ın nasıl verileceğini de yazdırdım; bir document transformer Bearer security scheme'ini ekliyor, bir operation transformer da yalnız RequireAuthorization() olan endpoint'leri işaretliyor:
options.AddOperationTransformer((operation, context, _) =>
{
bool requiresAuth = context.Description.ActionDescriptor.EndpointMetadata.OfType<IAuthorizeData>().Any()
&& !context.Description.ActionDescriptor.EndpointMetadata.OfType<IAllowAnonymous>().Any();
if (requiresAuth)
{
operation.Security ??= [];
operation.Security.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", context.Document)] = []
});
}
return Task.CompletedTask;
});
Dokümandan endpoint listesini çıkarınca:
$ curl -s localhost:5080/openapi/v1.json | jq -r '.paths | to_entries[] | .key as $p | .value | to_entries[]
| "\(.key|ascii_upcase) \($p)\(if .value.security then " [Bearer]" else "" end)"'
POST /api/auth/login
GET /api/auth/me [Bearer]
GET /api/books
GET /api/books/{isbn}
GET /api/categories
POST /api/carts [Bearer]
GET /api/carts/{cartId} [Bearer]
PUT /api/carts/{cartId}/items/{isbn} [Bearer]
DELETE /api/carts/{cartId}/items/{isbn} [Bearer]
POST /api/orders [Bearer]
GET /api/orders/{orderId} [Bearer]
Bu küçük ek, ileride emeğini fazlasıyla geri veriyor: Postman ve Bruno OpenAPI dokümanını import ederken security scheme'e bakıp collection'ın auth ayarını kendisi kuruyor. Dokümanda bu bilgi yoksa her isteğe Authorization header'ını elle eklersin.
En hızlı keşif aracı da aynı dokümandan besleniyor: Scalar. Web API serisinin ".NET 9 Yenilikleri: ASP.NET Core'da Yerleşik OpenAPI, Scalar ve HybridCache" yazısında anlattığımız Scalar.AspNetCore paketini ekleyip Development'ta app.MapScalarApiReference() çağırınca http://localhost:5080/scalar adresinde endpoint listesi, şemalar ve her isteği tarayıcıdan gönderen bir test paneli açılıyor. Login'den aldığın token'ı Bearer alanına yapıştırınca korumalı endpoint'leri de deneyebiliyorsun (arayüzü burada ekran görüntüsüyle değil, Scalar'ın dokümanına göre anlatıyorum; endpoint'in sayfayı sunduğunu curl ile kontrol ettim). Scalar'ın sınırı da burada: gönderdiğin istekler bir dosyaya kaydolmuyor, token tarayıcıda kalıyor ve API yalnız Development'ta bu arayüzü açtığı için staging'de işe yaramıyor. Keşif için çok iyi, tekrar ve paylaşım için değil.
Asıl Karar: İstek Tanımları Repo'da mı, Bulutta mı?
Araçları dört soruyla karşılaştırdım: istek tanımları repo'da yaşıyor mu, CI'da koşabiliyor mu, ekip onları nasıl paylaşıyor ve secret'lar nerede duruyor.

Repo'da yaşayan bir istek tanımı, API koduyla aynı commit'te değişir. Baştaki örnekte POST /api/orders'a zorunlu alan ekleyen geliştirici, aynı pull request'te .http dosyasını ya da Bruno'nun YAML (insanın okuyup düzenleyebildiği, girintiyle yapı kuran bir veri formatı) dosyasını da günceller; reviewer diff'te ikisini yan yana görür. Branch değiştirdiğinde istekler de o branch'in hâline döner. Bulutta yaşayan bir collection ise koddan bağımsız bir sürüm çizgisine sahiptir: API değişir, collection bir süre eski kalır ve bunu ilk fark eden, ertesi sabah 400 alan kişi olur. Tahmin sorusunun cevabı da bu: değişiklik yalnız repo'daki tanımlara yansır. Emre'nin history'si ve Ayşe'nin bulut collection'ı ertesi sabah eski body'yi göndermeye devam eder.
Postman bu farkı kapatmak için v12 ile Native Git'i getirdi: desktop uygulamasında "Local View" açınca collection'lar repo'daki bir postman/ klasörüne YAML dosyaları olarak, Collection 3.0 formatında olarak yazılıyor, ekiple paylaşmak için de bulut tarafına push ediliyor. Bu, Postman'i matriste "şartlı" sütununa taşıyor; nasıl çalıştığını dördüncü yazıda Postman'in dokümanından anlatacağım. Bruno ise baştan beri dosya tabanlı; 3.1 sürümünden beri yeni collection'ları OpenCollection YAML formatında kaydediyor.
Secret konusu ayrı bir eksen. curl'de token bir environment variable'da ya da dosyada durur. .http dosyalarında commit'lenmeyen bir kullanıcı dosyası (http-client.env.json.user ya da Rider'daki http-client.private.env.json) vardır. Postman'de bir variable'ın shared value'su buluta senkronlanır, local value'su senkronlanmaz; asıl secret'lar için Postman Vault var. Bruno secret'ları .env dosyasında ya da bir secret manager'da tutuyor. Her araçta aynı kural geçerli: repo'ya giren dosyada token, şifre ya da API key durmamalı. Bu kuralın her araçta nasıl bozulduğunu ilgili yazılarda gerçek örnekle göreceğiz.
Maliyet: Postman'in 2026 Planları ve Bruno
Postman'in dokümanına göre planlar Mart 2026'da değişti: ücretsiz plan artık tek kullanıcıyla sınırlı ve takım oluşturamıyor. Önceden ücretsiz planda üç kişiye kadar davet edilebiliyordu. 14 Kasım 2026'da fiyat sayfasında gördüğüm tablo şöyle: Free 0 dolar ve 1 kullanıcı, Solo yıllık ödemeyle aylık 9 dolar ve 1 kullanıcı, Team yıllık ödemeyle kullanıcı başına aylık 19 dolar, Enterprise için satış ekibiyle görüşmek gerekiyor. Aynı değişiklikle collection run'ları ve mock server kullanımı bütün planlarda sınırsız oldu; ücretsiz planda API monitoring ayda 1.000 istekle sınırlı.
Pratik sonucu şu: iki kişilik bir ekip, collection'ı Postman bulutunda ortak düzenlemek istiyorsa Team planına geçmek zorunda. Ya da collection'ı JSON veya YAML olarak repo'ya koyup paylaşımı Git'e bırakır. Bu ikinci yol, Postman'i matriste Bruno'ya yaklaştırıyor.
Bruno'nun çekirdeği MIT (kullanmaya, değiştirmeye ve dağıtmaya izin veren kısa bir açık kaynak lisansı) lisanslı ve CLI'ı (@usebruno/cli) da MIT lisanslı. Uygulamanın Pro (yıllık ödemeyle kullanıcı başına aylık 6 dolar), Ultimate (11 dolar) ve Enterprise (18 dolar) sürümleri var. Fiyat sayfasına göre uygulama içindeki Git arayüzünün bazı işlemleri, sınırsız workspace ve bazı entegrasyonlar ücretli sürümlerde. Collection dosyaları sıradan dosyalar olduğu için kendi git komutlarınla commit'lemek her sürümde mümkün. .http dosyaları ve curl için ek maliyet yok, ama .http dosyalarının CI tarafı zayıf: Visual Studio'nun bir CLI'ı yok, JetBrains'in ijhttp'si ise kendi dialect'ini koşuyor. Bu farkı üçüncü yazıda gerçek koşuyla göstereceğim.
Hangi İhtiyaca Hangi Araç
| İhtiyaç | İlk tercih | Neden |
|---|---|---|
| Yeni bir endpoint'e hızlıca bakmak | Scalar ya da tek satır curl | Kurulum yok, OpenAPI'den hazır geliyor |
| Bir hatayı reproduce etmek, issue'ya eklemek | curl | Her makinede var; DevTools'tan "Copy as cURL" ile alınıyor |
| Her gün aynı akışı IDE'den çalıştırmak | .http dosyası | API projesinin yanında durur, code review'a girer |
| Frontend ve QA ile ortak, script'li collection | Bruno ya da Postman | Request chaining, assertion, environment yönetimi |
| Henüz yazılmamış bir endpoint'e karşı Angular geliştirmek | Postman mock server | Collection'daki kayıtlı örnek cevapları döner |
| Akışı CI'da koşup rapor almak | Bruno CLI, Postman CLI ya da Newman | JUnit raporu, çıkış kodu, environment dosyası |
Tek bir araca bağlanmak zorunda değilsin; Bookstore ekibinde makul bir düzen şöyle olurdu: backend geliştiriciler .http dosyalarını API projesinin yanında tutar, ortak akış Bruno collection'ı olarak repo'da durur ve CI'da koşar, hızlı keşif Scalar'da yapılır, hata raporları curl komutuyla açılır. Postman'i zaten kullanan bir ekip için de aynı mantık geçerli: collection'ı repo'ya taşıyıp CI'da Postman CLI ile koşmak.
Üretimde Dikkat
Bu araçların hepsi sonunda gerçek bir API'ye istek atıyor ve en sık hata, yanlış ortama istek atmak. Bir collection'da baseUrl environment'tan geliyorsa, environment seçicisinde production'ın seçili kalması bir DELETE isteğini gerçek veriye gönderir. Production için ayrı, salt okunur bir environment tut ya da production'ı bu araçların environment listesine hiç koyma.
İkinci risk, araçların gönderdiği trafiğin loglara düşmesi. Reverse proxy'ler ve web sunucularının access log'ları genellikle URL'yi query string'iyle birlikte yazar; bir araç token'ı query string'de gönderirse (?access_token=...) token log dosyasına girer. Token'ı her zaman Authorization header'ında gönder; API tarafında da query string'den token kabul etme.
Üçüncüsü, OpenAPI dokümanının ve Scalar'ın nerede açık olduğu. Bookstore.Api ikisini yalnız Development'ta map ediyor. Staging'de de açmak istersen ayrı bir karar olarak ver ve authentication'ın arkasına koy; dokümanın kendisi saldırgan için bir endpoint envanteridir (OWASP API Top 10 listesinin 8. ve 9. maddelerini işleyen API Güvenliği serisi yazısı, "Yanlış Yapılandırma ve Envanter", bu konuyu anlatıyor).
Sık Düşülen Tuzaklar
Aracı popülerliğine göre seçmek. "Herkes Postman kullanıyor" doğru olabilir, ama ekip collection'ı bulutta paylaşacaksa artık ücretli plan gerekiyor; bunu bütçe konuşulurken değil, araç seçilirken bil.
Elle denemeyi test sanmak. Bir collection'da yüz istek ve iki yüz pm.test olabilir; bunlar API'nin dışından, gerçek ağ üzerinden yapılan kontrollerdir ve hangi satırın kırıldığını debugger'la göstermez. Kalıcı davranış kontrolleri WebApplicationFactory testlerine gitmeli; collection'lar akışın uçtan uca ayakta olduğunu doğrulamak için kalmalı.
OpenAPI'den bir kez import edip unutmak. Import edilen collection o anki dokümanın kopyasıdır; API değiştikçe ayrışır. Postman ve Bruno'da yeniden import ya da senkronizasyon gerekiyor ve bu her zaman sorunsuz değil (dördüncü ve beşinci yazıda).
Token'ı aracın kaydedilen dosyasına yazmak. En sık görülen hali: .http dosyasında @token = eyJhbGciOi... satırı ya da Postman environment'ında shared value olarak kalmış bir production token'ı. Bir JWT'nin ortası base64 ile kolayca okunur; baştaki curl akışında aldığım token'ın payload kısmı şuydu:
{"aud":"bookstore-clients","iss":"bookstore-api","exp":1794647336,"iat":1794646436,"nbf":1794646436,"sub":"ada@example.com","email":"ada@example.com","name":"Ada Lovelace","role":"Customer"}
Token şifrelenmemiş, yalnız imzalanmış; repo'ya düşen bir token'ı süresi dolana kadar herkes kullanabilir ve içindeki bilgileri okuyabilir.
Küçük Bir Deneme
★☆☆ Bookstore.Api'yi çalıştırıp http://localhost:5080/scalar'ı aç. POST /api/auth/login ile grace@example.com olarak token al, Scalar'ın auth alanına yapıştır ve GET /api/auth/me'nin StoreStaff rolünü döndürdüğünü gör.
★★☆ Ada'nın token'ıyla bir sipariş ver, sonra Grace'in token'ıyla aynı siparişi GET /api/orders/{id} ile oku. Ardından ikinci bir müşteri ekleyecek şekilde appsettings.Development.json'u değiştirip (rolü Customer) o kullanıcının Ada'nın siparişini okuyamadığını, 404 aldığını doğrula. Neden 403 değil de 404 döndüğünü düşün.
★★★ Kendi ekibin için bu yazıdaki matrisi doldur: kullandığınız araçları satırlara, dört soruyu sütunlara yaz. "Secret nerede durur?" sütununda emin olmadığın her hücre için repo'da git log -p -S "Bearer ey" çalıştır ve geçmişte commit'lenmiş bir token olup olmadığına bak.
Bir sonraki yazıda bu tablonun ilk satırından başlıyoruz: curl ile method, header, --json, authentication, dosya yükleme ve hata kodları; ardından aynı akışı CI'da koşabilecek, hata durumunda doğru çıkış koduyla duran bir script'e dönüştürüyoruz.