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!
Structure
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 browserpackage.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 eitherwebpack.dev.js
orwebpack.prod.js
depending on the mode we are on. Think of it as awebpack.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:
http://localhost:8008/resetPassword?userId={userId}&token={token}
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') {
app.use(cors({
origin: `http://localhost:${portWebpack}`,
credentials: true
}));
}
app.use(express.static(path.resolve(__dirname,`../../dist`)))
app.get(/^(?!\/api\/)/,(_,res) => {
res.sendFile(path.resolve('index.html'))
})
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 runnpm install
, looking at thepackage.json
of the selected folder. You will see/node_modules
appear into each folder for which you have executed theinstall-...
.dev
will runwebpack-dev-server
, with HMR (Hot Module Reloading), which takes as a configurationwebpack.dev.js
. If you run this, the app will start building in development mode and a tab will open at the addresshttp://localhost:{PORT_WEBPACK}
(in my example islocalhost:8008
).build
: same, but for production, hence usingwebpack.prod.js
. It will create a bundle file located into your/dist
folder. This bundle will be reached by yourindex.html
entry page.server
andserver-dev
start the server by runningnode src/server/index.js
, just the mode production/development is set by use of theNODE_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 useconcurrently
that I have globally installed on my machine. Otherwise just run the two commands separatelynpm run server-dev
andnpm 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 (theindex.js
of React app) is to be found into thesrc/app
of the root directoryoutput
says that the output folder is/dist
, the bundle file is calledbundle.js
and the entry for the website is simply the root directory, ie, where ourindex.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 webpack.dev.js
:
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 webpack.prod.js
):
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!
Deploying
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!