npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yildizpay/http-adapter

v3.10.0

Published

Enterprise-grade, zero-dependency HTTP adapter for Node.js with pluggable client abstractions.

Readme

Node.js tabanlı kurumsal uygulamalar için tasarlanmış profesyonel ve yüksek oranda yapılandırılabilir bir HTTP client adaptörü. Fluent API, built-in resilience pattern'ları, güçlü bir interceptor sistemi ve kapsamlı bir exception hiyerarşisi sunar. Zero-dependency olan paketin çekirdeği Node.js Native Fetch API kullanır; ancak istenen farklı custom HTTP client'lara da kolayca genişletilebilir.

Temel Özellikler

  • Fluent Request Builder: Sezgisel ve zincirlenebilir bir API ile karmaşık HTTP isteklerini kolayca oluşturun.
  • Structured Exception Hierarchy: Her HTTP durum kodu ve ağ hatası, zengin metadata, isRetryable() sinyali ve toJSON() desteğiyle ayrı bir exception sınıfına dönüştürülür.
  • Response Validation: Herhangi bir request'e bir veya daha fazla ResponseValidator ekleyerek schema kısıtlamalarını veya business rule'ları response kodunuza ulaşmadan otomatik olarak denetleyebilirsiniz.
  • Interceptor Mimarisi: Loglama, kimlik doğrulama, hata yönetimi ve veri dönüşümü gibi middleware işlemlerini zahmetsizce entegre edin.
  • Resilience & Reliability: S2S entegrasyonlarında geçici hataları zarif bir şekilde yönetmek için Exponential Backoff gibi retry policy'ler ve built-in Circuit Breaker içerir.
  • Type Safety: Generic'ler kullanılarak tam olarak tiplendirilmiş request ve response'lar ile uygulama genelinde tip güvenliği sağlanır.
  • Test Edilebilirlik: Dependency injection düşünülerek tasarlandığından mock yazmak oldukça kolaydır.
  • Immutable Tasarım: Concurrent ortamlarda side effect'leri önlemek için core bileşenler immutable olarak tasarlanmıştır.

Kurulum

npm install @yildizpay/http-adapter
# veya
yarn add @yildizpay/http-adapter
# veya
pnpm add @yildizpay/http-adapter

Kullanım

1. Request Oluşturma

RequestBuilder ile istekleri temiz ve öz bir şekilde oluşturun.

import { RequestBuilder, HttpMethod } from '@yildizpay/http-adapter';

const request = new RequestBuilder('https://api.example.com')
  .setEndpoint('/users')
  .setMethod(HttpMethod.POST)
  .addHeader('Authorization', 'Bearer token')
  .setBody({ name: 'Ahmet Yılmaz', email: '[email protected]' })
  .build();

2. Adapter'ı Başlatma

Fluent builder API ile HttpAdapter oluşturun.

import { HttpAdapter, RetryPolicies } from '@yildizpay/http-adapter';

const adapter = HttpAdapter.builder()
  .withInterceptor(new AuthInterceptor(), new LoggingInterceptor())
  .withRetryPolicy(RetryPolicies.exponential(3))
  .withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 60000 })
  .withCorrelationId()                // x-correlation-id header'ını ilet (opt-in)
  .build();

Tek seferlik kurulum için HttpAdapter.create() de kullanılabilir.

const adapter = HttpAdapter.create(
  [new AuthInterceptor()],
  RetryPolicies.exponential(3),
  undefined,                    // Opsiyonel custom HTTP client
  new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 60000 }),
);

3. Request Gönderme

Request'i çalıştırın ve strongly-typed response alın.

interface UserResponse {
  id: string;
  name: string;
}

try {
  const response = await adapter.send<UserResponse>(request);
  console.log('Kullanıcı oluşturuldu:', response.data);
} catch (error) {
  console.error('Request başarısız oldu:', error);
}

Hata Yönetimi (Error Handling)

@yildizpay/http-adapter, her türlü ham hatayı — HTTP hataları, OS düzeyindeki ağ hataları veya tamamen beklenmedik exception'lar — yapılandırılmış ve tiplendirilmiş bir exception sınıfına dönüştürür. Bu sayede catch bloklarında ham durum kodlarını ya da hata kodlarını elle incelemenize gerek kalmaz.

