I'm looking for some best practices for "dry run" operations for REST APIs.
Say I have an endpoint that transfers money from account A to account B. I can initiate a transfer like so:
POST /transactions
{
"amount": 1000, // how much to transfer
"source": "A", // account to transfer from
"destination": "B" // account to transfer to
}
This action creates a "transaction", so the response would be a transaction object with payload containing:
{
"id": "txn-123",
"amount": 1000,
"source": "A",
"destination": "B",
"fees": 10, // any fees that were charged
"balance": 2500 // balance in source account AFTER transfer
}
I'd like to be able to perform a dry run for a couple reasons:
- to detrmine if the transfer will succeed (e.g. it may fail if there is a low balance in account A)
- to determine what the applicable fees would be beforehand
So, what is the best practice for a "dry run" concept? I can think of a few options:
- Pass a flag to the existing transfer endpoint to indicate a dry-run option. The flag could be a querystring, part of the payload, a header, etc. The exact implementation is up for debate, but conceptually I like this because it gives a clean interface to know all you have to know about performing a transfer from a single endpoint.
- Dedicated endpoint specifically for performing a transfer dry run. This "feels" safer because you don't inadvertently perform a destructive action since the live and dry-run endpoints are completely separate. On the other hand, you really should know what you're doing if you have access to a production system, so I'm not a huge fan.
- No dry-run concept. Just have a completely different set of endpoints to calculate fees, or get balance, or any other operation that helps you infer the outcome of a transfer. I don't like this because you're forcing the client to replicate all the logic that is already contained in the transfer endpoint.
These are my opinions on the matter so far, but I'd enjoy hearing other peoples' thoughts.
Option 3 is clearly wrong. You'd be forcing the client to do extra work for no good reason.
Choosing between options 1 and 2 is a matter of taste and the specifics of your API. It's not clear how reasonable it is to consider a
/dry-run-transaction
to be a separate kind of thing than a transaction.If you take option 2, consider making the
/dry-run-transaction
a short-lived resource, or don't persist it at all. That way clients can POST to see fees and you save on storage space.If you use option 1, I think the most technically correct option would be to include a property in the payload, such as
execute: false
. This returns all the information to the client, and lets them do a PUT on the transaction withexecute: true
to submit it exactly as it is. A disadvantage to this approach is you're going to have a bunch of non-executed transactions sitting around (forever?) because people are going to decide not to execute them after seeing the results.I don't think safety comes into the discussion at all. If you're really worried about it, just make
execute: false
the default for option 1.Another approach might be to have endpoints
/transactions
and/transaction-results
. If you post to/transaction
, you execute the transaction and get an id/result for a permanent transaction-result. If you POST to /transaction-results, you get a response with no id and a transient set of results for the transaction. Note I've thought about this for all of 20 seconds, so there might be a glaring problem with it I haven't seen. :-)