Host a React app and its Express server on the same EC2/VPS instance

I once worked on a React-Native app with login feature, for which we only needed a small Express server, and a priori no web pages… yet! We found out that we still needed to handle the reset password case… and for this reason, I decided to host server and app together, and to build a couple of React pages to address the needed feature. Today I will show you how I dealt with this chore.

The main point for doing this is, you must structure your project in a way that let both server and app communicate together.

For this article, I will use a very simple Express node server on the back-end, with a touch of mongoDB/mongoose. On front-end side, it’s a simple React app with react-router. Last but not least, I’m using webpack with a slightly customised configuration.

You can find the whole repo here :-). Might not be 100% up to date with packages versions but so you get the idea. Let’s go!


Here is how the structure of the project will look like.

So basically, you have a /dist folder where we host the webpack output bundle bundle.js. This bundle has to be accessible by both the React app and the Express server. We have an /src folder where we put both our React app and our server, respectively in /app and /server. Those two subfolders each have their own entry index.js and a package.json to manage their needed modules. Then, at root level we have:

  • index.html to access the front-end pages from the browser
  • package.json that contains the dev dependencies to create the bundle, and the scripts for testing, developing, building and eventually launching the app and the server altogether.
  • webpack.common.js that use either or depending on the mode we are on. Think of it as a webpack.config.js where we split the prod and the dev logic in two different file. It is not that much related to our case of the day, but I find it nice to have here. In the same vein, babel.config.js is for babel.
  • ecosystem.config.js (DO NOT SHARE ON GITHUB). If you are familiar with PM2, it is a nice way to launch a script on a EC2 instance and catch errors, relaunch, monitor, log and so on … I will explain you in a moment!
  • .env (DO NOT SHARE ON GITHUB ): it contains the following info:
PORT = 8080 // if your website entry is on port 8080
PORT_WEBPACK = 8008 // only for using the app on local machine
DBUSER= // user for mongodb
DBPASS= // pass for mongodb
CLUSTER= // cluster for mongodb

React App

It is a very simple app, I created it with the React boilerplate npx create-react-app. The sole purpose of these pages is to give to the user the ability to reset his password.

Here, Layout is just a way to wrap all the pages into a nice bootstrap style, that can totally be removed. <ResetPassword/> is our main form where the user types his new password, and <SuccessReset/> is the page where he is told that all went well. A <NotFound/> component handles all attempts to access a different url.

Regarding the <ResetPassword/> component, here is how it looks like:

The thing to remark here, apart from the fact that it is a normal form, with some fancy validation, is the fetch method, that uses a url like this:

const URL = process.env.NODE_ENV == 'production' ? 'api' : `http://localhost:${process.env.PORT}/api`;

If we are on production, the base URL will be the same as where the React page is hosted (8080 as per our .env file), we just add the /api because all our api methods will be called server-side with this prefix, as you will see when we define the server.

If we are on dev mode, we need to tell fetch that the server is localhost too, but located on a different port from PORT_WEBPACK where the React app is locally running. The server is to be run on PORT. We will therefore need to tell the server that while on development mode, all request coming from PORT_WEBPACK are authorised and CORS-approved, in order to avoid cross-origin block since app and server are not using the same port on local development.

Some Front-End checks block the form submission if the password doesn’t match some rules. Bear in mind that this is just UX stuffs, same routine should be done on Back-End side as well to avoid the user to store something that is irrelevant as auth credentials. If the request is successful, we redirect the user to /successReset where he is told that all went well.

The useQuery is a custom hook I wrote in order to retrieve in a json object all the query params that will need to come along with this page, in such format:


When the user makes the reseting password request (through the React-Native app, remember ?), the server creates a short-life token and pass it, along with user’s userId, into a reset link that is sent by email to the user. Once the user submits the form, the token and userId are sent back to the server, this time with the filled-in password. The server can then find which user wants his password changed, what new password it should be, and check that the token is the one that was priorly created when the user asked for a reset.

The package.json is left as it is by default. You can remove the /build folder that is created by the boilerplate. We will use the dist folder instead.

Server side of the app

If you look in detail at the Github repo you will see how to actually handle the situation of reseting a password: creating a secured token and store it with redis, checking that the user who resets the password is the one that received the reset link by email, store and retrieve the auth credentials from mongoDB and pass a JWT token to secure API calls … it’s all usable so feel free to copy/paste!

Now on our specific matter, let’s just focus on the following src/server/index.js, which is the entry point for the Express server:

The important bit for our matter here is from line 29:

