TODO - Basic
This ia a simple todo application which is discussed in this article:
https://dev.to/mdchaney/adding-a-confirmation-interstitial-on-create-in-rest-4an0
Posted on July 24, 2024
On X, Tom Rossi asked about handling a confirmation page on a form while conforming to RESTful routes.
This is an excellent question and one which I've answered in a couple of different ways in various projects. But, I'm a bit of a REST snob these days and I want any solution to be as RESTy as possible.
Well, that's a good question. Let's take the kind of straightforward method of doing this and consider it in the context of REST and Ruby on Rails, with a simple "todo list" application.
The TodoList application has a single model called "Todo". Each todo record has a varchar field called "body" and a boolean field called "finished". Very simple.
class CreateTodos < ActiveRecord::Migration[7.1]
def change
create_table :todos do |t|
t.string :body, limit: 100, null: false
t.boolean :finished, null: false, default: false
t.timestamps
end
end
end
The model:
class Todo < ApplicationRecord
normalizes :body, with: ->(value) { value.strip }
validates :body, presence: true, length: { maximum: 100 }
end
With the standard scaffold, we can now create, list, examine, edit, and delete todos.
If we hit the route /todos/new
, we'll get a form which we can fill in to create a new item. When we submit the form - which is a POST request to the /todos
route, the item is checked for validity and created if it's valid. This is standard REST.
How do you add a confirmation page in there?
This is where it gets unRESTy. Let's consider the flow with the interstitial in place:
One aspect of this to consider is that the user wants to see any validation errors at step 3. This brings up an issue because we then have to validate the data somehow after step 2 but before it's actually saved. With RoR, this requires a bit of thought because it's easier to wait until step 6. But users will complain that your application is moronic if they don't see errors until that late in the game. I might agree with them.
There are multiple ways to handle this. I have created a simple Rails app to demonstrate all of them:
This ia a simple todo application which is discussed in this article:
https://dev.to/mdchaney/adding-a-confirmation-interstitial-on-create-in-rest-4an0
Add a "please" parameter to the form, and only perform the "create" action if it is present. This allows you to show the information again and stick it all in hidden fields along with your "please" parameter. Then, the user can hit "Really Save" and it'll hit the same create route and actually save it.
This works (I know, I did it 20 something years ago) but it's unRESTy and makes me want to shower after I write it. The "create" route may or may not create the item depending on the presence of the "please" parameter.
A challenge with this method is that after you have the data payload it's required to use the HTTP post method to make sure your data is moved forward continuously. That means that if they hit "Make Changes" you need to either a) allow the "new" route to accept a post with some data or b) add a different route. In the sample app I use a "redo" route that accepts a post.
If you think about it in terms of an API, the whole thing is kind of silly because the client can simply send the "please" parameter the first time and not worry about confirmation, anyway. More on this later.
This is RESTier than the former solution. The idea here is that the form doesn't just POST to the /todos
route (which is "create" in RESTland). Instead, it posts to another route - such as /todos/confirm
- which validates the data and handles steps 3 and 4. Then, if the user is happy with the data, they can hit "Really Save" and the data is posted to the real "create" route. Note that this still requires the extra "redo" route as well.
This is definitely the RESTier way to do it. But one issue that this and the former method have in common is that the data is changeable and the user still has the capability of doing silly stuff. They're not constrained by the interface.
This fixes the problems with the second method. In this method, the data from the form is encrypted and signed, then stored in a hidden field. The data is shown to the user, but the only data that is transmitted back to the server on the "create" route is an encrypted (or just signed) blob that can be used to recreate the data.
This strikes me as less RESTy, and the reason is that the I feel (yes, this is purely subjective) that the data that is sent to the "create" route should be the data for the record in form data or JSON format.
There is another way that gets around that issue, and that is to simply create a cryptographic signature on the back end in step 4 which is sent back to the confirmation form along with the field data, then verified before the item is created. That's a little more work as you have to make sure to create the signature in a repeatable manner.
So, this is a reasonable way to do it, but it's still not as RESTy as I'd like. But I think it's RESTy enough.
I know I sound a bit anal about making sure the data wasn't changed, but this really is a UI issue that we're handling between the front end and the back end. The back end ultimately decides whether to accept a set of data or not based on its validations, and I personally believe that the UI part that makes this tricky is handling the fact that after the back end vouches for a set of data and says "it validated" we want to make sure that's the set of data that is later sent to it to be saved. It'll help prevent some weird errors.
Going back to the API - if you're creating something like this using React then the front end has to really handle everything. In that case, you can think about the discrete components easier:
Alternately, this also works if the server just sends the binary blob representing the marshalled/encrypted/signed data.
But, isn't this just the "please" parameter all over again? No. The reason it isn't is that the "please" parameter can be easily faked and sent to the server at any time. This method forces the front end to first ask the back end "is this data valid?", and then send the proof of validity (the signature) back along with the data so the server can trust that it validated it before.
Again, for me the easiest way to handle this is the binary blob. I use this pattern in multiple places, and sometimes it's not for confirmation directly. I have some multi-step forms that require some data to be entered, and then saved across the next page which might do something completely different. It's again important to ensure that after the server has vouched for the data that it get the exact same data next time.
Another common place to use this is in ordering. I don't want to create the order in the database until payment is secured. And I don't want the user to be able to edit the form and get free items or something like that.
Rails provides ActiveSupport::MessageEncryptor
to help encrypt and sign. The basic framework is this:
Later:
class TodoWithBlob < ApplicationRecord
normalizes :body, with: ->(value) { value.strip }
def self.encryptor
@encryptor ||=
begin
salt = "very salty salt"
key_gen = ActiveSupport::KeyGenerator.new(Rails.application.credentials.secret_key_base, iterations: 1000)
secret = key_gen.generate_key(salt,32)
@encryptor = ActiveSupport::MessageEncryptor.new(secret)
end
end
def as_json_string
attributes.except("id", "created_at", "updated_at").to_json
end
def as_encrypted_blob
self.class.encryptor.encrypt_and_sign(self.as_json_string)
end
def self.encrypted_blob_to_json(encrypted_blob)
JSON.parse(encryptor.decrypt_and_verify(encrypted_blob))
end
def self.from_encrypted_blob(encrypted_blob)
new(encrypted_blob_to_json(encrypted_blob))
end
validates :body, presence: true, length: { maximum: 100 }
end
I also use this basic pattern when handling orders that need to be validated but not saved in the database until the payment has processed.
The Universal ID gem will be usable here as well when signing is added to it.
We've looked at some various ways to try to keep our app as RESTy as possible while providing a decent UI to the user. The UI shows the user any errors immediately at form submission and assumes that when they confirm their input on the next page it will be valid. I strongly recommend one of the latter two options (signature or signed blob) to ensure that the data isn't tampered with intentionally or accidentally between validation on the server and ultimately saving it.
When I get time I'll use a similar technique to make a multi-page form.
Posted on July 24, 2024
Sign up to receive the latest update from our blog.
August 29, 2023