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

@assebc/ng-signal-http

v1.0.4

Published

Signal-native HTTP client for Angular. Wraps the native Fetch API and returns Angular signals directly — no toSignal(), no RxJS required.

Readme

ng-signal-http

npm version bundle size license Angular

Signal-native HTTP client for Angular. Wraps the native Fetch API and returns Angular signals directly — no toSignal(), no RxJS required.

Built for the post-zoneless Angular era using only @angular/core primitives.


Why ng-signal-http?

| Feature | @angular/common/http | ng-signal-http | |---|---|---| | Returns | Observable | Signal | | RxJS required | Yes | No | | Loading state | Manual | Built-in | | Error state | Manual | Built-in | | Reactive refetch | Manual (switchMap) | Automatic | | Request cancellation | Manual (takeUntil) | Automatic | | Retry | Manual (retryWhen) | Built-in | | Bundle size | ~25 KB | < 15 KB |

Before

export class UsersComponent {
  private http = inject(HttpClient);
  users = toSignal(this.http.get<User[]>('/api/users'), { initialValue: [] });
  // loading? error? refetch? — manual work.
}

After

export class UsersComponent {
  users = querySignal<User[]>('/api/users');
  // users.data(), users.loading(), users.error(), users.refetch() — done.
}

Install

npm install ng-signal-http

Peer dependencies: @angular/core and @angular/common ≥ 17.


Setup

Call provideSignalHttp() once in app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideSignalHttp } from 'ng-signal-http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideSignalHttp({
      baseUrl: 'https://api.example.com',
      timeout: 10_000,
    }),
  ],
};

Basic usage

GET — querySignal

import { Component } from '@angular/core';
import { querySignal } from 'ng-signal-http';

interface User { id: number; name: string; }

@Component({
  template: `
    @if (user.loading()) { <p>Loading…</p> }
    @if (user.error()) { <p>Error: {{ user.error()?.message }}</p> }
    @if (user.data()) { <p>{{ user.data()?.name }}</p> }
  `,
})
export class UserComponent {
  user = querySignal<User>('/users/1');
}

POST / PUT / PATCH / DELETE — mutationSignal

import { Component } from '@angular/core';
import { mutationSignal } from 'ng-signal-http';

interface CreateUser { name: string; email: string; }
interface User { id: number; name: string; email: string; }

@Component({
  template: `
    <button (click)="submit()" [disabled]="newUser.isPending()">Create</button>
    @if (newUser.data()) { <p>Created id: {{ newUser.data()?.id }}</p> }
    @if (newUser.error()) { <p>{{ newUser.error()?.message }}</p> }
  `,
})
export class CreateUserComponent {
  newUser = mutationSignal<CreateUser, User>(
    (input) => ({ url: '/users', method: 'POST', body: input }),
  );

  submit() {
    this.newUser.mutate({ name: 'Alice', email: '[email protected]' });
  }
}

Reactive queries

querySignal tracks every signal read inside the URL factory and automatically refetches when any of them change:

import { Component, signal } from '@angular/core';
import { querySignal } from 'ng-signal-http';

@Component({
  template: `
    <input type="number" [value]="userId()" (input)="userId.set(+$event.target.value)" />
    @if (user.loading()) { <span>Loading…</span> }
    <p>{{ user.data()?.name }}</p>
  `,
})
export class UserComponent {
  userId = signal(1);

  // Automatically refetches whenever userId() changes.
  user = querySignal<User>(() => `/users/${this.userId()}`);
}

Lazy queries

search = querySignal<Result[]>(() => `/search?q=${this.query()}`, { lazy: true });

onSearch() {
  this.search.refetch();
}

Polling

stats = querySignal('/dashboard/stats', { refetchInterval: 30_000 });

Refetch on focus / reconnect

feed = querySignal('/feed', {
  staleTime: 60_000,        // only refetch if data is older than 60 s
  refetchOnFocus: true,     // refetch when window regains focus (if stale)
  refetchOnReconnect: true, // refetch when network comes back online
});

Mutations

import { mutationSignal } from 'ng-signal-http';

updatePost = mutationSignal<{ id: number; title: string }, Post>(
  ({ id, ...body }) => ({ url: `/posts/${id}`, method: 'PUT', body }),
  {
    onSuccess: (post) => console.log('Updated:', post.title),
    onError: (err) => console.error('Failed:', err.message),
    onSettled: (data, err) => console.log('Done', data, err),
  },
);

// Calling mutate() while a previous request is in flight cancels the previous one.
await this.updatePost.mutate({ id: 1, title: 'New title' });

Interceptors

All hooks are optional and may return a Promise. They run in registration order.

provideSignalHttp({
  baseUrl: 'https://api.example.com',
  interceptors: [
    {
      // Attach an auth token to every request
      request: async (config) => ({
        ...config,
        headers: { ...config.headers, Authorization: `Bearer ${getToken()}` },
      }),

      // Log every response
      response: async (response) => {
        console.log(response.status, response.url);
        return response;
      },

      // Transform or react to errors
      error: async (err) => {
        if (err instanceof HttpError && err.isUnauthorized) {
          await refreshToken();
        }
        return err;
      },
    },
  ],
});

Error handling

Failed requests set the error signal to an HttpError with convenience getters:

import { HttpError, querySignal } from 'ng-signal-http';
import { effect } from '@angular/core';

const post = querySignal<Post>('/posts/1');

effect(() => {
  const err = post.error();
  if (!err) return;
  if (err instanceof HttpError) {
    if (err.isNotFound)     console.log('Not found');
    if (err.isUnauthorized) router.navigate(['/login']);
    if (err.isServerError)  console.error(`Server error ${err.status}`);
  }
});

Retry

// Retry up to 3 times immediately
querySignal('/data', { retry: 3 });

// Custom retry with exponential backoff
querySignal('/data', {
  retry: {
    count: 4,
    delay: (attempt) => 1000 * 2 ** (attempt - 1),
    shouldRetry: (err) => !(err instanceof HttpError && err.isClientError),
  },
});

AbortError (request cancellation or component destroy) is never retried.


Full API reference

provideSignalHttp(config?)

Registers the library. Call once in app.config.ts.

provideSignalHttp(config?: SignalHttpConfig): EnvironmentProviders

| SignalHttpConfig | Type | Description | |---|---|---| | baseUrl | string | Prefix prepended to all relative URLs | | headers | Record<string, string> | Default headers sent with every request | | timeout | number | Global request timeout in ms | | interceptors | HttpInterceptor[] | Request / response / error hooks |


querySignal<T>(url, options?)

querySignal<T>(url: string | UrlFactory, options?: HttpClientOptions<T>): HttpClientResult<T>

UrlFactory: () => string | RequestConfig

Options (HttpClientOptions<T>)

| Option | Type | Default | Description | |---|---|---|---| | initialValue | T | null | Signal value before the first successful fetch | | lazy | boolean | false | Skip the initial fetch; call refetch() manually | | retry | number \| RetryConfig | — | Retry on failure | | staleTime | number | — | Ms after which data is considered stale | | refetchInterval | number | — | Poll interval in ms | | refetchOnFocus | boolean | false | Refetch on window focus (only if stale) | | refetchOnReconnect | boolean | false | Refetch when network reconnects | | onSuccess | (data: T) => void | — | Called after a successful fetch | | onError | (error: Error) => void | — | Called after a failed fetch |

Return value (HttpClientResult<T>)

| Property | Type | Description | |---|---|---| | data | Signal<T \| null> | Response data; null until first success | | loading | Signal<boolean> | true while a request is in flight | | error | Signal<Error \| null> | Last error; cleared when a new fetch starts | | status | Signal<'idle' \| 'loading' \| 'success' \| 'error'> | Explicit state machine value | | isStale | Signal<boolean> | true if data is older than staleTime | | refetch() | () => Promise<void> | Manually trigger a new fetch | | invalidate() | () => void | Mark data as stale without triggering a fetch | | reset() | () => void | Abort in-flight request and restore initial state |


mutationSignal<TInput, TOutput>(factory, options?)

mutationSignal<TInput, TOutput>(
  requestFactory: (input: TInput) => RequestConfig,
  options?: MutationOptions<TInput, TOutput>
): MutationResult<TInput, TOutput>

Options (MutationOptions<TInput, TOutput>)

| Option | Type | Description | |---|---|---| | onSuccess | (data: TOutput, input: TInput) => void | Called on success | | onError | (error: Error, input: TInput) => void | Called on failure | | onSettled | (data: TOutput \| null, error: Error \| null, input: TInput) => void | Called after either outcome |

Return value (MutationResult<TInput, TOutput>)

| Property | Type | Description | |---|---|---| | isPending | Signal<boolean> | true while the request is in flight | | data | Signal<TOutput \| null> | Last successful response | | error | Signal<Error \| null> | Last error | | mutate(input) | (input: TInput) => Promise<TOutput> | Trigger the request | | reset() | () => void | Clear all state |


SignalHttpClient

Injectable service for imperative HTTP calls — guards, resolvers, one-off effects.

@Injectable({ providedIn: 'root' })
class SignalHttpClient {
  get<T>(url: string, options?: Partial<RequestConfig>): Signal<T | null>
  post<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>
  put<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>
  patch<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>
  delete<T>(url: string, options?: Partial<RequestConfig>): Signal<T | null>
  executeRequest<T>(config: RequestConfig): Promise<T>
}

get / post / put / patch / delete return a Signal<T | null> and must be called from an injection context. Use executeRequest for async/await patterns:

@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(SignalHttpClient);

  async login(credentials: Credentials): Promise<Token> {
    return this.http.executeRequest<Token>({
      url: '/auth/login',
      method: 'POST',
      body: credentials,
    });
  }
}

HttpError

class HttpError extends Error {
  readonly status: number;
  readonly response?: Response;

  get isClientError(): boolean  // 4xx
  get isServerError(): boolean  // 5xx
  get isTimeout(): boolean      // 408
  get isNotFound(): boolean     // 404
  get isUnauthorized(): boolean // 401
  get isForbidden(): boolean    // 403
}

RequestConfig

interface RequestConfig {
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  headers?: Record<string, string>;
  body?: unknown;
  params?: Record<string, string | number | boolean>;
  timeout?: number;      // overrides the global timeout for this request
  signal?: AbortSignal;  // merged with the internal AbortController
}

RetryConfig

interface RetryConfig {
  count: number;
  delay?: number | ((attempt: number) => number);   // ms; defaults to 0
  shouldRetry?: (error: Error, attempt: number) => boolean;
}

Migration from HttpClient

| Before (@angular/common/http) | After (ng-signal-http) | |---|---| | imports: [HttpClientModule] | providers: [provideSignalHttp()] | | inject(HttpClient).get<T>(url)Observable<T> | querySignal<T>(url)HttpClientResult<T> | | async pipe + manual loading flag | result.data() + result.loading() | | pipe(takeUntil(destroy$)) | automatic — cancelled on destroy | | pipe(switchMap(...)) for reactive deps | reactive factory: () => `/users/${id()}` | | pipe(retry(3)) | { retry: 3 } option | | pipe(catchError(...)) | result.error() signal + onError callback | | http.post<T>(url, body)Observable<T> | mutationSignal(...)MutationResult<T> |


Browser support

Any browser with native fetch support: Chrome/Edge 90+, Firefox 88+, Safari 14+. No IE11.

SSR is fully supported — window events (focus, online) are skipped on the server.


License

MIT