WHY HATEOAS A simple case study on the often ignored REST constraint Wayne Lee, June 2009 http://trilancer.wordpress.com/
Nov 01, 2014
WHY HATEOAS
A simple case study on the often ignored REST constraint
Wayne Lee, June 2009http://trilancer.wordpress.com/
Background•
Representational state transfer (REST)–
A software architecture style
for distributed
hypermedia
systems, e.g., the World Wide Web–
Introduced by Roy Fielding
in 2000
•
REST Constraints–
Identification of resources
–
Manipulation of resources through representations–
Self-descriptive messages
–
Hypermedia as the engine of application state
HATEOAS Hypermedia as the engine of application state
•
The model of application is an engine
that moves from one state
to another by
picking alternative state transitions in current set of representations
•
Or simply put:–
State: where the user is
•
i.e., current resources
–
Transitions: instructions on user’s next steps•
i.e., links / forms from current resources to others
Why HATEOAS•
Resources will evolve
over time
–
Naming, URI, location, partition …
•
Thus any assumptions
about server resources will break
eventually
–
URI pattern, valid state transitions …
•
HATEOAS is about reducing
assumptions–
Loose coupling between client & server
–
Allowing each to evolve independently
A Simple Case Study
Imagine You’re Building an Online Order Manager
Where You Start
ID Name
1 Tom
2 Jerry
… …
ID User_ID
123 1
456 2
… …
Users Table Orders Table
•
One
server
•
Two
DB tables
•
Free
user registration
Server 1: myorders.com
Your “REST” API V1•
Step 1: POST /login (user_name, password)
–
Session created•
Step 2: GET /orders
–
Get Order List, with user id implicitly
provided in session•
Step 3: GET /orders/{order_id}
–
Get specific Order data through cooked URI
•
Sample order list data in JSON format:[
order: {id:123},order: {id:456},
]•
URI cooking rules:
–
List_URI
= ‘/orders’–
Order_URI
= List_URI
+ order_list[n].order.id
•
Seems really simple for client app implementation, for now
Then lots client apps emerge …
•
From users, fans, solution providers, mashup
makers…
•
10s 100s 1000s …
•
Based on the simple “REST”
API V1
After some time …
•
Some suggest implicit
user id in session / cookie NOT
a good idea …
•
That user_name
should be included in URI
“REST” API V1.1•
Step 1: POST /login (user_name, password)–
Session created
•
Step 2: GET /{user_name}/orders–
Get Order List, with user name explicitly
provided
•
Step 3: GET /{user_name}/orders/{order_id}–
Get specific Order
data through cooked URI
•
URI cooking rules:–
User_name
retrieved from client local input
–
List_URI
= “/”
+ user_name
+ “/orders”–
Order_URI
= List_URI
+ order_list[n].order.id
•
Seems simple for client implementation still
But, what about old apps?
•
Just let them break? –
Not Acceptable
•
Make sure Orders
servlet
maintain backward compatibility:–
Retrieve user_name
from request URI
–
If NOT provided, retrieve from session data instead
•
In the end, API V1 still works for old apps
Then after some time …
•
You decide to add a paid
offerings:–
Free
accounts:
•
Data on the old host–
Professional accounts:
•
Data moved to a new faster server•
With a new domain name
Server 1: myorders.com
DB Changes
ID Name Type
1 Tom Free
2 Jerry Pro
… … …
ID User_ID
123 1
… …
Users Table Orders Table for Free Users
ID User_ID
456 2
… …
Orders Table for Pro Users
Server 2: pro.myorders.com
“REST” API V2•
Step 1: POST /login (user_name, password)–
Session created, with User_Type
returned
•
Step 2:–
Free accounts:
GET /{user_name}/orders–
Pro accounts:
GET pro.myorders.com/{user_name}/orders
•
Step 3:–
Free accounts:
GET /{user_name}/orders/{order_id}–
Pro accounts:
GET pro.myorders.com/{user_name}/orders/{order_id}
•
URI cooking rules:–
User_name
retrieved from client-side input–
User_type
received from server, “free”
or “pro”–
List_URI
= ((user_type
== ‘pro’) ? ‘pro.myorders.com/’
: ‘/’) + user_name
+ ‘/orders’–
Order_URI
= List_URI
+ order_list[n].order.id
•
Still ok for client implementation, nonetheless
Again, what about old apps?•
Just let them break? –
Still Not Acceptable
•
Modify Orders
servlet
logic again:–
Retrieve domain & user_name
from request URI
–
If NOT provided API V1.0•
Retrieve user_name
from session first, then
•
Lookup Users table to determine user_type, i.e., which DB to use–
If only user_name
provided API V1.1
•
Likewise, lookup Users table to determine which DB to use
•
In the end, API V1/V1.1 still works fine
As time goes by
•
You think it time for a VIP
offering:–
Free
accounts:
•
Data on the old host–
Professional accounts:
•
Data on a faster server•
With a new domain name
–
VIP accounts:•
Dedicated DB server
•
Custom domain name
Server 6: anna_box.myorders.comServer 5: Alf_shop.myorders.com
Server 4: mikeabc.myorders.comServer 3: susan_test.myorders.com
Server 1: myorders.com
DB Changes
ID Name Type Domain
1 Tom Free N/A
2 Jerry Pro N/A
3 Susan VIP susan_test
ID User_ID
123 1
… …
Users Table Orders Table for Free Users
ID User_ID
456 2
… …
Orders Table for Pro Users
Server 2: pro.myorders.com
ID data
789 …
… …
Orders Tables for VIP User
“REST” API V3•
Step 1: POST /login (user_name, password)–
Session created, with User_Type, User_Domain
returned
•
Step 2:–
Free accounts:
GET /{user_name}/orders–
Pro accounts:
GET pro.myorders.com/{user_name}/orders–
VIP accounts:
GET {user_domain}.myorders.com/orders
•
Step 3:–
Free accounts:
GET /{user_name}/orders/{order_id}–
Pro accounts:
GET pro.myorders.com/{user_name}orders/{order_id}–
VIP accounts:
GET {user_domain}.myorders.com/orders/{order_id}
•
URI cooking rules:–
User_name
retrieved from client-side input–
User_type
received from server, “free”
or “pro”
or “vip”–
User_domain
received from server, maybe null–
List_URI
= user_domain
? user_domain
+ ‘.myorders.com/orders’
: (user_type
== ‘pro’
? ‘pro.myorders.com/’
: ‘/’) + user_name
+ ‘/orders’–
Order_URI
= List_URI
+ order_list[n].order.id
•
Seems not that simple for client anymore …
Again, what about old apps?•
“We’ll support old client apps, as usual…”
•
Modify Orders
servlet
logic again:–
Retrieve domain & user_name
from request URI–
If domain name is “Pro”
API V2/V3•
Use DB on pro.myorders.com–
If domain name is not “Pro”
API V3•
Use DB on {domain_name}.myorders.com–
If NOTHING is provided API V1.0•
Retrieve user_name
from session first, then•
Then lookup Users table to get user_type, user_domain–
If user_type
is “Free”, use DB on myorders.com–
If user_type
is “Pro”, use DB on pro.myorders.com–
If user_type
is “VIP”, use DB on {user_domain}.myorders.com–
If only user_name
is provided API V1.1•
Likewise, lookup Users table to determine which DB to use
•
In the end, API V1/V1.1/V2 still works fine, sadly …
Things Can Get Even More Complicated
More requirements, more offerings, more functions, more features, more rules, clusters, load-balancers, data
partitions, backups …
So Will Servlet
Logic …
And maintenance, logging, testing, trouble-shooting …
And Client App Implementation Cost
What’s Wrong in the First Place?
So
“REST” API V1 •
Step 1: POST /login (user_name, password)–
Session created
•
Step 2: GET /orders–
Get Order List, with user id implicitly
provided in session
–
Should NOT let client assume the URI, if potential changes expected
•
Step 3: GET /orders/{order_id}–
Get specific Order data through cooked URI
–
Should NOT let client assume the URI pattern , if potential changes expected
•
More assumptions allowed = More tightly coupling•
Simple effort for one-time client implementation possibly huge, on-going & ever-increasing liability for the server
A True REST API V0.1 Instead•
Step 1: POST /login (user_name, password)–
Session created, user related resource descriptions returned
–
User_Data: { name: “tom”,
order_list_uri: “/tom/orders” }
•
Step 2: GET {User_Data.order_list_uri}–
Retrieve order list data, sample data:
–
Order_List
= [ order: {id:123, uri:“/tom/orders/123”}
… ]
•
Step 3: GET {Order_List[n].order.uri}–
Retrieve specific Order data through given URI
Same API Works across Various Account Types …
Free Pro VIPPOST /login POST /login POST /loginUser_Data: {
name: “tom”,
order_list_uri:
“/tom/orders”
}
User_Data: {
name: “jerry”,
order_list_uri:
“pro.myorders.com/jerry/orders”
}
User_Data: {
name: “susan”,
order_list_uri:
“susan_test.myorders.com/orders”
}
GET /tom/orders GET pro.myorders.com/jerry/orders GET susan_test.myorders.com/orders
Order_List
= [
order: {
id:123,uri:“/tom/orders/123”
}
]
Order_List
= [
order: {
id:456,
uri:“pro.myorders.com/jerry/orders/456”}
]
Order_List
= [
order: {
id:789, uri:“suasan_test.myorders.com/orders/78
9”}
]
GET /tom/orders/123 GET pro.myorders.com/jerry/orders/456
GET suasan_test.myorders.com/orders/7
89
… and Adaptable to Various Situations
•
Tom just upgrade from Free
account to Pro, with bulk data migration scheduled later …
And Tom can continue work across DBs
Order_List
= [order:{ id:123, uri: ‘/tom/orders/123’
},order:{ id:456, uri: ‘pro.myorders.com/tom/orders/456’
}]
•
Pro.myorders.com
is down for maintenance, and Pro_1 is up as backup …
And users will hardly notice the change
Order_List
= [order:{ id:123, uri: ‘pro_1.myorders.com/tom/orders/123’
}order:{ id:456, uri: ‘pro_1.myorders.com/tom/orders/456’
}]
Data from different DBs
mixed
Put It Visually
Imagine a Parking Lot
Different Zones for Different Parkers
Free parkersPro parkers
VIP parkerVIP parkerVIP parker
Gate
VIP parkerVIP parkerVIP parker
VIP parker
VIP parker
A once-free-now-VIP Parker who cannot get rid of old habits …
Free parkers
Pro parkers
VIP parkerVIP parkerVIP parker
Gate
VIP parkerVIP parkerVIP parker
VIP parker
VIP parker
“Sir, your lot is in the VIP zone around the corner…”“!!!!...”“Since you’re VIP customer, we’ll redirect
your car there for free
…”
“REST”
API without HATEOAS Be prepared to repeat this mess each and every day
Free parkers
Pro parkers
VIP parkerVIP parkerVIP parker
Gate
VIP parkerVIP parkerVIP parker
VIP parker
VIP parker
A HATEOAS API Scenario Instructions to each customer each time
Free parkersPro parkers
VIP parkerVIP parkerVIP parker
Gate
VIP parkerVIP parker
VIP parker
VIP parker
“Sir, your lot is being repaired, fortunately we’ve allocated a new one for you, here’s the route …”“I see, thanks a lot ”
UNDER CONSTRUCTION
RPC vs. HATEOAS Not necessarily future-proof but more efficient for now
Free parkersPro parkers
VIP parkerVIP parkerVIP parker
VIP Gate
VIP parkerVIP parkerVIP parker VIP parker
VIP parker
Pro
Gat
e
Free Gate
Old Gate Blocked
Old client:“!@#$$#%^&^%!!!!”
Take Away•
HATEOAS is essential, for–
APIs
as well as internal organization
of complex
systems that may evolve over time–
In order to minimize maintenance cost and support old client apps
•
However, mind that–
Loose coupling less efficiency
•
So, if you’re 100% sure something will never change, e.g., /login as login URL, just let
everyone assume
it forever