This page captures thoughts and ideas on how to design URIs for WOA/RESTful web-services and applications. Technically, URI design is irrelevant as all URIs should be discovered through request-response exchanges between the user agent and the server. However, in practice, well designed URIs have been conducive to good architecture.
Example 1: URIs for versioned multilingual documents
Problem statement: imagine having a model where you have a document identified by an docId that can have different translations (one of them being the default) and revisions (each translation has its own independent revision history).
Approach 1: using URI segments
One could model these resources in the following way.
Path
| Meaning
|
| /{docId} | current version in default translation |
| /{docId}/versions | list of versions in default translation (Q: should this redirect to /{docId}/translations/{lang}/versions using the default language?)
|
| /{docId}/versions/{version} | specific version for default translation (NOTE: this makes no sense since each translation has its own version list)
|
| /{docId}/translations | list of translations
|
| /{docId}/translations/{lang} | current version for a given translation
|
| /{docId}/translations/{lang}/versions | list of versions for a given translation
|
| /{docId}/translations/{lang}/versions/{version} | specific version for a given translation
|
Benefits - Usage is fairly straightforward.
| Drawbacks - URIs are longer than for other approaches.
- Has one degenerate case where a version of a document can be requested without setting the preferred translation first.
- No mechanism for URI discovery, meaning the user agent has hard-coded knowledge of URI structure.
- No mechanism for user agent
to specify alternates if requested translation does not exist. Instead
the alternate translation appears to be determined by the server.
|
Approach 2: Using query parameters
Or, with the same expressive power, one might do:
Path
| Meaning
|
| /{docId} | current version in default translation
|
| /{docId}/versions | list of versions in default translation (Q: should this redirect to /{docId}/versions?translation={default} using the default language?)
|
| /{docId}?version={v} | specific version for default translation (NOTE: this makes no sense since each translation has its own version list)
|
| /{docId}/translations | list of translations
|
| /{docId}?translation={lang} | current version for a given translation
|
| /{docId}/versions?translation={lang} | list of versions for a given translation
|
| /{docId}?translation={lang}&version={v} | specific version for a given translation |
Benefits - URIs are shorter than Approach 1.
| Drawbacks - Has one degenerate case where a version of a document can be requested without setting the preferred translation first.
- No mechanism for URI discovery, meaning the user agent has hard-coded knowledge of URI structure.
- No mechanism for user agent to specify alternates if requested
translation does not exist. Instead the alternate translation appears
to be determined by the server.
|
Approach 3: Using Accept-Language header and query parameters
The Accept-Language header can be used by a user agent to request resources in various language and even provide hints at suitable alternatives when the requested language is not available.
Path
| Meaning
|
| /{docId} | current version of preferred translation
|
| /{docId}/versions | list of versions in preferred translation
|
| /{docId}?version={v} | specific version for preferred translation
|
| /{docId}/translations | list of translations
|
For user agents that cannot provide a custom HTTP Accept-Language request header, the API could define an override query parameter (e.g. ?accept-language=...).
Benefits - URIs are shorter than Approach 1 & 2.
- Leverages the built-in HTTP mechanism for negotiating the preferred translation with a user agent.
- No degenerate case since server relies on the presence of the Accept-Language request header.
| Drawbacks - No mechanism for URI discovery, meaning the user agent has hard-coded knowledge of URI structure.
- Responses must include Vary response header to indicate proper caching behavior.
|
Addendum 1: URI discoverability
All above approaches fail to address one important aspect: URI discoverability. Without the means for the user agent to "discover" the various URIs that can be used to interact with the document, the user agent must hard-code the URI design. Such hard-coding prevents future changes the URI design or requires the server administrator to manage redirect from old designs to the latest design.
The solution is to return a hypermedia or XML document instead of the contents of the document for path /{docId}. The returned document provides descriptions for the various URIs that a document can be requested by.
Path
| Meaning
|
| /{docId} | hypermedia or XML document describing the resource
|
Let's assume for this example that the returned response is an XML document. It may look as follows:
<doc translation="en">
<link rel="contents">http://...</link>
<link rel="versions">http://...</link>
<link rel="translations">http://...</link>
</doc>
Benefits - Server remains in control of the URI design.
- User agent is completly decoupled from server design up to the shared semantics of the hypermedia/XML document used to discover the other links.
| Drawbacks - An additional request-response exchange is required to go from the /{docId} to the contents of the document.
|
Note: the request-response exchange could be avoided by defining a custom MIME type (e.g. application/x.doc-meta+xml) and using the Accept
request header, where the default behavior for /{docId} would be to
return the current verion of the translation specified by the Accept-Language header.