Skip to content

Instantly share code, notes, and snippets.

@jelbourn
Last active February 25, 2024 12:51
Show Gist options
  • Save jelbourn/6276338 to your computer and use it in GitHub Desktop.
Save jelbourn/6276338 to your computer and use it in GitHub Desktop.
Example of using an angular provider to build an api service. Subject of August 20th 2013 talk at the NYC AngularJS Meetup. http://www.meetup.com/AngularJS-NYC/events/134578452/See in jsbin: http://jsbin.com/iWUlANe/5/editSlides: https://docs.google.com/presentation/d/1RMbddKB7warqbPOlluC7kP0y16kbWqGzcAAP6TYchdw
/**
* Example of using an angular provider to build an api service.
* @author Jeremy Elbourn (@jelbourn)
*/
/** Namespace for the application. */
var app = {};
/******************************************************************************/
/**
* Interface for a model objects used with the api service.
* @interface
*/
app.ApiModel = function() {};
/**
* Data transformation done after fetching data from the server.
* @type {Function}
*/
app.ApiModel.prototype.afterLoad;
/**
* Data transformation done before posting / putting data to the server.
* @type {Function}
*/
app.ApiModel.prototype.beforeSave;
/******************************************************************************/
/**
* Configuration object for an api endpoint.
* @constructor
*/
app.ApiEndpointConfig = function() {
/**
* Map of actions for the endpoint, keyed by action name. An action has a HTTP
* method (GET, POST, etc.) as well as an optional set of default parameters.
* @type {Object.<string, {method: string, params: Object}>}
*/
this.actions = {};
/** The default actions defined for every endpoint. */
var defaultActions = {
'GET': 'get',
'PUT': 'update',
'POST': 'save',
'PATCH': 'patch',
'DELETE': 'remove'
};
// Add the default actions to this endpoint.
var self = this;
angular.forEach(defaultActions, function(alias, method) {
self.addHttpAction(method, alias);
});
};
/**
* Set the route for this endpoint. This is relative to the server's base route.
* @param {string} route
* @return {app.ApiEndpointConfig}
*/
app.ApiEndpointConfig.prototype.route = function(route) {
this.route = route;
return this;
};
/**
* Set the route for this endpoint. This is relative to the server's base route.
* @param {function(): app.ApiModel} model
* @return {app.ApiEndpointConfig}
*/
app.ApiEndpointConfig.prototype.model = function(model) {
this.model = model;
return this;
};
/**
* Adds an action to the endpoint.
* @param {string} method The HTTP method for the action.
* @param {string} name The name of the action.
* @param {Object=} params The default parameters for the action.
*/
app.ApiEndpointConfig.prototype.addHttpAction = function(method, name, params) {
this.actions[name] = {method: method.toUpperCase(), params: params};
};
/******************************************************************************/
/**
* An api endpoint.
*
* @constructor
* @param {string} baseRoute The server api's base route.
* @param {app.ApiEndpointConfig} endpointConfig Configuration object for the
* endpoint.
* @param {!Object} $injector The angular $injector service.
* @param {!Function} $resource The angular $resource service.
*/
app.ApiEndpoint = function(baseRoute, endpointConfig, $injector, $resource) {
this.config = endpointConfig;
this.$injector = $injector;
this.resource = $resource(baseRoute + endpointConfig.route, {},
endpointConfig.actions);
// Extend this endpoint objects with methods for all of the actions defined
// in the configuration object. The action performed depends on whether or
// not there is a model defined in the configuration; when there is a model
// defined, certain request types must be wrapped in order to apply the
// pre/post request transformations defined by the model.
var self = this;
angular.forEach(endpointConfig.actions, function(action, actionName) {
var actionMethod = self.request;
if (endpointConfig.model) {
if (action.method === 'GET') {
actionMethod = self.getRequestWithModel;
} else if (action.method === 'PUT' || action.method === 'POST') {
actionMethod = self.saveRequestWithModel;
}
}
self[actionName] = angular.bind(self, actionMethod, actionName);
});
};
/**
* Instantiates a model object from the raw server response data.
* @param {Object} data The raw server response data.
* @return {app.ApiModel} The server response data wrapped in a model object.
*/
app.ApiEndpoint.prototype.instantiateModel = function(data) {
var model = this.$injector.instantiate(this.config.model);
angular.extend(model, data);
model.afterLoad();
return model;
};
/**
* Perform a standard http request.
*
* @param {string} action The name of the action.
* @param {Object=} params The parameters for the request.
* @param {Object=} data The request data (for PUT / POST requests).
* @return {angular.$q.Promise} A promise resolved when the http request has
* a response.
*/
app.ApiEndpoint.prototype.request = function(action, params, data) {
return this.resource[action](params, data).$promise;
};
/**
* Perform an HTTP GET request and performs a post-response transformation
* on the data as defined in the model object.
*
* @param {string} action The name of the action.
* @param {Object=} params The parameters for the request.
* @return {angular.$q.Promise} A promise resolved when the http request has
* a response.
*/
app.ApiEndpoint.prototype.getRequestWithModel = function(action, params) {
var promise = this.request(action, params);
var instantiateModel = this.instantiateModel.bind(this);
// Wrap the raw server response data in an instantiated model object
// (or multiple, if response data is an array).
return promise.then(function(response) {
var data = response.data;
response.data = angular.isArray(data) ?
data.map(instantiateModel) : instantiateModel(data);
});
};
/**
* Performs an HTTP PUT or POST after performing a pre-request transformation
* on the data as defined in the model object.
*
* @param {string} action The name of the action.
* @param {Object=} params The parameters for the request.
* @param {Object=} data The request data (for PUT / POST requests).
* @return {angular.$q.Promise} A promise resolved when the http request has
* a response.
*/
app.ApiEndpoint.prototype.saveRequestWithModel = function(action, params, data) {
// Copy the given data so that the beforeSave operation doesn't alter the
// object state from wherever the request was triggered.
var model = angular.copy(data);
if (model && model.beforeSave) {
model.beforeSave();
}
return this.request(action, params, model);
};
/******************************************************************************/
/**
* Angular provider for configuring and instantiating as api service.
*
* @constructor
*/
app.ApiProvider = function() {
this.baseRoute = '';
this.endpoints = {};
};
/**
* Sets the base server api route.
* @param {string} route The base server route.
*/
app.ApiProvider.prototype.setBaseRoute = function(route) {
this.baseRoute = route;
};
/**
* Creates an api endpoint. The endpoint is returned so that configuration of
* the endpoint can be chained.
*
* @param {string} name The name of the endpoint.
* @return {app.ApiEndpointConfig} The endpoint configuration object.
*/
app.ApiProvider.prototype.endpoint = function(name) {
var endpointConfig = new app.ApiEndpointConfig();
this.endpoints[name] = endpointConfig;
return endpointConfig;
};
/**
* Function invoked by angular to get the instance of the api service.
* @return {Object.<string, app.ApiEndpoint>} The set of all api endpoints.
*/
app.ApiProvider.prototype.$get = ['$injector', function($injector) {
var api = {};
var self = this;
angular.forEach(this.endpoints, function(endpointConfig, name) {
api[name] = $injector.instantiate(app.ApiEndpoint, {
baseRoute: self.baseRoute,
endpointConfig: endpointConfig
});
});
return api;
}];
/******************************************************************************/
// Example of creating an angular module to house "core" functionality. It is
// here that you add any custom providers.
app.core = angular.module('core', ['ngResource']);
app.core.config(function($provide) {
$provide.provider('api', app.ApiProvider);
});
/******************************************************************************/
// Example of creating an angular module for your app or part of your app.
// A provider can be injected into a config function, which is run before
// normal services are instantiated.
app.component = angular.module('component', ['core']);
app.component.config(function(apiProvider) {
apiProvider.setBaseRoute('my/app/api/');
apiProvider.endpoint('songs')
.route('songs/:id')
.addHttpAction('POST', 'favorite', {isFavorite: true})
.model(app.Song);
apiProvider.endpoint('albums')
.route('albums/:id')
.model(app.Album);
});
/******************************************************************************/
// Example of making a request with the api service in a controller.
app.SongController = function($scope, $routeParams, api) {
var songsPromise = api.songs.get({id: $routeParams.id});
};
@adamreisnz
Copy link

Thanks for this! It inspired me to build https://github.com/meanie/angular-api

This is an Angular API service (part of the Meanie MEAN boilerplate) to easily define endpoints, optionally using data models. It allows you to easily bind functions on your models like .save() or .delete(), much like with Angular's $resource service but with more flexibility. It also works with params like $resource does, e.g. they can be extracted from supplied params or from the model.

Furthermore, it's fully configurable, but with sensible defaults. You can specify configuration globally for the whole API, per endpoint, or per action. Each next level will merge it's configuration with the previous.

As a bonus it comes with an optional $http decorator to filter out duplicate requests, and either reject them or simply return the promise of the already pending request.

Can be installed via Bower, npm or the Meanie CLI. Give it a go next time you need an API service!

@redlehnewo
Copy link

This is awesome. Thanks for sharing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment