Using URL Interpolation With $http In AngularJS
For the past two years, I've been using the $resource service in AngularJS to make my AJAX (Asynchronous JavaScript and XML) calls. But, I've always felt that the $resource service added a lot of cruft and complexity that I didn't need (or use). So, I've started digging into the underlying $http service as an alternative means of API consumption. That said, I did appreciate the way that the $resource service would interpolate my URLs. As such, I wanted to see how hard it would be to build URL interpolation on top of the $http service.
When I say "interpolation" in this context, what I mean is that AngularJS would move items from the request data into the URL where the data could be substituted-in for variable labels. So, for example, if I gave $resource the URL:
/api/friends/:id
... and I made a request that contained the key, "id", with the value, "4", the value of said ID would be merged into the URL:
/api/friends/4
Notice that the URL component, ":id", was replaced with the outgoing request value, "4". It would be easy enough to build these URLs with string concatenation; but, the interpolation approach makes the code easier to read - less noise, more signal.
In order to get this functionality to work with the underlying $http service, I would have to create some sort of proxy to the $http service that would manage the interpolation for me. As an experiment, I tried creating a new service, "httpi". The httpi service applies the URL interpolation to the request configuration object before passing the request config off to the underlying $http service.
To see this in action, I'm going to make several requests to the following URL with different sets of outgoing request data:
api/friends/( :listCommand | :id/:itemCommand )
Notice that this URL has three labels that can be interpolated:
- listCommand
- id
- itemCommand
My httpi service will strip out the (.|.) notation - it's just there to make the "happy paths" a bit more obvious.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Using URL Interpolation With $http In AngularJS
</title>
</head>
<body ng-controller="DemoController">
<h1>
Using URL Interpolation With $http In AngularJS
</h1>
<!-- Initialize scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.16.min.js"></script>
<script type="text/javascript">
// Define the module for our AngularJS application.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the main demo.
app.controller(
"DemoController",
function( $scope, httpi ) {
// NOTE: The (.|.) notation will be stripped out automatically; it's only
// here to improve readability of the "happy paths" of labels. The
// following urls are identical:
// --
// api/friends/( :listCommand | :id/:itemCommand )
// api/friends/:listCommand:id/:itemCommand
var url = "api/friends/( :listCommand | :id/:itemCommand )";
console.warn( "None of the API enpoints exist - they will all throw 404." );
// Clear list of friends - matching listCommand.
httpi({
method: "post",
url: url,
data: {
listCommand: "reset"
}
});
// Create a new friend - no matching URL parameters.
httpi({
method: "post",
url: url,
data: {
name: "Tricia"
}
});
// Get a given friend - ID matching.
httpi({
method: "get",
url: url,
data: {
id: 4
}
});
// Make best friend - ID, itemCommand matching.
httpi({
method: "post",
url: url,
data: {
id: 4,
itemCommand: "make-best-friend"
}
});
// Get besties - no matching URL parameters.
httpi({
method: "get",
url: url,
params: {
limit: "besties"
}
});
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I provide a proxy for the $http service that interpolates the URL of the
// request before executing the underlying HTTP call.
// --
// NOTE: The "i" stands for "interpolation".
app.service(
"httpi",
function( $http ) {
// Return the public API.
return( httpProxy );
// ---
// PUBLIC METHODS.
// ---
// I proxy the $http service and merge the params and data values into
// the URL before creating the underlying request.
function httpProxy( config ) {
config.url = interpolateUrl( config.url, config.params, config.data );
return( $http( config ) );
}
// ---
// PRIVATE METHODS.
// ---
// I move values from the params and data arguments into the URL where
// there is a match for labels. When the match occurs, the key-value
// pairs are removed from the parent object and merged into the string
// value of the URL.
function interpolateUrl( url, params, data ) {
// Make sure we have an object to work with - makes the rest of the
// logic easier.
params = ( params || {} );
data = ( data || {} );
// Strip out the delimiter fluff that is only there for readability
// of the optional label paths.
url = url.replace( /(\(\s*|\s*\)|\s*\|\s*)/g, "" );
// Replace each label in the URL (ex, :userID).
url = url.replace(
/:([a-z]\w*)/gi,
function( $0, label ) {
// NOTE: Giving "data" precedence over "params".
return( popFirstKey( data, params, label ) || "" );
}
);
// Strip out any repeating slashes (but NOT the http:// version).
url = url.replace( /(^|[^:])[\/]{2,}/g, "$1/" );
// Strip out any trailing slash.
url = url.replace( /\/+$/i, "" );
return( url );
}
// I take 1..N objects and a key and perform a popKey() action on the
// first object that contains the given key. If other objects in the list
// also have the key, they are ignored.
function popFirstKey( object1, object2, objectN, key ) {
// Convert the arguments list into a true array so we can easily
// pluck values from either end.
var objects = Array.prototype.slice.call( arguments );
// The key will always be the last item in the argument collection.
var key = objects.pop();
var object = null;
// Iterate over the arguments, looking for the first object that
// contains a reference to the given key.
while ( object = objects.shift() ) {
if ( object.hasOwnProperty( key ) ) {
return( popKey( object, key ) );
}
}
}
// I delete the key from the given object and return the value.
function popKey( object, key ) {
var value = object[ key ];
delete( object[ key ] );
return( value );
}
}
);
</script>
</body>
</html>
As you can see, the httpi service does nothing more than call interpolateUrl() before passing the config data off to the underlying $http service.
When we run the above code, we get the following requests in the network activity:
POST /api/friends/reset
POST /api/friends
GET /api/friends/4
POST /api/friends/4/make-best-friend
GET /api/friends?limit=besties
As you can see, the outgoing request data was interpolated in the URL. And, while you can't see it in this output, the corresponding request data was stripped out of the "params" and "data" collections (if they existed) in order to prevent redundant data points.
The URL interpolation was one of the features of $resource that I really liked. I'm happy to see that it's not too hard to implement the same feature on top of the $http service in AngularJS. This is definitely a direction that I want to move in.
Want to use code from this post? Check out the license.
Reader Comments
I took this concept, rolled it up into its own AngularJS module, and then grew the features ever so slightly:
www.bennadel.com/blog/2614-httpi-a-lightweight-resource-inspired-module-for-angularjs.htm
httpi({
method: "post",
url: url,
data: {
name: "Tricia"
}
});
What's the max length of name that we can post to server?
Thanks! I found this blog post useful, especially the interpolateUrl part
Great post, this was very helpful. Thanks! :)
(.|.) <-- definitely a happy path