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

@voyagerpoland/proxy

v0.1.0

Published

HTTP service proxy with Result Pattern integration for Angular

Downloads

118

Readme

@voyagerpoland/proxy

HTTP service proxy with Result Pattern integration for Angular. Wraps Angular HttpClient calls into ResultAsync<T> with built-in retry and circuit breaker support.

Installation

npm install @voyagerpoland/proxy

Peer dependencies

npm install @voyagerpoland/results neverthrow rxjs @angular/common

API Reference

fromHttp<T>(observable, errorMapper?)

Converts an Angular HTTP Observable<T> into a ResultAsync<T>.

import { fromHttp } from '@voyagerpoland/proxy';

const result = await fromHttp(httpClient.get<User>('/api/users/1'));
// Result<User> — Ok({ id: 1, name: 'Alice' }) or Err(AppError)

fromHttpError(error)

Converts an HttpErrorResponse (or compatible object) to an AppError with HTTP context.

import { fromHttpError } from '@voyagerpoland/proxy';

const appError = fromHttpError(httpErrorResponse);
// appError.type === ErrorType.NotFound (for 404)
// appError.context.get('url') === '/api/users/42'
// appError.context.get('statusText') === 'Not Found'

ServiceProxy (abstract class)

Abstract base class for Angular HTTP service proxies with built-in resilience. Retry and circuit breaker are enabled by default — no configuration needed.

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ServiceProxy } from '@voyagerpoland/proxy';
import type { ResultAsync } from '@voyagerpoland/results';
import { API_BASE_URL } from '../api-config';

// DTO: automatically generated from OpenAPI
import type { Order, CreateOrderRequest, OrderListResponse } from '../generated/models';

@Injectable({ providedIn: 'root' })
class OrderService extends ServiceProxy {
  constructor() {
    super(inject(HttpClient), inject(API_BASE_URL));
  }

  getOrder(id: number): ResultAsync<Order> {
    return this.get<Order>(`/orders/${id}`);
  }

  listOrders(status?: string, limit?: number): ResultAsync<OrderListResponse> {
    return this.get<OrderListResponse>('/orders', {
      status,
      limit: limit?.toString(),
    });
  }

  createOrder(request: CreateOrderRequest): ResultAsync<Order> {
    return this.post<Order>('/orders', request);
  }

  deleteOrder(id: number): ResultAsync {
    return this.del(`/orders/${id}`);
  }
}

3 lines per endpoint. DTO automatic from OpenAPI. Retry + circuit breaker for free.

Using in Angular components

@Component({
  /* ... */
})
class UserComponent {
  private readonly orders = inject(OrderService);

  loadOrder(id: number): void {
    this.orders.getOrder(id).match(
      (order) => this.order.set(order),
      (error) => this.error.set(error.message),
    );
  }
}

ServiceProxyOptions

All fields are optional — when omitted, sensible defaults are used:

interface ServiceProxyOptions {
  maxRetryAttempts?: number; // default: 3
  retryBaseDelayMs?: number; // default: 1000
  circuitBreakerThreshold?: number; // default: 5
  circuitBreakerTimeoutMs?: number; // default: 30000
  authHandler?: AuthHandler; // default: undefined (no auth handling)
}

Custom configuration example:

super(inject(HttpClient), inject(API_BASE_URL), {
  maxRetryAttempts: 5,
  retryBaseDelayMs: 500,
  circuitBreakerThreshold: 10,
  circuitBreakerTimeoutMs: 60_000,
});

AuthHandler

Optional strategy for reacting to HTTP 401 (Unauthorized) errors. When configured, ServiceProxy calls onUnauthorized() before returning an error, giving the handler a chance to refresh credentials and retry the request.

interface AuthHandler {
  onUnauthorized(): Promise<{ retry: boolean }>;
}

Flow:

Request → 401
  → authHandler defined?
    → NO: Err(Unauthorized) immediately (default behavior)
    → YES: await authHandler.onUnauthorized()
      → { retry: true }  → retry request once (with refreshed credentials)
        → success → Ok(T)
        → 401 again → Err(Unauthorized) — prevents infinite loop
      → { retry: false } → Err(Unauthorized)

