At Curious Minds we have developers working from different offices doing backend and frontend work on the same projects, so we need a deployment scenario that allows developers to make local changes without fear of breaking things for everyone, but also to be able to develop against a shared environment so that bugs can be replicated and everyone can be sure they’re on the same page. For the backend, this is simple enough: we deploy to three different backend servers besides the production system:
a ‘local’ deployment: a Vagrant virtual machine for fullstack and backend developers to run on their local machines while working on separate branches before they’ve been merged in to the main development branch.
A ‘development’ deployment, on a public-facing server (or at least accessible by our developers anywhere in the world) that replicates the production environment closely. Here code is bleeding edge and it’s ok if things break.
A ‘staging’ deployment, identical to the development deployment. Here we merge in changes that appear stable and tested on the ‘development’ server for a more thorough review before deployment to production. Here we treat the data as it if were production data and have testing done by real-world users.
For the frontend, we’ve been using Ember.js, and here things get a little more complicated. Developers need to be able to point their local ember serve instance at all three (local, development, staging) backend deployments so that they can ensure their local development branches of the frontend will work against the different deployments. This kind of flexibility is especially helpful because the frontend and backend are not always going to be synced up in terms of stability: sometimes a backend API feature might be considered stable, but the frontend feature that makes use of that API is still in progress. (This only applies to active development of the ember app. For deployments we use ember-cli-deploy). In that case we’ll want to point a “development” branch of the frontend against the staging backend. Or, if a frontend bug is discovered in staging — or even production! — that requires a hotfix, it’s helpful to be able to make the fix using
ember serve
CORS Headers
So how do you point a local emberapp against multiple backends? Our solution was to configure Cross Origin Request Headers headers on our backend deployments, and configure Ember Data’s adapter to make cross-domain requests. To do this, you’ll have to pick a specific origin URL that you’ll use to hit your ember server for each backend, and configure the Origin on each backend to use this URL:
Access-Control-Allow-Origin: http://emberapp.app-deploy.tld:4200/
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Note the URL used in the Allow-Origin header. It has to be the exact address used to hit the ember server, including protocol (http) and port (4200). You'll also have to map it to localhost in your local DNS configuration (for example, in your /etc/hosts
127.0.0.1 emberapp.local-deploy.app emberapp.development-deploy.tld emberapp.staging-deploy.tld
Be sure to also configure the Access-Control-Allow-Headers with any other headers necessary (wildcards (*) are not allowed, if you need the equivalent to a wildcard, you can use this list of headers.)
Configure Ember and Ember-data to support CORS requests Environment configuration
Once you have the CORS headers configured, you’re ready to set up your Ember app to support cross domain requests to your api. The easiest way is to configure ember environments, one for each backend, in config/environment.js .
var ENV = {
...
APP: {
...
usingCors: false,
corsWithCreds: false,
apiURL: null
}
};
Next, we use a
switch (environment) {
case 'local-backend':
ENV.APP.usingCors = true;
ENV.APP.corsWithCreds = true;
ENV.APP.apiURL = 'http://local-backend.app:8000'
break;
case 'development-backend':
ENV.APP.usingCors = true;
ENV.APP.corsWithCreds = true;
ENV.APP.apiURL = 'https://development-backend.tld/'
break;
case 'staging-backend':
ENV.APP.usingCors = true:
ENV.APP.corsWithCreds = true;
ENV.APP.apiURL = 'https://staging-backend.tld/'
break;
}
This will enable to point the emberapp to the backend we want to test against by just setting the environment when running ember serve, like so:
ember s --environment local-backend
Substitue local-backend
Patching the Ember-Data Adapter
Once you’ve got the environments set up, you’re ready to patch ember-data’s “adapter” to support CORS requests against the environment specific API URL by setting up your app/adapters/application.js like so (here we use the JSONAPIAdapter but this override will work with any adapter provided by Ember Data):
import JSONAPIAdapter from 'ember-data/adapters/json-api';
import ENV from '../config/environment';
export default JSONAPIAdapter.extend({
namespace: 'your-api-namespace',
host: ENV.APP.apiURL,
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
ajax(url, method, hash) {
if (ENV.APP.usingCors) {
if (hash === undefined) { hash = {}; }
hash.crossDomain = true;
if (ENV.APP.corsWithCreds) {
hash.xhrFields = { withCredentials: true };
}
}
}
return this._super(url, method, hash);
});
This override is pretty simple. It sets the host to the configured API URL, and ensures that XHR requests made by the adapter have the crossDomain
Patching jQuery’s ajax function
For many applications, this will be all you need to do. Running ember s against the configured environments will direct your ember data requests to the respective backend seamlessly. But you’ll still run into problems if you bypass Ember Data’s adapters at any point and make direct ajax calls through jQuery.
To handle that sort of situation, you’ll have to patch jQuery’s ajax function to also support CORS requests. Doing so is not so hard, and jQuery is stable enough that it likely won’t break in the near future. Here’s how we get it done (put this in app/initializers/services.js or some other initializer that will run early):
/*global jQuery */
import ENV from '../config/environment';
export function initialize() {
if (ENV.APP.usingCors) {
(function($) {
var _old = $.ajax;
$.ajax = function() {
var url, settings, apiURL = ENV.APP.apiURL;
/* Handle the different function signatures available for $.ajax() */
if (arguments.length === 2) {
url = arguments[0];
settings = arguments[1];
} else {
settings = arguments[0];
}
settings.crossDomain = true;
if (!settings.xhrFields) {
settings.xhrFields = {};
}
settings.xhrFields.withCredentials = true;
if (!url) {
url = settings.url;
}
/* If we still don't have an URL, execute the request and let jQuery handle it */
if (!url) {
return _old.apply(this, [settings]);
}
/* combine the apiURL and the url request if necessary */
if (!url.includes(apiURL)) {
/* Do we need a '/'? */
if (url[0] !== '/' && apiURL[apiURL.length-1] !== '/') {
url = '/' + url;
}
url = apiURL + url;
}
settings.url = url;
return _old.apply(this, [settings]);
};
})(jQuery);
}
}
This override is again fairly straightforward. It should handle 9 out of 10 cases of ajax requests in your emberapp.