Skip to content

Instantly share code, notes, and snippets.

@tomazzaman
Created September 26, 2016 15:10
Show Gist options
  • Save tomazzaman/1041f3625f5c4344a8d9f027f05e5fdf to your computer and use it in GitHub Desktop.
Save tomazzaman/1041f3625f5c4344a8d9f027f05e5fdf to your computer and use it in GitHub Desktop.
API middleware for redux
import superagent from 'superagent';
import { merge } from 'lodash';
import { camelizeKeys, decamelizeKeys } from 'humps';
import config from 'config';
const CALL_API = Symbol.for('Call API');
export const GENERIC_ERROR = 'GENERIC_ERROR';
const genericErrors = {
400: 'You\'ve attempted to make a request the server didn\'t expect',
401: 'You\'re not authenticated within the system, please log in.',
403: 'You\'re not authorized to perform this action',
404: 'The resource you\'re trying to fetch or modify does not exist',
500: 'An error occured on the server (not your fault). Admin has been contacted.',
};
/**
* Prepare headers for each request and include the token for authentication.
* @param {Boolean} token Authentication token
* @return {Object} Request headers
*/
function setRequestHeaders(token) {
const headers = {
Accept: 'application/vnd.referoo.v2+json',
};
if (token) headers.Authorization = `Bearer ${token}`;
return headers;
}
/**
* Check the state whether we have an auth token present
* @param {Object} Redux store
* @return {String} Authentication Token
*/
function getToken(store) {
const user = store.getState().user.toJS();
return user.authToken || null;
}
/**
* Api middleware to make async requests to the server(s). This is the only place
* in the app where XHR should be made. It automatically "catches" every action
* with CALL_API symbol and extracts it's data to build a proper request.
* If a callback is provided in the action, then it may update the calling
* component with request (upload) progress.
*/
export default store => next => action => {
const request = action[CALL_API];
// Ignore the action (pass it on) if it's not meant to make an API request
if (typeof request === 'undefined') return next(action);
/**
* Create a new action (because original should be immutable) and dispatch it
* into reducers. It's unnecessary to send request info, so we remove it.
* @param {Object} data Incoming JSON payload (from the API)
* @return {Object} Data for reducers
*/
function actionWith(newAction) {
const finalAction = merge({}, action, camelizeKeys(newAction));
delete finalAction[CALL_API];
return finalAction;
}
// Set defaults via destructuring
const [requestType, successType, failureType] = request.types;
const { method = 'GET', onProgress = () => {}, callback = () => {} } = request;
const body = decamelizeKeys(request.body);
// Dispatch a "loading" action (useful for showing spinners)
next(actionWith({ type: requestType }));
/**
* Completes the response from superagent.
* Note if you're unable to parse the results (this method with respond with
* something like "Parser is unable to parse response (...)") this means the
* returned JSON object isn't valid.
* @param {String} err Any possible errors, null if none
* @param {Object} response The response object, generated by underlying XHR.
*/
function completeResponse(err, response) {
if (response.ok) {
next(actionWith({ type: successType, body: response.body }));
callback();
}
if (err && genericErrors[err.status]) {
next(actionWith({ type: GENERIC_ERROR, message: genericErrors[err.status] }));
}
if (!response.ok) {
next(actionWith({ type: failureType, body: response.body }));
}
}
const preparedRequest = superagent(method, config.apiUrl + request.endpoint)
.set(setRequestHeaders(getToken(store)));
/**
* If the request has 'files' key on it, the request becomes multipart instead
* of JSON, which means we have to switch from regular .send() to a series
* of .field(name, value) calls (hence the loop). Superagent creates FormData
* behind the scenes, so we don't have to.
*/
if (request.file) {
preparedRequest.on('progress', event => onProgress(event));
const fileAttachKey = 'file';
preparedRequest.attach(fileAttachKey, request.file);
// FIXME: make sure to remove the eslint-disable lines and fix the error!
/* eslint-disable */
for (const property in body) {
if (body.hasOwnProperty(property)) {
preparedRequest.field(property, body[property]);
}
}
/* eslint-enable */
} else {
preparedRequest.send(body);
}
preparedRequest.end(completeResponse);
return null; // TODO: check whether we can return null or if it even matters.
};
@romanlex
Copy link

romanlex commented Jul 4, 2017

Hello...how I can use it? use applyMiddleware from redux package?
After this how I can use it in my actions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment