Stitching together vanilla React and Next.js with NGINX

This post was sparked by a question I saw on Indie Hackers.

It was extremely similar to an issue I had run into myself, so I thought I'd write something up about how I resolved it.

I was pretty deep into prototyping my application https://strands.io/ when I realized that SEO (Search Engine Optimization) would soon become a problem. I had user pages that I wanted to allow customization of: meta tags, title, description and potentially more down the road. I also needed any content I published, for example blog posts, to be SEO friendly.

After doing a bit of research, I came across Next.js, a framework for server-side rendered React apps. I skimmed some documentation and quickly realized this would solve all of my problems.

There was one issue: I would need to port and potentially rewrite large chunks of my code.

As mentioned, I was just prototyping, so I didn't want to spend much time doing all of that. Instead, I decided to create a Next.js application that would handle all of the SEO content separate from the base application I had.

The next question was how to piece these two together into one seamless application.

The answer is NGINX. I will not claim to be an expert on NGINX but I have quickly learned that it can do just about anything.

A couple of notes before we start on what this post will and will not go over:

It will cover:

  • How to create a basic client side application with routing
  • How to create a basic server side react application with Next.js
  • How to deploy your applications and configure NGINX to run both side by side

It will not cover:

You can follow along with everything below and grab all of the code here: https://github.com/fauc3t/react-vanilla-next-side-by-side

Starting off

First, let's create our base directory and enter it:

mkdir react-vanilla-next-side-by-side
cd react-vanilla-next-side-by-side

Next we'll create our client side ('vanilla') React application:

npx create-react-app client-side

And the server side application:

npx create-next-app server-side

Client side

This is a brief overview of what I want to do in the client-side application:

  • Create a Page component
  • Use React router to add a few different page routes

It's not much but it will be more than enough to do what we need for this blog post.

Under the src directory, let's create a directory for components:

cd ./client-side
mkdir ./src/components

In your favorite text editor, create a new component for a Page:

touch ./src/components/page.js

The Page component will be very simple. We just want it to display a title that is passed in through props:

import React from 'react';

function Page(props) {
    return (
        <h2>{props.title}</h2>
    );
}

export default Page;

Now open ./src/App.js. The only code we'll keep is:

import React from 'react';

function App() {
  return (

  );
}

export default App;

Now we'll need our Router. Run the following command in the base client-side directory:

npm install react-router-dom

Now that it's available to our project, add the following import:

import { BrowserRouter, Route, Switch } from 'react-router-dom';

Next we'll add a few routes to our application. I've decided to add the following:

  • /
  • /login
  • /signup
  • Catch all for pages not found

We'll need our Page component, so import it with:

import Page from './components/page';

Add the following code to our App return statement:

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Page from './components/page';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/" render={props => <Page title="HOME" />} />
        <Route exact path="/login" render={props => <Page title="LOGIN" />} />
        <Route exact path="/signup" render={props => <Page title="SIGNUP" />} />
        <Route render={props => <h2>Page not found</h2>} />
      </Switch>
    </BrowserRouter>
  );
}

export default App;

Now let's check to see if everything works as expected. In the client-side base directory run:

npm run start

Our application will run and default to the Page displaying HOME.

If you navigate to the other routes we added, you will also be able to see the specified titles:

  • http://localhost:3000/signup
  • http://localhost:3002/login

That's all for the client-side aspect!

Server side

Now let's enter the server-side base directory:

cd ../server-side

First, let's delete the index page as we want the application to land on the home page of the client-side app:

rm ./pages/index.js

All we are going to do in this application is add two sample pages:

about

touch ./pages/about.js
import React from 'react';
import Head from 'next/head'

const title = 'Sample About';
const description = 'Testing side by side';

function About() {
  return (
    <div>
        <Head>
            <title>{title}</title>
            <meta name="description" content={description} />
            <meta itemprop="name" content={title} />
            <meta itemprop="description" content={description} />
        </Head>
        <h2>About</h2>
    </div>
  )
}

export default About;

sample-blog-post

touch ./pages/sample-blog-post.js
import React from 'react';
import Head from 'next/head'

const title = 'Blog Post 1';
const description = 'Testing side by side';

function BlogPost() {
  return (
    <div>
        <Head>
            <title>{title}</title>
            <meta name="description" content={description} />
            <meta itemprop="name" content={title} />
            <meta itemprop="description" content={description} />
        </Head>
        <h2>Blog Post 1</h2>
    </div>
  )
}

export default BlogPost;

That's all! Now let's run our application:

npm run dev

If everything is correct, you will see a 404 on the default route. If you navigate to /about and /sample-blog-post, however, you will see the corresponding titles.

Cool. Now we're done with building our 'applications'.

Let's deploy them.

Note on what follows:

For the purpose of this post, I will be deploying to a Digital Ocean droplet. It doesn't really matter what kind of environment you are deploying to as long as it has the capability to run Node and NGINX.

I will NOT be going over how to get these installed running so if you need further help, see the documentation I posted above and never hesitate to reach out with questions :)

If you don't have a Digital Ocean account you can sign up here.
I am using a basic $5 instance with Ubuntu 18.04.3 (LTS) x64.

Deployment

First, ssh into your new host.

Now, let's go to the directory where we will deploy the applications. It can really be anywhere but I usually head to:

cd /var/www

Next, clone the applications and enter the directory:

git clone https://github.com/fauc3t/react-vanilla-next-side-by-side
cd react-vanilla-next-side-by-side

Install and build both of the applications:

cd ./client-side
npm install
npm run build

Note the build directory: /var/www/react-vanilla-next-side-by-side/client-side/build

cd ../server-side
npm install
npm run build

We will be running the Next.js application with PM2, the Node process manager.

So let's install that globally:

npm install pm2 -g

Now we can start the application:

pm2 start "npm run start" --name nextjs

This starts our server on the default port of 3000. If you'd like to change the port you can update the start script in package.json to add the port option:

"start": "next start -p 3001"

Just be sure to keep this in mind with the NGINX configuration below.

NGINX

We're almost there. The last aspect of this is to configure NGINX to stitch these two applications together.

Let's go to the configuration located at:

cd /etc/nginx/sites-enabled/default

We will be replacing the default configuration with the following code. I've made some notes in the comments to try and help clarify what each value does:

server {
    # The root build location for the client-side application
    root /var/www/react-vanilla-next-side-by-side/client-side/build;

    # Next.js static content location
    location /_next/static {
        proxy_pass http://127.0.0.1:3000;
    }

    # 
    location / {
        # proxy this request to the server-side application running on port 3000
		proxy_pass http://127.0.0.1:3000;

        # Intercept proxied response errors
        # http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_intercept_errors
        proxy_intercept_errors on;

        # Enables multiple redirects with error_page directive
        # http://nginx.org/en/docs/http/ngx_http_core_module.html#recursive_error_pages
        recursive_error_pages on;

        # Defines a location when for the error(s) specified (404 in our case)
        # http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page
        error_page 404 = @fallback;
	}

    # Fallback to the client-side application
    location @fallback {
        # Passes the request with $uri to the client-side application
        try_files $uri /index.html;        
    }

    listen 80 default_server;
    listen [::]:80 default_server;
}

This is a very sparse configuration but it does the job for our purpose. Some other things you may wish to add for your NGINX configuration are:

  • Proxy caching for static content
  • Proxy headers
  • Http version
  • Timeouts

Another thing I'd like to point out is the importance of application ordering. We must try the server-side application first because it will respond with a legitimate HTTP 404 response. If you switched the order, the vanilla react application will still respond with an HTTP 200 even if the route has not been specified.

The last thing to do is restart NGINX:

sudo systemctl restart nginx

If all is working as expected, you can now visit all 5 of the routes of your application and receive Page not found if you go to an unspecified route.

Questions

Thank you for taking the time to read. I hope this was helpful. Don't hesitate to reach out with any questions.

If you’re interested in more you can follow me on Twitter.

This post was also published on Medium.

Show Comments