BACK TO BLOG
ANGULAR 08 SEP 2026 · 7 MIN READ

Angular httpResource() 4 Patterns Worth Knowing

Ali Gamal Ali

Lead Front-End Developer · Luftborn


// QUICK REACTIONS

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

These are my thoughts from a year ago, when Alex released the RFC, and I was deep in studying the Resource pattern.

Based on a real HttpDemoService implementation. Each pattern below shows the same three layers: service → component → template.

All examples use the Official Joke API and valibot for runtime response validation.

Setup: Joke Type + Validation Schema

// jokes.ts
export interface Joke {
  id: number;
  type: string;
  setup: string;
  punchline: string;
}

Before jumping into the patterns, here are the shared types and schema used across all examples.

// schema/jokes.schema.ts
import { string, number, object, array } from 'valibot';

export const JokesSchema = array(
  object({
    id: number(),
    punchline: string(),
    setup: string(),
    type: string(),
  }),
);

Pattern 1: The Pure Form

This is httpResource at its bare minimum. Just a callback that returns a URL string. No options, no validation, no defaults.

Service:

@Service()
export Class HttpDemoService {
  jokes = httpResource<Joke[]>(() => 'https://official-joke-api.appspot.com/random_ten');
}Ï

That’s the whole thing. The callback is a reactive context; anything read inside it (signals, computed values) will trigger a re-fetch when it changes.

Component:


@Component({
  selector: 'app-jokes',
  templateUrl: './jokes.html',
})
export class JokesComponent {
  service = inject(HttpDemoService);
  jokes = this.service.jokes;
}

Template:

@if (jokes.isLoading()) {
  <p>Loading...</p>
}
@else {
  @for (joke of jokes.value(); track joke.id) {
    <p>{{ joke.setup }}</p>
  }
}

⚠️ jokes.value() can be undefined here since no defaultValue was set. Use optional chaining or an @if guard before iterating.

Pattern 2: Basic Fetch with Parse + Debug

The same static URL, but now with response validation via valibot and a debugName for Angular DevTools.

Service:


@Service()
export class HttpDemoService {
  httpResourceGetJokesWithoutParams = httpResource<Joke[]>(
    () => `https://official-joke-api.appspot.com/random_ten`,
    {
      debugName: 'JokeWithParamsResource',
      parse: (response) => parse(JokesSchema, response),
    }
  );
}

ℹ️ debugName shows up in Angular DevTools so you can identify this resource in the signals graph. parse runs valibot validation on the raw response if the API returns an unexpected shape, it throws before the data reaches your component.

Component:


@Component({
  selector: 'app-jokes',
  templateUrl: './jokes.html',
})
export class JokesComponent {
  service = inject(HttpDemoService);

  jokes = this.service.httpResourceGetJokesWithoutParams;

  reload() {
    this.jokes.reload();
  }
}

Template:

@if (jokes.isLoading()) {
  <p>Loading...</p>
}
@else if (jokes.error()) {
  <p>Error: {{ jokes.error()?.message }}</p>
}
@else {
  @for (joke of jokes.value(); track joke.id) {
    <div>
      <strong>{{ joke.setup }}</strong>
      <p>{{ joke.punchline }}</p>
    </div>
  }
  @empty {
    <p>No jokes found.</p>
  }
}

<button (click)="reload()">Reload</button>

Pattern 3: Signal-Driven Dynamic Params

Pass a signal in and the resource re-fetches automatically every time the value changes. No manual subscriptions, no switchMap.

Service:

httpResourceGetJokesWithParams = (jokesNumber: Signal<number>) => {
  return httpResource<Joke[]>(
    () => `https://official-joke-api.appspot.com/jokes/random/${jokesNumber()}`,
    {
      defaultValue: [],
      parse: (response) => parse(JokesSchema, response),
    }
  );
};

✅ defaultValue: [] means jokes.value() is always an array and never undefined. Safe to iterate without a null check in the template.

Component:


@Component({
  selector: 'app-jokes-dynamic',
  templateUrl: './jokes-dynamic.html',
})
export class JokesDynamicComponent {
  service = inject(HttpDemoService);

  count = signal<number>(5);

  jokes = this.service.httpResourceGetJokesWithParams(this.count);

  increase() {
    this.count.update(n => n + 1);
  }
}

When count updates, httpResource picks it up through its reactive context and fires a fresh request no manual trigger needed.

Template:

<input #input type="number" placeholder="Type count..."
  [value]="count()" (input)="count.set(input.valueAsNumber)">

@if (jokes.isLoading()) {
  <p>Fetching {{ count() }} jokes...</p>
}
@else {
  @for (joke of jokes.value(); track joke.id) {
    <p><strong>{{ joke.setup }}</strong> {{ joke.punchline }}</p>
  }
}

Pattern 4: Resource Overload with Headers and Query Params

When you need more than just a URL string. Use the object form to set headers, query params, HTTP method, or a request body.

Service:

httpResourceGetJokesWithOtherOverloadAndQueryParams = (jokesNumber: Signal<number>) => {
  return httpResource<Joke[]>(() => ({
    url: `https://official-joke-api.appspot.com/jokes/random`,
    method: 'GET',
    headers: { Accept: 'application/json' },
    params: { limit: jokesNumber() },
  }));
};