Exception Hiyerarşisi

BaseAdapterException
├── HttpException                    (herhangi bir HTTP response hatası)
│   ├── BadRequestException          (400)
│   ├── UnauthorizedException        (401)
│   ├── ForbiddenException           (403)
│   ├── NotFoundException            (404)
│   ├── ConflictException            (409)
│   ├── UnprocessableEntityException (422)
│   ├── TooManyRequestsException     (429)  ← isRetryable() = true
│   ├── InternalServerErrorException (500)
│   ├── BadGatewayException          (502)  ← isRetryable() = true
│   ├── ServiceUnavailableException  (503)  ← isRetryable() = true
│   ├── GatewayTimeoutException      (504)  ← isRetryable() = true
│   └── ... (tüm 4xx / 5xx kodları)
├── NetworkException                 (OS düzeyindeki bağlantı hataları)
│   ├── ConnectionRefusedException   (ECONNREFUSED)  ← isRetryable() = true
│   ├── TimeoutException             (ETIMEDOUT / ECONNABORTED / AbortError)  ← isRetryable() = true
│   ├── SocketResetException         (ECONNRESET)  ← isRetryable() = true
│   ├── DnsResolutionException       (ENOTFOUND / EAI_AGAIN)
│   └── HostUnreachableException     (EHOSTUNREACH / ENETUNREACH)
├── UnknownException                 (sınıflandırılamayan her türlü hata)
└── CircuitBreakerOpenException      (circuit açık, request gönderilmedi)

Exception Türüne Göre Yakalama

import {
  NotFoundException,
  TooManyRequestsException,
  TimeoutException,
  ConnectionRefusedException,
  CircuitBreakerOpenException,
  UnknownException,
} from '@yildizpay/http-adapter';

try {
  const response = await adapter.send<PaymentResponse>(request);
} catch (error) {
  if (error instanceof NotFoundException) {
    // HTTP 404 — kaynak bulunamadı
    console.error('Kaynak bulunamadı:', error.response.data);
  } else if (error instanceof TooManyRequestsException) {
    // HTTP 429 — retry'dan önce bekle
    const retryAfterMs = error.getRetryAfterMs();
    console.warn(`Rate limit aşıldı. ${retryAfterMs}ms sonra tekrar dene`);
  } else if (error instanceof TimeoutException) {
    // ETIMEDOUT / AbortError — downstream servis yavaş
    console.error('Request timeout:', error.code);
  } else if (error instanceof ConnectionRefusedException) {
    // ECONNREFUSED — downstream servis kapalı
    console.error('Servis kapalı:', error.requestContext?.url);
  } else if (error instanceof CircuitBreakerOpenException) {
    // Circuit açık — sunucuya istek gönderilmeden fail fast
    console.error('Circuit breaker açık. Request gönderilmedi.');
  } else if (error instanceof UnknownException) {
    // Beklenmedik bir hata — logla ve araştır
    console.error('Bilinmeyen hata:', error.toJSON());
  }
}

Type Guard'lar

instanceof kullanmadan type narrowing tercih ediyorsanız — fonksiyonel pipeline'larda veya modül sınırlarını geçerken kullanışlıdır — her exception sınıfının karşılık gelen bir type guard'ı mevcuttur:

import {
  isHttpException,
  isTimeoutException,
  isConnectionRefusedException,
  isCircuitBreakerOpenException,
} from '@yildizpay/http-adapter';

function handleError(error: unknown): void {
  if (isTimeoutException(error)) {
    // TypeScript artık biliyor: error, TimeoutException türünde
    scheduleRetry(error.requestContext?.url);
  } else if (isHttpException(error)) {
    // TypeScript artık biliyor: error, HttpException türünde
    reportToMonitoring(error.response.status, error.response.data);
  }
}

isRetryable() Sinyali

Her exception, hatanın geçici olup olmadığını ve retry'a değer olup olmadığını belirten bir isRetryable(): boolean metodu sunar. Custom retry decorator'lar yazarken ya da uygulama katmanında hatayı tekrar denemek isteyip istemediğinize karar verirken kullanışlıdır.

} catch (error) {
  if (error instanceof BaseAdapterException && error.isRetryable()) {
    return retryOperation();
  }
  throw error;
}

