Implementing soft deletes with Prisma and middleware
When developing our REST APIs, we often focus on implementing the four fundamental operations: creating, reading, updating, and deleting (CRUD). The most basic…
April 24, 2023
When developing our REST APIs, we often focus on implementing the four fundamental operations: creating, reading, updating, and deleting (CRUD). The most basic approach to removing a record from our database is to delete it permanently. In this article, we explore the idea of soft deletes that allow us to keep the removed entities in the database.
The most straightforward way of implementing the soft deletes feature is through a boolean flag.
CREATE TABLE categories ( id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL, is_deleted boolean DEFAULT false)
By adding the is_deleted boolean DEFAULT false line above, we add the is_deleted column that has the false value by default.
Whenever we want to remove a record in the categories table, we don’t have to delete it permanently. Instead, we change the value in the is_deleted column to true.
UPDATE categoriesSET is_deleted = trueWHERE name = 'NestJS'
The most important thing to understand about soft deletes is that they affect various queries we perform. Let’s try fetching a list of all the categories, for example.
SELECT * FROM categories
The above query returns all categories, including the deleted ones. So, to fix this issue, we need to filter out the removed records.
A significant benefit of soft deletes is that we can effortlessly restore the deleted record. This allows for a user experience far superior to restoring backups. For example, in our application, we can implement an undo button that changes the value in the is_deleted column back to false.
We can also take advantage of querying the deleted records from the database. They can prove to be helpful when generating various reports, for example.
Soft deletes can also be useful when dealing with relationships. For example, permanently deleting a record referenced in another table can cause a foreignconstraint violation. This does not happen with soft deletes because we don’t remove the entities from the database.
An important disadvantage of soft deletes is that we need to consider them in all related queries. Whenever we fetch our data and forget to filter by the is_deleted column, we might show the user the data they shouldn’t have access to. Implementing filtering can also affect our performance.
Another important consideration is related to the unique constraint. Let’s look at the users table we’ve defined in one of the previous parts of this series.
model User { id Int @id @default(autoincrement()) email String @unique name String password String address Address? @relation(fields: [addressId], references: [id]) addressId Int? @unique posts Post[]}
In our model, we require every email to be unique. When we delete a record permanently, we make the email available to other users. However, removing users through soft deletes does not make their email addresses available for reuse.
The first step when implementing soft deletes is to add the appropriate column to our table. A common approach to soft deletes is storing the deletion date instead of a simple boolean flag.
model Category { id Int @id @default(autoincrement()) name String posts Post[] deletedAt DateTime? @db.Timestamptz}
Above, we’ve added the deletedAt property, which is a timestamp with a timezone. We will assign it with value whenever we want to delete a record from our database.
Now, we need to generate our migration using the Prisma CLI.
npx prisma migrate dev --name category-add-deleted-at-column
Doing the above creates a new file in the migrations directory.
With middleware, we can perform an action before or after a query runs. To attach a middleware, we need to use the $use method. An appropriate place to do that is in our PrismaService class.
Above, we pass a callback to the $use method. Its first argument we call params, contains available parameters, such as the performed action, the model, and the arguments. The second argument, called next, is a function that calls the next action in the chain.
To implement soft deletes with middleware, we need to modify the Prisma query every time a record of a particular type is deleted. We need to change a delete action into the update action and provide the appropriate date.
Now we need to write a middleware that filters out removed categories whenever we fetch them. We need to handle fetching a single category and fetching multiple categories separately.
An advantage of the above approach is that with correctly written middleware, we won’t forget to handle the deletedAt column properly whenever we fetch or delete our entities.
However, this can make our code quite a bit messy. Instead of interacting with the categories table through the CategoriesService, we spread our business logic into unrelated parts of our application. It might also not be instantly apparent to our teammates that whenever they call the this.prismaService.category.delete method, it runs the update action under the hood.
The middleware approach might be suitable in less structured applications that don’t use the NestJS framework. However, in our case, we put the business logic related to categories in the CategoriesService. Therefore, it might make more sense to ditch middleware and implement soft deletes directly through our service for simplicity and readability. Nevertheless, it’s a very good example to learn how the middleware works in Prisma.
Implementing soft deletes with Prisma and middleware | NestJS.io