Skip to content

Instantly share code, notes, and snippets.

@brigand
Created June 17, 2018 12:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brigand/9816f97873b21dfa0548a96eeb090434 to your computer and use it in GitHub Desktop.
Save brigand/9816f97873b21dfa0548a96eeb090434 to your computer and use it in GitHub Desktop.
Custom redux api middleware. Take and modify to fit your needs.
/*
Example action:
{
type: 'MY_API',
payload: {
method: 'POST',
path: 'foo/:userId',
params: { userId: '123' },
query: { my_query: 1 },
body: { my_body: 2 },
actions: ['FETCH_FOO', 'FETCH_FOO_SUCCESS', 'FETCH_FOO_FAIL']
},
}
*/
export type Request<Q, B> = {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
path: string,
query?: Q,
params?: { [paramName: string]: string | number },
actions: [string, string, string],
body?: B,
map_success?: (res_body: any, res: any) => any,
map_error?: (res_body: any, res: any) => any,
meta?: any,
};
export type ApiAction<Q, B> = {
type: 'MY_API',
payload: Request<Q, B>,
};
export function apiAction<Q, B>(
request: Request<Q, B>,
meta: any,
): ApiAction<Q, B> {
return {
type: 'MY_API',
payload: request,
meta,
};
}
function debugPrintObject(obj: ?Object) {
if (!obj) return String(obj);
const keys = Object.keys(obj);
if (keys.length < 5) return keys.join(',');
return `${keys.slice(0, 4).join(',')}…`;
}
function debugPrintRequest(request: Request<any, any>) {
return `{path=${String(request.path)}, query={${debugPrintObject(
request.query,
)}}, body={${debugPrintObject(request.body)}}`;
}
async function performRequest<Q, B>(request: Request<Q, B>) {
let url = request.path;
if (url.startsWith('/'))
throw new Error(
`'path' can't start with /, for request ${debugPrintRequest(request)}`,
);
if (request.params) {
url = url.replace(/(^|\/):(\w+)(\/|$)/g, (m, pre, ident, post) => {
// $FlowFixMe
const param = request.params[ident];
if (param == null) {
throw new Error(
`Attempted to replace segment :${ident} but request.params didn't contain it.`,
);
}
return `${pre}${encodeURIComponent(String(param))}${post}`;
});
}
url = `/api/v1/${url}`;
if (request.query) {
url = `${url}?${qs.stringify(request.query)}`;
}
const fetchParams: any = {
method: request.method,
headers: {
},
};
if (request.body) {
fetchParams.headers['content-type'] = 'application/json';
}
try {
const res = await global.fetch(url, fetchParams);
let body = await res.json();
if (res.ok) {
if (request.map_success) {
body = request.map_success(body, res) || body;
}
return {
type: request.actions[1],
payload: body,
meta: {
headers: res.headers,
req: {
...request.query,
...request.body,
},
...request.meta,
},
};
}
if (request.map_error) {
body = request.map_error(body, res) || body;
}
return {
type: request.actions[2],
payload: body,
meta: {
headers: res.headers,
req: {
...request.query,
...request.body,
},
...request.meta,
},
};
} catch (e) {
console.error(`Fetch failed, or internal error`, e);
return {
type: request.actions[2],
payload: {
message: e.message,
},
meta: {
headers: {},
req: {
...request.query,
...request.body,
},
...request.meta,
},
};
}
}
const apiMiddleware = (store: any) => (next: any) => (action: any) => {
if (action.type !== 'MY_API') {
return next(action);
}
// Start request on next tick
Promise.resolve().then(async () => {
const resAction = await performRequest(action.payload);
store.dispatch({ ...resAction });
});
return next({
type: action.payload.actions[0],
payload: {
...action.payload.query,
...action.payload.body,
},
meta: {
req: {
...action.payload.query,
...action.payload.body,
},
...action.payload.meta,
},
});
};
export default apiMiddleware;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment