Isomorphic requests - Xantier/nerd-stack GitHub Wiki
One of the tricky parts about isomorphic applications is to figure out a good API to handle data loading in a situation where it can be initiated via XHR request or started directly from a server side command. In our application this is handled together with express router by wrapping our routes into an extra layer which determines which kind of response it would like to send back.
Since we are developing with Flux-ish (pure in frontend, not so in backend) architecture our requests go through our Flux Actions. If our Actions will need to fetch data from the DB they will create needed request payload and forward it to our fetcher script (in app/services/fetcher.js), which contains methods for GET, POST, DELETE and PUT. Our fetcher methods determine whether the request to retrieve is from an action on the client or on the server and acts accordingly. If the action is from the client side, fetcher makes an XHR request and hits our [hostname]/API/[requestPath] endpoint and express router forwards it to the correct route function. If the action is from the server side, fetcher, with a little help from serverMediator, retrieves the correct express route function based on the [requestPAth] and calls that.
Express router usually accepts a route definition and a function which handles the request, sometimes some middleware in between to do some middleware stuff like authentication or injecting some values. The function receives a request and a response and after it has done its job, it calls a method on response to return results to the browser. In our isomorphic application we are utilizing this standard express route functionality for both client and server request. For route functions that need to be isomorphic, working the same way in client and server we attach an extra function to our routes. The actual meat of our route handling becomes a middleware and returns it's results in a standard javascript object form by attaching a payload element to the response. This response element is then used on both client and server side to forward the data either to the browser or our server renderer. This way we avoid having to create multiple routers to handle requests from server and client side.
An example from this can be seen in apiRouter.js file few of our get methods are isomorphic:
router.get('/user', user.get, respond);
The route has a standard function (user.get) handling whatever meat needs to be churned. This user.get acts as a middleware and returns the data we need. After all the meat is churned, it will call next
function passing the control flow forward. In apiRouter this means that function respond
gets its turn. Response looks in its all simplicity like this:
function respond(req, res) {
res.send(JSON.stringify(res.payload));
}
We have extracted this responding so that we are able to use the same function to retrieve data for both server and client requests. For server requests our serverMediator handles determining which method is mapped to the requested route and calls that. Since the route method itself doesn't respond directly but only acts as a middleware to generate the response object we can use same functions for both server and client. Our serverMediator looks like this:
export function handleServerRequest(url, context) {
const matchingRoute = _.find(stack, function (route) {
return route.regexp.test(url);
});
if (matchingRoute) {
let res = {};
return matchingRoute.route.stack[0].handle(context, res, function () {
return res.payload;
});
}
}
In here we match the first middleware function of the request and call that passing in our context and an empty res object which is populated by the route function. The last parameter is our callback acting as the next()
function in place of the next function of the route.