Webpack and CORS: Cross-server Communications in React.js
Note: This article will cover one use case of Webpack which involves having a single webapp consisting of a React.js front-end, communicating with an Express.js REST API running all data transactions.
This article will NOT cover more complex cases such as running multiple Node.js servers through the same HTTP port (i.e. using proxies), the usage of sub-domains, or the usage of third-party services hosted on a different domain.
With this in mind, let’s jump into it…
First of all, if we put it simply, all web development projects will have at least two stages: DEV
and PROD
. (Again, I’m oversimplifying. I know I’m skipping over industry standards that apply TEST
and STAGING
stages, but for simplicity’s sake I’m omitting these concepts)
DEV
can happen either at your computer or at a cloud server provisioned with everything needed for you to write and test code on the fly. DEV environments hold no real data and are meant to be safe playgrounds for web app development.
PROD
, a.k.a production, is the culmination of all DEV
work, as in, a server in the cloud running the web app which end-users will use in their day-to-day. Ideally (emphasis in ideally) things should have already been thoroughly tested before reaching this stage, so that once your code arrives at PROD
, nothing can go wrong. PROD holds all user data.
Now, with that in mind, let me bluntly assert that:
Cross-Origin Resource Sharing (CORS) problems in Webpack/React.js projects will only arise during the DEV
stage of development.
The reasons why are quite easily explained through the following example:
Let’s say you’re building a web app consisting of a React.js front-end and an API which in the end (PROD)
will reside in https://yoursite.co (read more on how to implement https certificates for free here).
During development (DEV)
your front-end might be served at:
https://localhost:8080
and your API at:
https://localhost:8081/api/v1/
Here’s where your browser will begin to freak out when you try to access your web app, which, in turn, tries to access the API, because of that port difference. More on that in just a moment.
To finish up with the example, once your app reaches PROD, on the other hand, the routes will look like this:
https://yoursite.co
https://yoursite.co/api/v1/
Same address and same port keeps browsers happy and sane.
Some more context
To delve deeper into why the aforementioned applies, Webpack offers two stage-specific solutions to deliver your web app:
- For
DEV
stage: a webpack-dev-server - For
PROD
stage: a production packaging tool
The webpack-dev-server
The webpack-dev-server will serve your web app so that you can view it while developing. Needless to say, it should never be used in production environments.
The dev server can manage a multitude of development-specific processes such as compiling your .sass files on-the-fly, reloading your app in-browser whenever you make a change to your source code (a.k.a Hot Module Reloading), etc.
Since Hot Module Reloading is one of the strongest selling points of Webpack as a development tool, this means that your front-end is not served directly from the source files you have stored in your hard-drive, but generated in real-time and read from memory.
This limits the ability to serve your API from the same port as the front-end, so much so that in order to emulate the ability for your front-end to be able to speak to your API on the same port you would have to use a proxy to route HTTP requests accordingly.
Production packaging
Once a development cycle has ended and your web app is ready to be “pushed to PROD
“, when using Webpack you will simply run in your terminal:
$ webpack -p
this will generate a single bundle.js
file holding all of the code necessary to run your front-end.
For all intents and purposes, the most sane thing to do in order to serve your web app is to configure an HTTP server to serve the generated index.html
and bundle.js
files, as well as any other static asset (images, etc).
And the same server can be configured to serve your API through the appropriate routes.
It truly is as simple as that.
CORS Error Scenarios and Solutions
I will be showing you two scenarios which you may encounter out in the wild:
- One in which the API url is injected into the app so that we can develop knowing the app will fetch the content from the correct port.
The other one in which we simply want to call the API as a relative path to the current domain in bothDEV
andPROD
.
To make things easier to test for you, I have created an example app which I uploaded to github. You can find it here.
Be sure to clone it and follow the instructions from the README
to install its dependencies.
Before we continue, I’ll simply state that the example app is a React.js hello world
which has a HelloWorld
React component that loads the words Hello
and World
from a back-end API via the fetch
command (AJAX).
Solution 1: When injecting the API’s url
If you downloaded the example repo, checkout the branch broken
as follows:
$ git checkout broken
(click here to view in github)
Let’s look at the HelloWorld
component:
import React, { Component } from 'react';
// #####################################
// ############ IMPORTANT ##############
// Here the `API_URL` env var is being
// injected by the `server.js` file
const api_url = process.env.API_URL || '';
// ############ /IMPORTANT #############
// #####################################
// This is an over-simplified component
// which loads two strings via common
// `fetch` (ajax) calls.
//
// It is not meant to illustrate how
// react components should be built,
// in a real-world scenario data would
// be handled by a `Store` and managed
// by `Actions`.
class HelloWorld extends Component {
constructor(props) {
super(props);
this.state = {
hello: '',
world: ''
};
}
componentDidMount(){
const self = this;
fetch(api_url + '/api/v1/hello')
.then(function(response) {
return response.text();
}).then(function(text) {
self.setState({
hello: text
});
});
fetch(api_url + '/api/v1/world')
.then(function(response) {
return response.text();
}).then(function(text) {
self.setState({
world: text
});
});
}
render() {
return {this.state.hello} {this.state.world}!; }
}
export default HelloWorld;
As you can see from the code, the API_URL
environment variable is used to define the api_url
const, which is, well, the url where the API is at. If API_URL
is not present we simply leave the api_url
string empty so that we fetch from the site’s root. This is so that in DEV
we can inject the url with the different port, and then in PROD
we can assume that both the front-end and API will live under the same port. To inject the aforementioned API_URL
environment variable, I simply added it into the configurations loaded into server.js
from the webpack.config.js
file like so:
// External libraries
var webpack = require('webpack');
var webpackDevServer = require('webpack-dev-server');
var express = require('express');
// Local files
var config = require('./webpack.config.js');
var api = require('./api/api');
if (process.env.NODE_ENV === 'dev-server') {
// = DEV =
// This stands up the webpack-dev-server
// with Hot Module Reloading enabled.
// The following is needed in order for
// Hot Module Reloading to work.
config.entry.app.unshift('webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/dev-server');
// #########################################
// ############## IMPORTANT ################
// Here we are injecting the API_URL env var
// to be used by the `HelloWorld` component
config.plugins.unshift(new webpack.DefinePlugin({
'process.env':{
'API_URL': JSON.stringify('http://localhost:8081')
}
}));
// ############## /IMPORTANT ###############
// #########################################
// Initiate webpack-dev-server with the
// config we created in `webpack.config.js`
var compiler = webpack(config);
var server = new webpackDevServer(compiler, {
hot: true
});
server.listen(8080);
} else if (process.env.NODE_ENV === 'dev-api') {
// = DEV =
// This stands up the express.js API
var app = express();
// We define the API routes here
api.defineApi(app);
app.listen(8081, function () {
console.log('API is up!')
});
} else {
// = PROD =
// This is here for simplicity's sake,
// in a real-world application none of
// the development code should be copied
// over to the production server.
var app = express();
// We serve the bundle folder, which
// should contain an `index.html` and
// a `bundle.js` file only.
app.use('/', express.static('bundle'));
// We define the API routes here
api.defineApi(app);
app.listen(8080, function () {
console.log('Both front-end and API are up!')
});
}
Now if you run the app in DEV
mode you will see the following:
But if you run it in PROD
mode you’ll see a whole different story:
You can see clearly what I was stating before: the CORS
error only happens in DEV
not in PROD
Here’s where the first solution comes in. Considering that what we want is for the API to allow for its contents to be loaded from a different port, we need to fix things on the API side of things, not on Webpack. That’s where the cors
npm package comes in to save the day. First we install the package as so:
$ npm install cors --save-dev
Next, we make express use the package when in DEV
mode:
// External libraries
var webpack = require('webpack');
var webpackDevServer = require('webpack-dev-server');
var express = require('express');
// #######################
// ##### IMPORTANT #######
// Adding `cors` package
// so API allows calls by
// front-end (Allow-all)
var cors = require('cors');
// ##### /IMPORTANT ######
// #######################
// Local files
var config = require('./webpack.config.js');
var api = require('./api/api');
if (process.env.NODE_ENV === 'dev-server') {
// = DEV =
// This stands up the webpack-dev-server
// with Hot Module Reloading enabled.
// The following is needed in order for
// Hot Module Reloading to work.
config.entry.app.unshift('webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/dev-server');
// #########################################
// ############## IMPORTANT ################
// Here we are injecting the API_URL env var
// to be used by the `HelloWorld` component
config.plugins.unshift(new webpack.DefinePlugin({
'process.env':{
'API_URL': JSON.stringify('http://localhost:8081')
}
}));
// ############## /IMPORTANT ###############
// #########################################
// Initiate webpack-dev-server with the
// config we created in `webpack.config.js`
var compiler = webpack(config);
var server = new webpackDevServer(compiler, {
hot: true
});
server.listen(8080);
} else if (process.env.NODE_ENV === 'dev-api') {
// = DEV =
// This stands up the express.js API
var app = express();
// #####################
// ##### IMPORTANT #####
// make express use the
// `cors` middleware
app.use(cors());
// ##### /IMPORTANT ####
// #####################
// We define the API routes here
api.defineApi(app);
app.listen(8081, function () {
console.log('API is up!')
});
} else {
// = PROD =
// This is here for simplicity's sake,
// in a real-world application none of
// the development code should be copied
// over to the production server.
var app = express();
// We serve the bundle folder, which
// should contain an `index.html` and
// a `bundle.js` file only.
app.use('/', express.static('bundle'));
// We define the API routes here
api.defineApi(app);
app.listen(8080, function () {
console.log('Both front-end and API are up!')
});
}
I have already done the heavy lifting in the cors
branch, which you can checkout by running:
$ git checkout cors
(click here to view in github)
Now if you run the app in DEV
mode you should see:
Great!
But to be fair, injecting the API’s url into the app when in DEV
mode is a bit of a hack and adds unnecessary complexity. I have included this solution because in the real world people might need to monkey patch things as just described, but ideally I would urge you to consider the next solution.
Solution 2: Using a proxy for webpack-dev-server
Let’s take the previous example and refactor things a little. I’m going to remove that ugly url injection and the cors package from the server code:
// External libraries
var webpack = require('webpack');
var webpackDevServer = require('webpack-dev-server');
var express = require('express');
// #####################
// ##### IMPORTANT #####
// removed `cors` usage
// ##### /IMPORTANT ####
// #####################
// Local files
var config = require('./webpack.config.js');
var api = require('./api/api');
if (process.env.NODE_ENV === 'dev-server') {
// = DEV =
// This stands up the webpack-dev-server
// with Hot Module Reloading enabled.
// The following is needed in order for
// Hot Module Reloading to work.
config.entry.app.unshift('webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/dev-server');
// #########################################
// ############## IMPORTANT ################
// Removed `API_URL` plugin injection here
// ############## /IMPORTANT ###############
// #########################################
// Initiate webpack-dev-server with the
// config we created in `webpack.config.js`
var compiler = webpack(config);
var server = new webpackDevServer(compiler, {
hot: true
});
server.listen(8080);
} else if (process.env.NODE_ENV === 'dev-api') {
// = DEV =
// This stands up the express.js API
var app = express();
// #####################
// ##### IMPORTANT #####
// removed `cors` usage
// ##### /IMPORTANT ####
// #####################
// We define the API routes here
api.defineApi(app);
app.listen(8081, function () {
console.log('API is up!')
});
} else {
// = PROD =
// This is here for simplicity's sake,
// in a real-world application none of
// the development code should be copied
// over to the production server.
var app = express();
// We serve the bundle folder, which
// should contain an `index.html` and
// a `bundle.js` file only.
app.use('/', express.static('bundle'));
// We define the API routes here
api.defineApi(app);
app.listen(8080, function () {
console.log('Both front-end and API are up!')
});
}
And I’ll remove the usage of the API_URL
environment variable from the HelloWorld
component, which will make things look like this:
import React, { Component } from 'react';
// ###########################
// ####### IMPORTANT #########
// Removed `api_url` variable
// This is an over-simplified component
// which loads two strings via common
// `fetch` (ajax) calls.
//
// It is not meant to illustrate how
// react components should be built,
// in a real-world scenario data would
// be handled by a `Store` and managed
// by `Actions`.
class HelloWorld extends Component {
constructor(props) {
super(props);
this.state = {
hello: '',
world: ''
};
}
componentDidMount(){
const self = this;
// ###########################
// ####### IMPORTANT #########
// Removed `api_url` variable
fetch('/api/v1/hello')
.then(function(response) {
return response.text();
}).then(function(text) {
self.setState({
hello: text
});
});
fetch('/api/v1/world')
.then(function(response) {
return response.text();
}).then(function(text) {
self.setState({
world: text
});
});
// ####### /IMPORTANT ########
// ###########################
}
render() {
return {this.state.hello} {this.state.world}!;
}
}
export default HelloWorld;
Now if you run the app in DEV
mode you should see:
Now that’s not a CORS error, but it’s an error nonetheless. It’s not able to get the strings because now it’s trying to get them from the front-end port (8080) rather than the API port (8081). But with a quick fix in the webpackDevServer
definition:
// External libraries
var webpack = require('webpack');
var webpackDevServer = require('webpack-dev-server');
var express = require('express');
// #####################
// ##### IMPORTANT #####
// removed `cors` usage
// ##### /IMPORTANT ####
// #####################
// Local files
var config = require('./webpack.config.js');
var api = require('./api/api');
if (process.env.NODE_ENV === 'dev-server') {
// = DEV =
// This stands up the webpack-dev-server
// with Hot Module Reloading enabled.
// The following is needed in order for
// Hot Module Reloading to work.
config.entry.app.unshift('webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/dev-server');
// #########################################
// ############## IMPORTANT ################
// Removed `API_URL` plugin injection here
// ############## /IMPORTANT ###############
// #########################################
// Initiate webpack-dev-server with the
// config we created in `webpack.config.js`
var compiler = webpack(config);
// #########################################
// ############## IMPORTANT ################
// Added `proxy` configuration for API fix
var server = new webpackDevServer(compiler, {
hot: true,
proxy: {
'/api': {
target: 'http://localhost:8081',
secure: false
}
}
});
// ############## /IMPORTANT ###############
// #########################################
server.listen(8080);
} else if (process.env.NODE_ENV === 'dev-api') {
// = DEV =
// This stands up the express.js API
var app = express();
// #####################
// ##### IMPORTANT #####
// removed `cors` usage
// ##### /IMPORTANT ####
// #####################
// We define the API routes here
api.defineApi(app);
app.listen(8081, function () {
console.log('API is up!')
});
} else {
// = PROD =
// This is here for simplicity's sake,
// in a real-world application none of
// the development code should be copied
// over to the production server.
var app = express();
// We serve the bundle folder, which
// should contain an `index.html` and
// a `bundle.js` file only.
app.use('/', express.static('bundle'));
// We define the API routes here
api.defineApi(app);
app.listen(8080, function () {
console.log('Both front-end and API are up!')
});
}
Bingo!:
This is the ideal solution. It’s really simple and can be managed from a single spot in the code that spins up the DEV
server, with no way to muddy up production with API_URL
related errors. To test this solution simply checkout the proxy
branch:
$ git checkout proxy
(click here to view in github)
And things should run just as expected on both DEV
and PROD
.
Conclusion
Writing React.js code is complex enough as it is, so keep in mind the difference between DEV and PROD when setting up Webpack you will save yourself a lot of headaches.
Happy Coding!
—
If you have any questions don’t hesitate to comment below and connect with Jean on LinkedIn and Twitter.