Prevent Unwanted Overrides by Using Modification Dates
After connecting your client-side application to the backend, there's a significant risk that may be overlooked: unwanted overrides. Suppose you're using one application on two different devices - a mobile and a desktop. Imagine you're working on a document with the same title on both devices. You make minor changes on the mobile device, but significant alterations on the desktop. However, you forgot to refresh the data on mobile and you saved it.
If the system is not prepared for this, you'll likely lose all your progress from the desktop device, which is not only frustrating but also nonsensical. Following such a scenario, your app will probably lose many users.
You might wonder why not perform validation on the frontend, or refresh the data that the user is working on more frequently. Indeed, these are possible solutions, but the API should be secure and stable on its own. APIs should be independent of the presentation layer - they are APIs, after all. The endpoints accept inputs, and as a result, there is always the same, predictable outcome of the operation (in theory (~ ̄▽ ̄)~).
So, perhaps Web sockets? Yes, it could be a great solution, but not every application uses this technology. The fix is really simple: you can perform validation on the backend side, and return a specific operation symbol or code based on this issue. Then, your client application can react to it - display an error, make additional calls, or direct the user to a new version of the given entity.
Let's try to implement some kind of validation on the backend side to block the possibility of accidentally overriding the data by using modification date guards.
Crafting a Backend Solution
Every entity that is created in your database usually has a creation date and modification date. If not, it's a good idea to start adding these as they are important :-D. In our context, the creation date is irrelevant for the purpose of this article, so I'll focus on the latter.
When an entity is created in the database, the modification date is the same as the creation date. Later, when, let's say, the PUT:ENTITY
endpoint is called, the modification date is updated. Typically, it looks like this:
app.put('/entity/:id', (req, res) => {
const { id } = req.params;
const { data, mdate } = req.body;
const entity = await db.entity.get(id);
// Update modification date to current time.
await db.entity.update(id, { name: data.name, mdate: new Date().toISOString() });
res.send({
message: "Entity updated successfully",
entity
});
});
To prevent unexpected overrides within the payload
property, it is essential to include an additional parameter - the modification date - from the client application, or more specifically, from any API consumer. We can then make a straightforward comparison: payload.mdate !== entity.mdate
. If these values differ, it is evident that the user is attempting to modify an entity that has already been changed.
Some applications use a different solution; they pass the
mdate
as a header in every request. It's 100% up to you which way to choose, as both methods are valid.
In this case, we need to take action. Since we're unable to update the entity, we want to throw an error. Fortunately, there's a special status code for this situation: 409 Conflict
.
// Check if the entity exists and if the modification dates match.
if (mdate !== entity.mdate) {
return res.status(409).send({
message: "Conflict detected: The entity has been modified since your last update. Please fetch the latest version."
});
}
The complete code on the backend side looks like this:
app.put('/entity/:id', (req, res) => {
const { id } = req.params;
const { data, mdate } = req.body;
const entity = await db.entity.get(id);
if (mdate !== entity.mdate) {
return res.status(409).send({
message: "Conflict detected: The entity has been modified since your last update. Please fetch the latest version."
});
}
await db.entity.update(id, { name: data.name, mdate: new Date().toISOString() });
res.send({
message: "Entity updated successfully",
entity
});
});
Handling 409 Status on Client
Now, we're 100% protected, but we still need to handle this on the client side. What we should do is listen for a 409 status and display a slightly different error message that prompts the user to refresh the data. Of course, you can implement any behavior you prefer - this is just for demonstration purposes. Take a look at the following gif to see how it works: I modified the first entity on mobile and then attempted to override an outdated one on desktop.
The code that is implemented looks similar to this one:
import axios from 'axios';
import { toast } from 'react-toastify';
// Assuming you are using react-toastify for notifications.
function updateEntity(entity) {
axios.post('/api/entity/update', entity)
.then(response => {
toast.success("Entity updated successfully!");
})
.catch(error => {
if (error.response && error.response.status === 409) {
toast.error("Data conflict detected. Please refresh the data!");
} else {
toast.error("An unexpected error occurred!");
}
});
}
Summary
Now you know how important it is to protect your application, especially the API, from accidental overrides. The concept is simple, yet it is often overlooked in many APIs, potentially leading to strange behaviors. While some of these situations can be safeguarded on the Frontend side, a bug could leave you vulnerable.
The API should be stable and entirely reliable because it primarily handles the business logic - the frontend merely displays this information and reacts to changes.