Retry edilebilir exception'lar: TooManyRequestsException (429), BadGatewayException (502), ServiceUnavailableException (503), GatewayTimeoutException (504), TimeoutException, SocketResetException, ConnectionRefusedException.

toJSON() ile Structured Logging

Tüm exception'lar toJSON() metodunu override eder; bu sayede Pino, Winston gibi structured logger'larla tam uyumludur. JSON.stringify(error) çağrısı boş {} yerine eksiksiz bir log objesi üretir.

} catch (error) {
  if (error instanceof BaseAdapterException) {
    logger.error(error.toJSON());
    // {
    //   name: 'NotFoundException',
    //   message: 'Not Found',
    //   code: 'ERR_NOT_FOUND',
    //   stack: '...',
    //   response: {
    //     status: 404,
    //     data: { detail: 'Ödeme kaydı bulunamadı' },
    //     request: { method: 'GET', url: 'https://api.example.com/payments/123', correlationId: 'corr-abc' }
    //   }
    // }
  }
}

RequestContext — Güvenli Request Metadata

Her exception, kaynak request'ten alınan RequestContext objesini (method, url, correlationId) otomatik olarak taşır. Auth token'larının veya kişisel verilerin (PII) loglara sızmasını önlemek amacıyla header ve body bilgileri bu objeden kasıtlı olarak çıkarılmıştır.

} catch (error) {
  if (error instanceof NetworkException) {
    logger.warn({
      event: 'network_failure',
      exception: error.name,
      request: error.requestContext, // { method, url, correlationId }
    });
  }
}

Response Validator'lar

Request'e validator ekleyerek schema kısıtlamalarını veya business rule'ları response kodunuza ulaşmadan otomatik olarak denetleyebilirsiniz. Validator'lar HTTP çağrısı başarılı olduktan sonra, response-side interceptor'lardan önce sırayla çalışır. İlk hata veren validator chain'i durdurur.

import { ResponseValidator, ValidationException, Response } from '@yildizpay/http-adapter';

class PaymentStatusValidator implements ResponseValidator<IyzicoResponse> {
  validate(response: Response<IyzicoResponse>): void {
    if (response.data.status !== 'success') {
      throw new ValidationException(
        `Payment failed: ${response.data.errorMessage}`,
        response,
      );
    }
  }
}

// Zod, Joi gibi herhangi bir validation kütüphanesiyle çalışır
class PaymentSchemaValidator implements ResponseValidator<unknown> {
  validate(response: Response<unknown>): void {
    IyzicoResponseSchema.parse(response.data); // Zod uyuşmazlıkta exception fırlatır
  }
}

const request = new RequestBuilder('https://api.iyzipay.com')
  .setEndpoint('/payment/auth')
  .setMethod(HttpMethod.POST)
  .setBody(dto)
  .validateWith(new PaymentSchemaValidator(), new PaymentStatusValidator())
  .build();

Validation hatasını yakalamak:

import { isValidationException } from '@yildizpay/http-adapter';

} catch (error) {
  if (isValidationException(error)) {
    console.error('Validation başarısız:', error.message);
    console.error('Ham response:', error.response.data);
  }
}

Validator içinde fırlatılan BaseAdapterException olmayan hatalar (örn. ZodError) otomatik olarak ValidationException'a sarılır; orijinal hata cause'ta tutulur. Typed erişim için generic parametre kullanılabilir:

} catch (error) {
  if (isValidationException<ZodError>(error) && error.cause) {
    console.error('Schema hataları:', error.cause.issues);
  }
}

Validator kayıtlıyken tam interceptor lifecycle'ı:

onRequest → HTTP call → onResponse → validators → onResponseValidated → caller
                                          ↓ (hata durumunda)
                                       onError

onResponse her zaman çalışır. onResponseValidated yalnızca tüm validator'lar geçtiğinde çalışır — business açısından geçerli bir response gerektiren cache veya side effect işlemleri için idealdir.

Error Interceptor

Exception'lar business logic'e ulaşmadan önce interceptor seviyesinde yakalanabilir ve dönüştürülebilir.

import {
  HttpErrorInterceptor,
  Request,
  BaseAdapterException,
  UnauthorizedException,
} from '@yildizpay/http-adapter';

