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

@bpits/results-ngx

v2.0.2

Published

A robust Angular library for type-safe API result handling and form validation, designed to work seamlessly with the [BPITS.Results](https://www.nuget.org/packages/BPITS.Results) .NET library.

Readme

@bpits/results-ngx

A robust Angular library for type-safe API result handling and form validation, designed to work seamlessly with the BPITS.Results .NET library.

Table of Contents

Overview

@bpits/results-ngx provides a comprehensive solution for handling API responses and form validation in Angular applications. It offers:

  • Type-safe API responses with BaseApiResult<T, TStatusCode>
  • Integrated form validation that combines Angular Reactive Forms with server-side validation
  • Automatic error handling and display
  • Multi-step form support with validation state management
  • Request cancellation capabilities
  • Consistent error patterns across your application

Installation

npm install @bpits/results-ngx

Quick Start

1. Define Your Status Code Enum

Create an enum that matches your backend status codes. Important: Your enum must include specific values that the library requires to automatically handle network errors and unexpected responses:

// api/app-status-code.ts
export enum AppStatusCode {
  Ok = 1,
  GenericFailure = 2,
  BadRequest = 3,
  Unauthorized = 4,
  NotFound = 5,

  // These client-only status codes are REQUIRED by the library
  // for automatic error handling:
  RequestCancelled = 65533,  // When requests are cancelled
  UnexpectedFormat = 65534,  // When responses don't match expected format
  ServerUnreachable = 65535, // When network requests fail
}

The library automatically handles network failures, request cancellations, and malformed responses behind the scenes. It needs to know which status codes to use for these scenarios, which is where the ICustomStatusCodeProvider (next step) comes in to create the mapping.

2. Create Your API Types and Client

Create your app-specific types and API client:

// api/status-code-provider.ts
import { ICustomStatusCodeProvider } from '@bpits/results-ngx';
import { AppStatusCode } from './app-status-code';

export class AppStatusCodeProvider implements ICustomStatusCodeProvider<AppStatusCode> {
  readonly serverUnreachable = AppStatusCode.ServerUnreachable;
  readonly unexpectedFormat = AppStatusCode.UnexpectedFormat;
  readonly requestCancelled = AppStatusCode.RequestCancelled;
  readonly badRequest = AppStatusCode.BadRequest;
  readonly authenticationTokenInvalid = AppStatusCode.Unauthorized;
  readonly genericFailure = AppStatusCode.GenericFailure;
}

The ICustomStatusCodeProvider creates a bridge between your enum values and the library's automatic error handling. When network requests fail or responses are malformed, the library uses these mappings to set the appropriate status code.

// api/app-api-result.ts
import { BaseApiResult } from '@bpits/results-ngx';
import { AppStatusCode } from './app-status-code';

export type AppApiResult<T> = BaseApiResult<T, AppStatusCode>;

This type alias eliminates boilerplate throughout your application. Instead of writing BaseApiResult<User, AppStatusCode> everywhere, you can write AppApiResult<User>. The types remain fully interchangeable while dramatically improving readability.

// api/app-api-client.ts
import { HttpClient } from '@angular/common/http';
import { ApiClient } from '@bpits/results-ngx';
import { AppStatusCode } from './app-status-code';
import { AppStatusCodeProvider } from './status-code-provider';

export class AppApiClient extends ApiClient<AppStatusCode> {
  constructor(http: HttpClient) {
    super(http, new AppStatusCodeProvider(), 'https://api.your-domain.com/');
  }
}

Extending ApiClient bakes in your status code enum and provider, eliminating the need to specify these generics repeatedly. This creates a clean, app-specific API for making HTTP requests.

3. Build Your Services

Create services that use your API client. For best practice, define type guards for all your models - the library automatically validates responses using these guards:

// services/user.service.ts
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AppApiClient } from '../api/app-api-client';
import { AppApiResult } from '../api/app-api-result';
import { AppStatusCode } from '../api/app-status-code';

export interface User {
  id: string;
  name: string;
  email: string;
}

export interface CreateUserRequest {
  name: string;
  email: string;
}

// Type guard for User - crucial for automatic response validation
function isUser(obj: unknown): obj is User {
  if (!obj || typeof obj !== 'object') return false;
  const user = obj as Record<string, unknown>;
  return typeof user.id === 'string' && 
         typeof user.name === 'string' && 
         typeof user.email === 'string';
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private readonly _api = inject(AppApiClient);

  async getUserAsync(
    userId: string,
    cancelRequest$?: Observable<unknown>
  ): Promise<AppApiResult<User>> {
    try {
      const url = `users/${encodeURIComponent(userId)}`;
      return await this._api.getAsync(url, isUser, undefined, cancelRequest$);
    } catch (err) {
      console.error("Failed to get user!", err);
      return this._api.handleRequestError(err);
    }
  }

  async createUserAsync(
    request: CreateUserRequest,
    cancelRequest$?: Observable<unknown>
  ): Promise<AppApiResult<User>> {
    try {
      return await this._api.postAsync('users', request, isUser, undefined, cancelRequest$);
    } catch (err) {
      console.error("Failed to create user!", err);
      return this._api.handleRequestError(err);
    }
  }
}

The library automatically calls your type guard on every successful response. If the server returns data that doesn't match your expected structure, the library automatically returns an AppApiResult with statusCode: AppStatusCode.UnexpectedFormat. This protects your application from malformed API responses and provides additional type safety beyond TypeScript's compile-time checks.

4. Create Your Form Managers

Create app-specific form managers that eliminate boilerplate and automatically connect Angular Reactive Forms with API validation:

// forms/app-form-managers.ts
import { 
  BaseApiValidatedFormManager, 
  BaseApiValidatedStepFormManager,
  GenericFormGroup 
} from '@bpits/results-ngx';
import { AppStatusCode } from '../api/app-status-code';

export class AppApiValidatedFormManager<TFormGroup extends GenericFormGroup<TFormGroup>>
  extends BaseApiValidatedFormManager<TFormGroup, AppStatusCode> {
  // Inherit base behaviour and extend/override if necessary
  // You can add app-specific form validation logic here
}

export class AppApiValidatedStepFormManager<TFormGroup extends GenericFormGroup<TFormGroup>> 
  extends BaseApiValidatedStepFormManager<TFormGroup, AppStatusCode> {
  // Inherit base behaviour and extend/override if necessary
  // You can add app-specific step form logic here
}

These form managers provide several key benefits: they reduce boilerplate by eliminating repetitive validation checking code, automatically integrate with API responses so server validation errors appear on the correct form fields when you call applyApiValidationErrors(apiResult), seamlessly blend client-side (Angular Validators) and server-side validation errors, and bake in your status code enum for better type safety.

5. Create Forms with Validation

Use the form managers and FormField component for integrated validation. The FormField component automatically displays validation errors from both Angular Reactive Forms and API responses:

// components/user-form.component.ts
import { Component, inject } from '@angular/core';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { FormFieldComponent } from '@bpits/results-ngx';
import { AppApiValidatedFormManager } from '../forms/app-form-managers';
import { UserService, CreateUserRequest } from '../services/user.service';
import { AppStatusCode } from '../api/app-status-code';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [ ReactiveFormsModule, FormFieldComponent ],
  template: `
    <form [formGroup]="formGroup" (ngSubmit)="submitAsync()">
      <app-form-field 
        field="name"
        [formManager]="formManager"
        [formErrorMessages]="{required: 'Name is required'}">
        <label for="name">Name *</label>
        <input 
          id="name" 
          formControlName="name"
          [class]="formManager.getInvalidFieldClasses('name')" />
      </app-form-field>

      <app-form-field 
        field="email"
        [formManager]="formManager"
        [formErrorMessages]="{required: 'Email is required', email: 'Invalid email format'}">
        <label for="email">Email *</label>
        <input 
          id="email" 
          type="email"
          formControlName="email"
          [class]="formManager.getInvalidFieldClasses('email')" />
      </app-form-field>

      <button type="submit" [disabled]="formManager.isSubmitted && formGroup.invalid">
        Create User
      </button>
    </form>
  `
})
export class UserFormComponent {
  private readonly _userService = inject(UserService);
  private readonly _formBuilder = inject(FormBuilder);

  public readonly formManager = new AppApiValidatedFormManager(
    this._formBuilder.group({
      name: new FormControl<string>('', [ Validators.required ]),
      email: new FormControl<string>('', [ Validators.required, Validators.email ])
    })
  );

  public get formGroup() {
    return this.formManager.formGroup;
  }

  public async submitAsync() {
    if (!this.formManager.validate()) {
      return; // Client-side validation failed
    }

    const formValue = this.formGroup.value;
    const request: CreateUserRequest = {
      name: formValue.name!,
      email: formValue.email!
    };

    const result = await this._userService.createUserAsync(request);

    if (result.statusCode === AppStatusCode.Ok) {
      // Handle success
      console.log('User created:', result.value);
      this.formManager.reset();
    } else if (result.statusCode === AppStatusCode.BadRequest) {
      // This is the magic: server validation errors automatically 
      // appear on the correct form fields!
      this.formManager.applyApiValidationErrors(result);
    } else {
      // Handle other errors
      console.error('Failed to create user:', result.errorMessage);
    }
  }
}

How the FormField component works: The component requires a form manager parameter and automatically shows validation errors from three sources: Angular Validators (required, email, etc.) with your custom error messages, API validation errors from result.errorDetails when you call applyApiValidationErrors(), and global form-level validation errors. Field-specific errors take precedence over global errors, and you just need to wrap your form inputs with <app-form-field>

  • errors appear automatically with zero additional configuration.

The key insight here is that when your server returns validation errors in the errorDetails object (like {"email": ["Email address is already in use"]}), calling formManager.applyApiValidationErrors(result) makes those errors automatically appear below the correct form fields. No manual error handling required!

Core Concepts

BaseApiResult

The core type for all API responses. You should create your own type alias:

// Your app-specific type
export type AppApiResult<T> = BaseApiResult<T, AppStatusCode>;

// Base type structure
type BaseApiResult<T, TResultStatusEnum> = {
  statusCode: TResultStatusEnum;
  value: T | null;
  errorMessage: string | null;
  errorDetails: Record<string, string[]> | null;
}

Properties:

  • statusCode: Enum value indicating the result status
  • value: The actual data (null for failures)
  • errorMessage: General error message
  • errorDetails: Field-specific validation errors

ApiClient

Base class for making HTTP requests with automatic result parsing. You should extend this with your own implementation:

// Your app-specific API client
export class AppApiClient extends ApiClient<AppStatusCode> {
  constructor(http: HttpClient) {
    super(http, new AppStatusCodeProvider(), 'https://api.your-domain.com/');
  }
}

// Base class methods available:
abstract class ApiClient<TResultStatusEnum> {
  // HTTP methods
  getAsync<T>(url, typeGuard?, options?, cancelRequest$?): Promise<BaseApiResult<T, TResultStatusEnum>>

  postAsync<T>(url, payload, typeGuard?, options?, cancelRequest$?): Promise<BaseApiResult<T, TResultStatusEnum>>

  patchAsync<T>(url, payload, typeGuard?, options?, cancelRequest$?): Promise<BaseApiResult<T, TResultStatusEnum>>

  deleteAsync<T>(url, typeGuard?, options?, cancelRequest$?): Promise<BaseApiResult<T, TResultStatusEnum>>

  // Error handling
  handleRequestError<T>(error): BaseApiResult<T, TResultStatusEnum>
}

Form Managers

You should create app-specific form managers that extend the base classes:

// Your app-specific form managers
export class AppApiValidatedFormManager<TFormGroup extends GenericFormGroup<TFormGroup>>
  extends BaseApiValidatedFormManager<TFormGroup, AppStatusCode> {
  // Add your app-specific validation logic here
}

export class AppApiValidatedStepFormManager<TFormGroup extends GenericFormGroup<TFormGroup>>
  extends BaseApiValidatedStepFormManager<TFormGroup, AppStatusCode> {
  // Add your app-specific step form logic here
}

BaseApiValidatedFormManager: Integrates Angular Reactive Forms with API validation
BaseApiValidatedStepFormManager: Extends the base manager for multi-step forms

API Client Usage

Making HTTP Requests

// GET request with type validation
const userResult = await apiClient.getAsync('users/123', isUser);

// POST request with payload
const createResult = await apiClient.postAsync('users', createRequest, isUser);

// PATCH request
const updateResult = await apiClient.patchAsync('users/123', updateData, isUser);

// DELETE request
const deleteResult = await apiClient.deleteAsync('users/123');

Type Guards and Validation

Type guards ensure response data matches expected types:

function isUser(obj: unknown): obj is User {
  if (!obj || typeof obj !== 'object') return false;
  const user = obj as Record<string, unknown>;
  return typeof user.id === 'string' &&
    typeof user.name === 'string' &&
    typeof user.email === 'string';
}

// Use with API calls
const result = await apiClient.getAsync('users/123', isUser);
if (result.statusCode === AppStatusCode.Ok) {
  // result.value is guaranteed to be User type
  console.log(result.value.name);
}

Request Cancellation

Cancel requests using observables:

import { Subject, takeUntil } from 'rxjs';

const cancelSubject = new Subject<void>();

// Start request
const resultPromise = apiClient.getAsync('users', isUserArray, undefined, cancelSubject);

// Cancel after 5 seconds
setTimeout(() => cancelSubject.next(), 5000);

const result = await resultPromise;
if (result.statusCode === AppStatusCode.RequestCancelled) {
  console.log('Request was cancelled');
}

Error Handling

async function handleApiCall() {
  const result = await userService.getUserAsync('123');

  switch (result.statusCode) {
    case AppStatusCode.Ok:
      // Handle success
      return result.value;

    case AppStatusCode.NotFound:
      // Handle not found
      showMessage('User not found');
      break;

    case AppStatusCode.ServerUnreachable:
      // Handle network issues
      showMessage('Server unreachable. Please try again.');
      break;

    default:
      // Handle unexpected errors
      showMessage('An unexpected error occurred');
      break;
  }
}

Form Management

Basic Form Validation

export class MyFormComponent {
  public readonly formManager = new AppApiValidatedFormManager(
    this.formBuilder.group({
      name: [ '', Validators.required ],
      email: [ '', [ Validators.required, Validators.email ] ]
    })
  );

  async submit() {
    if (!this.formManager.validate()) {
      return; // Form has client-side validation errors
    }

    // Form is valid, proceed with submission
    const result = await this.submitToApi(this.formManager.formGroup.value);

    if (result.statusCode === AppStatusCode.BadRequest) {
      // Apply server validation errors
      this.formManager.applyApiValidationErrors(result);
    }
  }
}

API Validation Integration

Server validation errors are automatically applied to form fields:

// Server returns validation errors
const result = {
  statusCode: AppStatusCode.BadRequest,
  value: null,
  errorMessage: "Validation failed",
  errorDetails: {
    "email": [ "Email address is already in use" ],
    "name": [ "Name must be at least 2 characters" ]
  }
};

// Apply to form
this.formManager.applyApiValidationErrors(result);

// Errors are now displayed in FormFieldComponents

Multi-Step Forms

import { ControlStepMap } from '@bpits/results-ngx';
import { AppApiValidatedStepFormManager } from '../forms/app-form-managers';

const stepMap = new ControlStepMap({
  name: 1,
  email: 1,
  address: 2,
  phone: 2,
  preferences: 3
});

const stepFormManager = new AppApiValidatedStepFormManager(formGroup, stepMap);

// Validate current step before moving to next
const canMoveToNext = stepFormManager.validateCanMoveToNextStep(currentStep);

// Find earliest step with validation errors
const errorStep = stepFormManager.findEarliestStepWithValidationError(apiResult);

Form Field Component

The FormFieldComponent automatically displays validation errors:


<app-form-field
  field="email"
  [formManager]="formManager"
  [formErrorMessages]="{
    required: 'Email is required',
    email: 'Please enter a valid email'
  }">
  <label for="email">Email Address</label>
  <input
    id="email"
    type="email"
    formControlName="email"
    [class]="formManager.getInvalidFieldClasses('email')" />
</app-form-field>

Advanced Patterns

Custom Error Handling

Create reusable error handling utilities:

// utils/error-handler.ts
import { AppApiResult } from '../api/app-api-result';

export function handleCommonApiErrors(
  result: AppApiResult<unknown>,
  messageService: MessageService
)
{
  switch (result.statusCode) {
    case AppStatusCode.Unauthorized:
      messageService.showError('You are not authorized to perform this action');
      break;
    case AppStatusCode.NotFound:
      messageService.showError('The requested resource was not found');
      break;
    case AppStatusCode.ServerUnreachable:
      messageService.showError('Unable to connect to server. Please try again.');
      break;
    default:
      messageService.showError('An unexpected error occurred');
      break;
  }
}

// Use in components
const result = await this.userService.createUserAsync(request);
if (result.statusCode !== AppStatusCode.Ok) {
  handleCommonApiErrors(result, this.messageService);
}

Form Change Tracking

Track and restore form changes:

export class FormComponent implements OnInit, OnDestroy {
  ngOnInit() {
    // Enable change tracking
    this.formManager.trackFormChanges(true);

    // Subscribe to changes
    this.formManager.onFormValueChanged$.subscribe(event => {
      if (event.hasChanged) {
        console.log('Form has unsaved changes');
      }
    });
  }

  ngOnDestroy() {
    this.formManager.onDestroy();
  }

  restoreChanges() {
    this.formManager.restoreTrackedChanges();
  }

  resetChanges() {
    this.formManager.resetTrackedChanges();
  }
}

Validation Message Customization

Use the pipe directly for custom validation display:

// In component template
{
  {
    apiResult | apiValidationMessage
  :
    'fieldName'
  }
}

// In component code
import { ApiValidationMessagePipe } from '@bpits/results-ngx';

const hasError = ApiValidationMessagePipe.hasError(apiResult, 'email');
const errors = ApiValidationMessagePipe.getErrors(apiResult, 'email');

Best Practices

1. Consistent Status Code Definitions

Keep your frontend status codes synchronized with your backend:

// Ensure these match your backend enum exactly
export enum AppStatusCode {
  Ok = 1,
  GenericFailure = 2,
  BadRequest = 3,
  // ... other codes
}

2. Type Guards for All API Responses

Always use type guards to ensure type safety:

function isUserArray(obj: unknown): obj is User[] {
  return Array.isArray(obj) && obj.every(isUser);
}

3. Centralized Error Handling

Create utility functions for common error scenarios:

export const ApiErrorHandlers = {
  handleStandardErrors: (result: AppApiResult<unknown>) => {
    // Handle common patterns
  },

  handleFormErrors: (result: AppApiResult<unknown>, formManager: AppApiValidatedFormManager<any>) => {
    if (result.statusCode === AppStatusCode.BadRequest) {
      formManager.applyApiValidationErrors(result);
      return true;
    }
    return false;
  }
};

4. Proper Cleanup

Always clean up form managers and subscriptions:

export class MyComponent implements OnDestroy {
  ngOnDestroy() {
    this.formManager.onDestroy();
  }
}

5. Request Cancellation for Navigation

Cancel pending requests when users navigate away:

export class MyComponent implements OnDestroy {
  private readonly destroy$ = new Subject<void>();

  async loadData() {
    const result = await this.apiClient.getAsync('data', typeGuard, undefined, this.destroy$);
    // Handle result
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Integration with Backend

This library is designed to work seamlessly with the BPITS.Results .NET library. When your backend returns ApiResult<T> responses, they will be automatically parsed and validated by the frontend.

Backend (.NET):

[HttpPost]
public async Task<ApiResult<UserDto>> CreateUser([FromBody] CreateUserRequest request)
{
    var result = await _userService.CreateUserAsync(request);
    return ApiResult.FromServiceResult(result.MapValue(u => u?.ToDto()));
}

Frontend (Angular):

const result = await this.userService.createUserAsync(request);
// result is automatically typed as AppApiResult<User>

The status codes, error messages, and validation details flow seamlessly from backend to frontend, providing a consistent developer experience across your entire application stack.