Deploy nodejs/react app on a single port/domain.

Momo Tidjani
4 min readFeb 23, 2021

Most of us surely know where to place the following snippet and how it works. If you don’t then you found this article at the right time 😏.

server {
listen 80;
listen [::]:80;

root /var/www/example/html;
index index.html;

server_name example.com www.example.com;
location /api/ {
proxy_pass http://127.0.0.1:4000/;
}
location / {
try_files $uri $uri/ =404;
}

}

If you guessed it is a nginx config code then you are really concerned but worry no more because after reading this article you won’t need that anymore (for react/node apps).

Deploying node/react on a single domain is a great challenge and many solutions (even the one above) can solve this issue. In a few steps I’ll present to you the best way (just my opinion) of solving this problem. If you have any suggestions, kindly put them in comments.

Code

You can go directly to this git repository you will find a Typescript and a JavaScript version in sub folders respectively. I will strongly recommend you to read at least the second and third paragraphs if you pulled the code directly..

The solution to this issue is quite straight. Let’s jump directly into it.

Create the apps

For sure we need a react app and a node app. For this we’ll use the official create react app. It is necessary you keep your apps in the same base directory.

npx create-react-app client
# or
yarn create react-app client

Done with react app lets spin up a basic node server with minimal express code.

mkdir server && cd server

server/package.json

{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "nodemon src/app.js"
},
"dependencies": {
"express": "4.17.1"
},
"devDependencies": {
"nodemon": "1.18.4"
},
"keywords": []
}

server/src/app.js

const express = require(“express”);
const app = express();
const PORT = process.env.PORT || 4000;app.get(“/”, function(req, res) {
res.send(“Hello World”);
});
app.listen(PORT, () => {
console.log(`Listening on PORT ${PORT}`);
});

Check file structure and startscript.sh

Check your file structure and add startscript.sh in the base directory as below.

startscript.sh

#!/usr/bin/env bash
cd ./server
#yarn ## Need to to install dependencies on first lunch
rm -r ./clientbuild || true #remove previous build file
cd ../client
#yarn ## Need to to install dependencies on first lunch
yarn run build
mv ./build ../server/clientbuild
cd ../server
npm start

The build script build client (production) and moves the resulting “build” folder into the server folder. It then builds the server(typescript) and starts it.

At this point you should have your app running on http://localhost:4000 this should print our “Hello world message”.

Redirect client request (static files and routes) to the client.

This is where the magic operates. We need to send react it requests (static files and routes)and to nodejs it own (REST routes and others).

Update server/src/app.js as below

const express = require("express");
const path = require('path');
const app = express();
const PORT = process.env.PORT || 4000;app.get("/api/", function(req, res) {
res.send("Hello World");
});
app.use(express.static(path.join(__dirname, '../clientbuild')));
app.get("*", function(req, res) {
return res.sendFile(path.resolve(__dirname, '../clientbuild', 'index.html'));
});
app.listen(PORT, () => {
console.log(`Listening on PORT ${PORT}`);
});

What are we doing?

  • Redirect static files
app.use(express.static(path.join(__dirname, '../clientbuild')));

this line serves static files(html, js, images…) from clientbuild folder.

  • Point all get requests (ones left out by server) to react index.html
app.get("*", function(req, res) {
return res.sendFile(path.resolve(__dirname, '../clientbuild', 'index.html'));
});

Next

Give sufficient rights to startscript.sh then start it in terminal (./startscript.sh)

If everything went well, at http://localhost:4000 you should see a nice react app running. you should have the following in you server folder.

Dockerfile and docker-compose.yml files (Optional).

If you don’t plan to deploy your app with docker then you can move to the next section.

Nice! you didn’t jump to the next section. create two files at the base folder (near startscript.sh). Name them Dockerfile and docker-compose.yml. Since this is not a docker article I will assume you continue reading here because you are familiar with docker.

put the following code in:

Dockerfile

FROM node:12.2.0-alpine
USER root
WORKDIR /app
RUN npm i -g pm2## Install client dependencies
WORKDIR /app/server/
COPY ./server/package*.json ./
RUN npm i
# Build server
COPY ./server .
## Install client dependencies
WORKDIR /app/client/
COPY ./client/package*.json ./
RUN npm i
# Build client
COPY ./client .
RUN npm run build
RUN mv ./build ../server/clientbuild
## Run app
WORKDIR /app/server/
EXPOSE 4000
CMD ["pm2-runtime", "--port", "4000", "src/app.js"]

Note the build steps similar to startscript.sh. You smartly guess that this is a transcription of startscript.sh into Dockerfile markup.

docker-compose.yml

version: "3"
services:
app:
image: medium_frontend_image
container_name: medium_frontend
build:
context: .
dockerfile: Dockerfile
ports:
- "4000:4000"
volumes:
- /app/node_modules

Needless adding further explanation, note the call on Dockerfile at line 10.

Deploy

Move your project into your server and either

$ ./startscript.sh
or
$ docker-compose up -d

And you got your react/node app running on one port (4000). You can now add the necessary config (nginx, apache or any other) to proxy your domain name to http://localhost:4000.

Word of caution!

Don’t use this if your front-end and backend have concurrent routes. This applies mostly to get routes as the client is not concerned with other verbs. Remember server routes will have priority over client.

It is recommended to follow the “/api/**” routes path for server routes to separate client/server routes to minimize route conflicts.

--

--