if (process.env.NODE_ENV !== 'production') {  
origin: `http://localhost:${portWebpack}`,
credentials: true
app.get(/^(?!\/api\/)/,(_,res) => {

First, we tell the server that in case of development mode, we must authorise server calls coming from the port PORT_WEBPACK, it’s just us trying to call the api for reseting the password from our webpack-dev locally running app! So we wrap the origin with the cors module

Second, we tell the server that our static assets (the React compiled bundle) are located into the dist folder of the root directory.

Last, we catch any attempt to call any request than doesn’t start with /api, and we serve back the index.html file as a response. This is our entry html page from the root directory. All routes after these lines have the /api as a prefix as you can see in the rest of the file. They don’t deal with the react app but only with database and pure back-end stuffs.

All is good on server side, now let’s look at how to put into motion all those things together!

Binding Things Together

Back at the root level of the repo. Now look at the package.json at the root of the project:

You can see that no dependencies are included here, but only devDependencies. We need these modules only while running the building/dev scripts. You need to run them from the root. The scripts are the following:

  • install-{main|app|server} will go into the according folder and run npm install, looking at the package.json of the selected folder. You will see /node_modules appear into each folder for which you have executed the install-....
  • dev will run webpack-dev-server, with HMR (Hot Module Reloading), which takes as a configuration If you run this, the app will start building in development mode and a tab will open at the address http://localhost:{PORT_WEBPACK} (in my example is localhost:8008).
  • build: same, but for production, hence using It will create a bundle file located into your /dist folder. This bundle will be reached by your index.html entry page.
  • server and server-dev start the server by running node src/server/index.js, just the mode production/development is set by use of the NODE_ENV in the script.
  • start-dev is a concatenation of starting the server in dev mode, and starting webpack in dev mode too. You will want to use this for testing locally that your app and the server talks to each other accordingly. You can use concurrently that I have globally installed on my machine. Otherwise just run the two commands separately npm run server-dev and npm run dev on two terminal tabs.

It is to note that both webpack configuration files use the webpack.common.js that looks like this:

Not to mention the module rules, that are just the way webpack handles various file types in your React app, you can note the following:

  • entry : path.resolve(__dirname,’src’,’app’) means that the entry (the index.js of React app) is to be found into the src/app of the root directory
  • output says that the output folder is /dist, the bundle file is called bundle.js and the entry for the website is simply the root directory, ie, where our index.html is located, and contains the following body section:
<body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src='/bundle.js'></script> // here is the magic</body>

Now let’s look at the

While running locally, you’ll have to tell webpack how the dev-server works, by the use of the devServer property. And of course for both production and development webpack config files, do not forget to have you env data parsed and accessible, by using dotenv as shown above (it will be the same on

Now let’s run locally with npm run start-dev, and go to the page localhost:8008/resetPassword. You will see the following:

You can try to write a password matching the rules, and submit … You should see an error under the reset Password, stating that the token is missing… which means that the server actually responded to your attempt of reseting the password!

If you try to go to the localhost:8080/resetPassword instead, you should see a blank page … Yes, now you are on the server port, so you are asking the server to retrieve the bundle that is in the dist folder.… which doesn’t exist yet since you must first run npm run build to create a production bundle in the dist folder! If you run npm run build and then refresh the page, you should see the same page as in dev mode (but it will not hot-reload though).

Right now you are in dev mode, but as I saif above, the server is already responding. You can try to pass: localhost:8008/resetPassword?token=FAKETOKEN&userId=FAKEID and and when submitting, it will now have different error message saying: request has expired, please have a reset email sent again. The server responds and doesn’t block you for CORS reasons! Great!


For this part, I use an AWS EC2 instance, but any VPS could do. I also use nginx, for which tons of documentation exist, I use this one as an example.

First, log into your instance, and clone your git repo at the entry of the instance (ubuntu for me)

Then modify your nginx configuration to route to the port 8080. It’s achieved by editing the /etc/nginx/sites-available/default nginx config file (do not forget to restart afterwards with sudo systemctl restart nginx ):

The intent here is to redirect the user hitting the domain name to the instance’s 8080 port. Do not forget the update the VPS/EC2 inbound security rules for this port so that anyone can visit it. If you have a domain name, think of adding the entry to your DNS setup so that you can reach your server by typing the domain name in your browser… If not, by using AWS you will still be able to access your site by using the public DNS (IPv4):

Run the successive commands npm run install-app, npm run install-server and npm run install-main. All the packages are now installed on your server!

Before running the app, you will need some env parameters here. I personally use PM2 which lets you define an ecosystem config. Just create an config file at the root level of your app: sudo nano ecosystem.config.js, and edit it as follow:

You have a nice script that will 1. build a bundle file with webpack and put it into the /dist folder, and 2. start the server in production mode.

just run pm2 start ecosystem.config.js, and after a small moment (building…) hit the endpoint of your website:


Try to fill the form and submit, you should get answers from the server. If you run pm2 logs on the terminal of your instance, you will see more details of what happens server-side!

That’s it! Hope this helps!




Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Writing Chrome Extensions Using Svelte-Kit and Manifest v3

Creating a Location Aware “Hot and Cold” Mobile App

Rendering performance monitoring on Android

Playing with Gulp [Part 2] — What can Gulp do?

Unit, integration and e2e. What to write?

Coding Terms | JWT

Basics about JavaScript.

Mutability and immutability in Dart: Understanding var, const and final

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Geoffroy Mounier

Geoffroy Mounier

More from Medium

What Is Node.js and Why You Should Use It

What is Node.

[JavaScript Series] How to install Node.js on your PC/Laptop

Node Js Overview