How HTTP Works

Method · unsafe · idempotent

> DELETE /users/42 HTTP/1.1>  Host: api.example.com>  Authorization: Bearer <token>

HTTP DELETE Method: Remove Resources

Learn how the HTTP DELETE method works, when to use it, and best practices for deleting resources in REST APIs.

Reviewed 4 min readbeginner2 sourcesTry itMarkdown
Safe
No
Idempotent
Yes
Cacheable
No
Request body
No defined semantics
Response body
Optional
Spec
RFC 9110 §9.3.5
On this page

TL;DR: DELETE removes the resource at a URL. Return 204 when it’s gone, 200 if you send back a body, or 202 if deletion happens later. It’s idempotent: a second DELETE leaves the same state, even if it returns 404. The server decides whether that means erasing data or just hiding it.

What is DELETE?

DELETE targets whatever the URL identifies. That might be a database row or a file, but it doesn’t have to be. The server can archive the data, keep an audit record, or refuse. RFC 9110 §9.3.5 leaves the underlying storage up to the server.

curl -i -X DELETE 'https://api.example.com/documents/notes'

A successful deletion usually looks like this (responses on this page are trimmed examples):

HTTP/1.1 204 No Content
Date: Mon, 05 Oct 2026 12:00:00 GMT

A 204 carries no body and no Content-Length.

Idempotency

Delete the same resource twice and you end up in the same place: it’s gone. The status codes don’t have to match. The first call can return 204 and the second 404, and that’s still idempotent, because idempotency is about server state, not the response. This assumes nobody recreated the resource between calls. Logging each attempt is fine too.

Response Codes

Return 204 when the resource is gone and there’s nothing to say, 200 when you return a body describing the result, or 202 when you’ve queued the deletion but haven’t done it yet. For a resource that’s already gone, return 404 (which also covers resources the caller may not see) or 204 so retries look like success. Pick one and document it.

If deletion is blocked by dependent data, return 409 Conflict. If the client sent If-Match and the version no longer matches, return 412 Precondition Failed.

Implementation

This Express server keeps documents in memory and returns 404 on a repeated delete:

const express = require('express')
const app = express()
const documents = new Map([['notes', { title: 'Notes' }]])
app.delete('/documents/:id', (req, res) => {
  if (!documents.delete(req.params.id)) {
    return res.status(404).json({ error: 'Not found' })
  }
  res.status(204).end()
})
app.listen(3000)

Run curl -i -X DELETE http://localhost:3000/documents/notes twice and you’ll get 204, then 404. It has no authentication, so add the access checks described below before exposing anything private.

If your API’s contract says 404 means “already deleted”, the client can treat it as success:

async function deleteDocument(id) {
  const response = await fetch(`/api/documents/${encodeURIComponent(id)}`, {
    method: 'DELETE'
  })
  if (response.status === 202) return { pending: true }
  if (response.status === 404 || response.status === 204) return { pending: false }
  if (!response.ok) throw new Error(`DELETE failed: ${response.status}`)
  return { pending: false }
}

Check the contract before reusing that 404 rule elsewhere: some services return 404 to hide resources you can’t access. The helper also never parses a 204 as JSON.

Handling Dependencies

When a resource has children, decide whether deleting it rejects, cascades, or detaches them. Enforce that in the database, with a constraint or inside the delete’s transaction. A count query followed by a separate delete lets another request insert a child in the gap.

For soft deletion in PostgreSQL, add a condition so a retry keeps the original deletion timestamp:

UPDATE posts
SET deleted_at = CURRENT_TIMESTAMP
WHERE id = $1 AND deleted_at IS NULL;

Put the authorization check in the same statement or transaction. For dependencies, a count in application code can’t replace a foreign-key constraint, since a concurrent insert can land right after it. Document whether dependencies cause 409, get detached, or are removed with the parent.

Bulk Delete

Sending a JSON array of IDs in a DELETE body is tempting, but a DELETE body has no defined meaning in HTTP and some servers and proxies reject it. Use a POST to a dedicated endpoint instead:

curl -i 'https://api.example.com/document-deletions' \
  -H 'Content-Type: application/json' \
  --data '{"ids":["notes","draft"]}'

That endpoint is your API’s design, not an HTTP standard. Give it its own retry policy and check authorization for each document.

Getting DELETE right

Only parse JSON when the status has a body; 204 doesn’t. A 202 means deletion is still pending. To delete only the version the client inspected, send the strong ETag from GET in If-Match, and check it atomically with the delete.

DELETE responses aren’t cacheable. A successful DELETE invalidates the cached response for that URL, but not collection pages that list the resource. See RFC 9111 §4.4.

Security Considerations for DELETE Endpoints

Check that the caller may delete this specific resource, not just that a session exists. Otherwise anyone who guesses an ID can delete it. Cookie-authenticated endpoints also need CSRF protection; CORS alone won’t provide it.

With soft deletes, keep the original deletedAt on retries, as the SQL above does. Hiding a row also isn’t the same as erasing personal data from backups and other stores.

In practice

Express.js
// DELETE /users/:id
app.delete('/users/:id', async (req, res) => {
  const deleted = await db.users.delete(req.params.id)
  if (!deleted) return res.status(404).json({ error: 'Not found' })
  res.status(204).end()
})

// Soft delete pattern
app.delete('/posts/:id', async (req, res) => {
  await db.posts.update(req.params.id, { deletedAt: new Date() })
  res.status(204).end()
})
Next.js App Router
// app/api/users/[id]/route.ts
export async function DELETE(
  _req: Request,
  { params }: { params: { id: string } }
) {
  const deleted = await db.users.delete(params.id)
  if (!deleted) return new Response(null, { status: 404 })
  return new Response(null, { status: 204 })
}
Django
# views.py
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def user_detail(request, user_id):
    if request.method == 'DELETE':
        deleted, _ = User.objects.filter(pk=user_id).delete()
        if not deleted:
            return JsonResponse({'error': 'Not found'}, status=404)
        return HttpResponse(status=204)

Frequently asked questions

What is the HTTP DELETE method?

DELETE asks the server to remove the resource at a URL. The server decides what happens underneath: it might erase the data, archive it, or keep it for audit.

Is DELETE idempotent?

Yes. Two identical DELETEs leave the same state as one. The first might return 204 and the second 404; idempotency is about the effect, not the status code.

Should DELETE have a request body?

Usually not. A DELETE body has no defined meaning in HTTP, and some servers and proxies reject it. Put the target in the URL, or use a documented POST endpoint for bulk deletes.

What status code should DELETE return?

Return 204 when the resource is gone, 200 if you send a body describing the result, or 202 if deletion is queued. For an already absent target, pick 404 or 204 and document it.

Sources

  1. MDN Web Docs: DELETEdeveloper.mozilla.org
  2. RFC 9110: DELETErfc-editor.org

Keep going

Browse /search