@DeleteMapping
Maps HTTP DELETE requests to a handler method; delete the resource and usually answer 204 No Content.
@DeleteMapping("/{id}")- Added in Spring Boot Spring Framework 4.3
Parameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | String[]= {} | Alias for path. One or more URI patterns such as "/{id}". Combined with any class-level @RequestMapping path. |
$pathoptional | String[]= {} | The URI patterns this method handles. Same as value; use whichever reads better. |
$paramsoptional | String[]= {} | Narrows the mapping by query parameters: "type", "!type" or "type=pdf". |
$headersoptional | String[]= {} | Narrows the mapping by request headers, for example "X-Feature=beta". |
$consumesoptional | String[]= {} | Media types the request body must have (checked against Content-Type). A mismatch gives 415. |
$producesoptional | String[]= {} | Media types the method can write (checked against Accept). No overlap gives 406. |
$versionoptional | String= "" | API version this mapping serves, such as "1.1" or "1.1+". Needs API versioning configured. |
$nameoptional | String= "" | A name for the mapping, used by MvcUriComponentsBuilder to build links. Rarely needed. |
Return value
n/aShortcut for @RequestMapping(method = RequestMethod.DELETE). The method handles DELETE requests for the mapped path.
Errors
Wrong method on a matching path gives 405. Database constraint failures (rows still referenced) surface as exceptions you should map to 409.
Changelog
- PHP Spring Framework 7.0Added the version attribute for first-class API versioning.
@DeleteMapping is @RequestMapping(method = DELETE). A DELETE removes the resource at the URL, so the path almost always ends in an identifier: DELETE /api/comments/42.
Which status to return
The common answer is 204 No Content: the work is done and there is nothing to say. A void method returns 200 with an empty body by default, so add @ResponseStatus(HttpStatus.NO_CONTENT) or return ResponseEntity.noContent().build().
DELETE is idempotent: after one call or five, the resource is gone. That raises a design choice for the second call. Returning 204 again treats "already gone" as success, which makes client retries trivial. Returning 404 tells the client the id was wrong, which helps catch bugs. Both are valid; pick one and use it across the API.
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void delete(@PathVariable long id) {
service.delete(id);
}Hard or soft delete
Physically deleting rows is simple, but you lose the audit trail, and foreign keys may refuse the delete. A soft delete sets a deletedAt column and filters it out of reads. It costs a filter on every query and a plan for eventually purging data, which privacy law may require.
Mistakes people make
- A body on DELETE. HTTP gives it no defined meaning and some proxies and clients drop it. For bulk deletes, pass ids as repeated query parameters or use a POST action such as
/api/drafts/bulk-delete. - Missing authorisation. Delete endpoints are the most damaging to expose by accident. Check ownership in the service, not only the role.
- Unmapped constraint errors. Deleting a customer who still has orders throws a data integrity exception. Without a handler the client gets a 500; map it to 409 Conflict with a clear message.
Example
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
record Comment(long id, String text) {}
("/api/comments")
public class CommentController {
private final Map<Long, Comment> store = new ConcurrentHashMap<>(Map.of(1L, new Comment(1, "Nice post")));
// Idempotent: deleting something already gone still ends in the same state.
("/{id}")
(HttpStatus.NO_CONTENT)
void delete( long id) {
store.remove(id);
}
}
When to use it
- Letting a user remove their own comment
- Unsubscribing a webhook by id from an admin console
- Archiving notes with a soft delete so support can restore them
Frequently asked questions
Should a DELETE return the deleted object?
It may return 200 with a body, but most APIs return 204. If the client needs the final state, it should have read it before deleting.
Can I use @RequestBody with @DeleteMapping?
Spring will bind one, but HTTP leaves a DELETE body without defined semantics and intermediaries may discard it. Prefer the path or query parameters.
More examples
204 or 404 with ResponseEntity
ResponseEntity<Void> lets one method return either status. Map.remove returns the old value, so a null means the id was not there.
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
record Webhook(long id, String url) {}
@RestController
public class WebhookController {
private final Map<Long, Webhook> store = new ConcurrentHashMap<>();
// Tell the client when the id never existed: 404 instead of 204.
@DeleteMapping("/api/webhooks/{id}")
ResponseEntity<Void> delete(@PathVariable long id) {
return store.remove(id) != null
? ResponseEntity.noContent().build()
: ResponseEntity.notFound().build();
}
}
Soft delete with JPA
The transaction lives in the service, where the loaded entity is managed, so setting deletedAt is written on commit without calling save. The controller stays a thin HTTP edge. Every read must then exclude rows where deletedAt is set, for example with a repository method or Hibernate's @SQLRestriction.
import java.time.Instant;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@Entity
class Note {
@Id
@GeneratedValue
Long id;
String text;
Instant deletedAt;
}
interface NoteRepository extends JpaRepository<Note, Long> {}
@Service
class NoteService {
private final NoteRepository notes;
NoteService(NoteRepository notes) {
this.notes = notes;
}
// Soft delete: keep the row for audits and undo, hide it from reads.
@Transactional
void softDelete(long id) {
notes.findById(id).ifPresent(note -> {
if (note.deletedAt == null) {
note.deletedAt = Instant.now();
}
});
}
}
@RestController
public class NoteController {
private final NoteService service;
NoteController(NoteService service) {
this.service = service;
}
@DeleteMapping("/api/notes/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void delete(@PathVariable long id) {
service.softDelete(id);
}
}
Bulk delete with query parameters
Repeated id parameters bind to a List<Long>. That keeps the request body empty, which every proxy and client handles consistently.
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DraftController {
private final Map<Long, String> drafts = new ConcurrentHashMap<>();
// DELETE /api/drafts?id=3&id=7 -- ids in the query, not in a body.
@DeleteMapping("/api/drafts")
@ResponseStatus(HttpStatus.NO_CONTENT)
void deleteMany(@RequestParam("id") List<Long> ids) {
ids.forEach(drafts::remove);
}
}
Discussion