BACK TO BLOG
ANGULAR 08 SEP 2026 · 6 MIN READ

httpResource and Beyond (text , blob and arrayBuffer)

Ali Gamal Ali

Lead Front-End Developer · Luftborn


// QUICK REACTIONS

Enjoyed this article? Leave a signal without writing a comment.

httpResource and Beyond

Need To Read : httpResource() 4 Patterns Worth Knowing

The first article walked through the four patterns of the default httpResource(), the one that parses JSON.

What do you do when the server hands you a plain string, an image, or raw bytes instead of JSON?

Based on the same real HttpDemoService, extended with three response-type sub-constructors. Each example shows the same three layers: service, component, template.

Live examples pull an image from picsum.photos, plain text from baconipsum, and audio from MDN’s CC0 sound files. All three send permissive CORS headers, so they run straight from the browser.

The default is JSON. These three are not.

httpResource(() => url) assumes the backend returns JSON and parses it for you. When the body is something else, you reach for a sub-constructor that tells Angular how to read the response:

  • httpResource(…) → parsed object or array (JSON)
  • httpResource.text(…) → string
  • httpResource.blob(…) → Blob
  • httpResource.arrayBuffer(…) → ArrayBuffer

All three are part of the public httpResource** **API in Angular v22. They take the same arguments as the JSON version (a reactive URL or request function, plus options), they just decode the body differently.

.text(): when the body is a plain string

Use it when the server returns text, not JSON. Health check endpoints that reply “ok”, CSV exports, server logs, sanitized HTML, no JSON.parse, no schema.

In service we do:

 httpResourceWithText = (
  type: Signal<'meat-and-filler' | 'all-meat'>,
  paragraphs: Signal<number>,
) =>
httpResource.text(
  () => `https://baconipsum.com/api/?type=${type()}&paras=${paragraphs()}&format=text`,
);

The URL function reads type() and paragraphs(), so changing either signal refetches automatically. Same reactivity as the JSON version.

In component we do:

 textType = signal<'meat-and-filler' | 'all-meat'>('meat-and-filler');
textParagraphs = signal<number>(1);

textResource = this.service.httpResourceWithText(this.textType, this.textParagraphs);

In HTML then we do:

 @if (textResource.isLoading()) {
  <p>Fetching text...</p>
} @else if (textResource.error()) {
  <p>Error: {{ textResource.error()?.message }}</p>
} @else if (textResource.value()) {
  <p class="whitespace-pre-wrap">{{ textResource.value() }}</p>
}

textResource.value() is string | undefined. The string arrives raw off the wire, no parsing step in between.

Using the httpResource.text()

.blob(): when you want a file the browser can render

A Blob is a file-shaped object the browser already knows how to display or download. This is the right choice for image previews, PDF downloads, or anything you hand back to the DOM through an object URL.

In service we do:

 httpResourceWithBlob = (width: Signal<number>, height: Signal<number>) =>
httpResource.blob(() => `https://picsum.photos/${width()}/${height()}`);

In component we do:

 blobWidth = signal<number>(300);
blobHeight = signal<number>(200);

blobResource = this.service.httpResourceWithBlob(this.blobWidth, this.blobHeight);
blobUrl = signal<string | null>(null);

private readonly blobUrlEffect = effect((onCleanup) => {
  const blob = this.blobResource.value();
  if (!blob) {
    this.blobUrl.set(null);
    return;
  }
  const url = URL.createObjectURL(blob);
  this.blobUrl.set(url);
  onCleanup(() => URL.revokeObjectURL(url));
});

In HTML then we do:

 @if (blobResource.isLoading()) {
  <p>Fetching image...</p>
} @else if (blobUrl()) {
  <img [src]="blobUrl()!" alt="Random image" />
}

URL.createObjectURL() allocates a URL that lives until the document unloads. Revoke it. The onCleanup callback runs before the effect re-runs (a reload or new dimensions) and on destroy, so each object URL is released instead of leaking for the life of the page.

Using httpResource.blob()

.arrayBuffer(): when you process the raw bytes yourself

An ArrayBuffer is just a block of raw bytes in memory. Reach for it when you feed the response into an API that wants a BufferSource and will not take a Blob.

The textbook case is Web Audio. AudioContext.decodeAudioData() decodes compressed audio into a playable AudioBuffer, and its input has to be an ArrayBuffer. A Blob is rejected outright. So you fetch the sound as raw bytes, decode it, and play it on a click.

In service we do:

 httpResourceWithArrayBuffer = (audioUrl: Signal<string>) =>
httpResource.arrayBuffer(() => audioUrl());

In component we do:

 audioUrl = signal<string>('https://.../t-rex-roar.mp3');

arrayBufferResource = this.service.httpResourceWithArrayBuffer(this.audioUrl);

private audioCtx: AudioContext | null = null;
decodedAudio = signal<AudioBuffer | null>(null);

private readonly decodeEffect = effect(() => {
  const buffer = this.arrayBufferResource.value();
  this.decodedAudio.set(null);
  if (!buffer) return;

  const ctx = (this.audioCtx ??= new AudioContext());
  ctx.decodeAudioData(buffer.slice(0))
    .then((decoded) => this.decodedAudio.set(decoded));
});

In HTML then we do:

 @if (decodedAudio(); as audio) {
  <button (click)="play()">Play sound</button>
  <span>{{ audio.duration.toFixed(2) }} s, {{ audio.numberOfChannels }} ch</span>
}

decodeAudioData() detaches the buffer you pass it. After the call, the original ArrayBuffer has a byteLength of 0 and is unusable. Pass a copy with buffer.slice(0) if you still need the bytes elsewhere, for a size readout or a checksum.

This is the line that separates blob() from arrayBuffer(). decodeAudioData(), crypto.subtle.digest(), and WebAssembly.instantiate() all take a BufferSource, never a Blob. Fetch the type the next step actually needs and skip the conversion.

Using httpResource.arrayBuffer()

blob() vs arrayBuffer(): which one?

They both carry binary data, so the choice comes down to what you do next:

  • Show an image, offer a download, build an object URL → blob()
  • Feed bytes to Web Audio, Web Crypto, or WebAssembly → arrayBuffer()
  • Parse a binary header with DataView or a typed array → arrayBuffer()
  • Hand the file to a FormData upload → blob()

Rule of thumb: blob() when the browser consumes the file, arrayBuffer() when your code consumes the bytes.

The shared gotcha: value() is T | undefined

None of the examples above pass a defaultValue. That is intentional, and it changes the type. Without defaultValue, these sub-constructors return HttpResourceRef<T | undefined>, so:

  • text().value() is string | undefined
  • blob().value() is Blob | undefined
  • arrayBuffer().value() is ArrayBuffer | undefined

The value is undefined while idle or loading. Guard reads with hasValue() or an @if, exactly like the JSON patterns in the first note. Pass a defaultValue if you would rather have a stable non-null value from the start.

Choosing a response type

  • httpResource() (default) → parsed object or array — normal REST and API payloads
  • text() → string — plain text, CSV, HTML, logs
  • blob() → Blob — file downloads, image previews, object URLs
  • arrayBuffer() → ArrayBuffer — you inspect, transform, or decode bytes in memory

Everything else carries over from the default httpResource(): the reactive URL function, the parse option, reload(), the status signals, and the hasValue() guard. If you want a refresher on those, the prequel covers them in detail.

Follow me for more Angular content: X · LinkedIn · Bluesky

// QUICK REACTIONS

Enjoyed this article? Leave a signal without writing a comment.


// DISCUSSION

Reader Comments (0)

Register or log in to leave a comment.

// NO COMMENTS YET