Skip to content

Instantly share code, notes, and snippets.

@twalker
Created October 15, 2015 17:20
Show Gist options
  • Save twalker/db5aeaf9a9829e511f52 to your computer and use it in GitHub Desktop.
Save twalker/db5aeaf9a9829e511f52 to your computer and use it in GitHub Desktop.
a fetch abstraction for RESTful calls to a single api
/**
* 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)
}
}
/**
* 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)
}
}
/* 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()
})
})
})
})
// 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