@DeleteMapping

Maps HTTP DELETE requests to a handler method; delete the resource and usually answer 204 No Content.

Signature
@DeleteMapping("/{id}")
  • Added in Spring Boot Spring Framework 4.3

Parameters

ParameterTypeDescription
$valueoptionalString[]= {}Alias for path. One or more URI patterns such as "/{id}". Combined with any class-level @RequestMapping path.
$pathoptionalString[]= {}The URI patterns this method handles. Same as value; use whichever reads better.
$paramsoptionalString[]= {}Narrows the mapping by query parameters: "type", "!type" or "type=pdf".
$headersoptionalString[]= {}Narrows the mapping by request headers, for example "X-Feature=beta".
$consumesoptionalString[]= {}Media types the request body must have (checked against Content-Type). A mismatch gives 415.
$producesoptionalString[]= {}Media types the method can write (checked against Accept). No overlap gives 406.
$versionoptionalString= ""API version this mapping serves, such as "1.1" or "1.1+". Needs API versioning configured.
$nameoptionalString= ""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

Example · java
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) {}

@RestController
@RequestMapping("/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.
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    void delete(@PathVariable 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.

Example · java
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.

Example · java
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.

Example · java
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

  • Be the first to comment on this lesson.