Skip to content
Angular ng http 4 min read

POST, PUT, PATCH & DELETE

Reading data is only half the story — most real applications also create, update, and remove server resources. Angular’s HttpClient exposes dedicated post(), put(), patch(), and delete() methods that send a request body, let you attach headers, and return a typed Observable of the server’s response. This page shows how to wire up these mutating requests cleanly inside services and signal-based components.

Choosing the right verb

Each HTTP verb carries a specific meaning. Picking the correct one keeps your API predictable and lets caches, proxies, and clients behave correctly.

VerbPurposeBodyIdempotent
POSTCreate a new resourceYesNo
PUTReplace a resource entirelyYesYes
PATCHPartially update a resourceYesNo
DELETERemove a resourceUsually noYes

Idempotent means calling the request multiple times has the same effect as calling it once. PUT and DELETE are idempotent; POST is not, which is why double-submitting a form can create duplicates.

Creating data with POST

post<T>(url, body) serializes the body to JSON by default and infers the response type from the generic argument. Inject HttpClient with the modern inject() function and return the observable from a service method.

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Task {
  id: number;
  title: string;
  done: boolean;
}

export type NewTask = Omit<Task, 'id'>;

@Injectable({ providedIn: 'root' })
export class TaskService {
  private http = inject(HttpClient);
  private base = '/api/tasks';

  create(task: NewTask): Observable<Task> {
    return this.http.post<Task>(this.base, task);
  }
}

The server receives the body as application/json automatically. When you subscribe, the emitted value is the parsed response — typically the freshly created resource including its server-assigned id.

Replacing with PUT and partial updates with PATCH

Use put() when you send the complete representation of a resource, and patch() when you only send the fields that changed. Both take the URL and a body.

update(id: number, task: Task): Observable<Task> {
  // Full replacement — every field is sent.
  return this.http.put<Task>(`${this.base}/${id}`, task);
}

toggleDone(id: number, done: boolean): Observable<Task> {
  // Partial update — only the changed field is sent.
  return this.http.patch<Task>(`${this.base}/${id}`, { done });
}

Sending a minimal PATCH body reduces payload size and avoids accidentally overwriting fields you did not intend to touch.

Deleting resources

delete() removes a resource. Most APIs respond with 204 No Content, so type the response as void (or unknown if the server returns a body).

remove(id: number): Observable<void> {
  return this.http.delete<void>(`${this.base}/${id}`);
}

Sending custom headers

Pass an options object as the final argument to attach headers — for example an authorization token or an idempotency key. Use HttpHeaders for individual values or a plain object.

import { HttpHeaders } from '@angular/common/http';

createWithKey(task: NewTask, idempotencyKey: string): Observable<Task> {
  const headers = new HttpHeaders({
    'Idempotency-Key': idempotencyKey,
    'X-Source': 'web',
  });
  return this.http.post<Task>(this.base, task, { headers });
}

Prefer attaching cross-cutting headers like auth tokens in a functional interceptor instead of repeating them in every call. Reserve per-request headers for values that are genuinely request-specific.

Reading the full response

By default you receive only the response body. Set observe: 'response' to access the status code and headers — useful for reading a Location header after a POST.

createAndLocate(task: NewTask): Observable<string | null> {
  return this.http
    .post<Task>(this.base, task, { observe: 'response' })
    .pipe(map((res) => res.headers.get('Location')));
}

Calling from a signal-based component

In a standalone component, subscribe to the mutating call and reflect the result in signals. Always update local state from the server’s response rather than assuming success.

import { Component, signal, inject } from '@angular/core';
import { TaskService, Task } from './task.service';

@Component({
  selector: 'app-tasks',
  standalone: true,
  template: `
    <button (click)="add()" [disabled]="saving()">Add task</button>
    @if (saving()) {
      <p>Saving…</p>
    }
    @for (t of tasks(); track t.id) {
      <p>{{ t.title }} — {{ t.done ? '✓' : '○' }}</p>
    }
  `,
})
export class TasksComponent {
  private service = inject(TaskService);
  tasks = signal<Task[]>([]);
  saving = signal(false);

  add(): void {
    this.saving.set(true);
    this.service.create({ title: 'Write docs', done: false }).subscribe({
      next: (created) => this.tasks.update((list) => [...list, created]),
      complete: () => this.saving.set(false),
    });
  }
}

Output:

POST /api/tasks  201 Created
{ "id": 42, "title": "Write docs", "done": false }

Best practices

  • Match the verb to intent: POST to create, PUT to replace, PATCH to update partially, DELETE to remove.
  • Type every request with a generic (post<Task>, delete<void>) so the compiler catches shape mismatches.
  • Update local signals from the server’s response, not from optimistic guesses, to stay in sync with persisted state.
  • Send minimal PATCH bodies — only the fields that actually changed.
  • Disable submit buttons while a request is in flight to prevent duplicate POST requests.
  • Move shared headers (auth, tracing) into a functional interceptor instead of repeating them per call.
  • Remember that HttpClient observables are cold — nothing is sent until you subscribe (or use async/toSignal).
Last updated June 14, 2026
Was this helpful?