Django and Create-React-App together on Heroku


Django and Create-React-App together on Heroku

2019/01/25

Tags: Django React Heroku

React is awesome. Django is great. And together, they can provide a clean separation of frontend and backend concerns. An ideal way to host React app is to serve it over a CDN like CloudFront and have it make API calls to the backend API, possibly on a different subdomain. For small apps (in terms of scope and usage), this is an overkill. And if you’re using Heroku on top of that, it doesn’t make sense, financial-wise, to have two separate paid apps for backend and frontend. Following tutorial will make Django serve its usual URLs and have it also serve React builds with GZIP compression.

Assumptions

Final Result

Development Workflow

Production Features

Project Setup

Let’s start by creating the project directory and changing to it:

mkdir datasets 
cd datasets/

Django Setup

Virtualenv

Create a virtualenv and source it:

virtualenv -p python3 venv
source venv/bin/activate

Edit the above command if you need to use Python 2. See Virtualenv -p reference.

Django

Install Django and some required dependencies which we’ll use later on:

pip install Django whitenoise gunicorn

Save requirements file:

pip freeze > requirements.txt

You should run the above command again when you add a Python package using pip install so that the requirement file will be in sync with your local virtual environment.

Let’s create a Django project in the current directory and a new app called data:

django-admin startproject datasets .
./manage.py startapp data

The last . makes django-admin not create a new subdirectory as project root, which it does normally.

Add the created app to settings.py:

INSTALLED_APPS = [
    ....other apps....
    'data',
]

# Ideally, this should be the domains that you'll be running the app for
# for the purpose of this tutorial, let's set this to catch-all value
ALLOWED_HOSTS = ['*']

Static Files Configuration

Add Whitenoise middleware to settings.py:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    ....other middlewares....
]

And set the following configuration for static files:

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'


REACT_APP_DIR = os.path.join(BASE_DIR, 'frontend')

STATICFILES_DIRS = [
    os.path.join(REACT_APP_DIR, 'build', 'static'),
]

Procfile

Create a file called Procfile with the following contents:

web: gunicorn datasets.wsgi

Git

We’ll be using Git for version control and Heroku deploys. Let’s create a .gitignore file:

*.log
*.pot
*.pyc
.env
.env.staging
__pycache__/
local_settings.py
db.sqlite3
media

# don't track virtualenv, ever!
venv/

# don't track node_modules because we'll be using yarn install and it will create this
node_modules

# On our project configuration, the output of the collectstatic will be stored in 
# staticfiles directory. We are ignoring files inside staticfiles but we'll track this # folder itself so that collectstatic can work on Heroku
staticfiles/*

Now, initialize the Git itself and create our first commit:

git init
git add .

The staticfiles configuration in this project will make collectstatic command copy all static files to staticfiles. This directory should exist beforehand. As Heroku docs say,

Django won’t automatically create the target directory (STATIC_ROOT) that collectstatic uses, if it isn’t available. You may need to create this directory in your codebase, so it will be available when collectstatic is run. Git does not support empty file directories, so you will have to create a file inside that directory as well.

But, we shouldn’t track this folder in Git as it can be easily regenerated, and it is not tracked as it is in .gitignore. To fix this with Git, we create a .gitkeep file inside the staticfiles folder, and add it with -f as:

mkdir staticfiles/
touch staticfiles/.gitkeep
git add -f staticfiles/.gitkeep

Now, finally create the first commit:

git commit -m "initial commit"

React setup

Let’s create a React app in frontend/ folder:

create-react-app frontend

and add this to git as well:

git add .
git commit -m "Add React"

On the package.json configuration of React, add the following:

  "proxy": "http://localhost:8000"

This will make Create-React-App’s development server proxy requests to our Django app. So, on our code, we could do something like:

Axios.get('/api/datasets/')

and it will proxy the request to http://localhost:8000/api/datasets. Very useful for development.

Serving React build from Django

Since we have the frontend/build/static folder already on the static files configuration, Django will serve them if it exists. One remaining piece of configuration is serving React’s index.htmlon the root of the application. To do so, let’s create a view:

import os
import logging
from django.http import HttpResponse
from django.views.generic import View
from django.conf import settings


class FrontendAppView(View):
    """
    Serves the compiled frontend entry point (only works if you have run `yarn
    build`).
    """
    index_file_path = os.path.join(settings.REACT_APP_DIR, 'build', 'index.html')

    def get(self, request):
        try:
            with open(self.index_file_path) as f:
                return HttpResponse(f.read())
        except FileNotFoundError:
            logging.exception('Production build of app not found')
            return HttpResponse(
                """
                This URL is only used when you have built the production
                version of the app. Visit http://localhost:3000/ instead after
                running `yarn start` on the frontend/ directory
                """,
                status=501,
            )

And, a URL at root to serve this:

from django.contrib import admin
from django.urls import path, include, re_path
from data.views import FrontendAppView

urlpatterns = [
    path('admin/', admin.site.urls),
	.... other urlpatterns.....
	# have it as the last urlpattern for BrowserHistory urls to work
    re_path(r'^', views.FrontendAppView.as_view()),
]

Heroku Deployment

package.json

Since we’ll be using heroku/nodejsbuildpack as well, it expects a package.json file in the root directory. We can utilize it to specify commands to build the React app and even ask it to cache node_modules inside the frontend/folder so that subsequent React builds are faster.

Create the file:

touch package.json

and add the following contents:

{
  "name": "datasets",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "",
  "author": "Your Name",
  "license": "MIT",
  "private": true,
  "scripts": {
    "heroku-prebuild": "NODE_ENV=production cd frontend/ && yarn install && yarn build && cd .."
  },
  "cacheDirectories": [
    "frontend/node_modules"
  ]
}

yarn.lock

Create an empty yarn.lock file so that the Heroku will also make yarn available when using heroku/nodejs buildpack:

touch yarn.lock

Now, let’s try deployment to Heroku by creating an app and setting multiple buildpacks:

heroku apps:create -a datasets
heroku buildpacks:set heroku/python
heroku buildpacks:add --index 1 heroku/nodejs

git push heroku master

The Heroku app creation command automatically adds a git remote as heroku. The project name datasets is not available. So, you should come up with something catchy.

With the --index flag, we add heroku/nodejs as the first buildpack so that the React build is done before the Python buildpack runs collectstatic.

Once deployed, open heroku open and you should see your React app available on root path.

Many thanks to a blog post by Gavin at FusionBox for the idea which I expanded upon for Heroku.