export class GlobalErrorInterceptor implements HttpErrorInterceptor {
  async onError(error: BaseAdapterException, request: Request): Promise<never> {
    if (error instanceof UnauthorizedException) {
      await this.tokenService.refresh();
    }
    // Caller'ın handle edebilmesi için hatayı yeniden fırlat
    throw error;
  }
}

Resilience & Retry

Ağ kararsızlığı kaçınılmazdır. Bu adaptör, sağlam retry stratejileri tanımlamanıza olanak tanır.

Built-in Retry Policy'ler

| Policy | Factory | Davranış | |---|---|---| | Exponential Backoff | RetryPolicies.exponential(attempts) | Her denemede gecikme iki katına çıkar, küçük jitter eklenir — varsayılan seçim | | Fixed Delay | RetryPolicies.fixedDelay(attempts, delayMs) | Her retry arasında sabit bekleme süresi | | Linear Backoff | RetryPolicies.linearBackoff(attempts, stepMs) | Gecikme doğrusal büyür (attempt × stepMs) | | Full Jitter | RetryPolicies.fullJitter(attempts, baseMs) | Üstel cap içinde tamamen rastgele gecikme — concurrent yükü yaymak için en iyi seçim | | Decorrelated Jitter | RetryPolicies.decorrelatedJitter(attempts, baseMs, maxDelayMs) | AWS tarafından önerilen algoritma, concurrent istemciler arasında en geniş yayılımı sağlar |

import { RetryPolicies } from '@yildizpay/http-adapter';

// Tüm policy'ler varsayılan olarak 429, 502, 503, 504 ve ağ hatalarında retry yapar
RetryPolicies.exponential(3);
RetryPolicies.fixedDelay(3, 1000);          // her denemede 1 sn bekleme
RetryPolicies.linearBackoff(3, 500);        // 500 ms, 1000 ms, 1500 ms
RetryPolicies.fullJitter(3, 100);           // [0, 2^attempt * 100 ms] aralığında rastgele
RetryPolicies.decorrelatedJitter(3, 100);   // AWS decorrelated jitter, cap 30 sn

Özel Retry Predicate

retryIf() ile herhangi bir policy'nin varsayılan retry kararını (error.isRetryable()) override edebilirsiniz. Düz bir fonksiyon ya da RetryPredicate interface'ini implement eden bir sınıf kabul eder.

import { RetryPolicies, RetryPredicate, BaseAdapterException, isNetworkException } from '@yildizpay/http-adapter';

// Inline fonksiyon
const policy = RetryPolicies.exponential(3)
  .retryIf((error) => isNetworkException(error));

// Sınıf tabanlı predicate
class BusinessRetryPredicate implements RetryPredicate {
  shouldRetry(error: BaseAdapterException): boolean {
    return error.isRetryable() && myCircuitIsAllowing();
  }
}

const policy = RetryPolicies.fullJitter(3).retryIf(new BusinessRetryPredicate());

Circuit Breaker

Tamamen çökmüş bir downstream servisi beklemeye karşı sisteminizi korumak için CircuitBreaker kullanabilirsiniz. Belirli sayıda ardışık hata alındığında circuit açılır ve yanıt vermeyen servise gereksiz istek göndermeksizin anında CircuitBreakerOpenException fırlatır.

import { CircuitBreaker, CircuitBreakerOpenException } from '@yildizpay/http-adapter';

const breaker = new CircuitBreaker({
  failureThreshold: 5,         // 5 hatadan sonra circuit'i aç
  resetTimeoutMs: 30000,       // 30 saniye sonra half-open test isteği gönder
  successThreshold: 1,         // 1 başarılı half-open request sonrası circuit'i kapat
});

// CircuitBreakerOpenException bir sonraki deneme zamanını taşır
try {
  await adapter.send(request);
} catch (err) {
  if (err instanceof CircuitBreakerOpenException) {
    console.warn(`Circuit açık. ${err.retryAfterMs()}ms sonra tekrar dene`);
  }
}

Durum makinesi

  [CLOSED] ──(failureThreshold aşıldı)──▶ [OPEN]
     ▲                                        │
     │                               (resetTimeoutMs)
     │                                        │
     └──(successThreshold karşılandı)── [HALF_OPEN] ──(hata)──▶ [OPEN]

HALF_OPEN'da neden sadece bir probe isteği geçer?

Node.js, tek iş parçacıklı (single-threaded) bir event loop üzerinde çalışır; ancak async/await yapısı kooperatif çoklu görev (cooperative multitasking) modelini hayata geçirir: bir coroutine await noktasında askıya alındığında, event loop diğer coroutine'leri çalıştırmaya devam edebilir. Bu davranış, HALF_OPEN durumunda kritik bir risk yaratır: herhangi bir guard mekanizması olmaksızın, o anda gelen tüm istekler aynı HALF_OPEN durumunu okuyup eş zamanlı olarak ilerleyebilir — yeni toparlanmaya başlayan bir servisi bir anda çok sayıda istekle boğabilir.

Bunu önlemek için circuit breaker bir probe flag kullanır: yalnızca ilk çağıran probe slotunu alır; probe tamamlanana kadar diğer tüm eş zamanlı çağrılar CircuitBreakerOpenException ile reddedilir. Bu bilinçli bir tasarım kararıdır — birkaç isteği feda ederek servisin gerçekten sağlıklı olup olmadığı güvenli biçimde doğrulanır.

Observability

Adaptör, iki katmanlı bir observability sistemiyle birlikte gelir. Observer'lar salt okunurdur — request veya response'u değiştiremezler. Metrics, yapılandırılmış loglama ve dağıtık izleme için observer'ları, pipeline'ı değiştirmeniz gerektiğinde ise interceptor'ları kullanın.

HttpAdapterObserver

Builder üzerindeki .withObserver() metoduyla adaptöre tek bir observer bağlanır.

| Hook | Ne zaman tetiklenir | |---|---| | onRequestStart(request) | Tüm request interceptor'larından sonra, HTTP çağrısından hemen önce | | onRequestSuccess(response, durationMs) | Başarılı yanıt sonrasında (retry süresi dahil) | | onRequestFailure(error, durationMs) | Son hata çağırana iletildiğinde | | onRetry(attempt, error, delayMs) | Her retry planlandığında, backoff gecikmesinden önce |

import { HttpAdapterObserver, HttpAdapter, RetryPolicies } from '@yildizpay/http-adapter';

class MetricsObserver implements HttpAdapterObserver {
  onRequestSuccess(_response: Response, durationMs: number): void {
    metrics.histogram('http.request.duration', durationMs);
  }

  onRequestFailure(error: BaseAdapterException, durationMs: number): void {
    metrics.increment('http.request.error', { type: error.name });
  }

  onRetry(attempt: number, _error: BaseAdapterException, delayMs: number): void {
    logger.warn(`Retry denemesi ${attempt}, ${delayMs}ms sonra`);
  }
}

const adapter = HttpAdapter.builder()
  .withRetryPolicy(RetryPolicies.exponential(3))
  .withObserver(new MetricsObserver())
  .build();

CircuitBreakerObserver

Fluent .observe() metodu aracılığıyla bir CircuitBreaker instance'ına observer bağlanır.

| Hook | Ne zaman tetiklenir | |---|---| | onStateChange(from, to) | Her state geçişinde (CLOSED↔OPEN↔HALF_OPEN) | | onSuccess() | Her başarılı çalıştırma sonrasında | | onFailure(error) | Bir hata sayıldığında (isFailure predicate true döndürdüğünde) | | onProbeRejected() | HALF_OPEN'da eş zamanlı bir çağıran reddedildiğinde |

import { CircuitBreaker, CircuitBreakerObserver, CircuitState } from '@yildizpay/http-adapter';

class CircuitMetricsObserver implements CircuitBreakerObserver {
  onStateChange(from: CircuitState, to: CircuitState): void {
    logger.warn(`Circuit breaker: ${from} → ${to}`);
    metrics.increment('circuit_breaker.state_change', { from, to });
  }

  onProbeRejected(): void {
    metrics.increment('circuit_breaker.probe_rejected');
  }
}

const adapter = HttpAdapter.builder()
  .withCircuitBreaker(
    new CircuitBreaker({ failureThreshold: 5 })
      .observe(new CircuitMetricsObserver()),
  )
  .withObserver(new MetricsObserver())
  .build();

Interceptors

Interface Segregation Principle (ISP) sayesinde gereksiz metodları implement etmek zorunda kalmazsınız. Yalnızca ihtiyaç duyduğunuz lifecycle event'e göre HttpRequestInterceptor, HttpResponseInterceptor veya HttpErrorInterceptor interface'ini implement edebilirsiniz.

1. Request Interceptor (Örn: Auth Token)

Request'ler gönderilmeden önce Authorization gibi header'ları otomatik ekleyebilirsiniz.

import { HttpRequestInterceptor, Request } from '@yildizpay/http-adapter';

export class AuthInterceptor implements HttpRequestInterceptor {
  async onRequest(request: Request): Promise<Request> {
    request.addHeader('Authorization', 'Bearer benim-gizli-tokenim');
    return request;
  }
}

2. Response Interceptor (Örn: Veri Dönüşümü)

Gelen tüm response'ları merkezi olarak şekillendirebilir veya loglayabilirsiniz.

import { HttpResponseInterceptor, Response } from '@yildizpay/http-adapter';

export class TransformResponseInterceptor implements HttpResponseInterceptor {
  async onResponse(response: Response): Promise<Response> {
    if (response.status === 201) {
      console.log('Kaynak başarıyla oluşturuldu!');
    }
    return response;
  }
}

3. Error Interceptor (Örn: Global Hata Yönetimi)

Sunucudan gelen hatalı HTTP kodlarını (4xx, 5xx) veya ağ hatalarını tek bir yerden yakalayıp yönetebilirsiniz.

import {
  HttpErrorInterceptor,
  Request,
  BaseAdapterException,
  UnauthorizedException,
} from '@yildizpay/http-adapter';

export class GlobalErrorInterceptor implements HttpErrorInterceptor {
  async onError(error: BaseAdapterException, request: Request): Promise<never> {
    if (error instanceof UnauthorizedException) {
      console.error(`${error.requestContext?.url} endpoint'ine yetkisiz erişim!`);
    }
    throw error;
  }
}

Correlation ID Propagation

Her request otomatik olarak bir systemCorrelationId üretir; bu ID loglama ve hata context'i için içsel olarak kullanılır. İstersen downstream servislere giden request header'larına da eklenebilir.

Propagation opt-in'dir — adapter'da .withCorrelationId() çağrılarak etkinleştirilir:

const adapter = HttpAdapter.builder()
  .withCorrelationId()                    // 'x-correlation-id' olarak iletir (default)
  .withCorrelationId('x-request-id')      // custom header adı kullanır
  .build();

Per-request override, adapter config'inin önüne geçer:

const request = new RequestBuilder('https://api.example.com')
  .setEndpoint('/payments')
  .withCorrelationId('x-trace-id')        // bu request için custom header ile etkinleştir
  .build();

const request2 = new RequestBuilder('https://api.example.com')
  .setEndpoint('/internal')
  .withoutCorrelationId()                 // bu request için devre dışı bırak
  .build();

Header resolution sırası: per-request header → adapter header → 'x-correlation-id'.

Request Bazlı Override

Adapter'ın global konfigürasyonu (retry policy, circuit breaker, interceptorlar) varsayılan olarak tüm isteklere uygulanır. Tek bir isteğin farklı davranması gerektiğinde RequestBuilder, global config'i tamamen geçersiz kılan per-request override'lar sunar.

Retry Policy

import { RetryPolicies } from '@yildizpay/http-adapter';

// Override: bu istek için farklı bir policy kullan
const request = new RequestBuilder('https://api.example.com')
  .setEndpoint('/v1/payments')
  .withRetryPolicy(RetryPolicies.decorrelatedJitter(5))
  .build();

// Disable: global policy'den bağımsız olarak bu istek için retry'ı kapat
const sensitiveRequest = new RequestBuilder('https://api.example.com')
  .setEndpoint('/v1/refunds')
  .withoutRetry()
  .build();

Circuit Breaker

// Override: bu istek için ayrı bir circuit breaker kullan
const request = new RequestBuilder('https://api.example.com')
  .setEndpoint('/v1/payments')
  .withCircuitBreaker(new CircuitBreaker({ failureThreshold: 3 }))
  .build();

// Bypass: bu istek için circuit breaker'ı tamamen atla
const probeRequest = new RequestBuilder('https://api.example.com')
  .setEndpoint('/health')
  .withoutCircuitBreaker()
  .build();

Interceptor Hariç Tutma

// Bir class'ın tüm instance'larını hariç tut (örn. hassas isteklerde loglama)
const request = new RequestBuilder('https://api.example.com')
  .setEndpoint('/v1/payments')
  .withoutInterceptor(LoggingInterceptor)
  .build();

// Belirli bir instance'ı hariç tut (aynı class'tan birden fazla instance varsa)
const loggingInterceptor = new LoggingInterceptor('payments');

const request = new RequestBuilder('https://api.example.com')
  .setEndpoint('/v1/payments')
  .withoutInterceptorInstance(loggingInterceptor)
  .build();

Test Araçları

@yildizpay/http-adapter, production bundle'ını şişirmeden kullanabileceğiniz hazır test double'ları, spy'lar ve noop yardımcıları içeren ayrı bir testing sub-path'i ile gelir:

import {
  MockHttpAdapter,
  MockHttpClient,
  NoopInterceptor,
  SpyInterceptor,
  SpyObserver,
} from '@yildizpay/http-adapter/testing';

MockHttpAdapter

HttpAdapterContract interface'ini implemente eden, gerçek HTTP çağrısı yapmadan response'ları kontrol etmenize olanak tanıyan tam özellikli bir bellek içi test double'ı.

Response Yapılandırma

const adapter = new MockHttpAdapter();

// Her çağrı için aynı response
adapter.mockResolvedValue({ STATUS: 'SUCCESS', ORDER_ID: '123' });

// Her çağrı için aynı hata
adapter.mockRejectedValue(new ServiceUnavailableException(...));

// FIFO sırasıyla tüketilen tek seferlik response'lar; kuyruk bitince fallback'e düşer
adapter
  .mockResolvedOnce({ STATUS: 'PENDING' })
  .mockResolvedOnce({ STATUS: 'SUCCESS' })
  .mockResolvedValue({ STATUS: 'UNKNOWN' });   // fallback

// Özel factory — tam Request nesnesini alır
adapter.mockImplementation((request) => ({
  STATUS: request.body?.type === 'REFUND' ? 'REFUNDED' : 'SUCCESS',
}));

Endpoint Bazlı Mocking

onEndpoint() ile response'ları ve assertion'ları tek bir path'e kilitleyebilirsiniz. Endpoint response'ları global response'lardan önceliklidir.

adapter.onEndpoint('/api/payments').mockResolvedValue({ STATUS: 'SUCCESS' });
adapter.onEndpoint('/api/refunds').mockRejectedValue(new NotFoundException(...));

// Endpoint başına tek seferlik kuyruk
adapter.onEndpoint('/api/payments')
  .mockResolvedOnce({ STATUS: 'PENDING' })
  .mockResolvedValue({ STATUS: 'SUCCESS' });

Assertion'lar

// Test edilen kod çalıştıktan sonra:
adapter.assertCalledTimes(2);
adapter.assertCalledWith('/api/payments', { method: HttpMethod.POST });
adapter.assertCalledWithBody(0, { AMOUNT: '100', CURRENCY: 'TRY' });
adapter.assertNthCalledWith(1, '/api/payments');
adapter.assertLastCalledWith('/api/refunds');
adapter.assertCallOrder('/api/payments', '/api/refunds');
adapter.assertNotCalled();

// Kısayol getter'lar
adapter.callCount;      // number
adapter.firstCall;      // Request | undefined
adapter.lastCall;       // Request | undefined
adapter.wasCalled();    // boolean
adapter.wasNotCalled(); // boolean

Endpoint scope'ları da aynı assertion API'sini kendi path'leriyle sınırlı olarak sunar:

const scope = adapter.onEndpoint('/api/payments');
scope.assertCalledTimes(1);
scope.assertCalledWith({ body: { MERCHANT_ID: 'M001' } });
scope.wasCalled();

RequestMatcher — Kısmi Request Eşleştirme

