HTTP

Method

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.

3 min read beginner Try in Playground

TL;DR: DELETE removes a resource. It’s idempotent—deleting something twice has the same effect as deleting it once.

What is DELETE?

DELETE requests the server to remove the resource at the target URL.

DELETE /api/users/123 HTTP/1.1
Host: api.example.com
Authorization: Bearer token123
```text

```http
HTTP/1.1 204 No Content

Idempotency

DELETE is idempotent—multiple identical requests produce the same result:

// First call: deletes the resource
await fetch('/api/users/123', { method: 'DELETE' }) // 204 No Content

// Second call: resource already gone
await fetch('/api/users/123', { method: 'DELETE' }) // 404 Not Found (or 204)
```javascript

The end state is the same: the resource doesn't exist.

## Response Codes

| Code | Meaning                 | When to Use                   |
| ---- | ----------------------- | ----------------------------- |
| 200  | Deleted, returning data | Include deleted resource info |
| 204  | Deleted, no content     | Most common                   |
| 202  | Accepted for deletion   | Async/queued deletion         |
| 404  | Not found               | Resource doesn't exist        |
| 409  | Conflict                | Can't delete (dependencies)   |

## Implementation

### Express.js

```javascript
app.delete('/api/users/:id', async (req, res) => {
  const { id } = req.params

  const user = await User.findByIdAndDelete(id)

  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }

  res.status(204).send()
})

With Soft Delete

app.delete('/api/users/:id', async (req, res) => {
  const user = await User.findByIdAndUpdate(req.params.id, { deletedAt: new Date() }, { new: true })

  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }

  res.status(204).send()
})
```javascript

### Client-Side

```javascript
async function deleteUser(id) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'DELETE',
    headers: { Authorization: `Bearer ${token}` }
  })

  if (response.status === 404) {
    console.log('User already deleted')
    return true
  }

  if (!response.ok) {
    throw new Error(`Delete failed: ${response.status}`)
  }

  return true
}

Handling Dependencies

app.delete('/api/categories/:id', async (req, res) => {
  const { id } = req.params

  // Check for dependent resources
  const productCount = await Product.countDocuments({ categoryId: id })

  if (productCount > 0) {
    return res.status(409).json({
      error: 'Cannot delete category with products',
      productCount
    })
  }

  await Category.findByIdAndDelete(id)
  res.status(204).send()
})
```text

## Bulk Delete

For deleting multiple resources, use POST (since DELETE shouldn't have a body):

```http
POST /api/users/bulk-delete HTTP/1.1
Content-Type: application/json

{"ids": [123, 456, 789]}
app.post('/api/users/bulk-delete', async (req, res) => {
  const { ids } = req.body
  const result = await User.deleteMany({ _id: { $in: ids } })
  res.json({ deleted: result.deletedCount })
})

Best Practices

  1. Require authentication - Always verify user can delete
  2. Consider soft delete - Set deletedAt instead of removing
  3. Handle cascades - Delete or reassign dependent resources
  4. Return 204 - No body needed for successful delete
  5. Be idempotent - 404 on already-deleted is acceptable

Security Considerations for DELETE Endpoints

DELETE endpoints require careful authorization because the consequences of unauthorized deletion are often irreversible. Every DELETE handler should verify two things: that the requester is authenticated, and that the authenticated user has permission to delete the specific resource being targeted.

Ownership checks are the most common gap. It is not enough to verify that a user is logged in — you must also verify that the resource belongs to them or that they have an administrative role. A common vulnerability is checking authentication but not ownership, allowing any authenticated user to delete any other user’s data by guessing or enumerating resource IDs.

Soft delete is worth considering for any data that users might want to recover. Instead of permanently removing the row from the database, set a deletedAt timestamp and filter it out of normal queries. This gives you an audit trail, enables recovery from accidental deletions, and makes it easier to comply with data retention requirements. Hard delete can still be implemented as a separate administrative operation that runs after a retention period.

Rate limiting DELETE endpoints is important for preventing bulk deletion attacks. An attacker who gains access to a valid session token could script rapid deletions across many resources. Applying stricter rate limits to write operations (POST, PUT, PATCH, DELETE) than to read operations (GET) is a reasonable default. For particularly sensitive deletions, consider requiring re-authentication or a confirmation step, similar to how password managers require the master password before deleting stored credentials.

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 requests the server to remove the resource at the specified URL. It's idempotent—deleting the same resource multiple times has the same effect.

Is DELETE idempotent?

Yes. Deleting a resource multiple times results in the same state (resource gone). The first call returns 200/204, subsequent calls may return 404.

Should DELETE have a request body?

Generally no. Some servers ignore DELETE bodies. If you need to send data, consider using POST with a delete action instead.

What status code should DELETE return?

200 OK with response body, 204 No Content without body, or 202 Accepted for async deletion. Return 404 if resource doesn't exist.

Keep Learning