Skip to content

Instantly share code, notes, and snippets.

@izmailoff
Forked from genomics-geek/README.md
Created January 22, 2020 03:45
Show Gist options
  • Save izmailoff/b4519ccc51841452b8d3d63562046280 to your computer and use it in GitHub Desktop.
Save izmailoff/b4519ccc51841452b8d3d63562046280 to your computer and use it in GitHub Desktop.
Setting up a Dockerized web application with Django REST APIs, ReactJS with Redux pattern, and Webpack Hot Reloading! Mouthful.

Guide on how to create and set up a Dockerized web app using Django REST APIs and ReactJS

Hopefully this will answer "How do I setup or start a Django project using REST Framework and ReactJS?"

This is a guide to show you step by step how this can be setup. If you just want to get started, use the cookiecuter I set up cookiecutter-django-reactjs. It basically is a fork of pydanny's cookiecutter, just added the front-end stuff :).

I created this because it was SUCH a pain in the ass setting up a project using all the latest technologies. After some research, I figured it out and have it working. The repo that implements this is located here. Feel free to use it as a boilerplate ;)

Main features:

  • Django REST APIs
  • ReactJS with Redux Pattern
  • Webpack module bundler manager
  • Hot Reloading of ReactJS components for quicker development
  • Dockerized

NOTE: This guide was built using:

  • Django 1.10.2
  • Django REST Framework 3.4.7
  • NodeJS 6.7.0 with NPM 3.10.7
  • ReactJS 15.3.2
  • Redux 3.6.0
  • Webpack 1.13.2
  • Karma 1.3.0
  • Mocha 3.0.2

Setting up your machine

On MacOSX

Set up bash profile and module loading

Look here for examples.

Install Git

https://sourceforge.net/projects/git-osx-installer/files/

git config --global user.name "[Full Name]"
git config --global user.email "[Email]"

Install Homebrew

Follow this guide for help!

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

brew doctor
brew tap homebrew/python
brew update
brew upgrade

Install Brew packages

brew install wget
brew install pyenv
brew install pyenv-virtualenv
brew install editorconfig
brew install nvm
brew install node
brew install nginx
brew install htop

Make Node directory

mkdir $HOME/.nvm

Create your ssh key

ssh-keygen -t rsa -C "[label]"

Install Python3

pyenv install 3.5.1
pyenv global 3.5.1
gpip install --upgrade pip setuptools wheel virtualenv

Setup version control with Git

Initialize git repository

git init

Create a GitHub or GitLab repository

I recommend using GitHub or GitLab for version control. Once you have an account, create a repository so you can push all your code.

Push your code to your online repository

git add .
git commit -m "first commit"
git remote add origin https://github.com/[fill-in-here].git
git push -u origin master

Create dot files for project

EditorConfig

EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs. This file will initialize the coding style for this project.

EditorConfig settings based on:

Example .editorconfig file

root = true

# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

# Python settings
[*.py]
line_length=120
indent_style = space
indent_size = 4

# JavaScript, JSX, CSS, SCSS, HTML settings
[*.{js,jsx,css,scss,html}]
indent_style = space
indent_size = 2

# Markdown settings
[*.md]
trim_trailing_whitespace = false

# Config files
[*.conf]
indent_style = space
indent_size = 2

Flake8

Flake8 is a is a wrapper around these tools: PyFlakes. pep8. It is a linting utility for Python. When used together with Atom, Atom Linter Plugin and Atom Flake8 Plugin, this will mark source code that doesn't conform to settings in .flake8 file.

Example .flake8 file:

[flake8]
max-line-length = 125

ESLint

The pluggable linting utility (tool to verify code quality) for JavaScript and JSX. When used together with Atom, Atom Linter Plugin and Atom ESLint Plugin, this will mark mark source code that doesn't conform to settings in .eslintrc file.

ESLint settings based on:

Example .eslintrc file:

{
  "extends": "airbnb",
  "ecmaFeatures": {
    "jsx": true,
    "modules": true
  },
  "env": {
    "browser": true,
    "es6": true,
    "node": true
  },
  "parser": "babel-eslint",
  "rules": {
    "react/jsx-uses-react": 2,
    "react/jsx-uses-vars": 2,
    "react/react-in-jsx-scope": 2,
    "no-var": 0,
    "vars-on-top": 0,
    "comma-dangle": 0,
    "arrow-body-style": ["error", "as-needed"],
    "import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
    "max-len": ["error", { "comments": 120, "code": 120 }],
    "no-console": ["error", { "allow": ["warn", "error"] }],
    "no-unused-expressions": ["error", { "allowShortCircuit": true }]
  },
  "plugins": [
    "react"
  ]
}