limit becomes a query param on the URL: /jokes/random?limit=5. The object is rebuilt on every signal change, so reactivity works the same way as the string version.

Component:


@Component({
  selector: 'app-jokes-query',
  templateUrl: './jokes-query.html',
})
export class JokesQueryComponent {
  service = inject(HttpDemoService);

  limit = signal<number>(3);

  jokes = this.service.httpResourceGetJokesWithOtherOverloadAndQueryParams(this.limit);
}

Template:

<select #select (change)="limit.set(+select.value)">
  <option value="3">3 jokes</option>
  <option value="5">5 jokes</option>
  <option value="10">10 jokes</option>
</select>

@if (jokes.isLoading()) {
  <span>Fetching...</span>
}
@else {
  @for (joke of jokes.value(); track joke.id) {
    <div>
      <p>{{ joke.setup }}</p>
      <p>{{ joke.punchline }}</p>
    </div>
  }
}

Bonus Pattern: Conditional Fetch (Skip When Null)

Return undefined from the request function to tell httpResource to do nothing. The resource stays idle and cancels any in-flight request.

Service:

httpResourceGetJokesWithCondition = (jokesNumber: Signal<number | null>) => {
  return httpResource<Joke[]>(() => {
    const jokeParam = jokesNumber();
    return jokeParam
      ? `https://official-joke-api.appspot.com/jokes/random/${jokeParam}`
      : undefined;
  });
};

⚠️ Returning undefined is the signal to skip the request. The resource enters an ‘idle’ state, and value() returns undefined (unless you set a defaultValue).

Component:


@Component({
  selector: 'app-jokes-conditional',
  templateUrl: './jokes-conditional.html',
})
export class JokesConditionalComponent {
  service = inject(HttpDemoService);

  jokesCount = signal<number | null>(null);

  jokes = this.service.httpResourceGetJokesWithCondition(this.jokesCount);
}

Template:

<input
  #input
  type="number"
  placeholder="Enter a count to load jokes"
  (input)="jokesCount.set(input.valueAsNumber)" />

@if (jokesCount() === null) {
  <p>Enter a number above to fetch jokes.</p>
}
@else if (jokes.isLoading()) {
  <p>Loading...</p>
}
@else if (jokes.error()) {
  <p>Something went wrong: {{ jokes.error()?.message }}</p>
}
@else {
  @for (joke of jokes.value(); track joke.id) {
    <div>
      <strong>{{ joke.setup }}</strong>
      <p>{{ joke.punchline }}</p>
    </div>
  }
  @empty {
    <p>No jokes found for that count.</p>
  }
}

Quick Comparison

HttpResource usage comparison

Resource Signal API

Every httpResource() call returns the same interface:

resource.value()      // T | undefined (defaultValue if set) throws if called in error state
resource.hasValue()   // true only when resolved with data (safe gate before calling value())
resource.isLoading()  // boolean
resource.error()      // Signal<Error | undefined>
resource.status()     // Signal<ResourceStatus>
resource.reload()     // manually re-trigger the request
resource.destroy()    // cancel and clean up

Where ResourceStatus is:

type ResourceStatus = 'idle' | 'error' | 'loading' | 'reloading' | 'resolved' | 'local';
  • **Idle: **No request, no value: reactive function returned undefined
  • Loading: Fetching for the first time, value() is undefined
  • Reloading: Re-fetching for the same params, value() still holds the previous result
  • **Resolved: **Fetch complete, value() has the data
  • Error: Fetch failed, value() is undefined
  • Local: Value was set manually via .set() or .update()

Safe Value Access: Avoiding the Internal Resource Error

🚫 Calling resource.value() when the resource is in an error state throws at runtime. Always guard reads with hasValue(). This is called out explicitly in the official Angular docs.

There are two ways to guard against this.

Option 1: Guard in the template with hasValue()

The Angular docs recommend checking hasValue() first, then error(), then isLoading(). This order ensures value() is only reached when data is actually available.

@if (jokes.hasValue()) {
  @for (joke of jokes.value(); track joke.id) {
    <p>{{ joke.setup }}</p>
  }
}
@else if (jokes.error()) {
  <p>Something went wrong.</p>
}
@else if (jokes.isLoading()) {
  <p>Loading...</p>
}

Option 2: Guard in the component with hasValue() + computed()

Use hasValue() inside a computed() to derive a null-safe signal. Keeps the template clean and lets you pass the value to child components or derive further state from it.


export class JokesComponent {
  service = inject(HttpDemoService);
  jokes = this.service.httpResourceGetJokesWithoutParams;

  jokesValue = computed(() =>
    this.jokes.hasValue() ? this.jokes.value() : null
  );
}

Then in the template:

@if (jokesValue()) {
  @for (joke of jokesValue()!; track joke.id) {
    <p>{{ joke.setup }}</p>
  }
}
@else if (jokes.error()) {
  <p>Something went wrong.</p>
}

Which one to pick

  • hasValue() in template. Best for simple components, straightforward conditional rendering
  • computed() with hasValue() Best for reusing the value in multiple places, passing it to child components, or deriving further state from it

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