RESTful API: An Engineering Perspective on Designing Intuitive API Responses
"As an API user, I would like an HTTP 200 with line-by-line errors in 'Add/Update Record'."
I hope that didn't scare you off. It's the summary line of a ticket I filed recently while testing Quick Base's new RESTful JSON API, and it needs some explanation. I'm going to use it as the excuse to introduce myself, and to discuss what it's like to be the QA guy on a team that is designing and implementing a new API layer on top of a platform that's been around as long as Quick Base has. This specific technical point deserves explaining, of course, but we'll be sure to get it into the API portal documentation somewhere as well.
About Me
I go by "JMike", which is my first initial and middle name. It sounded odd to me at first, in my teens, but after a few years it grew on me. Forty years down the road, now, it sounds completely natural. My first software job was to find bugs in MATLAB at The MathWorks in 1991. After a few years of that, I took a break to get a master's degree in numerical analysis and take some time off to play cards. That jack of diamonds in my profile picture isn't just for "J" Mike; I have a pretty good poker story about that card if you ever want to hear it. I came back to software testing and have been working in the Boston area ever since.
I tend to work on products that are scientific or financial applications or software tools, and a software tester, I try very hard to achieve and maintain the perspective of a power user of the product. I use that perspective to help guide my judgment. That judgment is important, especially in the early going of the development of new functionality. It's easy and comfortable to write a bug ticket saying "When I pass a negative number to foobar() it crashes", because that's entirely a verifiable observation of fact that will probably be seen as useful and important. It gets more delicate when you want to say something more like "This whole new function is undocumented. I'm not sure what your intent was for this parameter, but can I suggest that it's not working like anyone would expect it to be used, so can you do something about that. If you're open to suggestion, how about something like this?", because there's so much judgment wrapped up in it.
Development is a Team Effort
I've been at Quick Base for almost a year now, and I've spent the last four months with the team working on our new RESTful APIs. There's a lot of legacy information to learn here! But I'm finding bugs and having productive discussions with the team, so I feel welcome and useful.
For our new RESTful APIs, one thing I've been pushing is to sharpen up the syntax throughout. Sometimes, when you're primarily the user of an API rather than the developer of that API, it's easier for you to be objective and to see its errors, gaps, or just things that just make you go "huh?". I'd like to think that my background makes me a little more sensitive to syntactic consistency than some people are - and as a result I file a lot of picky syntax tickets and the rest of the team plows through them.
So with all the background, here's a chance to segue into this specific technical point. I'll try to go from general to specific here. Bear with me.
Being RESTful
When we say that the new Quick Base API is "RESTful", we're basically saying that we understand the structural and design concepts of the Representational State Transfer (REST) architecture, and that we're developing with those concepts in mind. That means doing our best to provide a system that behaves according to those concepts, and to provide an API to that system that satisfies clients' expectations about the functionality and syntax available to the user of such a system. But that gets pretty deep.
I guess the main abstract thing I've learned to be aware of in a RESTful system is that the server side might be layered, and some layer might be caching a bit of information. And the main concrete things I've learned are what the different HTTP methods mean, what URLs tend to look like, and what HTTP response codes mean.
The result is that, in the new API, we are presenting operations in terms of standard HTTP methods, and using them in a way that we think will make intuitive sense to its users. If you're retrieving information, it will be an HTTP GET. If you're causing computation on the server, it will be an HTTP POST. If you're deleting something, it will be an HTTP DELETE.
HTTP Status Codes
If you are a builder who uses the Quick Base XML API, you may know that you're building and executing an HTTP POST call, sometimes with an XML body wrapped up in a <qdbapi>
tag. You may have also seen that you usually get a response from our API with an HTTP 200 and a chunk of XML.
(By "usually", I mean that if you set the X_QUICKBASE_RETURN_HTTP_ERROR
parameter as described in https://help.quickbase.com/api-guide/optional_parameters.html, Quick Base will respond with an HTTP 400 when there's an API error. But that parameter is not commonly used by most of our builders today.)
To make things more intuitive, we are making an effort to return a standard HTTP return status to all calls, rather than defining - and requiring you to learn - something like our own unique home-brewed <errcode>
list from the XML API. In the broadest sense this means we will return something in the 200s (usually just 200, but sometimes more specific codes when appropriate) when your request has succeeded, something in the 400s when the request fails for a reason that is usually on the client's (your) side, and something in the 500s when your request has failed for a reason that is usually on the server's (our) side.
We also have come up with a general agreement that when you call something in the new RESTful API, if we return a 400- or 500-level response, we will promise that nothing has changed on the server side - in other words, nothing has changed in your app - as a result of this request. We will also send you some additional details in the JSON body of the response:
{ "message": a brief error message, "details": more details about the error }
Responding to Record Adds/Updates
So let's get to the specific API Insert/Update records. In database terminology this is an "upsert" API, which allows you to create records and update others, even in the same API call. (I can't resist a little digression here - note that it is possible you might specify an existing record and "update" it with the same data it already has, in which case it doesn't change anything.) We have made the general architectural decision that, when you attempt to upsert records, we will allow successful row(s) to go through, while reporting specific errors on other row(s) that could not be processed. The fact that we are subdividing the request into individual rows means that each row might have a different status. And there's no industry consensus on exactly what HTTP code(s?) to return, in what structure or format, when some or all of the individual records fail in a combined request.
So we made the decision to return an HTTP 200 if an insert/update records request has the correct format and "makes sense". We then process all rows that have no error, and we include a lineErrors
element in the JSON body to indicate the specific error(s) in the specific row(s) that failed.
Let me give you a specific example. Say your app has a table with ten records in it, whose record IDs run from 1 through 10. Say field 6 is the only required field in the table, and it is a numeric field. If you submit a request to "Insert/Update Records" with this JSON body:
{
"to": table_dbid,
"data": [
{"6": {"value": 100}},
{"6": {"value": "illegal string value"}},
{"6": {"value": 200}}
]
}
you will get this response:
{
"data": [],
"metadata": {
"createdRecordIds": [
11,
12
],
"lineErrors": {
"2": [
"Incompatible value for field with ID \"6\"."
]
},
"totalNumberOfRecordsProcessed": 3,
"unchangedRecordIds": [],
"updatedRecordIds": []
}
}
The way this lineErrors
element got into the response is a pretty good illustration of my role on the team.
Very early on in the development of this API, if you submitted a request with an error in one or more lines, you wouldn't get any direct indication about which line(s) had failed. This fits into a pattern I've seen many times in my many years as a software tester, by the way: when you are working with an API, the normal success cases are generally much better polished than the error cases. (Sometimes it's an oversight, and sometimes - as it turns out was true in this case - the developers had thought about this case but hadn't yet decided on what the result should look like.). And when a specific piece of information is missing in an API in a specific error situation, it's sometimes a lot easier for the user of the interface to see that gap in a specific situation than it was for the builder to have anticipated the error.
So I filed a ticket for the developers - one of those uncomfortable ones that is about halfway between dry fact and possibly-controversial opinion. "As an API user, I would like record-by-record error messaging from the upsert API." I didn't have a specific syntax in mind; I just knew that the user would require the information. This ticket started a good conversation among the team. A solution was designed and implemented, I did a few more test iterations to find smaller problems, and now we have an error response that seems reasonable and natural.
What's Next
My point in bringing up this specific example is to show that our team sees API syntax as an important point, because it affects you, our customers. As you begin to learn and use this new API, you may come up with a syntax idea of your own, or see a confusing result that you wish had more documentation in the API portal. If that happens, I encourage you to let us know. Feel free to report this kind of issue as you would any others - if something in API syntax is broken or incorrect, report it to Care; if you have a suggestion for improvement you can let us know by voting or adding a new idea in Uservoice. As always, we will be listening for new ideas, and looking for ways to make your experience better than ever.
------------------------------J. Michael Hammond
------------------------------