GitIgnore settings

This will tell Git which files to ignore for version control.

Example .gitignore file:

# virtual environments
venv

# Python files
*.pyc
__pycache__

# Python Notebook dot files
.ipynb*

# Secret environment settings
.env
env

# Node and Webpack files
node_modules
npm-debug.log
webpack-stats*

# Static assests
staticfiles
static_cdn
media_cdn

# Ignore sqlite
db.sqlite3

# Apple files
.DS_Store

DockerIgnore settings

This will tell Docker what files to ignore.

Example .dockerignore file:

# virtual environments
venv

# Python files
*.pyc
__pycache__

# Python Notebook dot files
.ipynb*

# Secret environment settings
.env
env

# Node and Webpack files
node_modules
npm-debug.log
webpack-stats*

# Static assests
staticfiles
static_cdn
media_cdn

# Ignore sqlite
db.sqlite3

# Apple files
.DS_Store

Set up PostgreSQL database

On MacOSX

Install Postgres

On MacOSX we can use the Postgress App. Just download and move the app into your Applications folder. Follow the instructions, its the easiest way I have seen to set up PostgresSQL!

Create an optimized user for our Django projects

Login to the database from the terminal using:

psql
CREATE USER django WITH PASSWORD '[password]';
ALTER ROLE django SET client_encoding TO 'utf8';
ALTER ROLE django SET default_transaction_isolation TO 'read committed';
ALTER ROLE django SET timezone TO 'UTC';
ALTER USER django CREATEDB;

Create a database for this specific Django project

Login to the database from the terminal using:

psql
CREATE DATABASE [project-name];
GRANT ALL PRIVILEGES ON DATABASE [project-name] TO django;

Create a Virtual Environment

It’s common practice to use a virtual environment for your Python projects in order to create self-contained development environments. The goal of pyenv is to prevent different versions of Python and libraries/packages from messing with each other. It’s like an isolated, soundproof room within your home where you can scream as loud as you want, about anything you want, and nobody else outside that room can hear it.

Use pyenv-virtualenv to create a virtual environment for your project:

pyenv virtualenv [project-name]
pyenv activate [project-name]

Create environment variables file - .env

This file will serve as environment variables that will serve our project. Django and Docker mostly will use this file.

Here is an example of .env file:

SECRET_KEY=[secret-key]
DEBUG=true
DJANGO_SETTINGS_MODULE=django_config.settings.local
ALLOWED_HOSTS=localhost 127.0.0.1 0.0.0.0
DATABASE_URL=postgres://django:[password]@localhost:5432/[project-name]

MAILGUN_API_KEY=[mailgun-api-key]
MAILGUN_DEFAULT_FROM_EMAIL=[email]

POSTGRES_PASSWORD=[password]
POSTGRES_USER=django
POSTGRES_DB=[project-name]

EMAIL_PORT=1025
EMAIL_HOST=localhost
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
DEFAULT_FROM_EMAIL=[email]

Create Python requirements and install local dependencies

Python requirements folder

mkdir requirements

Base Python requirements - ./requirements/base.txt

# Django
django==1.10.4

# Configuration
dj-database-url==0.4.1
python-decouple==3.0
whitenoise==3.2.2

# Django 3rd party modules
django-allauth==0.30.0
django-extensions==1.7.5
django-model-utils==2.6
django-rest-auth==0.9.0
django-webpack-loader==0.4.1
djangorestframework==3.5.3
djangorestframework-jwt==1.9.0

# Python-PostgreSQL Database Adapter
psycopg2==2.6.2

# Time zones support
pytz==2016.10

Local environment Python requirements - ./requirements/local.txt

# Import all base requirements
-r base.txt

# Django debug toolbar
django-debug-toolbar==1.6

# Code linter
flake8==3.2.1

Production environment Python requirements - ./requirements/production.txt

# Import all base requirements
# Pro-tip: Try not to put anything here. Avoid dependencies in production that aren't in development.
-r base.txt

# WSGI Handler
gunicorn==19.6.0

# Email backend
django-anymail==0.7

Install Local Python requirements

pip install -r requirements/local.txt

Setup Django project

Initialize Django project

I always start Django projects the same way, just so it can create the initial folder as django_config. This is just personal perference on how I like to structure Django projects.

django-admin startproject django_config .

Reconfigure Django settings into a Python module

mkdir django_config/settings
touch django_config/settings/__init__.py
mv django_config/settings.py django_config/settings/base.py

Configure Base Django settings - ./django_configs/settings/base.py

"""
Django settings for django_config project.

Generated by 'django-admin startproject' using Django 1.10.2.

For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""

import datetime
import dj_database_url
import os

from decouple import config


# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
APPS_DIR = os.path.join(BASE_DIR, 'django_apps')


# SECRET KEY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/ref/settings/#std:setting-SECRET_KEY
# SECURITY WARNING: keep the secret key used in production secret!

SECRET_KEY = config('SECRET_KEY')


# DEBUG
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/ref/settings/#debug
# SECURITY WARNING: don't run with debug turned on in production!

DEBUG = config('DEBUG', cast=bool)


# MANAGER CONFIGURATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/ref/settings/#admins
# https://docs.djangoproject.com/en/1.10/ref/settings/#managers

ADMINS = (
    ("""Michael A. Gonzalez""", '[email protected]'),
)

MANAGERS = ADMINS


# APP CONFIGURATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/ref/settings/#installed-apps

DJANGO_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

THIRD_PARTY_APPS = [
    'allauth',
    'allauth.account',
    'rest_framework',
    'rest_framework.authtoken',
    'rest_auth',
    'rest_auth.registration',
    'webpack_loader',
]

LOCAL_APPS = []

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS


# MIDDLEWARE CONFIGURATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/topics/http/middleware/

DJANGO_SECURITY_MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
]

DJANGO_MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

MIDDLEWARE = DJANGO_SECURITY_MIDDLEWARE + DJANGO_MIDDLEWARE


# URL Configuration
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/ref/settings/#root-urlconf

ROOT_URLCONF = 'django_config.urls'


# TEMPLATE CONFIGURATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/topics/templates/

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(APPS_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]


# WGSI CONFIGURATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/ref/settings/#wsgi-application

WSGI_APPLICATION = 'django_config.wsgi.application'


# DATABASE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
    'default': dj_database_url.parse(config('DATABASE_URL')),
}
DATABASES['default']['ATOMIC_REQUESTS'] = True

# Added this to support deployment on Heroku
# https://devcenter.heroku.com/articles/django-app-configuration
db_from_env = dj_database_url.config(conn_max_age=500)
DATABASES['default'].update(db_from_env)


# PASSWORD VALIDATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# GENERAL CONFIGURATION
# ------------------------------------------------------------------------------
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
# https://docs.djangoproject.com/en/1.10/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

SITE_ID = 1


# STATIC FILE CONFIGURATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/howto/static-files/

STATIC_URL = '/static/'

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

STATIC_ROOT = os.path.join(BASE_DIR, 'static_cdn')


# MEDIA CONFIGURATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/ref/settings/#media-root
# https://docs.djangoproject.com/en/1.10/ref/settings/#media-url

MEDIA_URL = "/media/"

MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media_cdn")


# Django REST framework
# ------------------------------------------------------------------------------
# http://www.django-rest-framework.org/

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),
}


# Django REST-AUTH framework
# ------------------------------------------------------------------------------
# https://github.com/Tivix/django-rest-auth/
# https://github.com/GetBlimp/django-rest-framework-jwt

REST_USE_JWT = True

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=1),
    'JWT_ALLOW_REFRESH': True,
}


# EMAIL CONFIGURATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/topics/email/

EMAIL_PORT = config('EMAIL_PORT')

EMAIL_HOST = config('EMAIL_HOST')

EMAIL_BACKEND = config('EMAIL_BACKEND')

DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL')

Configure Local Django settings - ./django_configs/settings/local.py

import socket

from .base import *


# Webpack Loader by Owais Lane
# ------------------------------------------------------------------------------
# https://github.com/owais/django-webpack-loader

WEBPACK_LOADER = {
    'DEFAULT': {
        'BUNDLE_DIR_NAME': 'builds-dev/',
        'STATS_FILE': os.path.join(BASE_DIR, 'webpack', 'webpack-stats.dev.json')
    }
}

# Django Debug Toolbar
# ------------------------------------------------------------------------------
# https://github.com/jazzband/django-debug-toolbar

MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)

INSTALLED_APPS += ('debug_toolbar', )

INTERNAL_IPS = ['127.0.0.1', '10.0.2.2', ]

# Hack to have debug toolbar when developing with docker
ip = socket.gethostbyname(socket.gethostname())
INTERNAL_IPS += [ip[:-1] + "1"]

Configure Production Django settings - ./django_configs/settings/production.py

from .base import *


# Webpack Loader by Owais Lane
# ------------------------------------------------------------------------------
# https://github.com/owais/django-webpack-loader

WEBPACK_LOADER = {
    'DEFAULT': {
        'BUNDLE_DIR_NAME': 'builds/',
        'STATS_FILE': os.path.join(BASE_DIR, 'webpack', 'webpack-stats.production.json')
    }
}


# Use Whitenoise to serve static files
# ------------------------------------------------------------------------------
# https://whitenoise.readthedocs.io/

MIDDLEWARE = DJANGO_SECURITY_MIDDLEWARE + ['whitenoise.middleware.WhiteNoiseMiddleware'] + DJANGO_MIDDLEWARE

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'


# Use Gunicorn as WSGI HTTP server
# ------------------------------------------------------------------------------
# http://gunicorn.org/

INSTALLED_APPS += ('gunicorn', )


# SITE CONFIGURATION
# ------------------------------------------------------------------------------
# Hosts/domain names that are valid for this site
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts

ALLOWED_HOSTS = config(
    'ALLOWED_HOSTS',
    cast=lambda v: [d for d in [s.strip() for s in v.split(' ')] if d],
    default='',
)


# EMAIL CONFIGURATION - Anymail with Mailgun
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/1.10/topics/email/
# https://github.com/anymail/django-anymail

INSTALLED_APPS += ('anymail', )

ANYMAIL = {
    'MAILGUN_API_KEY': config('MAILGUN_API_KEY'),
}


# LOGGING CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse'
        }
    },
    'formatters': {
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s '
                      '%(process)d %(thread)d %(message)s'
        },
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        },
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'verbose',
        },
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True
        },
        'django.security.DisallowedHost': {
            'level': 'ERROR',
            'handlers': ['console', 'mail_admins'],
            'propagate': True
        }
    }
}

Setup initial placeholder landing page

Create the django_apps module

mkdir -p django_apps/templates
touch django_apps/__init__.py

Create the base.html template:

{% load staticfiles %}
<html class="{% block html_class %}{% endblock html_class %}" lang="en">
  <head>
    <!-- Allows you to inject head elements here -->
    {% block head %}

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->

    <meta name="author" content="">
        <meta name="description" content="">
        <meta name="keywords" content="">

    <title>{% block head_title %}{% endblock head_title %}</title>

    <!-- Allows you to inject CCS here -->
    {% block stylesheets %}

    <!-- Third-party CSS libraries go here -->
    <!-- Latest compiled and minified Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

    <!-- Optional Bootstrap theme -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">

    <!-- FontAwesome http://fontawesome.io/ -->
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">

    <!-- Animate CSS https://github.com/daneden/animate.css -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css">

    <!-- Project specific Stylesheets -->
    {% endblock stylesheets %}

    <!-- Title Icon -->
    <!-- <link rel="shortcut icon" href=""> -->

    {% endblock head %}

  </head>
  <body class="{% block body_class %}{% endblock body_class %}">

    <!-- Allows you to inject body content here -->
    {% block body %}
    {% endblock body %}

    <!-- Project specific JavaScript -->
    <!-- Allows you to inject JavaScript here -->
    {% block javascript %}
    {% endblock javascript %}

    <!-- Google Analytics goes here -->
    {% block google_analytics %}
    {% endblock google_analytics %}

  </body>
</html>

Create the index.html template:

{% extends 'base.html' %}
{% load staticfiles %}

{% block body %}
<h1>Hello World</h1>
{% endblock body %}

Update URLs to include Django Debug toolbar and the new index template

Update urls.py:

from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
from django.views.generic import TemplateView
from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token, verify_jwt_token

urlpatterns = [
    url(r'^$', TemplateView.as_view(template_name="index.html")),
    url(r'^rest-auth/', include('rest_auth.urls')),
    url(r'^rest-auth/registration/', include('rest_auth.registration.urls')),
    url(r'^api-token-auth/', obtain_jwt_token),
    url(r'^api-token-refresh/', refresh_jwt_token),
    url(r'^api-token-verify/', verify_jwt_token),
    url(r'^admin/', admin.site.urls),
]

if settings.DEBUG:
    import debug_toolbar
    urlpatterns += [
        url(r'^__debug__/', include(debug_toolbar.urls)),
    ]

Migrate Django to PostgreSQL database and create superuser

python manage.py migrate --settings=django_config.settings.local
python manage.py createsuperuser --settings=django_config.settings.local

Create directory for static assets

mkdir static

Run Django development webserver

python manage.py runserver --settings=django_config.settings.local

Setup NodeJS project

We will use NodeJS to manage our JavaScript front-end project. We will use NPM to manage and install JavaScript pacakges.

Initialize NPM

npm init

Install webpack packages

npm install --save-dev webpack@1 webpack-dev-server@1 webpack-bundle-tracker

Install babel compiler and plugins

npm install --save-dev babel-cli babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2 css-loader style-loader

Install Helpful Libraries

npm install --save-dev axios lodash

Install Redux and ReactJS and associated plugins

npm install --save react react-dom
npm install --save-dev redux redux-logger redux-thunk prop-types react-bootstrap react-fontawesome react-redux react-router@3 react-router-redux@4 react-cookie

Install React Hot Reloader 3 and associated plugins

npm install --save-dev react-hot-loader@next redux-devtools redux-devtools-dock-monitor redux-devtools-log-monitor

Install Unit Testing packages

npm install --save-dev karma mocha expect deepfreeze karma-mocha karma-webpack karma-sourcemap-loader karma-chrome-launcher karma-babel-preprocessor enzyme

Install ESLint

npm install --save-dev eslint eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-config-airbnb babel-eslint

Setup Webpack as front-end module bundler

Webpack is module bundler. It bundles all JavaScript, JSX, etc. code for our project and manages our codebase to be split into bundles to be loaded in in our different environments. This also includes some goodies like hot-reloading ;). This is extremely convienent when developing React components.

Create our Webpack directory

mkdir webpack

Create our base configuration for Webpack - webpack.base.config.js

module.exports = {

  module: {
    loaders: [{
      test: /\.(js|jsx)$/,
      exclude: /node_modules/,
      loader: 'babel-loader',
      query: {
        presets: ['es2015', 'stage-2', 'react']
      }
    }, {
      test: /\.css$/,
      loader: 'style-loader!css-loader'
    }]
  },

  resolve: {
    modulesDirectories: ['node_modules'],
    extensions: ['', '.js', '.jsx']
  }
};

Create local configuration for Webpack - webpack.local.config.js

var path = require('path');
var BundleTracker = require('webpack-bundle-tracker');
var webpack = require('webpack');
var config = require('./webpack.base.config.js');

config.entry = {
  main: [
    'webpack-dev-server/client?http://0.0.0.0:3000',
    'webpack/hot/only-dev-server',
    'react-hot-loader/patch',
    path.join(__dirname, '../static/js/src/main/index')
  ]
};

config.devtool = 'inline-sourcemap';
config.output = {
  path: path.join(__dirname, '../static/builds-development/'),
  filename: '[name]-[hash].js',
  publicPath: 'http://0.0.0.0:3000/static/builds/',
};

config.plugins = [
  new webpack.HotModuleReplacementPlugin(),
  new BundleTracker({ filename: './webpack/webpack-stats.dev.json' }),
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify('development'),
      BASE_URL: JSON.stringify('http://0.0.0.0:8000/'),
    }
  })
];

config.module.loaders[0].query.plugins = ['react-hot-loader/babel'];

config.devServer = {
  inline: true,
  progress: true,
  hot: true,
  historyApiFallback: true,
  host: '0.0.0.0',
  port: 3000
};

module.exports = config;

Create Production configuration for Webpack - webpack.production.config.js

var path = require('path');
var webpack = require('webpack');
var BundleTracker = require('webpack-bundle-tracker');
var config = require('./webpack.base.config.js');

config.entry = {
  main: [
    path.join(__dirname, '../static/js/src/main/index')
  ]
};

config.output = {
  path: path.join(__dirname, '../static/builds/'),
  filename: '[name]-[hash].min.js',
  publicPath: '/static/js/builds/'
};

config.plugins = [
  new BundleTracker({ filename: './webpack/webpack-stats.production.json' }),
  new webpack.optimize.DedupePlugin(),
  new webpack.optimize.OccurenceOrderPlugin(),
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify('production'),
      BASE_URL: JSON.stringify('http://0.0.0.0/'),
    }
  }),
  new webpack.optimize.UglifyJsPlugin({
    mangle: false,
    sourcemap: false
  })
];

module.exports = config;

Create the entry point for our front-end project

mkdir -p static/js/src/main
mkdir -p static/builds
mkdir -p static/builds-development
touch static/js/src/main/index.jsx

Wire up Django/ReactJS application

NOTE: I have made some changes to this. I wanted to include the redux-dev-tools library. Refer to this commit to see the changes.

In this step, we will make sure Django and ReactJS are communicating. We will now be able to use Django as our backend server and ReactJS to handle all of the frontend.

Write placeholder React Component

Write a simple React component as a placeholder for now.

mkdir -p static/js/src/main/components/

./static/js/src/main/components/hello-world.jsx should look like this:

import React from 'react';

const HelloWorld = () => {
  return (
    <h1>Hello World!</h1>
  );
};

export default HelloWorld;

./static/js/src/main/index.jsx should look like this:

import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import HelloWorld from './components/hello-world';

ReactDOM.render(
  <AppContainer>
    <HelloWorld />
  </AppContainer>,
  document.getElementById('react-root')
);

if (module.hot) {
  module.hot.accept('./components/hello-world', () => {
    const HelloWorld = require('./components/hello-world').default;
    ReactDOM.render(
      <AppContainer>
        <HelloWorld />
      </AppContainer>,
      document.getElementById('react-root')
    );
  });
}

Update HTML template to enable hot reloading

We need to tell the Django template to load the latest javascript bundle. Because we are using Django webpack loader, we can enable hot reloading during development ;). The index.html page should now look like this:

{% extends 'base.html' %}
{% load staticfiles %}
{% load render_bundle from webpack_loader %}

{% block body %}
<div id="react-root"></div>
{% endblock body %}

{% block javascript %}
{% render_bundle 'main' %}
{% endblock javascript %}

Setup Karma and Mocha for running JavaScript Unittests

Karma is a spectular test runner for JavaScript. Karma will allow a productive testing environment, one where we can just write the code and get instant feedback from our tests.

Setup Karma configuration - ./webpack/karma.config.js

var webpackConfig = require('./webpack.local.config.js');

webpackConfig.entry = {};

module.exports = function (config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['mocha'],

    // list of files / patterns to load in the browser
    files: [
      '../static/js/src/test_index.js'
    ],

    // list of files to exclude
    exclude: [],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      '../static/js/src/test_index.js': ['webpack', 'sourcemap'],
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,
    autoWatchBatchDelay: 300,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['Chrome'],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false,

    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity,

    // Webpack
    webpack: webpackConfig,
    webpackServer: {
      noInfo: true
    }
  });
};

Setup testing structure for our project

We set this up with an example test because if not Karma will complain ;)

static/js/src/test_index.js:

var testsContext = require.context('.', true, /.spec$/);
testsContext.keys().forEach(testsContext);

static/js/src/example.spec.js:

import expect from 'expect';

describe('Something abstract', () => {
  it('works', () => {
    expect(1).toEqual(1);
  });
});

Customize NodeJS commands for ease of use

We can add commonly used commands to package.json.

Adjust the scripts so that it looks like:

"scripts": {
  "build-development": "webpack --config webpack/webpack.local.config.js --progress --colors",
  "build-production": "webpack --config webpack/webpack.production.config.js --progress --colors",
  "watch": "webpack-dev-server --config webpack/webpack.local.config.js",
  "test": "./node_modules/karma/bin/karma start webpack/karma.config.js --log-level debug"
}

Now we can conviently run:

npm run [script]

Dockerize application

We will use Docker and Docker compose to make add containers for all of the services that run our web application.

Our application will consist of the following services

  1. PostgreSQL database
  2. Django
  3. Node (only for development for hot reloading ;) )
  4. Nginx

Development Docker environment

1. Create docker-compose-development.yml file:

version: '2'

volumes:
  postgres_data_dev: {}
  postgres_backup_dev: {}

services:
  postgres:
    container_name: postgres_server
    build: ./docker_compose/postgres
    volumes:
      - postgres_data_dev:/var/lib/postgresql/data
      - postgres_backup_dev:/backups
    env_file: .env

  node:
    container_name: node_server
    build:
      context: .
      dockerfile: ./docker_compose/node/development/Dockerfile
    command: /start.sh
    volumes:
      - .:/app
    ports:
      - "3000:3000"

  django:
    container_name: django_server
    build:
      context: .
      dockerfile: ./docker_compose/django/development/Dockerfile
    command: /start.sh
    depends_on:
      - postgres
      - node
    env_file: .env
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    links:
      - postgres

2. Create Django docker-compose build:

mkdir -p docker_compose/django/development

Create docker_compose/django/development/Dockerfile:

FROM python:3.5

ENV PYTHONUNBUFFERED 1

# Setup Debian linux
RUN export DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install build-essential curl
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash -
RUN apt-get install -y nodejs
RUN nodejs -v && npm -v

# Requirements have to be pulled and installed here, otherwise caching won't work
COPY ./requirements /requirements
COPY package.json /app/

WORKDIR /app
COPY . /app

RUN pip install -r /requirements/local.txt && npm install && npm run build-development

COPY ./docker_compose/django/development/start.sh /start.sh
COPY ./docker_compose/django/entrypoint.sh /entrypoint.sh
RUN sed -i 's/\r//' /entrypoint.sh \
    && sed -i 's/\r//' /start.sh \
    && chmod +x /entrypoint.sh \
    && chmod +x /start.sh

ENTRYPOINT ["/entrypoint.sh"]

Create docker_compose/django/development/start.sh file. This will run the development server:

#!/bin/sh
python manage.py migrate
python manage.py runserver 0.0.0.0:8000

Create docker_compose/django/entrypoint.sh file. This will make sure we can connect to PostgreSQL from Django server:

#!/bin/bash
set -e
cmd="$@"

# This entrypoint is used to play nicely with the current cookiecutter configuration.
# Since docker-compose relies heavily on environment variables itself for configuration, we'd have to define multiple
# environment variables just to support cookiecutter out of the box. That makes no sense, so this little entrypoint
# does all this for us.

# the official postgres image uses 'postgres' as default user if not set explictly.
if [ -z "$POSTGRES_USER" ]; then
    export POSTGRES_USER=postgres
fi

# If not DB is set, then use USER by default
if [ -z "$POSTGRES_DB" ]; then
    export POSTGRES_DB=$POSTGRES_USER
fi

# Need to update the DATABASE_URL if using DOCKER
export DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres:5432/$POSTGRES_DB


function postgres_ready(){
python << END
import sys
import psycopg2
try:
    conn = psycopg2.connect(dbname="$POSTGRES_DB", user="$POSTGRES_USER", password="$POSTGRES_PASSWORD", host="postgres")
except psycopg2.OperationalError:
    sys.exit(-1)
sys.exit(0)
END
}

until postgres_ready; do
  >&2 echo "Postgres is unavailable - sleeping"
  sleep 1
done

>&2 echo "Postgres is up - continuing..."
exec $cmd

3. Create NodeJS docker-compose build:

mkdir -p docker_compose/node/development

Create docker_compose/node/development/Dockerfile:

FROM node:latest

COPY . /app

WORKDIR /app
RUN npm install

COPY ./docker_compose/node/development/start.sh /start.sh
RUN sed -i 's/\r//' /start.sh
RUN chmod +x /start.sh

Create docker_compose/node/development/start.sh file. This will run the development server:

#!/bin/sh
npm run watch

4. Create PostgreSQL docker-compose build:

mkdir -p docker_compose/postgres

Create docker_compose/postgres/Dockerfile

FROM postgres:latest

Production Docker environment

1. Create docker-compose.yml file:

version: '2'

volumes:
  postgres_data: {}
  postgres_backup: {}

services:
  postgres:
    container_name: postgres_server
    build: ./docker_compose/postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - postgres_backup:/backups
    env_file: .env

  django:
    container_name: django_server
    build:
      context: .
      dockerfile: ./docker_compose/django/production/Dockerfile
    user: django
    depends_on:
      - postgres
    command: /start.sh
    env_file: .env

  nginx:
    container_name: nginx_server
    build: ./docker_compose/nginx
    depends_on:
      - django
    ports:
      - "0.0.0.0:80:80"

2. Create Django docker-compose build:

mkdir -p docker_compose/django/production

Create docker_compose/django/production/Dockerfile:

FROM python:3.5

ENV PYTHONUNBUFFERED 1

# Setup Debian linux
RUN export DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install build-essential curl
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash -
RUN apt-get install -y nodejs
RUN nodejs -v && npm -v

# Requirements have to be pulled and installed here, otherwise caching won't work
COPY ./requirements /requirements
COPY package.json /app/

WORKDIR /app
COPY . /app

RUN pip install -r /requirements/production.txt \
    && groupadd -r django \
    && useradd -r -g django django \
    && npm install && npm run build-production

RUN chown -R django /app

COPY ./docker_compose/django/production/start.sh /start.sh
COPY ./docker_compose/django/entrypoint.sh /entrypoint.sh
RUN sed -i 's/\r//' /entrypoint.sh \
    && sed -i 's/\r//' /start.sh \
    && chmod +x /entrypoint.sh \
    && chown django /entrypoint.sh \
    && chmod +x /start.sh \
    && chown django /start.sh

ENTRYPOINT ["/entrypoint.sh"]

Create docker_compose/django/production/start.sh file. This will run the web server:

#!/bin/sh
python /app/manage.py collectstatic --noinput
/usr/local/bin/gunicorn django_config.wsgi -w 4 -b 0.0.0.0:5000 --chdir=/app

3. Create NGINX docker-compose build

mkdir -p docker_compose/nginx

Create docker_compose/nginx/Dockerfile:

FROM nginx:latest
ADD nginx.conf /etc/nginx/nginx.conf

Create docker_compose/nginx/nginx.conf:

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
  '$status $body_bytes_sent "$http_referer" '
  '"$http_user_agent" "$http_x_forwarded_for"';

  access_log  /var/log/nginx/access.log  main;

  sendfile        on;
  #tcp_nopush     on;

  keepalive_timeout  65;

  #gzip  on;

  upstream app {
    server django:5000;
  }

  server {
    listen 80;
    charset     utf-8;



    location / {
      # checks for static file, if not found proxy to app
      try_files $uri @proxy_to_app;
    }

    # django app
    location @proxy_to_app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass   http://app;

    }
  }
}

Helpful utilities for dealing with Django APIs with React

We will create helpful utilities to consume APIsin a secure fashion. We will use the following libraries:

  • react-cookie
  • axios

Front-end utilities

1. Create utilities directory

mkdir -p static/js/src/utilities/

2. Create script to interact with application cookie

We will use the react-cookie library (Makes this so easy!). We will use Cross-Site Request Forgery (CSRF) token and JSON web token (JWT) to secure our application.

It will make our application safe against Cross-site scripting (XSS) and CSRF attacks by following these strategies:

  1. Store JWT in a httpOnly and secure cookie
  2. Use a CSRF token with any methods that alter data on the server (PUT, POST, DELETE, PATCH).
  3. GET should NOT cause changes server side. This is reserved for PUT, POST, DELETE, PATCH methods.

Create the static/js/src/utilities/cookie.js file. It should look like this:

import cookies from 'react-cookie';


// This is the place where we can load elements from a cookie to be used in our app

// Django CRSF Token is stored in a cookie
const csrftoken = cookies.load('csrftoken');

// JWT is going to be saved into cookie
// cookies.save('jwt', response.data.token, { secure: true, httpOnly: true });
// Therefore it will automatically be sent in the header of all API requests
// JWT will NOT be accessible to JavaScript because it is httpOnly :)

export { csrftoken };

3. Create script to use to interact with Django APIs

We will use the axios library to set this up. Create the static/js/src/utilities/api.js file. It should look like this:

import axios from 'axios';
import * as cookie from './cookie';


const api = axios.create({
  baseURL: process.env.BASE_URL,
  timeout: 1000,
});

// CRSF token is needed in all requests that can make a change server side
api.defaults.headers.post['X-CSRFToken'] = cookie.csrftoken;
api.defaults.headers.put['X-CSRFToken'] = cookie.csrftoken;
api.defaults.headers.patch['X-CSRFToken'] = cookie.csrftoken;
// api.defaults.headers.delete['X-CSRFToken'] = cookie.csrftoken; // Currently axios can't set headers for DELETE

// Since we will only be using JSON APIs, add Content-Type: application/json to header as default
api.defaults.headers.post['Content-Type'] = 'application/json';
api.defaults.headers.put['Content-Type'] = 'application/json';
api.defaults.headers.patch['Content-Type'] = 'application/json';

// Since we will only be using JSON APIs, add Accept: application/json to header as default
api.defaults.headers.get.Accept = 'application/json';
api.defaults.headers.post.Accept = 'application/json';
api.defaults.headers.put.Accept = 'application/json';
api.defaults.headers.patch.Accept = 'application/json';

// JWT is going to be saved into cookie
// cookies.save('jwt', response.data.token, { secure: true, httpOnly: true });
// Therefore it will automatically be sent in the header of all API requests
// JWT will not be accessible to JavaScript because it is httpOnly :)

export default api;

NOTE: Currently axios does not allow setting headers on DELETE method. I raised this issue and it should be included in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment