Spoosh
Core

Response Format

Understanding the SpooshResponse structure

All Spoosh API calls return a consistent SpooshResponse object. This makes error handling predictable across your application.

Response Structure

type SpooshResponse<TData, TError> =
  | {
      status: number;
      data: TData;
      error?: undefined;
      headers?: Headers;
      aborted?: false;
    }
  | {
      status: number;
      data?: undefined;
      error: TError;
      headers?: Headers;
      aborted?: boolean;
    };

Every response includes:

FieldTypeDescription
statusnumberHTTP status code
dataTData | undefinedResponse data (present on success)
errorTError | undefinedError object (present on failure)
headersHeaders | undefinedResponse headers
abortedboolean | undefinedTrue if request was aborted
inputobject | undefinedThe request input (body, query, params)

Handling Responses

The injection functions automatically parse the response and provide separate data and error signals:

import { Component, computed } from "@angular/core";
import { injectRead } from "./api/client";

@Component({
  selector: "app-user-profile",
  template: `
    @if (loading()) {
      <app-spinner />
    } @else if (error()) {
      <app-error [message]="error()!.message" />
    } @else {
      <div>{{ data()?.name }}</div>
    }
  `,
})
export class UserProfileComponent {
  private user = injectRead((api) =>
    api("users/:id").GET({ params: { id: 1 } })
  );

  data = computed(() => this.user.data());
  error = computed(() => this.user.error());
  loading = computed(() => this.user.loading());
}

Aborting Requests

All injection functions return an abort function to cancel in-flight requests:

import { Component, signal, computed } from "@angular/core";
import { injectRead } from "./api/client";

@Component({
  selector: "app-search-users",
  template: `
    <div>
      <input [value]="searchTerm()" (input)="onSearchInput($event)" />
      @if (loading()) {
        <button (click)="abort()">Cancel</button>
      }
      @for (user of data(); track user.id) {
        <app-user-card [user]="user" />
      }
    </div>
  `,
})
export class SearchUsersComponent {
  searchTerm = signal("");

  private searchResults = injectRead(
    (api) => api("search").GET({ query: { q: this.searchTerm() } }),
    { enabled: () => this.searchTerm().length > 0 }
  );

  data = computed(() => this.searchResults.data());
  loading = computed(() => this.searchResults.loading());
  abort = () => this.searchResults.abort();

  onSearchInput(event: Event) {
    this.searchTerm.set((event.target as HTMLInputElement).value);
  }
}

injectWrite Abort

import { Component, computed } from "@angular/core";
import { injectWrite } from "./api/client";

@Component({
  selector: "app-create-post",
  template: `
    <div>
      <button (click)="handleSubmit()" [disabled]="loading()">Submit</button>
      @if (loading()) {
        <button (click)="abort()">Cancel</button>
      }
    </div>
  `,
})
export class CreatePostComponent {
  private createPost = injectWrite((api) => api("posts").POST());

  loading = computed(() => this.createPost.loading());
  abort = () => this.createPost.abort();

  async handleSubmit() {
    await this.createPost.trigger({
      body: { title: "New Post", content: "..." },
    });
  }
}

injectPages Abort

import { Component, computed } from "@angular/core";
import { injectPages } from "./api/client";

@Component({
  selector: "app-post-feed",
  template: `
    <div>
      @if (loading()) {
        <button (click)="abort()">Cancel Loading</button>
      }
      <!-- ... -->
    </div>
  `,
})
export class PostFeedComponent {
  private posts = injectPages(
    (api) => api("posts").GET({ query: { limit: 20 } }),
    {
      canFetchNext: ({ lastPage }) => !!lastPage?.data?.nextCursor,
      nextPageRequest: ({ lastPage }) => ({
        query: { cursor: lastPage?.data?.nextCursor },
      }),
      merger: (pages) => pages.flatMap((p) => p.data?.items ?? []),
    }
  );

  data = computed(() => this.posts.data());
  loading = computed(() => this.posts.loading());
  abort = () => this.posts.abort();
  fetchNext = () => this.posts.fetchNext();
}

Input Echo

The response includes the input that was sent with the request:

createUser = injectWrite((api) => api("users").POST());

// After: await createUser.trigger({ body: { name: "John", email: "john@example.com" } })
// createUser.input?.body contains { name: "John", email: "john@example.com" }

This is useful for optimistic updates and debugging.

On this page