New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@voyagerpoland/proxy

Package Overview
Dependencies
Maintainers
2
Versions
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@voyagerpoland/proxy

HTTP service proxy with Result Pattern integration for Angular

latest
Source
npmnpm
Version
0.1.0
Version published
Maintainers
2
Created
Source

@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

EventFieldsWhen
RequestStartEventmethod, url, startedAtBefore first HTTP attempt
RequestCompleteEventmethod, url, durationMs, retryAttemptsAfter successful response (including retries)
RequestFailEventmethod, url, durationMs, retryAttempts, errorAfter all retries exhausted
RetryAttemptEventmethod, url, attempt, delayMs, errorOn each retry attempt
CircuitBreakerStateChangedEventoldState, newState, failureCount, lastErrorWhen 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

StepTimeWho
Add endpoint in C#5 minBackend developer
swagger.json regenerates automatically0 minCI/CD
npm run api:generate regenerates DTO0 minnpm script
Add 1 method in service facade (3 lines)1 minFrontend developer
Total~6 min

Alternative generators

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

GeneratorAngular HttpClientDTORequiresNotes
ng-openapi-genYesYesNode.jsRecommended, lightweight
openapi-generatorYesYesJVMMore mature, heavier
NSwagYesYes.NET SDKBest .NET integration

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

HTTP Status Code Mapping

StatusErrorTypeClassification
0UnavailableTransient
400ValidationBusiness
401UnauthorizedBusiness*
403PermissionBusiness
404NotFoundBusiness
409ConflictBusiness
422BusinessBusiness
429TooManyRequestsTransient
503UnavailableTransient
504TimeoutTransient
OtherUnexpectedInfrastructure

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

License

MIT

FAQs

Package last updated on 07 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts