Using your own server with Gatsby, ExpressJS and NGINX with a reverse proxy

Gatsby offers high page score and favors great SEO. It becomes an excellent option to build static sites. But is that all?

Not really. You can also build pages that are able to collect user data. There are more than a few popular solutions out there, some of them don’t even require you to code anything.

But what if you don’t mind coding, you have your own server or you are one of those types that just build all by yourself? (Who? Me!!?)

This can be useful to build a comment section for your blog, to build a survey or just to save email addresses.

Can you have your Gatsby static site talking to your own server?

Yes, you can! Here is a solution that uses a reverse-proxy setup in NGINX and a minimum ExpressJS server to supercharge your static page.

This article contains affiliate links. If you click on it and make a purchase, I earn a small commission without any extra cost for you.


  1. What you need

  2. Setting-up Gatsby

  3. Form component

  4. Express server

    4.1. Project structure

    4.2. Create server

    4.3. Test in development

  5. Production

    5.1. Frontend server block

    5.2. Backend server block

  6. Final test

  7. Repositories

What you need

  • Server you can ssh to.

This tutorial is about deploying and application in production. There are many different ways to enable a reverse proxy server. Here you will see it done in a virtual machine with Ubuntu, NodeJS and NGINX.

Cloud hosting

When it comes to hosting websites there are multiple options out there, but each one comes with trade-offs. For me, price is important, but also the freedom to configure my servers in any way I see fit.

Digital Ocean satisfies those conditions. Initially I was worried about having to spend too much time setting it up, but the tutorials and the help of their community is remarkable.

Heroku is another hoster with traditionally good benefit over cost, but they configure everything for you. Some see that as an advantage, but it becomes problematic as soon as the project grows. I know from experience.

The link above contains a discount at Digital Ocean. It grants $100 of credit over 60 days towards any of their plans.

Setting-up Gatsby

Start by installing the Gatsby CLI, if you haven’t already. Create a new site.

npm install -g gatsby-cli
gatsby new gatsby-plus-backend
cd gatsby-plus-backend
gatsby develop

Let’s build a small form to collect some user input.

Form component

Stop the build server (Ctrl-C again) and bring back the develop version (gatsby develop).

Create a file in the components folder with the following content:

import React, { useState } from "react"

const Form = () => {
  const [title, setTitle] = useState('')
  const [excerpt, setExcerpt] = useState('')
  const [author, setAuthor] = useState('')

  const handleSubmit = async e => {

    try {
      let payload = {
        poem: title,
        excerpt: excerpt,
        author: author

      const response = await fetch('/api/save-poem/', {
        method: "POST",
        cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
        credentials: "omit", // include, *same-origin, omit
        headers: { "Content-Type": "application/json" },
        redirect: "follow", // manual, *folslow, error
        referrer: "client", // no-referrer, *client
        body: JSON.stringify(payload),

      const answer = await response.json()

      if (answer.success) {
      else {
    catch (err) {
      alert('Error connecting to backend:', err)

  const handleTitleChange = e => {

  const handleExcerptChange = e => {

  const handleAuthorChange = e => {

  return (
    <div className='backend'>
      <form onSubmit={handleSubmit} >
        <input placeholder='Poem title' id='title' onChange={handleTitleChange} />
        <input placeholder='Excerpt' id='excerpt' onChange={handleExcerptChange} />
        <input placeholder='Poem author' id='author' onChange={handleAuthorChange} />

        <button type='submit'>Send</button>

export default Form

This is a small form with two input fields and a submit button. The fields are used to collect title and author of a poem.

Text is stored in the state as it is typed in the input fields. The send button sends the stored values to the save-poem endpoint, which you will create later.

All built with React hooks. Isn’t that beautiful?

Next, import your new component to the home page.

import React from "react"
import { Link } from "gatsby"

import Layout from "../components/layout"
import Image from "../components/image"
import SEO from "../components/seo"
import Form from "../components/form" // highlight-line

And render it:

const IndexPage = () => (
    <SEO title="Home" />
    <h1>Hi people</h1>
    <p>Welcome to your new Gatsby site.</p>
    <p>Now go build something great.</p>
    <div style={{ maxWidth: `300px`, marginBottom: `1.45rem` }}>
      <Image />
    <Form /> // highlight-line
    <Link to="/page-2/">Go to page 2</Link>

export default IndexPage

Now we need a server to receive that call.

Express server

You will create a simple server with ExpressJS.

Project structure

You have a choice to make.

It is possible to create the server inside the folder of your Gatsby project, but I don’t recommend it because when you deploy the server you will have to run npm install in production, which your Gatsby project doesn’t need.

With Gatsby you can install all packages in your developer machine, run gatsby build and send the resulting “public” folder to production.

And that is a good alternative because installing packages is a heavy task. If you can do it in your developer machine, then you may be able to save some money by acquiring a server that is not so powerful.

So, keeping your Express server in a separate folder will allow you to keep building your Gatsby site in your dev environment. And that is a major time - and money - saver.

Create server

Move out of your Gatsby project folder to create the server:

mkdir myserver
cd myserver
npm init

Follow the prompts to configure your project. Set the entry point to server.js. Change the other default options if you want.

Install dependencies:

npm install express axios express-validator --save

Here you have ExpressJS, Axios to better handle requests, and Express-validator to give you some protection from ill intent.

The validator is not necessary nor is the objective of this tutorial, but handling user input is such a serious issue that I included it here.

Install also Nodemon to keep the server running in your development machine. Nodemon is great because it automatically restarts the server when you change something, saving some time.

Nodemon is a global dependency, so:

npm i -g nodemon

Create a very simple server:

const express     = require('express')
const app         = express()
const port        = 3000
const bodyparser  = require('body-parser')
const path        = require('path')

// Middleware
const	apiRoutes   = require('./middleware/api.js')

// Bodyparser
app.use(bodyparser.urlencoded({ extended: true }))

// Routing
app.use('/api', apiRoutes)

app.listen(port, () => console.log(`Server listening on port ${port}`))

The apiRoutes in the middleware section is where you will write you endpoints. So create it:

const express           = require('express')
const router            = express.Router()
const axiosjs           = require('axios')
const https             = require('https')
const { body }          = require('express-validator')
const { sanitizeBody }  = require('express-validator')

const axios = axiosjs.create({
  headers: {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*'
})'/save-poem', [
], (req, res) => {
  console.log('New poem', JSON.stringify(req.body, 0, 2))

  return res.status(200).json({
    success: true,
    message: 'Poem saved'

module.exports = router

With this code you are using an Express router to capture the post requests.

Axios handles the HTTP part. The validator is configured with the notEmpty, trim, and escape rules to clean up malicious text.

Then you output the request to the terminal and return a success message to the frontend.

In a real application, this is where you would save the data to a database.

For the purpose of checking your progress, add a GET route right before the /save-poem endpoint:

const axios = axiosjs.create({
  headers: {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*'

router.get('/welcome', (req, res) => { // highlight-line
  return res.status(200).json({ // highlight-line
    success: true, // highlight-line
    message: 'Welcome to my api' // highlight-line
  }) // highlight-line
}) // highlight-line'/save-poem', [

Time to test your client / server setup in development environment.

Test in development

Navigate to the root of your server project and run it with Nodemon:

cd myserver
nodemon server.js

It should say it is listening in port 3000.

Check it: open a browser and navigate to localhost:3000/api/welcome. You should see the json object we created above for the welcome route.

{"success":true,"message":"Welcome to my api"}

Great. Now, as it is, Gatsby will address all API requests to static assets, which is what we need in production.

But to test the routes in development mode, we need to proxy the calls to the port we just opened with Nodemon.

Don’t worry, Gatsby has you covered with developmentMiddleware. Add this to your /gatsby-config file:

var proxy = require("http-proxy-middleware") // highlight-line

module.exports = {
  siteMetadata: {
    title: `Gatsby Default Starter`,
    description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
    author: `@gatsbyjs`,
	developMiddleware: app => { // highlight-line
    app.use( // highlight-line
      "/api/", // highlight-line
      proxy({ // highlight-line
        target: "http://localhost:3000" // highlight-line
      }) // highlight-line
    ) // highlight-line
  }, // highlight-line
  plugins: [

From the root of your Gatsby project start it in development mode:

gatsby develop

Go to your browser and navigate to localhost:8000. Add the following text to your input fields:

Poem title: The more loving one
Excerpt: If equal affection cannot be, let the more loving one be me
Poem author: W. H. Auden

Click on send and you should see an alert message attesting the poem was saved.

If you check the terminal window where you have Nodemon running, there too you should see a message with the text you added to your input fields.

Hooray! It works!

Now let’s deploy it.


This is but one way of deploying and application like this. In this example you will use NGINX on Ubuntu.

The installation of NGINX on Linux is not the objective of this tutorial. You can find detailed instructions in sites like Digital Ocean.

The key factor is the reverse proxy configuration that is responsible for serving frontend, backend, and allowing communication between them.

For this setup, build your Gastby site on your development machine. As discussed above, this is a great way to save resources on you production server. And then commit changes to your repository. So:

cd gatsby-plus-backend
gatsby build
git add .
git commit -m 'Ready for deployment'
git push

This way your repository will contain root files and two folders: “src” and “public”. The “public” folder is all you need to serve your frontend in production, so you could choose to commit only that.

For this tutorial and for simplicity, everything is going.

Now ssh into your server and create folders for your backend server and for your Gatsby frontend.

mkdir apps/frontend
mkdir apps/backend

Transfer your apps into these folders with Git.

Next create a link between your frontend public folder and the folder that NGINX uses to serve your site. This way, you don’t have to move files around every time you commit changes. Just pull with Git and you are good to go.

sudo ln /home/user/apps/frontend/public /var/www/

Mind you that user and will be different for you.

The backend can already be put to work. Start it with PM2:

pm2 start server.js --log-date-format="YYYY-MM-DD HH:mm Z"

Check if the server was properly started:

pm2 logs

If there are errors, type ctrl-c to stop the logs and do:

pm2 ls
pm2 stop server

Now move on to the incantations necessary to create the server blocks for NGINX, starting with the frontend.

Frontend server block

The example below serves with Certbot SSL certificates. If you need a hand setting those up, check out this tutorial.

server {
  if ($host = {
    return 301 https://$host$request_uri;
  } # managed by Certbot

  if ($host = {
    return 301 https://$host$request_uri;
  } # managed by Certbot

  listen 80;
  listen [::]:80;

  return 404; # managed by Certbot

server {
  listen [::]:443 ssl; # managed by Certbot
  listen 443 ssl; # managed by Certbot

  ssl on;
  ssl_certificate       /etc/letsencrypt/live/; # managed by Certbot
  ssl_certificate_key   /etc/letsencrypt/live/; # managed by Certbot
  include               /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  ssl_dhparam           /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /etc/letsencrypt/live/;

  root /var/www/;
  index index.html index.htm index.nginx-debian.html;

  add_header Strict-Transport-Security max-age=500;

  location / {
    # First attempt to server request as file, then as directory, then fallback to 404.
    try_files $uri $uri/ =404;

  if ($scheme != "https") {
    return 301 https://$host$request_uri;
  } # managed by Certbot

As before, replace “” with your URL.

This returns 301 for the port 80. For HTTPS, on port 443, it uses your SSL certificate and serves the contents of the folder.

At the bottom, the location / bracket is the way you serve a static site with NGINX.

Now to the backend server block.

Backend server block

Still on the same file, add a new location entrance:

  location / {
    # First attempt to server request as file, then as directory, then fallback to 404.
    try_files $uri $uri/ =404;

  location /api {
    proxy_set_header 'Access-Control-Allow-Origin' '';
    proxy_set_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
    proxy_set_header 'Access-Control-Allow-Headers' 'X-Requested-With,Accept,Content-Type,Origin';
    proxy_redirect off;
    proxy_buffering on;
    proxy_set_header	Host						$host;
    proxy_set_header	X-Real-IP				$remote_addr;
    proxy_set_header	X-Forwarded-For	$proxy_add_x_forwarded_for;
    proxy_set_header	origin					`';

  if ($scheme != "https") {
    return 301 https://$host$request_uri;
  } # managed by Certbot

This block allows calls to your website /api URL to find your local server on port 3000. Neat!

Restart NGINX

sudo systemctl restart nginx
sudo systemctl status nginx

Final test

Open a browser and visit your production URL.

Add a poem:

Poem title: To a mouse

Excerpt: The best laid schemes of mice and men go often askew

Poem author: Robert Burns

If all is good, you should see the confirmation alert. Congratulations! That was quite a feat.

You may also enjoy my other tutorial about migrating a blog from Wordpress to GatsbyJS.


Here is the repo for the Gatsby website: Gatsby-Express-NGINX

And this is the minimal Express server: Minimal-Gatsby-Backend

Questions? Let me know in the comments section.

November 12, 2019.