Key design decisions:

  • Max 1 retry after auth refresh — prevents infinite loops
  • Only 401, not 403 — Permission errors are not token problems
  • Handler owns coordination — single-refresh guard is the handler's responsibility
  • Framework-agnostic — works with BFF, token refresh, or any auth strategy

Example: BFF (Duende IdentityServer)

class BffAuthHandler implements AuthHandler {
  constructor(private window: Window) {}

  async onUnauthorized(): Promise<{ retry: boolean }> {
    const returnUrl = this.window.location.pathname;
    this.window.location.href = `/bff/login?returnUrl=${encodeURIComponent(returnUrl)}`;
    // Redirect leaves the SPA — never resolves
    return { retry: false };
  }
}

@Injectable({ providedIn: 'root' })
class OrderService extends ServiceProxy {
  constructor() {
    super(inject(HttpClient), inject(API_BASE_URL), {
      authHandler: new BffAuthHandler(window),
    });
  }
}

Example: Token refresh (legacy)

class TokenAuthHandler implements AuthHandler {
  private refreshInProgress: Promise<{ retry: boolean }> | null = null;

  constructor(
    private authService: AuthService,
    private router: Router,
  ) {}

  async onUnauthorized(): Promise<{ retry: boolean }> {
    // Single-refresh guard — concurrent 401s share one refresh
    if (this.refreshInProgress) {
      return this.refreshInProgress;
    }

    this.refreshInProgress = this.doRefresh();
    try {
      return await this.refreshInProgress;
    } finally {
      this.refreshInProgress = null;
    }
  }

  private async doRefresh(): Promise<{ retry: boolean }> {
    try {
      await this.authService.refreshToken();
      return { retry: true };
    } catch {
      this.router.navigate(['/login'], {
        queryParams: { returnUrl: this.router.url },
      });
      return { retry: false };
    }
  }
}

After a successful refresh, ServiceProxy retries the original HttpClient call. Angular interceptors run again on the new request, automatically attaching the refreshed Bearer token.

ProxyDiagnostics

Optional callback-based telemetry hooks. When configured, ServiceProxy emits events for request lifecycle, retry attempts, and circuit breaker state changes. Vendor-neutral — wire to Application Insights, OpenTelemetry, or any provider.

interface ProxyDiagnostics {
  onRequestStart?: (event: RequestStartEvent) => void;
  onRequestComplete?: (event: RequestCompleteEvent) => void;
  onRequestFail?: (event: RequestFailEvent) => void;
  onRetryAttempt?: (event: RetryAttemptEvent) => void;
  onCircuitBreakerStateChanged?: (event: CircuitBreakerStateChangedEvent) => void;
}

Configuration:

super(inject(HttpClient), inject(API_BASE_URL), {
  diagnostics: {
    onRequestStart: (e) => console.log(`${e.method} ${e.url}`),
    onRequestComplete: (e) =>
      console.log(`${e.method} ${e.url} ${e.durationMs}ms (${e.retryAttempts} retries)`),
    onRequestFail: (e) => console.error(`${e.method} ${e.url} failed: ${e.error.type}`),
    onRetryAttempt: (e) => console.warn(`Retry #${e.attempt} for ${e.url}, delay ${e.delayMs}ms`),
    onCircuitBreakerStateChanged: (e) => console.warn(`Circuit: ${e.oldState} → ${e.newState}`),
  },
});

Event types

| Event | Fields | When | | --------------------------------- | ------------------------------------------------------- | --------------------------------------------- | | RequestStartEvent | method, url, startedAt | Before first HTTP attempt | | RequestCompleteEvent | method, url, durationMs, retryAttempts | After successful response (including retries) | | RequestFailEvent | method, url, durationMs, retryAttempts, error | After all retries exhausted | | RetryAttemptEvent | method, url, attempt, delayMs, error | On each retry attempt | | CircuitBreakerStateChangedEvent | oldState, newState, failureCount, lastError | When circuit breaker transitions |

Example: Application Insights adapter

import { ApplicationInsights } from '@microsoft/applicationinsights-web';
import type { ProxyDiagnostics } from '@voyagerpoland/proxy';

export function createAppInsightsDiagnostics(appInsights: ApplicationInsights): ProxyDiagnostics {
  return {
    onRequestComplete: (event) => {
      appInsights.trackDependencyData({
        id: crypto.randomUUID(),
        name: `${event.method} ${event.url}`,
        duration: event.durationMs,
        success: true,
        responseCode: 200,
        properties: { retryAttempts: String(event.retryAttempts) },
      });
    },
    onRequestFail: (event) => {
      appInsights.trackDependencyData({
        id: crypto.randomUUID(),
        name: `${event.method} ${event.url}`,
        duration: event.durationMs,
        success: false,
        responseCode: Number(event.error.code.replace('HTTP_', '')) || 0,
        properties: {
          retryAttempts: String(event.retryAttempts),
          errorType: event.error.type,
        },
      });
    },
    onRetryAttempt: (event) => {
      appInsights.trackEvent({
        name: 'ServiceProxy.RetryAttempt',
        properties: {
          url: event.url,
          attempt: String(event.attempt),
          errorType: event.error.type,
        },
      });
    },
    onCircuitBreakerStateChanged: (event) => {
      appInsights.trackEvent({
        name: 'ServiceProxy.CircuitBreakerStateChanged',
        properties: {
          oldState: event.oldState,
          newState: event.newState,
          failureCount: String(event.failureCount),
        },
      });
    },
  };
}

composeDiagnostics(...providers)

Combines multiple ProxyDiagnostics providers into one. Each event is forwarded to all providers that define the corresponding callback.

import { composeDiagnostics } from '@voyagerpoland/proxy';
import type { ProxyDiagnostics } from '@voyagerpoland/proxy';

// Provider 1: console logging (dev)
function consoleDiagnostics(): ProxyDiagnostics {
  return {
    onRequestStart: (e) => console.log(`→ ${e.method} ${e.url}`),
    onRequestComplete: (e) => console.log(`✓ ${e.method} ${e.url} ${e.durationMs}ms`),
    onRequestFail: (e) => console.error(`✗ ${e.method} ${e.url} failed: ${e.error.type}`),
    onRetryAttempt: (e) => console.warn(`↻ Retry #${e.attempt} for ${e.url}`),
  };
}

// Provider 2: Application Insights (prod) — see adapter example above

// Compose both
const diagnostics = composeDiagnostics(
  consoleDiagnostics(),
  createAppInsightsDiagnostics(appInsights),
);

super(inject(HttpClient), inject(API_BASE_URL), { diagnostics });

Providers that don't define a callback for a given event are simply skipped. You can compose any number of providers.

Integration Guide: OpenAPI DTO Generation + ServiceProxy

@voyagerpoland/proxy provides the ServiceProxy base class. The consumer project is responsible for generating TypeScript DTOs from the backend's OpenAPI spec. Together they give 3 lines per endpoint with full type safety.

Architecture overview

┌─────────────────────────────────────────────────┐
│  Backend .NET                                    │
│  ASP.NET Core → swagger.json (automatically)     │
└──────────────────────┬──────────────────────────-┘
                       │
                       ▼
┌──────────────────────────────────────────────────┐
│  ng-openapi-gen (build-time)                     │
│  swagger.json → TypeScript DTO (models only)     │
│                                                  │
│  Generated:                                      │
│  - models/order.ts          (interface Order)    │
│  - models/create-order-request.ts                │
│  - models/order-list-response.ts                 │
└──────────────────────┬───────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────┐
│  Service facade (hand-written, ~3 lines/endpoint)│
│                                                  │
│  class OrderService extends ServiceProxy {       │
│    getOrder(id) = this.get<Order>(`/orders/${id}`)│
│  }                                               │
└──────────────────────────────────────────────────┘

Step 1: Install ng-openapi-gen

npm install -D ng-openapi-gen

Step 2: Get the OpenAPI spec

Download swagger.json from the backend (or point to a URL):

curl -o swagger.json https://api.example.com/swagger/v1/swagger.json

Step 3: Configure ng-openapi-gen

Create ng-openapi-gen.json in the project root:

{
  "$schema": "node_modules/ng-openapi-gen/ng-openapi-gen-schema.json",
  "input": "swagger.json",
  "output": "src/app/api/generated",
  "ignoreUnusedModels": false,
  "services": false
}

"services": false — generate only models, not services. Service facades are hand-written with ServiceProxy.

Step 4: Add npm scripts

{
  "scripts": {
    "api:generate": "ng-openapi-gen --config ng-openapi-gen.json",
    "api:update": "curl -o swagger.json https://api.example.com/swagger/v1/swagger.json && npm run api:generate",
    "prebuild": "npm run api:generate"
  }
}

Step 5: Run the generator

npm run api:generate

This produces files under src/app/api/generated/models/:

src/app/api/generated/
  models/
    order.ts                    ← interface Order { id: number; ... }
    create-order-request.ts     ← interface CreateOrderRequest { ... }
    order-list-response.ts      ← interface OrderListResponse { ... }

Step 6: Provide API_BASE_URL token

// src/app/api/api-config.ts
import { InjectionToken } from '@angular/core';

export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
// app.config.ts
import { API_BASE_URL } from './api/api-config';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(), { provide: API_BASE_URL, useValue: 'https://api.example.com' }],
};

Step 7: Write the service facade

// src/app/api/order.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import type { ResultAsync } from '@voyagerpoland/results';
import { ServiceProxy } from '@voyagerpoland/proxy';
import { API_BASE_URL } from './api-config';

// Types generated from OpenAPI (Step 5)
import type { Order, CreateOrderRequest, OrderListResponse } from './generated/models';

@Injectable({ providedIn: 'root' })
export class OrderService extends ServiceProxy {
  constructor() {
    super(inject(HttpClient), inject(API_BASE_URL));
  }

  getOrder(id: number): ResultAsync<Order> {
    return this.get<Order>(`/orders/${id}`);
  }

  listOrders(status?: string, limit?: number): ResultAsync<OrderListResponse> {
    return this.get<OrderListResponse>('/orders', {
      status,
      limit: limit?.toString(),
    });
  }

  createOrder(request: CreateOrderRequest): ResultAsync<Order> {
    return this.post<Order>('/orders', request);
  }

  deleteOrder(id: number): ResultAsync {
    return this.del(`/orders/${id}`);
  }
}

Step 8: Use in components

@Component({
  /* ... */
})
export class OrderListComponent {
  private readonly orderService = inject(OrderService);

  orders = signal<Order[]>([]);
  error = signal<string | null>(null);

  loadOrders(): void {
    this.orderService.listOrders('active', 50).match(
      (response) => this.orders.set(response.items),
      (error) => this.error.set(error.message),
    );
  }
}

Target project structure

src/app/api/
  generated/              ← ng-openapi-gen output (git-ignored or committed)
    models/
      order.ts
      create-order-request.ts
      order-list-response.ts
      ...
  api-config.ts           ← API_BASE_URL injection token
  order.service.ts        ← extends ServiceProxy + generated DTO
  payment.service.ts
  user.service.ts

Cost per new endpoint

| Step | Time | Who | | ---------------------------------------- | ---------- | ------------------ | | Add endpoint in C# | 5 min | Backend developer | | swagger.json regenerates automatically | 0 min | CI/CD | | npm run api:generate regenerates DTO | 0 min | npm script | | Add 1 method in service facade (3 lines) | 1 min | Frontend developer | | Total | ~6 min | |

Alternative generators

If ng-openapi-gen doesn't fit your needs:

| Generator | Angular HttpClient | DTO | Requires | Notes | | --------------------- | ------------------ | --- | -------- | ------------------------ | | ng-openapi-gen | Yes | Yes | Node.js | Recommended, lightweight | | openapi-generator | Yes | Yes | JVM | More mature, heavier | | NSwag | Yes | Yes | .NET SDK | Best .NET integration |

All generators work the same way: generate only models ("services": false), write service facades with ServiceProxy.

HTTP Status Code Mapping

| Status | ErrorType | Classification | | ------ | --------------- | -------------- | | 0 | Unavailable | Transient | | 400 | Validation | Business | | 401 | Unauthorized | Business* | | 403 | Permission | Business | | 404 | NotFound | Business | | 409 | Conflict | Business | | 422 | Business | Business | | 429 | TooManyRequests | Transient | | 503 | Unavailable | Transient | | 504 | Timeout | Transient | | Other | Unexpected | Infrastructure |

* When authHandler is configured, 401 triggers onUnauthorized() before returning Err. See AuthHandler.

License

MIT