Tüm assertCalledWith varyantları, method, body, headers ve queryParams üzerinde derin kısmi eşleştirme için opsiyonel bir RequestMatcher kabul eder. Yalnızca belirttiğiniz alanlar kontrol edilir; gerçek request'teki fazladan alanlar göz ardı edilir.

adapter.assertCalledWith('/api/payments', {
  method: HttpMethod.POST,
  body: { AMOUNT: '100' },              // gerçek body'deki fazladan key'ler göz ardı edilir
  headers: { 'x-merchant-id': 'M001' },
});

Body eşleştirmesi derin kısmi eşitlik kullanır — iç içe nesneler kısmen eşleştirilir, NaN Object.is ile doğru şekilde ele alınır, Date instance'ları değer olarak karşılaştırılır ve array'ler düz nesnelerle karıştırılmaz.

Strict Mode

Strict mode, global bir default yapılandırılmış olsa bile kayıt dışı bir endpoint'e çağrı yapıldığında anında hata fırlatır. Testlerde beklenmedik HTTP çağrılarını yakalamak için kullanışlıdır.

const adapter = new MockHttpAdapter({ strict: true });
adapter.onEndpoint('/api/payments').mockResolvedValue({ STATUS: 'SUCCESS' });

// '/api/users' kayıtlı olmadığından anında hata fırlatır
await adapter.send(request);

Reset

reset(), mevcut onEndpoint() referanslarını geçersiz kılmadan tüm çağrıları, kuyruğu ve varsayılan davranışları temizler.

beforeEach(() => adapter.reset());

MockHttpClient

HttpClientContract transport katmanı için daha düşük seviyeli bir test double'ı. Tam adapter pipeline'ı yerine özel HttpClient wrapper'larını test ederken kullanılır. MockHttpAdapter ile aynı kuyruk API'sine ve assertion yardımcılarına sahiptir.

const client = new MockHttpClient();
client.mockResolvedValue({ data: { id: 1 }, status: 200, headers: {} });

const result = await client.request(config);
client.assertCalledTimes(1);
client.assertCalledWith({ method: HttpMethod.POST });

Noop Yardımcılar

Herhangi bir yan etki olmadan bir contract'ı karşılayan pass-through implementasyonlar. Bir hook sağlanması zorunlu olduğunda ancak davranışı test için önem taşımadığında kullanılır.

| Sınıf | İmplemente Ettiği | |---|---| | NoopInterceptor | Tüm dört HttpInterceptor hook'u — her değeri değiştirmeden döndürür | | NoopObserver | Tüm HttpAdapterObserver hook'ları — boş metodlar | | NoopCircuitBreakerObserver | Tüm CircuitBreakerObserver hook'ları — boş metodlar |

const adapter = HttpAdapter.builder()
  .withInterceptor(new NoopInterceptor())
  .withObserver(new NoopObserver())
  .build();

Spy Yardımcılar

Her çağrıyı kaydeder ve değerleri değiştirmeden iletir. Tam request pipeline'ını mock'lamadan bir hook'un çağrıldığını doğrulamanız gerektiğinde kullanın.

const interceptorSpy = new SpyInterceptor();
const observerSpy = new SpyObserver();

const adapter = HttpAdapter.builder()
  .withInterceptor(interceptorSpy)
  .withObserver(observerSpy)
  .build();

await adapter.send(request);

// SpyInterceptor
expect(interceptorSpy.requestCalls).toHaveLength(1);
expect(interceptorSpy.responseCalls).toHaveLength(1);
expect(interceptorSpy.errorCalls).toHaveLength(0);

// SpyObserver
expect(observerSpy.requestStartCalls).toHaveLength(1);
expect(observerSpy.successCalls[0].durationMs).toBeGreaterThan(0);

// Testler arasında sıfırlama
interceptorSpy.reset();
observerSpy.reset();

| Spy | Kayıt Dizileri | |---|---| | SpyInterceptor | requestCalls, responseCalls, responseValidatedCalls, errorCalls | | SpyObserver | requestStartCalls, successCalls, failureCalls, retryCalls |

Katkıda Bulunma

Katkılarınızı her zaman bekliyoruz! Lütfen bir Pull Request göndermekten çekinmeyin.

Lisans

Bu proje MIT Lisansı altında lisanslanmıştır.