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});
};
@10thfloor
Copy link

Very useful.

@stryju
Copy link

stryju commented Feb 20, 2014

https://gist.github.com/jelbourn/6276338#file-api-provider-js-L118

wouldn't that ALWAYS return false?
shouldn't that check endpointConfig.model instead?

@SimplGy
Copy link

SimplGy commented Feb 21, 2014

This is beautiful.

@soee
Copy link

soee commented Jan 16, 2015

Hi, i'm quite new to Angular and trying to use this provider. I'm facing problem with https://gist.github.com/jelbourn/6276338#file-api-provider-js-L172-L176 If i try to use it as it is, i have undefined response in my controller, but if i do: promise.then ... and the return at the end: return promise; than i have the response in my controller. Am i doing something wrong here?

@philippspinnler
Copy link

Thank you for this. It was a true inspiration.

@inongogo
Copy link

inongogo commented Mar 4, 2015

I was looking at this and I have a newbie question about how to configure and use it. I saw the examples but my problem is that I don't know where in the Angular app the different parts should be placed.

I've started an Angular app with a modified ngbp as boilerplate. Ideally, I would like to configure the basic stuff in the app.js file, like these lines of code:

(function(app) {
app.config(function ($stateProvider, $urlRouterProvider, $provide) {
$urlRouterProvider.otherwise('/home');
});

app.run(function () {});
app.controller('AppController', function ($scope) { });
app.core = angular.module('core', ['ngResource']);
app.core.config(function($provide) {
    app.ApiProvider.setBaseRoute('http://localhost:31460/api');
    $provide.provider('api', app.ApiProvider);
});

}(angular.module("myApp", [
'templates-app',
'templates-common',
'angular-api-provider',
etc...
)], window);

And then I would like to configure the following code in the controller´s module config:
app.ApiProvider.endpoint('user')
.route('user/:id')
.model(app.Song);

Is this possible or how should I go about it?
Again, sorry for such a trivial question!
Thanks,

@extantpedant
Copy link

I had a problem using this with a model, but resolved it.

This

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);
  });
};

Becomes

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).
  promise.then(function(response) {
    var data = response.data;
    response.data = angular.isArray(data) ?
        data.map(instantiateModel) : instantiateModel(data);
  });
 return promise;
};

Also, if you want to use Angular's strict DI mode, you have to use dependency-annotated objects when using $injector.instantiate.
Assuming your model prototype stands alone,

app.ApiEndpoint.prototype.instantiateModel = function(data) {
  var model = this.$injector.instantiate(this.config.model);
  angular.extend(model, data);
  model.afterLoad();

...

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
    });

Becomes

app.ApiEndpoint.prototype.instantiateModel = function(data) {
  var model = this.$injector.instantiate([this.config.model]);
  angular.extend(model, data);
  model.afterLoad();

...

app.ApiProvider.prototype.$get = ['$injector', function($injector) {
  var api = {};

  var self = this;
  angular.forEach(this.endpoints, function(endpointConfig, name) {
    api[name] = $injector.instantiate(['baseRoute', 'endpointConfig', '$injector', '$resource', app.ApiEndpoint], {
      baseRoute: self.baseRoute,
      endpointConfig: endpointConfig
    });

Hopefully that was helpful to some poor sap like me banging his head against his desk somewhere.

@extantpedant
Copy link

Oh, one more thing.
With this

app.ApiEndpoint = function(baseRoute, endpointConfig, $injector, $resource) {
  this.config = endpointConfig;
  this.$injector = $injector;
  this.resource = $resource(baseRoute + endpointConfig.route, {},
      endpointConfig.actions);
...
  var self = this;
  angular.forEach(endpointConfig.actions, function(action, actionName) {
    var actionMethod = self.request;

    if (endpointConfig.model) {
...

You have to add a property like model: true in each endpoint.

myApiEndpoint.endpoint('awesome')
  .customAction('get', 'get', {isArray: true, model: true})
  .model(awesomeModel)

The following enables a model for all relevant actions as long as .model() is specified.

  angular.forEach(endpointConfig.actions, function(action, actionName) {
    var actionMethod = self.request;

    if (self.config.model) {
...

@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