RESTful web services are the rage. APIs for public consumption are everywhere and developers are being tasked daily with the chore of implementing new ones, and ColdBox has a fantastic set of tools for helping you to do just that. As developers, though, we often fall in to the trap of thinking of API's only in terms of delivering data when, in reality, with a bit of foresight and architectural planning, we can create API's that enable conversations with our consumers. In a recent Coldbox Restful Roadshow presentation, two of the "Lessons Learned" I noted, from developing and working with ReSTful APIs over the past few years, included these two recommendations:
- Use all of the ( HTTP ) Verbs
- Use all of the ( HTTP Status ) Codes
By taking the time to learn the language of HTTP verbs and status codes, a developer can implement APIs that enable rich, interactive conversations with consumers of their service. Let's say I were to take a trip across the pond from the US to visit the UK. Upon arrival, I check in to my hotel and then make a trip to the pub down the street for a taste of the local brew and, maybe, some grub.
Now, let's re-envision my trip to the corner pub as if it were an ongoing conversation between an API consumer and provider. Again, it's just a corner pub, so that API is fairly limited:
- THIRSTY DEVELOPER: "Hello! Just flew in to town (and, boy, are my arms tired!). May I see a menu?
HTTP Request:
To start out, I send an OPTIONS request to the pub's representative to find out what's available:1234Request URL: https:
//cornerpub
.co.uk
/api
Accept: application
/json
Request Method: OPTIONS
- CORNER CORNER PUB: "Here's a menu. Have at it, Yank!"
HTTP Response:
The bartender provides me with the pub's options which includes the menu:12345678910111213141516171819202122232425262728293031323334353637383940Status Code: 200
Content Type: application
/json
Response:
{
"menu"
: {
"food"
:
"/api/food"
"drinks"
"/api/drinks"
}
"order"
: {
"POST"
:{
"description"
:
"Create an order"
,
"href"
:
"/orders"
,
"params"
: {
"firstName"
:{
"required"
:
true
,
"type"
:
"string"
,
"description"
:
"Customer's first name"
},
"lastName"
:{
"required"
:
false
,
"type"
:
"string"
,
"description"
:
"Customer's last name"
},
"tableNumber"
:{
"required"
:
false
,
"type"
:
"integer"
,
"description"
:
"The customers table number. Required if not seated at the bar."
},
"barSeating"
:{
"required"
:
false
,
"default"
:
false
,
"type"
:
"boolean"
,
"description"
:
"Whether the patron is seated at the bar."
}
},
}
}
}
- THIRSTY DEVELOPER: "Great, I'm thirsty let's see what's on the drink menu."
HTTP Request:1234Request URL: https:
//cornerpub
.co.uk
/api/drinks
Accept: application
/json
Request Method: OPTIONS
- Like turning the page to the drink menu, the API responds again with:
12345678910111213141516171819202122232425262728293031323334353637
Status Code: 200
Content Type: application
/json
Response:
{
"beer"
: {
"title"
:
"Beer Menu"
,
"params"
:{
"roomtemp"
:{
"type"
:
"boolean"
,
"required"
:
false
,
"description"
:
"Beer which is served at room temperature"
},
"cold"
:{
"type"
:
"boolean"
,
"required"
:
false
,
"description"
:
"Beer which is served cold"
},
}
"href"
:
"/api/drinks/beer"
},
"wine"
: {
"title"
:
"Wine Menu"
,
"params"
:{
"red"
:
"Red wine"
,
"white"
:
"White wine"
}
"href"
:
"/api/drinks/wine"
},
"mixed"
: {
"title"
:
"Mixed Drinks"
,
"params"
{}
"href"
:
"/api/drinks/mixed"
}
}
- THIRSTY DEVELOPER: "A cold beer sounds great! Let's see what you have!"
HTTP Request:1234Request URL: https:
//cornerpub
.co.uk
/api/drinks/beer
?cold=
true
Accept: application
/json
Request Method: GET
- CORNER PUB: "You're in the UK, Yank. We don't have any cold beer."
-
123
Status Code: 204
Content Type: application
/json
- THIRSTY DEVELOPER: "OK, fine. I'm not used to room temperature beer but, since that's all you have, may I see that menu please?"
- Since there were only two options and one of them didn't have anything, I simply request to see the full list:
HTTP Request:1234Request URL: https:
//cornerpub
.co.uk
/api/drinks/beer
Accept: application
/json
Request Method: GET
- CORNER PUB: "Sure, mate. Here you go - 5 at a time:"
- Our pub responds with a status of
206
which signifies partial content. - Note that rather just dumping an array of items, the pubs response separates the sorting of the products from a top-level hashmap. From a performance standpoint, this makes the consumption, re-sorting, and locating of nested data within the individual results much more efficient for the consumer, as well as for the pub, when it comes time to order.
- The API response also includes additional information on the recordset and paging:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
Status Code: 206
Content Type: application
/json
Response:
{
"recordset"
:{
"total"
: 35,
"limit"
: 5
"order"
:
"brand"
,
"orderDirection"
:
"asc"
,
"sorted"
:[
"2743ce3ea2e04dbf972f8873a7f6e3c7"
,
"f6c4198809ce433ea3dce954bd95719b"
,
"5dd41ab29f584bc68e2563c67c5d271f"
,
"111ca56ba1e14cd3bb707e30c4a816da"
,
"82058b6576af41a0a04ea1b457423d32"
],
"href"
: {
"next"
:
"/drinks/beer?page=2"
}
},
"beers"
:{
"82058b6576af41a0a04ea1b457423d32"
: {
"name"
:
"Bishop's Farewell"
,
"href"
:
"/drinks/beer/82058b6576af41a0a04ea1b457423d32"
,
"order"
: {
"href"
:
"/order"
,
"params"
:{
"menutype"
:
"beer"
,
"id"
:
"82058b6576af41a0a04ea1b457423d32"
}
}
},
"111ca56ba1e14cd3bb707e30c4a816da"
: {
"name"
:
"Guinness Foreign Extra Stout"
,
"href"
:
"/drinks/beer/111ca56ba1e14cd3bb707e30c4a816da"
,
"order"
: {
"href"
:
"/order"
,
"params"
:{
"menutype"
:
"beer"
,
"id"
:
"111ca56ba1e14cd3bb707e30c4a816da"
}
}
},
"f6c4198809ce433ea3dce954bd95719b"
: {
"name"
:
"Imperial Brown Stout London 1856"
,
"href"
:
"/drinks/beer/f6c4198809ce433ea3dce954bd95719b"
,
"order"
: {
"href"
:
"/order"
,
"params"
:{
"menutype"
:
"beer"
,
"id"
:
"f6c4198809ce433ea3dce954bd95719b"
}
}
},
"2743ce3ea2e04dbf972f8873a7f6e3c7"
: {
"name"
:
"Kipling"
,
"href"
:
"/drinks/beer/2743ce3ea2e04dbf972f8873a7f6e3c7"
,
"order"
: {
"href"
:
"/order"
,
"params"
:{
"menutype"
:
"beer"
,
"id"
:
"2743ce3ea2e04dbf972f8873a7f6e3c7"
}
}
},
"5dd41ab29f584bc68e2563c67c5d271f"
: {
"name"
:
"Landlord Pale Ale"
,
"href"
:
"/drinks/beer/5dd41ab29f584bc68e2563c67c5d271f"
,
"order"
: {
"href"
:
"/order"
,
"params"
:{
"menutype"
:
"beer"
,
"id"
:
"5dd41ab29f584bc68e2563c67c5d271f"
}
}
}
}
}
- THIRSTY DEVELOPER: "Great! I'd like to place an order."
HTTP Request:1234567891011Request URL: https:
//cornerpub
.co.uk
/api/orders
Accept: application
/json
Request Method: POST
Request Body:
{
"firstName"
:
"Jon"
,
"barSeating"
:
true
}
- CORNER PUB: "Very well. Here's your order information:"
-
The pub responds with a status of
201
, signifying that the order has been created and provides us with the endpoint for future operations:
12345678910Status Code: 201
Content Type: application
/json
Response:
{
"id"
:
"fb5331bf7d9e4dae90f25a190b16037f"
,
"href"
:
"/api/orders/fb5331bf7d9e4dae90f25a190b16037f"
,
}
THIRSTY DEVELOPER: "Perfect. I'd like to order a beer. Tell me how."
- HTTP Request:
1234
Request URL: https:
//cornerpub
.co.uk
/api/orders/fb5331bf7d9e4dae90f25a190b16037f
Accept: application
/json
Request Method: OPTIONS
- CORNER PUB: "Blimey! You sure are needy... **sighs** Here you go:"
-
123456789101112131415161718192021222324252627282930313233343536373839
Status Code: 200
Content Type: application
/json
Response:
{
"PUT"
:{
"description"
:
"Add items to this order"
,
"href"
:
"/api/orders/fb5331bf7d9e4dae90f25a190b16037f"
,
"params"
: {
"itemType"
:{
"required"
:
true
,
"type"
:
"string"
,
"description"
:
"The menu type of the item to order"
},
"itemId"
:{
"required"
:
true
,
"type"
:
"string"
,
"description"
:
"The id of the item to order"
}
}
},
"PATCH"
{
"description"
:
"Update the order"
,
"href"
:
"/api/orders/fb5331bf7d9e4dae90f25a190b16037f"
,
"params"
: {
"closeOrder"
:{
"required"
:
false
,
"type"
:
"boolean"
,
"description"
:
"Use if closing an order"
}
}
}
"DELETE"
:{
"description"
:
"Cancel this order"
,
"href"
:
"/api/orders/fb5331bf7d9e4dae90f25a190b16037f"
}
}
- THIRSTY DEVELOPER: "Thanks so much. I'll have the Kipling, then."
- HTTP Request:
12345678910
Request URL: https:
//cornerpub
.co.uk
/api/orders/fb5331bf7d9e4dae90f25a190b16037f
Accept: application
/json
Request Method: PUT
Request Body:
{
"itemType"
:
"beer"
,
"itemId"
:
"2743ce3ea2e04dbf972f8873a7f6e3c7"
}
- CORNER PUB: [ The bartender's busy and sends a nod, signifying that he's received your order ]:
- Since the order cannot be fulfilled immediately, the server reponds with a status of
202
, saying that the order has been accepted and provides information on how to check the status of the order.12345678910Status Code: 202
Content Type: application
/json
Response:
{
"href"
:
"/api/orders/fb5331bf7d9e4dae90f25a190b16037f/status"
,
"estimatedWait"
:120000
}
- THIRSTY DEVELOPER (after the two minutes specified in the milliseconds wait time have passed): "Any update on my beer?"
I send aHEAD
request, to the Pub's API, which requires nothing to be returned but a status code:
HTTP Request:1234Request URL: https:
//cornerpub
.co.uk
/api/orders/fb5331bf7d9e4dae90f25a190b16037f/status
Accept: application
/json
Request Method: HEAD
- CORNER PUB: "Sorry, mate. I had to tap a new keg. Your order is ready now:"
When we sent a status inquiry, the pub now replies with a code of205
, which tells us to reset our content and we're good to go.123Status Code: 205
Content Type: application
/json
- THIRSTY DEVELOPER: "Excellent!"
Now we reset our order and the the item has been delivered!
HTTP Request:12345678910111213141516171819202122232425262728Status Code: 200
Content Type: application
/json
Response:
{
"href"
:
"/api/orders/fb5331bf7d9e4dae90f25a190b16037f"
,
"recordset"
:{
"total"
: 1,
"limit"
: 5
"order"
:
"timeAdded"
,
"orderDirection"
:
"asc"
,
"sorted"
:[
"2743ce3ea2e04dbf972f8873a7f6e3c7"
],
"href"
: {
"next"
:
"/drinks/beer?page=2"
}
},
"items"
:{
"2743ce3ea2e04dbf972f8873a7f6e3c7"
: {
"name"
:
"Kipling"
,
"href"
:
"/api/orders/fb5331bf7d9e4dae90f25a190b16037f/items/2743ce3ea2e04dbf972f8873a7f6e3c7"
}
}
}
Obviously, this is a simplified example, but the idea of using a range of verbs and status codes to express the "conversation" between an API and its consumers could be expanded to include additional transactions, options. In our pub example, we could take additional steps to close the order out and request the check, find out options for ordering food, or add another beer ( we're thirsty! ).
ColdBox's built-in features for handling HTTP request verbs and data responses with expressive status codes make implementing rich "conversational" APIs painless, allowing you, the developer, to focus on the big-picture details of implementation, rather than the writing code to deal with the internals of how it will be accomplished. Happy coding!
Add Your Comment