Soft Delete 🗑 – Node.js, MongoDB

In this article, we will teach you, how you can implement soft delete in your Node.js app with MongoDB as database.

We also created a tutorial on how to do soft delete in Laravel, check our tutorial here.

Soft delete 🗑 is useful when you do not want to actually delete the record from database, but mark the data as deleted.

You might have seen “This message has been deleted” on WhatsApp đŸ’Ŧ when you delete a message. That was soft delete. Because if you actually delete the message from database, then you would have no reference if there was a message at that time.

It is also helpful in cases where you want to allow the user to delete some data, but wants the administrator 👨đŸģ‍đŸ’ŧ to see the data. So when user deletes the data, it will not actually delete the data from database. It will simply hide it from user, but administrator can still see it. Only administrator can actually delete the data.

We will use “users” as entity for the sake of this tutorial.

In this article, you will learn:

  1. Insert users in MongoDB
  2. Fetch non-trashed users
  3. Soft delete a user
  4. Fetch trashed users
  5. Restore a user
  6. Permanently delete a user

1. Insert users in MongoDB ➕

Following API will insert a new document in MongoDB “users” collection using Node.js:

// API to insert data
app.post("/insert", async function (request, result) {
    // Get input fields
    const name = request.fields.name || "";

    // Insert data in database
    await db.collection("users")
        .insertOne({
            name: name,
            deletedAt: null // This will save the time when user was deleted
        });

    // Return response to the client
    result.json({
        status: "success",
        message: "User has been inserted."
    });
});

It will get user name from client app and insert a new document in “users” collection. Note that it creates another field “deletedAt” and set it’s value to null. Having NULL value means that the user is not deleted.

2. Fetch non-trashed users

Then we are going to fetch all the users that are not deleted.

// API to fetch non-trashed users
app.post("/fetch", async function (request, result) {
    const users = await db.collection("users")
        .find({
            deletedAt: null
        }).toArray();

    result.json({
        status: "success",
        message: "Users has been fetched.",
        users: users
    });
});

Here we are using filter to fetch all the records where deletedAt field is null.

3. Soft delete a user ␥

Now we will create an API that will soft delete a user.

// API to move the user to trash can
app.post("/delete", async function (request, result) {
    // Get user ID
    const _id = request.fields._id || "";

    // Don't delete the document,
    // just update the deletedAt field
    await db.collection("users")
        .updateOne({
            _id: ObjectId.createFromHexString(_id)
        }, {
            $set: {
                deletedAt: new Date()
            }
        });

    result.json({
        status: "success",
        message: "User has been moved to trash can."
    });
});

Here, we are first getting the user ID from client app. Then we are setting the deletedAt field to the current date.

If you run the /fetch API again, you will not see that user’s data in the response. Even though the record exists in the database, it will not be returned.

4. Fetch trashed users

Now that we have trashed the user, we need to find a way to see all those trashed users. So we will create an API that will return all the users that has been soft-deleted.

// API to see all trashed users
app.post("/trashed", async function (request, result) {
    const users = await db.collection("users")
        .find({
            deletedAt: {
                $ne: null
            }
        }).toArray();

    result.json({
        status: "success",
        message: "Trashed users has been fetched.",
        users: users
    });
});

At this point, you can give administrator 2 options, either:

  1. Restore the user
  2. Delete the user permanently

5. Restore a user â†Šī¸

Restoring the user will simply set the deletedAt field of user back to null, like it was when user was created.

// API to restore a user
app.post("/restore", async function (request, result) {
    const _id = request.fields._id || "";

    await db.collection("users")
        .updateOne({
            _id: ObjectId.createFromHexString(_id)
        }, {
            $set: {
                deletedAt: null
            }
        });

    result.json({
        status: "success",
        message: "User has been restored."
    });
});

This will also receive a user ID that needs to be restored. If you call the /fetch API again, you will see that user in the response.

6. Permanently delete a user 🗑

Permanently deleting a user will actually deletes the user from database. There will no way to restore it once done (unless you have a backup 🎒 of your database).

// API to actually delete the user from database
app.post("/delete-permanently", async function (request, result) {
    const _id = request.fields._id || "";

    await db.collection("users")
        .deleteOne({
            $and: [{
                _id: ObjectId.createFromHexString(_id)
            }, {
                deletedAt: {
                    $ne: null
                }
            }]
        });

    result.json({
        status: "success",
        message: "User has been permanently deleted."
    });
});

Usually, this access is given to super admin only.

Bonus â­ī¸

One more thing you can do, is if you have saved the user ID in other collections as well, then you can save the data in the dependent collections before removing them.

For example, if you have an “orders” collection, where you are saving user’s orders in the following format:

{
    "_id": ObjectId("1234567890"),
    "userId": ObjectId("0987654321")
}

If you permanently delete the user, then this order’s data will be lost as well. So best practice is to:

  1. Fetch the user.
  2. Put in orders collection.
  3. Delete the user.

Following is the code for that:

const user = await db.collection("users")
    .findOne({
        _id: ObjectId.createFromHexString(_id)
    });

if (user == null) {
    result.json({
        status: "error",
        message: "User not found."
    });
    return;
}

await db.collection("orders")
    .updateMany({
        userId: user._id
    }, {
        $set: {
            user: {
                _id: user._id,
                name: user.name
            }
        }
    });

Now, when you delete the user, your “orders” collection will save the user data for historical use.

{
    "_id": ObjectId("1234567890"),
    "userId": ObjectId("0987654321"),
    "user": {
        "_id": ObjectId("0987654321"),
        "name": "Adnan"
    }
}

Conclusion 📝

So that’s it. That’s how you can create a soft delete feature in your Node.js application with MongoDB as backend. It is very helpful in almost every project and I would recommend everyone must apply it. Whenever user delete something, soft delete it so if user done that by mistake, he will be able to recover it.

Laravel Soft Delete – Create a Trash Can using PHP and MySQL

Laravel soft delete allows you to enable some models that will not be permanently deleted by just calling the delete() method. Instead, you need to call the forceDelete() method in order to permanently delete the data.

The deleted models will not be fetched from Eloquent or Query Builder queries. Instead, you need to call a separate function in order to fetch the records along with deleted ones, we will get on that later in this article. Laravel soft delete has several advantages:

  • First, it works like your PC’s recycle bin. If you accidentally deleted from record from your admin panel, it will not be deleted permanently at once.
  • Second, you can display all deleted data on a separate page.
  • You have a backup of your deleted data, so in the future, if you need some old data, you can always get it from recycle bin or trash can.
  • Can display all data to the user and highlight the data that is deleted.
  • Always be able to restore the accidentally deleted records.

Let’s get started

So let’s get started by creating a simple Laravel soft delete module. For the sake of this article, we will be implementing a trash can in our user’s model.

1. Creating a column in the user’s table

First, you need to run the following command to create a migration for soft deleting:

php artisan make:migration add_column_soft_delete_in_users_table

Then you need to open your migration file and add the following line in the Schema::table function inside up() method:

$table->softDeletes();

Then you need to run the migration using the following command:

php artisan migrate

Open your phpMyAdmin and you will see a new column at the end of the user’s table named deleted_at. Its default value will be NULL and it will have a current date & time value once you call the delete() method.

2. Laravel Soft Delete Eloquent Model

Open your user model usually in App\Models\User.php and update it as the following:

use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable, SoftDeletes;

    ///////////
}

After that, whenever you call the delete() method on any user object, you will see that deleted_at the field will be updated to the current date & time of that user.

3. Display all Users with Pagination

Let’s say that you have an admin panel. We will be displaying all users (non-deleted) on one page. So first create a route for this in your routes/web.php file:

use App\Http\Controllers\UserController;

Route::group([
    "prefix" => "users"
], function () {
    Route::get("/", [UserController::class, "index"]);

    // [other routes goes here]
});

We have created a group because we will be adding more routes to it. For example, route to delete a user, route to display all deleted users, route to restore a user, and route to permanently delete a user. Run the following command if you do not have the UserController:

php artisan make:controller UserController

Now, create an index() method in your UserController class. Following is what your UserController should look like:

use App\Models\User;

public function index()
{
    $users = User::orderBy("id", "desc")->paginate();
    $trashed = User::onlyTrashed()->count();

    return view("users/index", [
        "users" => $users,
        "trashed" => $trashed
    ]);
}

Create a folder named users and inside this folder, create a file in resources/views/users/index.blade.php. This file should have the following code:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css" />
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/js/bootstrap.min.js"></script>

<a href="{{ url('/users/trashed') }}">
    Trash ({{ $trashed }})
</a>

<table class="table table-bordered">
    <thead>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Email</th>
        </tr>
    </thead>

    <tbody>
        @foreach ($users as $user)
            <tr>
                <td>{{ $user->id }}</td>
                <td>{{ $user->name }}</td>
                <td>{{ $user->email }}</td>
            </tr>
        @endforeach
    </tbody>
</table>

{{ $users->appends(request()->except("page"))->links() }}

Refresh the page now and you will see a list of all users. Now we need to create a button to delete a user.

4. Delete a User

To delete a user, we will first a heading in our <thead> tag:

<th>Actions</th>

Then in <tbody>, we need to create a form which when submit will first ask for confirmation. If confirmed, then it will mark the user as deleted.

<td>
	<form method="POST" action="{{ url('/users/delete') }}" onsubmit="return confirm('Are you sure you want to delete this user ?');">
		{{ csrf_field() }}
		<input type="hidden" name="id" value="{{ $user->id }}" required>
		<button type="submit" class="btn btn-danger">
			Delete
		</button>
	</form>
</td>

Refresh the page and you will see a delete button along with each user. Then create a route on your web.php file:

Route::post("/delete", [UserController::class, "destroy"]);

The above lines go in the [other routes goes here] section of web.php. Then create a method in your UserController to mark the user as deleted:

public function destroy()
{
    $user = User::find(request()->id);
    if ($user == null)
    {
        abort(404);
    }

    $user->delete();
    return redirect()->back();
}

You can delete the user in one line too, like this: User::destroy(request()->id); but fetching the user first has some advantages:

  • You can perform any other sub-functions before deleting a user.
  • Or check if the user exists or not, and display the proper messages accordingly.

Try deleting a user now, you will see that will no longer be displayed in your Bootstrap table. But you can still see the record in your database using phpMyAdmin. However, you will notice that the deleted_at the field is updated and now has the UTC date and time value when the delete operation was performed.

5. Show All Deleted Users

At 3rd step, we create an anchor tag that displays all the trashed user’s counts. Now is the time to create a route for it. To create a route on your routes/web file.

Route::get("/trashed", [UserController::class, "trashed_users"]);

Then create the following method in your UserController:

public function trashed_users()
{
    $trashed = User::onlyTrashed()->orderBy("id", "desc")->paginate();

    return view("users/trashed", [
        "trashed" => $trashed
    ]);
}

After that, create a file resources/views/users/trashed.blade.php. It will have the following code:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css" />
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/js/bootstrap.min.js"></script>

<table class="table table-bordered">
    <thead>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Email</th>
            <th>Deleted at</th>
            <th>Actions</th>
        </tr>
    </thead>

    <tbody>
        @foreach ($trashed as $user)
            <tr>
                <td>{{ $user->id }}</td>
                <td>{{ $user->name }}</td>
                <td>{{ $user->email }}</td>
                <td>{{ $user->deleted_at }}</td>
            </tr>
        @endforeach
    </tbody>
</table>

{{ $trashed->appends(request()->except("page"))->links() }}

Refresh the page now and you will see all deleted users in a table. You will also be able to view the date and time when they were deleted.

6. Restore the Deleted Records

First, create another <td> tag at the end of <tbody> tag. In this table cell, we will create a button that will ask for confirmation. And when confirmed, will restore the user.

<td>
    <form method="POST"
        action="{{ url('/users/restore') }}"
        onsubmit="return confirm('Are you sure you want to restore this user ?');">

        {{ csrf_field() }}

        <input type="hidden" name="id" value="{{ $user->id }}" required />
        <button type="submit" class="btn btn-success">
            Restore
        </button>
    </form>
</td>

Then create a route in your web/routes.php that will handle this request.

Route::post("/restore", [UserController::class, "do_restore"]);

After that, create the following method in your UserController:

public function do_restore()
{
    $user = User::withTrashed()->find(request()->id);
    if ($user == null)
    {
        abort(404);
    }

    $user->restore();
    return redirect()->back();
}

Refresh the page now and you will see a “Restore” button along with each delete user. On clicking that, you will be asked for confirmation. If confirmed, you will no longer see that record in the trash can. But you will start seeing that record in all users table.

If you open your database using phpMyAdmin, you will see that user will again have the deleted_at column value as NULL.

7. Permanently Delete Records

Where you are displaying all soft-deleted records in your trash can, you are already displaying a button to restore the record. Now is the time to display another button that will permanently delete the record from the database. Create a button along with the “Restore” button in your trash can:

<form method="POST"
    action="{{ url('/users/delete-permanently') }}"
    onsubmit="return confirm('Are you sure you want to permanently delete this user ?');">

    {{ csrf_field() }}

    <input type="hidden" name="id" value="{{ $user->id }}" required />
    <button type="submit" class="btn btn-danger">
        Delete
    </button>
</form>

Then, you need to create a route that will handle this request.

Route::post("/delete-permanently", [UserController::class, "delete_permanently"]);

Then you need to create a method in your UserController that will permanently delete that record from the database.

public function delete_permanently()
{
    $user = User::withTrashed()->find(request()->id);
    if ($user == null)
    {
        abort(404);
    }

    $user->forceDelete();
    return redirect()->back();
}

Refresh the page now and click on the delete button from the trash can. You will be asked for confirmation. Once confirmed, you will no longer see that record in the trash can nor in all users table. If you check the database, you will see that, that user’s row has been permanently deleted from the database.

Get our social networking site project developed in Laravel:

Social Networking Site in Laravel – Node JS, Socket IO

So that’s how you can implement a complete Laravel soft delete module in your web application.

[wpdm_package id=’1545′]