How I deployed a full stack React/Express app to production
Tags: devops, aws, webdev
Published: 2020-12-28 18:52:46

---
title: How I deployed a full stack React/Express app to production
published: true
description: My experience deploying a full stack React/Express application to production using AWS
tags: devops, aws, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/huudw9ph9otl144s8n7o.jpg
---

I recently [launched](https://dev.to/dmahely/introducing-artest-a-music-quiz-game-3lf) a [web application](https://artestgame.com) that consisted of a React frontend that communicated with an Express server. It was my first time working with this stack and everything worked smoothly on my local, but then came the time for deployment. *Cue the ominous music*.

There are many many articles and tutorials on how to deploy this kind of app to production, to the point where I decided not to follow any of them and make up my own way. I had been wanting to practice using AWS so all the services used here are on that platform. I'm not claiming this is the best way but it's just what worked for me.

## TL;DR

- Domain name registered on Route 53
- React frontend deployed to Amplify
- Express backend hosted on a Lightsail instance

# Deploying the Frontend

Amplify is an AWS service for hosting SPAs. It connects directly with your GitHub or GitLab repository, has continuous deployment set up on each push on master, and is totally free. It also has other awesome services such as deploying test branches, or generating preview builds when a PR is created. 

Start by navigating to the Amplify console in AWS and click on the _Connect App_ button. 

![Getting started with the Amplify console](https://dev-to-uploads.s3.amazonaws.com/i/nexqbj5ket2afwalzsi8.png)

After selecting your code hosting provider, Amplify will ask for read-only access to your account and you can go on with selecting a repo and a branch to deploy from.

![Add a repository branch on Amplify](https://dev-to-uploads.s3.amazonaws.com/i/u4rzj5foqxqdlsurv23h.png)

Afterwards, you'll be asked to verify the build process generated for you, and soon enough your app will be live! 🙌 Amplify's generated URLs follow this format `branchName.randomCharacters.amplifyapp.com`, so your website URL may look something like this https://master.d1dgz538iqckwr.amplifyapp.com/. 

In my code I was using an environment variable, and I was confused on how Amplify will be able to read it since the `.env` file was in `.gitignore` and therefore not committed, and I had no way to access the server on which the app is hosted. Then my eye caught the side menu where _Environment Variables_ is written. Here, I was able to add the key and value pair for my environment variable. 

![Environment variables on Amplify](https://dev-to-uploads.s3.amazonaws.com/i/dvkaxpbolst5zvp3ck9v.png)

Updating environment variables requires redeploying the site, so go back to the app page and click on any of the steps shown in green. This will show you your build history and related data, plus a _redeploy this version_ button which you should click. Now, your deployed app can use the environment variable you specified. 👏

![Application build pipeline on Amplify](https://dev-to-uploads.s3.amazonaws.com/i/68pl11nmox69yee92s1u.png)

Another great thing about using Amplify is that you get an SSL certificate installed on your website and automatic redirection from HTTP to HTTPS.

# Registering the Domain Name

Route 53 is a DNS service offered by AWS that integrates nicely with Amplify. It allows you to get your own domain name and connect it to your deployed app easily.

First, go to the Route 53 page in the AWS console, where you'll see the following component letting you know that you can create a new domain or transfer an existing domain into Route 53. In my case, I needed a brand new domain.

![Register domain form in Route 53](https://dev-to-uploads.s3.amazonaws.com/i/cbdppz5ue2eazcwmjpj7.png)

Just type in the domain name you'd like to have and you'll get a list of all the available options with prices, plus some related suggestions. Once you settle on a domain, add it to your cart and check out. You'll be asked for your contact details for verification.

![Available domain names in Route 53](https://dev-to-uploads.s3.amazonaws.com/i/v04nmpojgo3s5xar3ekc.png)

Now go back on your application in the Amplify console, click on _Domain Management_ in the side menu, and you'll be able to see the _Add domain_ button.

![Domain management in Amplify](https://dev-to-uploads.s3.amazonaws.com/i/u8z2ul6v4dizh4ejvxfs.png)

Note that the domain name takes a while to get set up, and you'll get an email when it's finished. Mine was ready in about 15 minutes. A glass of water and some stretches later, visit your domain name to see your deployed frontend application. 🥳

# Deploying the Backend

There are a lot of options for deploying an Express backend. I wanted to try out serverless but I found it too complex. I saw a lot of tutorials using EC2 because it's included in the free tier, but it also requires a lot more setup. I opted to use AWS Lightsail which is a lightweight version of EC2 that lets you spin up virtual servers in no time. 

Simply navigate to the Lightsail console in AWS console, and click on _Create instance_. Here, you'll need to pick an operating system for your server, a geographical region and a blueprint. The blueprint is a preconfigured image already set up to run your applications. I selected a Node.js blueprint.

![Available blueprints in Lightsail](https://dev-to-uploads.s3.amazonaws.com/i/0vgizdm9s81atsmo75ik.png)

I chose the smallest instance size, with a memory of 512MB, 1 virtual CPU and 20GB of SSD storage. All of this comes at a cost of $3.50 a month, with the first month being offered for free. After your instance is created, you'll be able to see it in the Lightsail console. 

Chances are your Express application is running on a custom port like ports 3000 or 4000. All of these ports are closed by default to protect the instance. You can open custom ports by clicking on the three vertical dots in the instance component and clicking on _Manage_ then clicking on the _Networking_ tab. In this page, you can attach a static IP address for your instance for free, so that the IP address doesn't keep changing every time the instance is rebooted or taken down and restarted. A little further down under _Firewall_, you can add a rule to open up your port to all IP addresses.

Now that the server is set up and accessible on the internet on the specified port of the attached static IP address, you can ssh into it and get your application code. Lightsail allows ssh via a browser terminal, but you can also download the private key from your account page and use your own local terminal. After logging into the server, I cloned the backend repo from GitHub, installed dependencies then reloaded the sever. I used PM2 as a process manager. Now my endpoints are accessible on the internet! 🎊 I also went back into the Route 53 to add an A record to my hosted zone to have a subdomain for my backend: https://api.example.com.

This setup means that whenever I make changes in the code, I have to ssh into the server, run `git pull` and `pm2 reload app`, which is too manual for my liking. I looked into GitHub Actions that continuously deploy to Lightsail instances but didn't find any good options. I'm fine with keeping it like this for the moment, since I know my backend repo won't have a lot of changes.  

# Connecting the two

When I was developing on my local, I had a `proxy` option added in `package.json` in my frontend. It was hardcoded to the value of my backend url, and after a bit of research it seemed that replacing the hardcoded value with an environment variable was too much of a hassle. Instead, I removed the proxy option and added the environment variable in each `fetch` request.

```js
const token = await fetch(`${process.env.REACT_APP_BACKEND_URL}/token`);
```

At this point, my frontend is live and has the URL for my backend which is also live, so they should be able to talk to each other, right? Not so fast! My frontend was being served over HTTPS while my backend was served over HTTP. The browser rightly didn't allow me to make a request from a secure domain to an insecure one. 

With bitnami, you can install an SSL certificate on your server for free with one command: `sudo /opt/bitnami/bncert-tool`. More info on that [here](https://docs.bitnami.com/aws/how-to/generate-install-lets-encrypt-ssl/). This tool also lets you enable automatic HTTP to HTTPS redirection. After this step, I could type in my server URL with an HTTP and see it get resolved into an HTTPS on the browser. 

At this point, I reloaded my frontend via the Amplify console to try to get the API requests going. In the browser's network tab, I was able to see that the request is going to the correct URL: https://api.example.com:4000/, but my request is failing without even reaching the server! On a closer look, I noticed the error shown next to the _failed_ status: `NET::ERR_SSL_PROTOCOL_ERROR`. Here I realized that I've been trying to access a custom port using the HTTPS protocol. 

I thought of a couple of options: either run my Express server on port 443 to be served over HTTPS, or somehow make my custom port be served over HTTPS.  After checking out a lot of [options](https://dev.to/dmahely/pm2-listening-on-port-443-shows-eaddrinuse-address-already-in-use-443-1aho), I was able to get it working by setting up the Apache virtual host to redirect all traffic from ports 80 and 443 to port 4000. 


Finally, my full stack application was truly up and running! 🎉 If you're curious what the application is, check out my [announcement post](https://dev.to/dmahely/introducing-artest-a-music-quiz-game-3lf).

---
Thanks for reading 👋 Until next time.
_Cover photo by [Marek Szturc](https://unsplash.com/@marxgall?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText) on Unsplash_