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.
| Verb | Purpose | Body | Idempotent |
|---|---|---|---|
POST | Create a new resource | Yes | No |
PUT | Replace a resource entirely | Yes | Yes |
PATCH | Partially update a resource | Yes | No |
DELETE | Remove a resource | Usually no | Yes |
Idempotent means calling the request multiple times has the same effect as calling it once.
PUTandDELETEare idempotent;POSTis 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:
POSTto create,PUTto replace,PATCHto update partially,DELETEto 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
PATCHbodies — only the fields that actually changed. - Disable submit buttons while a request is in flight to prevent duplicate
POSTrequests. - Move shared headers (auth, tracing) into a functional interceptor instead of repeating them per call.
- Remember that
HttpClientobservables are cold — nothing is sent until yousubscribe(or useasync/toSignal).