Star

Angular httpResource Mutations: POST, PUT, and DELETE with the Resource API

Perform POST, PUT, and DELETE mutations with Angular's httpResource API. Covers mutate(), optimistic updates, and signal-based error handling.

RJ
RAMA JONNADA
Senior Frontend Developer with 4+ years of experience in Angular and modern web technologies.

TL;DR: httpResource() handles reactive GET requests automatically. For mutations (POST, PUT, DELETE), you use httpResource with a method override, or pair a plain httpResource for reads with a signal-based service method for writes. After a mutation completes, call resource.reload() to refresh the data — or apply optimistic updates directly to the resource value before the response arrives.

Angular's httpResource() and resource() APIs, now stable in Angular 22, are excellent for fetching and reactively displaying server data. But most Angular developers only see examples showing GET requests. Real applications need to create, update, and delete records too. This guide covers the full mutation story — POST, PUT, and DELETE — with concrete patterns you can use today.

If you are new to httpResource, the Angular resource API guide on angular.dev is the right starting point. The ZyraUI docs also have examples of resource-driven UI patterns.


Quick Recap: httpResource for GET

httpResource() is a thin wrapper around HttpClient that creates a Resource — an object with .value(), .isLoading(), .error(), and .reload() signals. It re-fetches automatically when its URL or params signal changes.

import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';

interface Post {
  id: number;
  title: string;
  body: string;
}

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-posts',
  template: `
    @if (postsResource.isLoading()) { <p>Loading...</p> }
    @for (post of postsResource.value() ?? []; track post.id) {
      <p>{{ post.title }}</p>
    }
  `,
})
export class PostsComponent {
  readonly postsResource = httpResource<Post[]>('/api/posts');
}

That covers reads. Mutations are a different shape.


Pattern 1: httpResource with a Method Override

httpResource accepts a full request config object, not just a URL. You can set method, body, and headers — which means you can drive a POST request reactively from a signal.

import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';

interface CreatePostPayload {
  title: string;
  body: string;
}

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-create-post',
  template: `
    <button (click)="submit()">Create post</button>
    @if (createResource.isLoading()) { <span>Saving...</span> }
    @if (createResource.error()) { <span>Error saving post.</span> }
  `,
})
export class CreatePostComponent {
  private readonly pendingPayload = signal<CreatePostPayload | null>(null);

  // httpResource only runs when pendingPayload() is non-null
  readonly createResource = httpResource<Post>(() => {
    const payload = this.pendingPayload();
    if (!payload) return undefined; // returning undefined suspends the resource
    return {
      url: '/api/posts',
      method: 'POST',
      body: payload,
    };
  });

  submit(): void {
    this.pendingPayload.set({ title: 'New post', body: 'Hello world' });
  }
}

Returning undefined from the httpResource factory function is the official way to suspend it — the resource stays idle (.value() is undefined, .isLoading() is false) until the factory returns a real request config. Setting pendingPayload fires the POST and the resource's loading/value/error signals update reactively.

This pattern works well for one-shot mutations like form submissions. For repeated mutations on the same resource (create multiple posts), call pendingPayload.set(newPayload) again — httpResource re-fires because its factory dependency changed.


Pattern 2: Pair httpResource (read) with a Service Method (write)

For CRUD screens where you fetch a list and also mutate it, the cleanest separation is to keep httpResource for the reactive read and use a plain inject(HttpClient) call for mutations. After the mutation succeeds, call .reload() on the resource to refresh.

import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { httpResource, HttpClient } from '@angular/common/http';

interface Todo {
  id: number;
  title: string;
  done: boolean;
}

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-todos',
  template: `
    @for (todo of todosResource.value() ?? []; track todo.id) {
      <label>
        <input type="checkbox" [checked]="todo.done"
               (change)="toggleDone(todo)" />
        {{ todo.title }}
      </label>
      <button (click)="deleteTodo(todo.id)">Delete</button>
    }
    <button (click)="addTodo()">Add todo</button>
  `,
})
export class TodosComponent {
  private readonly http = inject(HttpClient);

  readonly todosResource = httpResource<Todo[]>('/api/todos');

  addTodo(): void {
    this.http
      .post<Todo>('/api/todos', { title: 'New todo', done: false })
      .subscribe(() => this.todosResource.reload());
  }

  toggleDone(todo: Todo): void {
    this.http
      .put<Todo>(`/api/todos/${todo.id}`, { ...todo, done: !todo.done })
      .subscribe(() => this.todosResource.reload());
  }

  deleteTodo(id: number): void {
    this.http
      .delete(`/api/todos/${id}`)
      .subscribe(() => this.todosResource.reload());
  }
}

.reload() triggers a fresh GET request from todosResource — the list updates reactively after each mutation. This is the approach the Angular team recommends for most CRUD use cases and is covered in the Angular resource API guide.


Pattern 3: Optimistic Updates

For fast-feeling UIs, you can update resource.value() immediately before the server confirms, then reconcile (or revert) when the response arrives. Resources expose a mutate() method exactly for this.

import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { httpResource, HttpClient } from '@angular/common/http';

interface Todo {
  id: number;
  title: string;
  done: boolean;
}

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-optimistic-todos',
  template: `
    @for (todo of todosResource.value() ?? []; track todo.id) {
      <label>
        <input type="checkbox" [checked]="todo.done"
               (change)="toggleDone(todo)" />
        {{ todo.title }}
      </label>
    }
  `,
})
export class OptimisticTodosComponent {
  private readonly http = inject(HttpClient);

  readonly todosResource = httpResource<Todo[]>('/api/todos');

  toggleDone(todo: Todo): void {
    const updated = { ...todo, done: !todo.done };

    // 1. Apply optimistic update immediately — the UI flips the checkbox now
    this.todosResource.mutate(current =>
      (current ?? []).map(t => (t.id === todo.id ? updated : t))
    );

    // 2. Send the real request
    this.http.put<Todo>(`/api/todos/${todo.id}`, updated).subscribe({
      next: () => {
        // Server confirmed — optionally reload for server-authoritative data
        // this.todosResource.reload();
      },
      error: () => {
        // Revert on failure
        this.todosResource.mutate(current =>
          (current ?? []).map(t => (t.id === todo.id ? todo : t))
        );
      },
    });
  }
}

mutate() takes an updater function that receives the current resource value and returns the new value. Angular updates .value() synchronously — the template re-renders immediately, with no spinner visible to the user.


Pattern 4: Mutation State with a Writable Signal

When you need to track whether a mutation is in flight (to disable a button or show a spinner), a small writable signal alongside the resource is the simplest approach — no extra library needed.

import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
import { httpResource, HttpClient } from '@angular/common/http';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-article-editor',
  template: `
    <button (click)="save()" [disabled]="saving()">
      {{ saving() ? 'Saving...' : 'Save' }}
    </button>
    @if (saveError()) {
      <p class="error">{{ saveError() }}</p>
    }
  `,
})
export class ArticleEditorComponent {
  private readonly http = inject(HttpClient);

  readonly articleResource = httpResource<{ id: number; body: string }>(
    () => `/api/articles/${this.articleId()}`
  );

  readonly articleId = signal(42);
  readonly saving = signal(false);
  readonly saveError = signal<string | null>(null);

  save(): void {
    this.saving.set(true);
    this.saveError.set(null);

    const body = this.articleResource.value()?.body ?? '';

    this.http.put(`/api/articles/${this.articleId()}`, { body }).subscribe({
      next: () => {
        this.saving.set(false);
        this.articleResource.reload();
      },
      error: (err) => {
        this.saving.set(false);
        this.saveError.set(err.message ?? 'Save failed. Please try again.');
      },
    });
  }
}

Using ZyraUI for Mutation-Driven UIs

Mutation UIs typically need buttons, spinners, and error alerts — all of which ZyraUI ships as standalone Angular components. Here is the save pattern above using zyra-button and zyra-alert:

import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
import { httpResource, HttpClient } from '@angular/common/http';
import { ZyraButtonComponent, ZyraAlertComponent, ZyraSpinnerComponent } from '@zyra-ui/angular';

@Component({
  selector: 'app-article-editor',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ZyraButtonComponent, ZyraAlertComponent, ZyraSpinnerComponent],
  template: `
    @if (saveError()) {
      <zyra-alert variant="danger">{{ saveError() }}</zyra-alert>
    }

    <zyra-button
      variant="primary"
      [disabled]="saving()"
      (click)="save()"
    >
      @if (saving()) { <zyra-spinner size="sm" /> }
      {{ saving() ? 'Saving...' : 'Save article' }}
    </zyra-button>
  `,
})
export class ArticleEditorComponent {
  private readonly http = inject(HttpClient);

  readonly articleId = signal(42);
  readonly saving = signal(false);
  readonly saveError = signal<string | null>(null);

  readonly articleResource = httpResource<{ id: number; body: string }>(
    () => `/api/articles/${this.articleId()}`
  );

  save(): void {
    this.saving.set(true);
    this.saveError.set(null);
    const body = this.articleResource.value()?.body ?? '';

    this.http.put(`/api/articles/${this.articleId()}`, { body }).subscribe({
      next: () => { this.saving.set(false); this.articleResource.reload(); },
      error: (err) => {
        this.saving.set(false);
        this.saveError.set(err.message ?? 'Save failed.');
      },
    });
  }
}

zyra-alert renders accessible error banners that match your active theme. zyra-spinner is a standalone loading indicator that drops inline. Explore all 60+ components at zyraui.dev, including alerts, spinners, buttons, and more.


resource() vs httpResource() for Mutations

httpResource() is purpose-built for HTTP and gives you .reload() and .mutate(). The lower-level resource() accepts any async loader and is more flexible but requires more wiring. For HTTP mutations, prefer the httpResource + HttpClient combination shown above — it is simpler and the Angular team's recommended approach. See the Angular resource API reference for the full API surface.


Wrapping Up

Angular's httpResource() is not limited to GET requests — its factory function can return any HttpRequest-shaped config, including method overrides for POST and PUT. For CRUD screens, pairing a httpResource for reads with inject(HttpClient) for writes and calling .reload() after mutations keeps the code straightforward and the data fresh. Add optimistic updates via .mutate() when the UI needs to feel instant. See the ZyraUI components page for the UI layer that pairs well with these patterns.


Frequently asked questions

Can httpResource() send POST requests?

Yes. Return a request config object from the factory function with method: 'POST' and a body property. The resource fires the POST when the factory returns a non-undefined value. To fire it on demand, drive the factory with a writable signal that you set when the user submits.

What is the difference between resource.reload() and resource.mutate()?

reload() triggers a fresh HTTP request from the resource — the server is the source of truth and the response replaces the current value. mutate() synchronously updates the local .value() signal without a network request — use it for optimistic updates where you want instant UI feedback before the server confirms.

Should I use httpResource or inject(HttpClient) for mutations?

Both work. For one-off mutations (form submission, delete action), inject(HttpClient) with a plain subscribe() call is simpler and keeps your intent clear. For reactive mutation flows where the trigger changes over time (re-submitting as a form signal changes), the httpResource factory pattern handles that cleanly. Most CRUD screens benefit from using httpResource for reads and HttpClient for writes.

How do I handle errors from httpResource mutations?

When using httpResource as the mutation driver, the .error() signal on the resource carries the HTTP error. When using HttpClient directly, handle errors in the subscribe({ error: ... }) callback and write the error into your own writable signal. The signal approach pairs cleanly with @if (errorSignal()) in the template.

Topics
angularhttpresourceresource apisignalshttp