Created
October 15, 2015 17:20
-
-
Save twalker/db5aeaf9a9829e511f52 to your computer and use it in GitHub Desktop.
a fetch abstraction for RESTful calls to a single api
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* minimal configuration object. | |
*/ | |
import assign from 'object-assign' | |
// default configuration is based on the hostname. | |
const cfg = { | |
'localhost': { | |
// mocks | |
baseApiUrl: '//localhost:3001/api/' | |
}, | |
'foo.com': { | |
baseApiUrl: '//foo.com/api/' | |
} | |
}[window.location.hostname] | |
export default { | |
get (key) { | |
return cfg[key] | |
}, | |
assign (obj) { | |
return assign(cfg, obj) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Wrapper around fetch for RESTful xhr requests to the api. | |
*/ | |
// polyfill Promise and fetch | |
import {Promise} from 'es6-promise' // eslint-disable-line | |
import 'whatwg-fetch' | |
import config from '../lib/config' | |
import assign from 'object-assign' | |
import {checkStatus, parseJSON} from '../lib/fetch-handlers' | |
export default { | |
get (relativeUrl, opts = {}) { | |
let url = `${config.get('baseApiUrl')}${relativeUrl}` | |
return window.fetch(url, assign({ | |
headers: {'Accept': 'application/json'} | |
}, opts)) | |
.then(checkStatus) | |
.then(parseJSON) | |
}, | |
post (relativeUrl, opts = {}) { | |
const url = `${config.get('baseApiUrl')}${relativeUrl}` | |
return window.fetch(url, assign({ | |
method: 'post', | |
headers: {'Content-Type': 'application/json'} | |
}, opts, (opts.body ? { body: JSON.stringify(opts.body) } : {}))) | |
.then(checkStatus) | |
.then(parseJSON) | |
}, | |
put (relativeUrl, opts = {}) { | |
const options = assign({ | |
method: 'put' | |
}, opts) | |
return this.post(relativeUrl, options) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global describe it before beforeEach afterEach sinon */ | |
// info on stubbing window.fetch: | |
// http://rjzaworski.com/2015/06/testing-api-requests-from-window-fetch | |
import {assert} from 'chai' | |
import config from './config' | |
import api from './fetch-api' | |
describe('fetch-api', () => { | |
before(() => config.assign({ baseApiUrl: 'localhost/' })) | |
beforeEach(() => { | |
sinon.stub(window, 'fetch') | |
let res = new window.Response('{"hello":"world"}', { | |
status: 200, | |
headers: { | |
'Content-type': 'application/json' | |
} | |
}) | |
window.fetch.returns(Promise.resolve(res)) | |
}) | |
afterEach(() => { | |
window.fetch.restore() | |
}) | |
describe('get(url, opts)', () => { | |
it('should prefix the url with baseApiUrl', (done) => { | |
api.get('foo') | |
.then((json) => { | |
// console.log(window.fetch.getCall(0).args[1]) | |
assert.isTrue(window.fetch.calledWith('localhost/foo', { | |
headers: { | |
'Accept': 'application/json' | |
} | |
})) | |
done() | |
}) | |
}) | |
it('should resolve to json', (done) => { | |
api.get('foo') | |
.then((json) => { | |
assert.deepEqual(json, { hello: 'world' }) | |
done() | |
}) | |
}) | |
it('should assign options', (done) => { | |
let opts = { method: 'fake', headers: {'Accept': 'garbage', 'Content-Type': 'garbage'} } | |
api.get('foo', opts) | |
.then((json) => { | |
assert.isTrue(window.fetch.calledWith('localhost/foo', { | |
method: 'fake', | |
headers: { | |
'Accept': 'garbage', | |
'Content-Type': 'garbage' | |
} | |
})) | |
done() | |
}) | |
}) | |
}) | |
describe('post(url, opts)', () => { | |
it('should use the post verb', (done) => { | |
api.post('foo') | |
.then((json) => { | |
const args = window.fetch.getCall(0).args[1] | |
assert.equal(args.method, 'post') | |
done() | |
}) | |
}) | |
it('should JSON.stringify the body option', (done) => { | |
api.post('foo', { body: { baz: 'qux' } }) | |
.then((json) => { | |
const args = window.fetch.getCall(0).args[1] | |
assert.equal(args.body, '{"baz":"qux"}') | |
done() | |
}) | |
}) | |
}) | |
describe('put(url, opts)', () => { | |
it('should use the put verb', (done) => { | |
api.put('foo') | |
.then((json) => { | |
const args = window.fetch.getCall(0).args[1] | |
assert.equal(args.method, 'put') | |
done() | |
}) | |
}) | |
}) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// response handlers for fetch (xhr2+) | |
// see: https://github.com/github/fetch | |
export function parseJSON (response) { | |
return response.json() | |
} | |
// Checks the response status for an error status code. | |
// if found, rejects with an error message from the default text, | |
// or a message from the json body. | |
export function checkStatus (response) { | |
if (response.status >= 200 && response.status < 300) { | |
return response | |
} | |
return new Promise(function (resolve, reject) { | |
const error = new Error(response.statusText) | |
error.response = response | |
const isJSON = /application\/json/.test(response.headers.get('Content-Type')) | |
if (!isJSON) { | |
reject(error) | |
} else { | |
response | |
.json() | |
.then( | |
// add json body to error | |
function (body) { | |
error.body = body | |
// update the error message to that from the response body | |
if (body.message) { | |
// TODO: define consistent error JSON schema between orch layer and stores | |
// Your resident reductionist suggests: 500 { message: "Something when wrong!" } | |
error.message = body.message | |
} else if (Array.isArray(body) && body[0].message) { | |
// NOTE: the orch layer is currently returning an Array instead of an object. | |
// e.g. [{"property":"Email", "message": "Invalid e-mail address...", "errorMessages":[]}] | |
error.message = body[0].message | |
} | |
reject(error) | |
}, | |
// catch JSON.parse(body) errors | |
function () { | |
reject(error) | |
} | |
) | |
} | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment