Why DELETE is Critical in CRUD
The DELETE operation completes the CRUD lifecycle and is essential for data management.
Data Hygiene Removes obsolete, duplicate, or irrelevant data to maintain database performance
User Control Allows users to manage their data, exercise “right to be forgotten”, and remove unwanted content
Storage Management Prevents unnecessary data accumulation and reduces storage costs
Compliance Meets legal requirements like GDPR for data deletion
Without DELETE, applications cannot fully manage their data lifecycle, leading to bloated databases and poor user experience.
Common Security Issues
Using links for deletion creates serious security vulnerabilities.
The Problem with GET Requests
Unsafe Implementation ❌
Safe Implementation ✓
<!-- DO NOT USE THIS -->
< a href = "/delete/{{ user['id'] }}" > Delete </ a >
Problems:
Uses GET method for state-changing operation
Can be triggered by web crawlers
Vulnerable to CSRF attacks
Can be executed accidentally
Browser prefetching may trigger deletion
< form action = "/delete/{{ user['id'] }}" method = "POST" class = "delete-form" >
< button type = "submit" class = "delete-btn" > Delete </ button >
</ form >
Benefits:
Uses POST method appropriately
Not triggered by crawlers
Can include CSRF tokens
Requires explicit user action
HTTP Method Guidelines:
GET : Read data (idempotent, safe)
POST : Create/modify data (non-idempotent)
DELETE : Remove data (idempotent)
Implementing Safe Deletion
<! DOCTYPE html >
< html >
< head >
< title > Users List </ title >
< style >
table {
border-collapse : collapse ;
width : 100 % ;
margin-top : 20 px ;
}
th , td {
border : 1 px solid #ddd ;
padding : 12 px ;
text-align : left ;
}
th {
background-color : #4CAF50 ;
color : white ;
}
.delete-form {
display : inline ;
}
.delete-btn {
background-color : #f44336 ;
color : white ;
border : none ;
padding : 6 px 12 px ;
cursor : pointer ;
border-radius : 4 px ;
}
.delete-btn:hover {
background-color : #da190b ;
}
</ style >
</ head >
< body >
< h1 > Registered Users </ h1 >
{% if message %}
< div class = "alert" > {{ message }} </ div >
{% endif %}
< table >
< thead >
< tr >
< th > ID </ th >
< th > Name </ th >
< th > Email </ th >
< th > Actions </ th >
</ tr >
</ thead >
< tbody >
{% for user in users %}
< tr >
< td > {{ user['id'] }} </ td >
< td > {{ user['name'] }} </ td >
< td > {{ user['email'] }} </ td >
< td >
< form action = "/delete/{{ user['id'] }}"
method = "POST"
class = "delete-form"
onsubmit = " return confirmDelete ('{{ user[' name '] }}')" >
< button type = "submit" class = "delete-btn" > Delete </ button >
</ form >
</ td >
</ tr >
{% endfor %}
</ tbody >
</ table >
< script >
function confirmDelete ( userName ) {
return confirm ( `Are you sure you want to delete user " ${ userName } "? \n\n This action cannot be undone.` );
}
</ script >
</ body >
</ html >
Step 2: Flask Route Handler
import sqlite3
from flask import Flask, render_template, request, redirect, url_for, flash
app = Flask( __name__ )
app.secret_key = 'your-secret-key-here'
def get_db_connection ():
conn = sqlite3.connect( 'users.db' )
conn.row_factory = sqlite3.Row
return conn
@app.route ( '/' )
def index ():
conn = get_db_connection()
users = conn.execute( 'SELECT * FROM users ORDER BY id' ).fetchall()
conn.close()
return render_template( 'index.html' , users = users)
@app.route ( '/delete/<int:user_id>' , methods = [ 'POST' ])
def delete_user ( user_id ):
try :
conn = get_db_connection()
# Get user info before deletion for logging
user = conn.execute( 'SELECT * FROM users WHERE id = ?' ,
(user_id,)).fetchone()
if user is None :
flash( 'User not found' , 'error' )
conn.close()
return redirect(url_for( 'index' ))
# Delete the user
conn.execute( 'DELETE FROM users WHERE id = ?' , (user_id,))
conn.commit()
conn.close()
flash( f 'User " { user[ "name" ] } " deleted successfully' , 'success' )
except sqlite3.Error as e:
flash( f 'Error deleting user: { str (e) } ' , 'error' )
return redirect(url_for( 'index' ))
if __name__ == '__main__' :
app.run( debug = True )
Key Implementation Details
@app.route ( '/delete/<int:user_id>' , methods = [ 'POST' ])
By specifying methods=['POST'], Flask will reject GET requests to this endpoint, preventing accidental or malicious deletions.
@app.route ( '/delete/<int:user_id>' , methods = [ 'POST' ])
def delete_user ( user_id ):
The <int:user_id> ensures only integer IDs are accepted. Invalid types return 404 automatically.
user = conn.execute( 'SELECT * FROM users WHERE id = ?' ,
(user_id,)).fetchone()
if user is None :
flash( 'User not found' , 'error' )
return redirect(url_for( 'index' ))
Always verify the record exists before attempting deletion to provide meaningful feedback.
flash( f 'User " { user[ "name" ] } " deleted successfully' , 'success' )
Provide clear confirmation to the user about what action was performed.
Enhanced Safety Features
1. Client-Side Confirmation
Modern Modal Confirmation
< style >
.modal {
display : none ;
position : fixed ;
z-index : 1000 ;
left : 0 ;
top : 0 ;
width : 100 % ;
height : 100 % ;
background-color : rgba ( 0 , 0 , 0 , 0.5 );
}
.modal-content {
background-color : white ;
margin : 15 % auto ;
padding : 20 px ;
border-radius : 8 px ;
width : 400 px ;
text-align : center ;
}
.modal-buttons {
margin-top : 20 px ;
}
.confirm-btn {
background-color : #f44336 ;
color : white ;
padding : 10 px 20 px ;
border : none ;
border-radius : 4 px ;
margin : 0 5 px ;
cursor : pointer ;
}
.cancel-btn {
background-color : #ccc ;
color : black ;
padding : 10 px 20 px ;
border : none ;
border-radius : 4 px ;
margin : 0 5 px ;
cursor : pointer ;
}
</ style >
< div id = "deleteModal" class = "modal" >
< div class = "modal-content" >
< h3 > Confirm Deletion </ h3 >
< p id = "deleteMessage" ></ p >
< div class = "modal-buttons" >
< button class = "cancel-btn" onclick = " closeModal ()" > Cancel </ button >
< button class = "confirm-btn" onclick = " confirmDeletion ()" > Delete </ button >
</ div >
</ div >
</ div >
< script >
let pendingDeleteForm = null ;
function showDeleteModal ( userName , form ) {
event . preventDefault ();
pendingDeleteForm = form ;
document . getElementById ( 'deleteMessage' ). textContent =
`Are you sure you want to delete user " ${ userName } "?` ;
document . getElementById ( 'deleteModal' ). style . display = 'block' ;
}
function closeModal () {
document . getElementById ( 'deleteModal' ). style . display = 'none' ;
pendingDeleteForm = null ;
}
function confirmDeletion () {
if ( pendingDeleteForm ) {
pendingDeleteForm . submit ();
}
}
</ script >
<!-- Usage in table -->
< form action = "/delete/{{ user['id'] }}"
method = "POST"
onsubmit = " showDeleteModal ('{{ user[' name '] }}', this ); return false ;" >
< button type = "submit" class = "delete-btn" > Delete </ button >
</ form >
2. CSRF Protection
app.py with CSRF
Form with CSRF Token
from flask_wtf.csrf import CSRFProtect
app = Flask( __name__ )
app.secret_key = 'your-secret-key-here'
csrf = CSRFProtect(app)
@app.route ( '/delete/<int:user_id>' , methods = [ 'POST' ])
@csrf.protect () # Automatic CSRF validation
def delete_user ( user_id ):
# deletion logic
pass
CSRF (Cross-Site Request Forgery) tokens prevent malicious websites from triggering actions on your site using a user’s authenticated session.
3. Authorization Check
from flask_login import login_required, current_user
@app.route ( '/delete/<int:user_id>' , methods = [ 'POST' ])
@login_required
def delete_user ( user_id ):
# Check if user has permission to delete
if not current_user.is_admin and current_user.id != user_id:
flash( 'You do not have permission to delete this user' , 'error' )
return redirect(url_for( 'index' ))
# Prevent users from deleting themselves
if current_user.id == user_id:
flash( 'You cannot delete your own account' , 'error' )
return redirect(url_for( 'index' ))
# Proceed with deletion
conn = get_db_connection()
conn.execute( 'DELETE FROM users WHERE id = ?' , (user_id,))
conn.commit()
conn.close()
flash( 'User deleted successfully' , 'success' )
return redirect(url_for( 'index' ))
Soft Delete Alternative
For cases where you need to preserve data or enable “undo”, implement soft deletes.
Add deleted_at column
Soft Delete Implementation
ALTER TABLE users ADD COLUMN deleted_at DATETIME ;
Soft deletes allow you to:
Implement “Restore” functionality
Maintain referential integrity
Keep audit trails
Comply with data retention policies
Best Practices Checklist
Use POST method
Never use GET requests for deletion operations Forms with method="POST" prevent accidental execution
Implement confirmation
Always ask users to confirm destructive actions JavaScript confirmation dialogs or modal windows
Add CSRF protection
Prevent cross-site request forgery attacks Use flask_wtf.csrf or similar libraries
Verify authorization
Ensure users can only delete data they own Check user permissions before deletion
Validate existence
Check if record exists before attempting deletion Provide meaningful error messages
Provide feedback
Inform users about the result of their action Use flash messages for success/error states
Consider soft deletes
For sensitive data, mark as deleted instead of removing Allows data recovery and audit trails
Never implement deletion via simple links or GET requests. This is a critical security vulnerability that can lead to data loss through:
Accidental clicks
Web crawler indexing
Browser prefetching
CSRF attacks
In production applications, consider implementing:
Audit logging for all deletions
Time-based data retention policies
Batch deletion with transaction support
Cascade deletion rules for related records