Swagger
Swagger is a simple and powerful tool for creating structured RESTful APIs. Swagger provides interactive documentation, client SDK generation and discoverability; however it does not provide an out-of-the-box solution for interacting with Angular applications. I attempted to solve this problem by creating a promise-based service - AngularSwaggerific
- to expose endpoints created by Swagger.
If you would like to jump right in and start using Angular Swaggerific in your own projects, head over to the official documentation to learn more.
Generating JSON from our Swagger API
The first thing we’ll need is a JSON
representation of our Swagger API, which can be generated using Swagger’s online editor (as a side note, Swagger APIs are written in YAML
). For this example, I’ll use one of Swagger’s sample APIs - petstore_full.yaml
. We can open this API by clicking on File > Open Example, and then download it in JSON
by clicking on File > Download JSON. We’ll use this generated JSON
as the foundation for that our service.
Our API
By looking at the generated JSON
, we can see that all of our endpoints are nested under the paths
key. Here’s a simplified version of how each endpoint is structured:
"/pets/findByStatus": {
"get": {
"tags": ["pet"],
"summary": "Finds Pets by status",
"description": "Multiple status values can be provided with comma seperated strings",
"operationId": "findPetsByStatus",
"produces": [
"application/json",
"application/xml"
],
"parameters": [{}],
"responses": {},
"security": [{}]
},
"put": {...},
"post": {...}
},
"/pets/addPet": {...},
"/stores/placeOrder": {..}
We can use this JSON
to construct a simple Object that will be returned by our service, so all of our Swagger endpoints can be accessed with ease. We can expose this Object through our service by injecting it into other services/controllers.
The API Object namespace will be created using the tags
and operationId
keys in the original Swagger JSON
. There may be cases where the operationId
is not provided for an endpoint, or there are no tags available, in which case we will have to find an alternative naming convention (we’ll tackle this problem later on in the post). Each endpoint in our API will be mapped to our Object, and trigger an HTTP request to it’s associated path.
At this point you may be a little confused, but I am hoping things will become more clear once we dive into the code. Now that we have an idea of what we need to do, lets set up the framework for our service.
Setting up our Service
(function() {
angular
.module('angular-swaggerific', [])
.factory('AngularSwaggerific', AngularSwaggerific);
AngularSwaggerific.$inject = [$log];
function AngularSwaggerific($log) {
// Private variables
var _json = null;
var _host = null;
return AngularSwaggerific;
/**
* Represents an AngularSwaggerific object.
* @constructor
*
* @param {String} json
* @returns {Object} api
*/
var AngularSwaggerific = function(json) {
this.api = {};
if (!json || json === '') {
$log.error("Oops! Swagger JSON must be provided!");
return {};
} else {
_json = json;
}
return this.init();
}
/**
* TODO
* Creates the AngularSwaggerific API object
*
* @returns {Object} api
*/
AngularSwaggerific.prototype.init = function() {
var self = this;
// Set the API host
var scheme = util.contains('https', _json.schemes) ? 'https://' : 'http://';
_host = scheme + _json.host + _json.basePath;
// TODO: Construct our API Object from the Swagger JSON
return self.api;
}
}
})(angular);
So far we have put together a basic skeleton of our service. All it does right now is return an empty Object named api
.
- The constructor takes the
JSON
representation of a Swagger API as it’s first and only argument. We store theJSON
in a private variable so it can be used easily in other methods. - The
init
method returns our constructedapi
, which we will be create fromjson
. Also note thatinit
initializes_host
with the API host information, which will be used later on in the code.
Creating our API Object
Let’s start constructing our API by looping through each endpoint in our json
, finding it’s associated tag
, and setting it as a property of api
(if it doesn’t already exist).
AngularSwaggerific.prototype.init = function() {
var self = this;
// Set the API host
var scheme = util.contains('https', _json.schemes) ? 'https://' : 'http://';
_host = scheme + _json.host + _json.basePath;
angular.forEach(_json.paths, function(value, key) {
angular.forEach(value, function(innerValue, innerKey) {
// Set the namespace equal to the associated tag
var namespace = innerValue.tags[0]
// Create the namespace if it doesn't already exist
if (!self.api[namespace]) {
self.api[namespace] = {};
}
});
});
return self.api;
}
So far so good - but what if the tags
property is empty or doesn’t exist? In that case, we’ll use the actual endpoint URL to find a suitable namespace. For example, the endpoint /pets/addPet will map to the pets
key. If our endpoint is simply /, then we’ll use the basePath
to generate our key instead (which is available on the generated json
). Another issue we may run into is that some URL’s contain dynamic variables - /pet/{id} - in which case we’ll strip all curly braces from the URL.
AngularSwaggerific.prototype.init = function() {
var self = this;
angular.forEach(_json.paths, function(value, key) {
angular.forEach(value, function(innerValue, innerKey) {
var namespace;
if (angular.isDefined(innerValue.tags)) {
// Set the namespace equal to the associated tag
namespace = innerValue.tags[0]
} else {
// Set the namespace equal to the first path variable
namespace = key.split('/')[1];
/**
* If there is no path variable (i.e. '/'), then set the namespace equal to
* the base path.
*/
if (!namespace || namespace === '') {
namespace = _json.basePath.split("/")[1];
}
}
// Remove curly braces from the namespace
namespace = namespace.replace(/[{}]/g, "");
// Create the namespace if it doesn't already exist
if (!self.api[namespace]) {
self.api[namespace] = {};
}
});
});
return self.api;
}
Awesome! The last step is setting the operationId
as a property on the namespace, which will in turn trigger a HTTP request and return a Promise
. If the operationId
does not exist, then we’ll use the HTTP method type (i.e. ‘get’, ‘post’, etc) instead.
AngularSwaggerific.prototype.init = function() {
var self = this;
angular.forEach(_json.paths, function(value, key) {
angular.forEach(value, function(innerValue, innerKey) {
var namespace;
// ... See the snippet above
// Create the namespace if it doesn't already exist
if (!self.api[namespace]) {
self.api[namespace] = {};
}
/**
* Map HTTP call to namespace[operationId].
* If there is no operationId, then use method (i.e. get, post, put)
*/
if (!innerValue.operationId) {
innerValue.operationId = innerKey;
}
(self.api[namespace])[innerValue.operationId] = function() {
// TODO: Trigger the request!
};
});
});
return self.api;
}
So far, our service converts a Swagger API into an Object with properties for each endpoint and returns it. For example, a Swagger API with the following endpoints:
- /pet/addPet
- /pet/deletePet
- /store/addStore
- /store/deleteStore
… will be converted into the following api
returned by our service:
{
"pet": {
"addPet": <function>,
"deletePet": <function>
},
"store": {
"addStore": <function>,
"deleteStore": <function>
}
}
Triggering the HTTP Request
Next we have to actually map each property in api
to trigger an HTTP request. We’ll create atrigger
method to take care of this for us.
/**
* Sets up and executes the HTTP request (using $http) for a specific path.
* @param {string} path
* @param {method} method
* @param {data} data
* @returns {Promise}
*/
AngularSwaggerific.prototype.trigger = function(path, method, data) {
var self = this;
var data = data || {};
var newPath = util.replaceInPath(path, data);
return $http({
method: method,
url: _host + newPath,
data: data || {}
})
}
The trigger
method:
- Accepts three arguments: the actual endpoint URL, the HTTP method and any (optional) data.
- Uses Angular’s
$http
service and returns aPromise
that can be resolved by the user accordingly.
Now all that is left to do is modify the init
method to use trigger
:
AngularSwaggerific.prototype.init = function() {
var self = this;
angular.forEach(_json.paths, function(value, key) {
angular.forEach(value, function(innerValue, innerKey) {
var namespace;
// .. See the snippets above
(self.api[namespace])[innerValue.operationId] = function(data) {
return self.trigger(key, innerKey, data);
};
});
});
return self.api;
}
Putting it all together
Using the Swagger pet store API as an example, here’s how AngularSwaggerific
can be injected and used inside of a service.
(function() {
'use strict';
angular
.module('myAwesomeApp')
.factory('PetService', PetService);
function PetService($log, $window, AngularSwaggerific) {
$log.log('PetService initialized ...');
// Initialize our Swagger API
// `$window.json` refers to the generated JSON from our Swagger API
var myAPI = AngularSwaggerific($window.json);
var service = {
addPet: addPet
};
return service;
/**
* Adds a pet
*
* @param {Object} data
* @return {Promise}
*/
function addPet(data) {
// Triggers a request to "/pet/addPet"
return myAPI.pet.addPet(data)
.then(function(response) {
// TODO: Handle success!
return response.data;
});
}
}
})();
- We inject the
AngularSwaggerific
into our service. We construct our API Object by passing our Swaggerjson
to theAngularSwaggerific
constructor. - We make a request to the /pet/addPet endpoint in the
addPet
usingmyAPI
.
And that’s all there is to it! We no longer have to go through the hassle of re-creating constants for our Swagger endpoints. Instead, we can utilize the json
generated by Swagger itself to always have an up-to-date reference to our API. The source code for AngularSwaggerific
is currently available on Github, feel free to contribute!