├── backend ├── backend │ ├── __init__.py │ ├── test_settings.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── pytest.ini ├── entrypoint.sh ├── .coveragerc ├── Dockerfile ├── Pipfile ├── manage.py ├── Pipfile.lock └── README.md ├── frontend ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── src │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── App.css │ ├── App.js │ ├── logo.svg │ └── serviceWorker.js ├── .gitignore ├── package.json ├── Dockerfile-vuejs-prod └── README.md ├── Pipfile ├── docker-compose.yml ├── nginx └── nginx-proxy.conf ├── LICENSE ├── .gitignore └── README.md /backend/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = backend.test_settings 3 | -------------------------------------------------------------------------------- /backend/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!bin/bash 2 | pip install -r requirements.txt 3 | python manage.py runserver 0.0.0.0:8000 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkuria/django-react-in-docker-microservices/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /backend/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = manage.py, *wsgi.py, *test_settings.py, *settings.py, *urls.py, *__init__.py, */apps.py, */tests/*, */migrations/* 3 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | RUN mkdir /backend 3 | WORKDIR /backend 4 | ADD requirements.txt /backend/ 5 | RUN pip install -r requirements.txt 6 | ADD . /backend/ 7 | EXPOSE 8000 8 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.6" 12 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "*" 10 | djangorestframework = "*" 11 | pytest = "*" 12 | pytest-django = "*" 13 | pytest-cov = "*" 14 | mixer = "*" 15 | 16 | [requires] 17 | python_version = "3.6" 18 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend/backend/test_settings.py: -------------------------------------------------------------------------------- 1 | # File: ./backend/backend/test_settings.py 2 | 3 | from .settings import * # NOQA 4 | 5 | DATABASES = { 6 | 'default': { 7 | 'ENGINE': 'django.db.backends.sqlite3', 8 | 'NAME': ':memory:', 9 | } 10 | } 11 | 12 | PASSWORD_HASHERS = ( 13 | 'django.contrib.auth.hashers.MD5PasswordHasher', 14 | ) 15 | 16 | DEFAULT_FILE_STORAGE = 'inmemorystorage.InMemoryStorage' -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | backend: 5 | build: ./backend 6 | volumes: 7 | - ./backend/:/backend # maps host diretory to internal container directory 8 | working_dir: /backend/ 9 | command: sh entrypoint.sh 10 | frontend: 11 | image: node:8.9 12 | command: sh entrypoint.sh 13 | working_dir: /frontend 14 | volumes: 15 | - ./frontend/:/frontend 16 | nginx: 17 | image: nginx:latest 18 | ports: 19 | - 80:8080 20 | volumes: 21 | - ./nginx/nginx-proxy.conf:/etc/nginx/conf.d/default.conf:ro 22 | - ./frontend/build:/var/www/frontend 23 | depends_on: 24 | - backend 25 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit src/App.js and save to reload. 12 |

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-scripts": "3.0.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /nginx/nginx-proxy.conf: -------------------------------------------------------------------------------- 1 | upstream api { 2 | server backend:8000; 3 | } 4 | 5 | server { 6 | listen 8080; 7 | location /test/ { 8 | proxy_pass http://api$request_uri; 9 | } 10 | 11 | location /api/ { 12 | proxy_pass http://api$request_uri; 13 | } 14 | 15 | location /static/rest_framework/ { 16 | proxy_pass http://api$request_uri; 17 | } 18 | 19 | # ignore cache frontend 20 | location ~* (service-worker\.js)$ { 21 | add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; 22 | expires off; 23 | proxy_no_cache 1; 24 | } 25 | 26 | location / { 27 | root /var/www/frontend; 28 | try_files $uri $uri/ /index.html; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | """backend URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 Peter Kuria 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /frontend/Dockerfile-vuejs-prod: -------------------------------------------------------------------------------- 1 | # The frontend framework used is Vue.js and all static web files are 2 | # finally served with a minimalistic web container based on nginx. The 3 | # Dockerfile for this static web server looks like this: 4 | 5 | 6 | # As you can see, it’s a multi-stage build. The build state will use 7 | # a node alpine image as the base and essentially run the build process. 8 | # Under the hood, vue uses webpack to combine all resources to the 9 | # static dist directory . The second stage takes the static resources 10 | # and adds them to a nginx container, again based on alpine. The 11 | # results is a container image with about 20MB. 12 | 13 | # build stage 14 | FROM node:lts-alpine as builder 15 | WORKDIR /app 16 | 17 | # install app dependencies 18 | # ENV PATH /usr/src/app/node_modules/.bin:$PATH 19 | 20 | # COPY package*.json ./ 21 | COPY package*.json ./app/package.json 22 | RUN yarn install 23 | RUN npm install react-scripts@2.1.2 -g --silent 24 | 25 | # set environment variables 26 | ARG REACT_APP_USERS_SERVICE_URL 27 | ENV REACT_APP_USERS_SERVICE_URL $REACT_APP_USERS_SERVICE_URL 28 | ARG NODE_ENV 29 | ENV NODE_ENV $NODE_ENV 30 | 31 | # create build 32 | # COPY . /usr/src/app 33 | COPY . . 34 | RUN npm run build 35 | 36 | # production stage 37 | FROM nginx:stable-alpine as production-stage 38 | COPY --from=build-stage /app/dist /usr/share/nginx/html 39 | EXPOSE 80 40 | CMD ["nginx", "-g", "daemon off;"] 41 | 42 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .env.notes 87 | .env.logs 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | keys.py 96 | 97 | /backend/mpesa-dataja-api/keys.py 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'm2)72%o^j$d@h@@@p$@n*r-nw=*oywx#+jl6g603!d94x1#m(9' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'backend.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'backend.wsgi.application' 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 80 | } 81 | } 82 | 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 119 | 120 | STATIC_URL = '/static/' 121 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This is a Django, Frontend with React in Docker/nginx/ Kubernetes cluster 2 | 3 | This project serves as an example of a deployment frontend (ReactJS) and backend (Django) using docker and nginx. 4 | 5 | ## How to run 6 | 7 | Make sure you have [docker](https://docs.docker.com/install/) and [docker-compose](https://docs.docker.com/compose/install/) installed and run 8 | 9 | ```shell 10 | docker-compose up 11 | ``` 12 | 13 | ## Want to use this project? 14 | 15 | 1. Fork/Clone/ or start afresh from local PC/ MAC 16 | 17 | ```sh 18 | mkdir django-react-in-docker-microservices && cd django-react-in-docker-microservices 19 | mkdir backend && cd backend 20 | 21 | ``` 22 | 23 | 1. Create and activate a virtualenv environment: I prefer to use **_pipenv_** 24 | 25 | ```sh 26 | pipenv --three && pipenv shell 27 | 28 | ``` 29 | 30 | ## start development: add dependencies 31 | 32 | 1. Install the requirements using pipenv : Django and djangorestframework, aloe-django 33 | 34 | ```sh 35 | (backend) $ django-admin startproject backend . && cd backend 36 | 37 | (backend) $ pipenv install Django && pipenv install djangorestframework && pipenv install aloe-django 38 | (backend) $ python manage.py migrate 39 | (backend) $ python manage.py startapp 40 | 41 | ``` 42 | 43 | Next add our **_backend_** app to the **INSTALLED_APPS** settings in our settings.py file. 44 | 45 | ## Git and CI Quick setup: I prefer to start working locally and push to github. On PC: 46 | 47 | ```sh 48 | # navigate to the root folder and initialize a git tracking. You can create a local branch 49 | $ cd .. 50 | $ cd .. 51 | $ git init && git commit -am "initial commit" 52 | 53 | # alternatively in one step 54 | $ git init 55 | $ $ git commit -am "initial commit" 56 | ``` 57 | 58 | ## Add and Commit changes 59 | 60 | **_git -am "message"_** 61 | 62 | ```sh 63 | $ echo README.md 64 | 65 | $ git commit -am "added and committed READme.md" 66 | 67 | # or push an existing repository from the command line 68 | $ git remote add origin git@github.com:peterkuria/django-react-in-docker-microservices.git 69 | 70 | # You should disable email privacy to enable push 71 | $ git push origin master 72 | ``` 73 | 74 | ## Run the tests 75 | 76 | ```sh 77 | $ cd django-react-in-docker-microservices 78 | $ python manage.py harvest 79 | ``` 80 | 81 | ## Create and configure your local database 82 | 83 | ## Merge the new branch with your master branch 84 | 85 | PULL the changes into the production folder 86 | Deploy 1. 87 | 88 | ## Frontend React APP 89 | 90 | We’re using React in this tutorial but our backend doesn’t care what frontend framework is used to consume our Todo list API. 91 | 92 | We are going to easily bootstrap our app with [create-react-app](https://github.com/facebook/create-react-app). 93 | 94 | Open up a new command line console so there are now two consoles open. Leave our existing backend open and still running our local server for our DRF API. 95 | 96 | In the new console install create-react-app globally with the following command. 97 | 98 | \$ yarn add global create-react-app 99 | 100 | Navigate to the root directory and 101 | 102 | ```sh 103 | 104 | $ cd ~/django-react-in-docker-microservices 105 | $ cd frontend 106 | # bootstrap your react app with create-react-app 107 | # npx create-react-app my-app 108 | $ yarn create react-app frontend 109 | 110 | Success! Created frontend at ~\django-react-in-docker-microservices\frontend\frontend 111 | Inside that directory, you can run several commands: 112 | 113 | yarn start 114 | Starts the development server. 115 | 116 | yarn build 117 | Bundles the app into static files for production. 118 | 119 | yarn test 120 | Starts the test runner. 121 | 122 | yarn eject 123 | Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! 124 | 125 | We suggest that you begin by typing: 126 | 127 | cd frontend 128 | yarn start 129 | 130 | Happy hacking! 131 | Done in 368.18s. 132 | 133 | ``` 134 | 135 | We can now run our React app with the command yarn start. 136 | 137 | \$ yarn start 138 | 139 | Update this App.js file. 140 | 141 | Our /api endpoint is in JSON. Your file will look like this: 142 | 143 | ```JSON 144 | [ 145 | { 146 | "id":1, 147 | "title":"To do App", 148 | "description":"Learn DRF." 149 | }, 150 | { 151 | "id":2, 152 | "title":"continue working on M-PESA API forked repo", 153 | "description":"M-MPESA/stripe payment gateways has revolutionized the way many send and receive money - don't be left behind" 154 | }, 155 | { 156 | "id":3, 157 | "title":"Next create a Django + Graphql React API", 158 | "description":"Graphql data CRUD operations with python Django rocks" 159 | } 160 | ] 161 | ``` 162 | 163 | We can mock that up in our React app in a variable list, load that list into our state, and then use map() to display all the items. Here’s the code. 164 | 165 | ```javascript 166 | // App.js 167 | import React, { Component } from "react" 168 | 169 | const list = [ 170 | { 171 | id: 1, 172 | title: "1st Item", 173 | description: "Description here." 174 | }, 175 | { 176 | id: 2, 177 | title: "2nd Item", 178 | description: "Another description here." 179 | }, 180 | { 181 | id: 3, 182 | title: "3rd Item", 183 | description: "Third description here." 184 | } 185 | ] 186 | 187 | class App extends Component { 188 | constructor(props) { 189 | super(props) 190 | this.state = { list } 191 | } 192 | 193 | render() { 194 | return ( 195 |
196 | {this.state.list.map(item => ( 197 |
198 |

{item.title}

199 | {item.description} 200 |
201 | ))} 202 |
203 | ) 204 | } 205 | } 206 | 207 | export default App 208 | ``` 209 | 210 | ## Add your Nginx 211 | 212 | ```shell 213 | upstream api { 214 | server backend:8000; 215 | } 216 | 217 | server { 218 | listen 8080; 219 | location /test/ { 220 | proxy_pass http://api$request_uri; 221 | } 222 | 223 | location /api/ { 224 | proxy_pass http://api$request_uri; 225 | } 226 | 227 | location /static/rest_framework/ { 228 | proxy_pass http://api$request_uri; 229 | } 230 | 231 | # ignore cache frontend 232 | location ~* (service-worker\.js)$ { 233 | add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; 234 | expires off; 235 | proxy_no_cache 1; 236 | } 237 | 238 | location / { 239 | root /var/www/frontend; 240 | try_files $uri $uri/ /index.html; 241 | } 242 | 243 | } 244 | 245 | ``` 246 | -------------------------------------------------------------------------------- /backend/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "af1fa9325426ed16cb26840923e991fdcad5cd21a6f3c67893290885cc9482ec" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "atomicwrites": { 20 | "hashes": [ 21 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", 22 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" 23 | ], 24 | "version": "==1.3.0" 25 | }, 26 | "attrs": { 27 | "hashes": [ 28 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", 29 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 30 | ], 31 | "version": "==19.1.0" 32 | }, 33 | "colorama": { 34 | "hashes": [ 35 | "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", 36 | "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" 37 | ], 38 | "markers": "sys_platform == 'win32'", 39 | "version": "==0.4.1" 40 | }, 41 | "coverage": { 42 | "hashes": [ 43 | "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", 44 | "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", 45 | "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", 46 | "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", 47 | "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", 48 | "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", 49 | "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", 50 | "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", 51 | "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", 52 | "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", 53 | "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", 54 | "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", 55 | "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", 56 | "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", 57 | "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", 58 | "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", 59 | "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", 60 | "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", 61 | "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", 62 | "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", 63 | "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", 64 | "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", 65 | "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", 66 | "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", 67 | "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", 68 | "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", 69 | "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", 70 | "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", 71 | "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", 72 | "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", 73 | "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", 74 | "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" 75 | ], 76 | "version": "==4.5.4" 77 | }, 78 | "django": { 79 | "hashes": [ 80 | "sha256:16a5d54411599780ac9dfe3b9b38f90f785c51259a584e0b24b6f14a7f69aae8", 81 | "sha256:9a2f98211ab474c710fcdad29c82f30fc14ce9917c7a70c3682162a624de4035" 82 | ], 83 | "index": "pypi", 84 | "version": "==2.2.4" 85 | }, 86 | "djangorestframework": { 87 | "hashes": [ 88 | "sha256:42979bd5441bb4d8fd69d0f385024a114c3cae7df0f110600b718751250f6929", 89 | "sha256:aedb48010ebfab9651aaab1df5fd3b4848eb4182afc909852a2110c24f89a359" 90 | ], 91 | "index": "pypi", 92 | "version": "==3.10.2" 93 | }, 94 | "faker": { 95 | "hashes": [ 96 | "sha256:74b32991f8e08e4f2f84858b919eca253becfaec4b3fa5fcff7fdbd70d5d78b1", 97 | "sha256:c2ce42dd8361e6d392276006d757532562463c8642b1086709584200b7fd7758" 98 | ], 99 | "version": "==0.9.1" 100 | }, 101 | "importlib-metadata": { 102 | "hashes": [ 103 | "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", 104 | "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" 105 | ], 106 | "version": "==0.19" 107 | }, 108 | "mixer": { 109 | "hashes": [ 110 | "sha256:3601e84134b79cd4ac663f79a39fc3005d6a3f37692e7062cc3c0a727ab79940", 111 | "sha256:f1ccab542a1304d7b3da5d6ebe9f53de1aac2288427121a3a54a63e08c3a5367" 112 | ], 113 | "index": "pypi", 114 | "version": "==6.1.3" 115 | }, 116 | "more-itertools": { 117 | "hashes": [ 118 | "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", 119 | "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" 120 | ], 121 | "version": "==7.2.0" 122 | }, 123 | "packaging": { 124 | "hashes": [ 125 | "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", 126 | "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe" 127 | ], 128 | "version": "==19.1" 129 | }, 130 | "pluggy": { 131 | "hashes": [ 132 | "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", 133 | "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" 134 | ], 135 | "version": "==0.12.0" 136 | }, 137 | "py": { 138 | "hashes": [ 139 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", 140 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" 141 | ], 142 | "version": "==1.8.0" 143 | }, 144 | "pyparsing": { 145 | "hashes": [ 146 | "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", 147 | "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" 148 | ], 149 | "version": "==2.4.2" 150 | }, 151 | "pytest": { 152 | "hashes": [ 153 | "sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d", 154 | "sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77" 155 | ], 156 | "index": "pypi", 157 | "version": "==5.0.1" 158 | }, 159 | "pytest-cov": { 160 | "hashes": [ 161 | "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", 162 | "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a" 163 | ], 164 | "index": "pypi", 165 | "version": "==2.7.1" 166 | }, 167 | "pytest-django": { 168 | "hashes": [ 169 | "sha256:264fb4c506db5d48a6364c311a0b00b7b48a52715bad8839b2d8bee9b99ed6bb", 170 | "sha256:4adfe5fb3ed47f0ba55506dd3daf688b1f74d5e69148c10ad2dd2f79f40c0d62" 171 | ], 172 | "index": "pypi", 173 | "version": "==3.5.1" 174 | }, 175 | "python-dateutil": { 176 | "hashes": [ 177 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 178 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 179 | ], 180 | "version": "==2.8.0" 181 | }, 182 | "pytz": { 183 | "hashes": [ 184 | "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", 185 | "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" 186 | ], 187 | "version": "==2019.2" 188 | }, 189 | "six": { 190 | "hashes": [ 191 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 192 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 193 | ], 194 | "version": "==1.12.0" 195 | }, 196 | "sqlparse": { 197 | "hashes": [ 198 | "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", 199 | "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" 200 | ], 201 | "version": "==0.3.0" 202 | }, 203 | "text-unidecode": { 204 | "hashes": [ 205 | "sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d", 206 | "sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc" 207 | ], 208 | "version": "==1.2" 209 | }, 210 | "wcwidth": { 211 | "hashes": [ 212 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 213 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 214 | ], 215 | "version": "==0.1.7" 216 | }, 217 | "zipp": { 218 | "hashes": [ 219 | "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", 220 | "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" 221 | ], 222 | "version": "==0.5.2" 223 | } 224 | }, 225 | "develop": {} 226 | } 227 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # A Django + GraphQL + Apollo + React Stack Demo 2 | 3 | In this workshop, we will address the following topics: 4 | 5 | ## [Part 1: The Backend](#part1) 6 | 7 | 1. [Create a new Django Project](#create-new-django-project) 8 | 1. [Create a Simple Django App](#create-simple-app) 9 | 1. [Add GraphQL to Django](#add-graphql-to-django) 10 | 1. [Add Message-DjangoObjectType to GraphQL Schema](#add-django-object-type) 11 | 1. [Add Mutation to GraphQL Schema](#add-mutation) 12 | 1. [Add JWT-Authentication to Django](#add-jwt) 13 | 14 | ## [Part 2: The Frontend](#part2) 15 | 16 | 1. [Create a new React Project](#create-new-react-project) 17 | 1. [Add ReactRouter to React](#add-react-router) 18 | 1. [Add Apollo to React](#add-apollo) 19 | 1. [Add Query with Variables for DetailView](#add-query-with-variables) 20 | 1. [Add Token Middleware for Authentication](#add-token-middleware) 21 | 1. [Add Login / Logout Views](#add-login-logout-views) 22 | 1. [Add Mutation for CreateView](#add-mutation-for-create-view) 23 | 1. [Show Form Errors on CreateView](#show-form-errors) 24 | 1. [Add Filtering to ListView](#add-filtering) 25 | 1. [Add Pagination to ListView](#add-pagination) 26 | 1. [Add Cache Invalidation](#cache-invalidation) 27 | 28 | ## Part 3: Advanced Topics 29 | 30 | I am planning to keep this repo alive and add some more best practices as I 31 | figure them out at work. Some ideas: 32 | 33 | 1. Create a higher order component "LoginRequired" to protect views 34 | 1. Create a higher order component "NetworkStatus" to allow refetching of 35 | failed queries after the network was down 36 | 1. Don't refresh the entire page after login/logout 37 | 1. Create Python decorator like "login_required" for mutations and resolvers 38 | 1. Some examples for real cache invalidation 39 | 1. Hosting (EC2 instance for Django, S3 bucket for frontend files) 40 | 41 | If you have more ideas, please add them in the issue tracker! 42 | 43 | Before you start, you should read a little bit about [GraphQL](http://graphql.org/learn/) and [Apollo](http://dev.apollodata.com/react/) and [python-graphene](http://docs.graphene-python.org/projects/django/en/latest/). 44 | 45 | If you have basic understanding of Python, Django, JavaScript and ReactJS, you 46 | should be able to follow this tutorial and copy and paste the code snippets 47 | shown below and hopefully it will all work out nicely. 48 | 49 | The tutorial should give you a feeling for the necessary steps involved when 50 | building a web application with Django, GraphQL and ReactJS. 51 | 52 | If you find typos or encounter other issues, please report them at the issue 53 | tracker. 54 | 55 | # Part 1: The Backend 56 | 57 | ## Create a new Django Project 58 | 59 | For this demonstration we will need a backend that can serve our GraphQL API. 60 | We will chose Django for this, so the first thing we want to do is to create a 61 | new Django project. If you are new to Python, you need to read about [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/), first. 62 | 63 | ```bash 64 | mkdir -p ~/django-react-in-docker-microservices/backend 65 | cd ~/django-react-in-docker-microservices/backend 66 | mkvirtualenv django-react-in-docker-microservices 67 | pip install django 68 | pip install pytest 69 | pip install pytest-django 70 | pip install pytest-cov 71 | pip install mixer 72 | django-admin startproject backend . 73 | cd backend 74 | ./manage.py migrate 75 | ./manage.py createsuperuser 76 | ./manage.py runserver 77 | ``` 78 | 79 | > You should now be able to browse to `localhost:8000/admin/` and login with 80 | > your superuser account 81 | 82 | We like to build our Django apps in a test-driven manner, so let's also create 83 | a few files to setup our testing framework: 84 | 85 | 86 | ```py 87 | # File: ./backend/backend/test_settings.py 88 | 89 | from .settings import * # NOQA 90 | 91 | DATABASES = { 92 | 'default': { 93 | 'ENGINE': 'django.db.backends.sqlite3', 94 | 'NAME': ':memory:', 95 | } 96 | } 97 | 98 | PASSWORD_HASHERS = ( 99 | 'django.contrib.auth.hashers.MD5PasswordHasher', 100 | ) 101 | 102 | DEFAULT_FILE_STORAGE = 'inmemorystorage.InMemoryStorage' 103 | ``` 104 | 105 | ```config 106 | # File: ./backend/pytest.ini 107 | 108 | [pytest] 109 | DJANGO_SETTINGS_MODULE = backend.test_settings 110 | ``` 111 | 112 | ```config 113 | # File: ./backend/.coveragerc 114 | 115 | [run] 116 | omit = manage.py, *wsgi.py, *test_settings.py, *settings.py, *urls.py, *__init__.py, */apps.py, */tests/*, */migrations/* 117 | ``` 118 | 119 | > From your `./backend` folder, you should now be able to execute `pytest --cov-report html --cov .` and then `open htmlcov/index.html` to see the coverage report. 120 | 121 | ## Create a Simple Django App 122 | 123 | At this point, our Django project is pretty useless, so let's create a simple 124 | Twitter-like app that allows users to create messages. It's a nice example for 125 | an app that has a CreateView, a ListView and a DetailView. 126 | 127 | ```bash 128 | cd ~/django-react-in-docker-microservices/backend/backend 129 | django-admin startapp simple_app 130 | cd simple_app 131 | mkdir tests 132 | touch tests/__init__.py 133 | touch tests/test_models.py 134 | ``` 135 | 136 | Whenever we create a new app, we need to tell Django that this app is now part 137 | of our project: 138 | 139 | ```py 140 | # File: ./backend/backend/settings.py 141 | 142 | INSTALLED_APPS = [ 143 | 'django.contrib.admin', 144 | 'django.contrib.auth', 145 | 'django.contrib.contenttypes', 146 | 'django.contrib.sessions', 147 | 'django.contrib.messages', 148 | 'django.contrib.staticfiles', 149 | 'simple_app', 150 | ] 151 | ``` 152 | 153 | First, let's create a test for our upcoming new model. The model doesn't do 154 | much, so we will simply test if we are able to create an instance and save it 155 | to the DB. We are using [mixer](https://github.com/klen/mixer) to help us with 156 | the creation of test-fixtures. 157 | 158 | ```py 159 | # File: ./backend/simple_app/tests/test_models.py 160 | 161 | import pytest 162 | from mixer.backend.django import mixer 163 | 164 | # We need to do this so that writing to the DB is possible in our tests. 165 | pytestmark = pytest.mark.django_db 166 | 167 | 168 | def test_message(): 169 | obj = mixer.blend('simple_app.Message') 170 | assert obj.pk > 0 171 | ``` 172 | 173 | Next, let's create our `Message` model: 174 | 175 | ```py 176 | # File: ./backend/simple_app/models.py 177 | # -*- coding: utf-8 -*- 178 | from __future__ import unicode_literals 179 | from django.db import models 180 | 181 | class Message(models.Model): 182 | user = models.ForeignKey('auth.User') 183 | message = models.TextField() 184 | creation_date = models.DateTimeField(auto_now_add=True) 185 | ``` 186 | 187 | Let's also register the new model with the Django admin, so that we can add 188 | entries to the new table: 189 | 190 | ```py 191 | # File: ./backend/simple_app/admin.py 192 | # -*- coding: utf-8 -*- 193 | from __future__ import unicode_literals 194 | from django.contrib import admin 195 | from . import models 196 | 197 | admin.site.register(models.Message) 198 | ``` 199 | 200 | Whenever we make changes to a model, we need to create and run a migration: 201 | 202 | ```bash 203 | cd ~/django-react-in-docker-microservices/backend/backend 204 | ./manage.py makemigrations simple_app 205 | ./manage.py migrate 206 | ``` 207 | 208 | > At this point you should be able to browse to `localhost:8000/admin/` and see the table of the new `simple_app` app. 209 | 210 | > You should also be able to run `pytest` and see 1 successful test. 211 | 212 | ## Add GraphQL to Django 213 | 214 | Since we have now a Django project with a model, we can start thinking about 215 | adding an API. We will use GraphQL for that. 216 | 217 | ```bash 218 | cd ~/django-react-in-docker-microservices/backend/backend 219 | pip install graphene-django 220 | ``` 221 | 222 | Whenever we add a new app to Django, we need to update our `INSTALLED_APPS` 223 | setting. Because of `graphene-django`, we also need to add one app-specific 224 | setting. 225 | 226 | ```py 227 | # File: ./backend/backend/settings.py 228 | INSTALLED_APPS = [ 229 | 'django.contrib.admin', 230 | 'django.contrib.auth', 231 | 'django.contrib.contenttypes', 232 | 'django.contrib.sessions', 233 | 'django.contrib.messages', 234 | 'django.contrib.staticfiles', 235 | 'graphene_django', 236 | 'simple_app', 237 | ] 238 | 239 | GRAPHENE = { 240 | 'SCHEMA': 'backend.schema.schema', 241 | } 242 | ``` 243 | 244 | Now we need to create our main `schema.py` file. This file is similar to our 245 | main `urls.py` - it's task is to import all the schema-files in our project and 246 | merge them into one big schema. 247 | 248 | ```py 249 | # File: ./backend/backend/schema.py 250 | 251 | import graphene 252 | 253 | class Queries( 254 | graphene.ObjectType 255 | ): 256 | dummy = graphene.String() 257 | 258 | 259 | schema = graphene.Schema(query=Queries) 260 | ``` 261 | 262 | Finally, we need to hook up GraphiQL in our Django `urls.py`: 263 | 264 | ```py 265 | # File: ./backend/backend/urls.py 266 | 267 | from django.conf.urls import url 268 | from django.contrib import admin 269 | from django.views.decorators.csrf import csrf_exempt 270 | 271 | from graphene_django.views import GraphQLView 272 | 273 | 274 | urlpatterns = [ 275 | url(r'^admin/', admin.site.urls), 276 | url(r'^graphiql', csrf_exempt(GraphQLView.as_view(graphiql=True))), 277 | url(r'^gql', csrf_exempt(GraphQLView.as_view(batch=True))), 278 | ] 279 | ``` 280 | 281 | > At this point you should be able to browse to `localhost:8000/graphiql` and run the query `{ dummy }` 282 | 283 | ## Add Message-DjangoObjectType to GraphQL Schema 284 | 285 | If you have used Django Rest Framework before, you know that you have to create 286 | serializers for all your models. With GraphQL it is very similar: You have to 287 | create Types for all your models. 288 | 289 | We will begin with creating a type for our Message model and when we are at it, 290 | we will also create a query that returns all messages. 291 | 292 | In good TDD fashion, we begin with a test for the type and a test for the 293 | query: 294 | 295 | ```py 296 | # File: ./backend/simple_app/tests/test_schema.py 297 | 298 | import pytest 299 | from mixer.backend.django import mixer 300 | 301 | from . import schema 302 | 303 | 304 | pytestmark = pytest.mark.django_db 305 | 306 | 307 | def test_message_type(): 308 | instance = schema.MessageType() 309 | assert instance 310 | 311 | 312 | def test_resolve_all_messages(): 313 | mixer.blend('simple_app.Message') 314 | mixer.blend('simple_app.Message') 315 | q = schema.Query() 316 | res = q.resolve_all_messages(None, None, None) 317 | assert res.count() == 2, 'Should return all messages' 318 | ``` 319 | 320 | In order to make our test pass, we will now add our type and the query: 321 | 322 | ```py 323 | # File: ./backend/simple_app/schema.py 324 | 325 | import graphene 326 | from graphene_django.types import DjangoObjectType 327 | 328 | from . import models 329 | 330 | 331 | class MessageType(DjangoObjectType): 332 | class Meta: 333 | model = models.Message 334 | interfaces = (graphene.Node, ) 335 | 336 | 337 | class Query(graphene.AbstractType): 338 | all_messages = graphene.List(MessageType) 339 | 340 | def resolve_all_messages(self, args, context, info): 341 | return models.Message.objects.all() 342 | ``` 343 | 344 | Finally, we need to update your main `schema.py` file: 345 | 346 | ```py 347 | # File: ./backend/backend/schema.py 348 | 349 | import graphene 350 | 351 | import simple_app.schema 352 | 353 | 354 | class Queries( 355 | simple_app.schema.Query, 356 | graphene.ObjectType 357 | ): 358 | dummy = graphene.String() 359 | 360 | 361 | schema = graphene.Schema(query=Queries) 362 | ``` 363 | 364 | > At this point, you should be able to run `pytest` and get three passing tests. 365 | > You should also be able to add a few messages to the DB at `localhost:8000/admin/simple_app/message/` 366 | > You should also be able to browse to `localhost:8000/graphiql/` and run the query: 367 | 368 | ```graphql 369 | { 370 | allMessages { 371 | id, message 372 | } 373 | } 374 | ``` 375 | 376 | The query `all_messages` returns a list of objects. Let's add another query 377 | that returns just one object: 378 | 379 | ```py 380 | # File: ./backend/simple_app/tests/test_schema.py 381 | 382 | from graphql_relay.node.node import to_global_id 383 | 384 | def test_resolve_message(): 385 | msg = mixer.blend('simple_app.Message') 386 | q = schema.Query() 387 | id = to_global_id('MessageType', msg.pk) 388 | res = q.resolve_messages({'id': id}, None, None) 389 | assert res == msg, 'Should return the requested message' 390 | ``` 391 | 392 | To make the test pass, let's update our schema file: 393 | 394 | ```py 395 | # File: ./backend/simple_app/schema.py 396 | 397 | from graphql_relay.node.node import from_global_id 398 | 399 | class Query(graphene.AbstractType): 400 | message = graphene.Field(MessageType, id=graphene.ID()) 401 | 402 | def resolve_message(self, args, context, info): 403 | rid = from_global_id(args.get('id')) 404 | # rid is a tuple: ('MessageType', '1') 405 | return models.Message.objects.get(pk=rid[1]) 406 | 407 | [...] 408 | ``` 409 | 410 | > At this point you should be able to run `pytest` and see four passing tests 411 | > You should also be able to browse to `graphiql` and run the query `{ message(id: "TWVzc2FnZVR5cGU6MQ==") { id, message } }` 412 | 413 | ## Add Mutation to GraphQL Schema 414 | 415 | Our API is able to return items from our DB. Now it is time to allow to write 416 | messages. Anything that changes data in GraphQL is called a "Mutation". We 417 | want to ensure that our mutation does three things: 418 | 419 | 1. Return a 403 status if the user is not logged in 420 | 1. Return a 400 status and form errors if the user does not provide a message 421 | 1. Return a 200 status and the newly created message if everything is OK 422 | 423 | ```py 424 | # File: ./backend/simple_app/tests/test_schema.py 425 | 426 | from django.contrib.auth.models import AnonymousUser 427 | from django.test import RequestFactory 428 | 429 | def test_create_message_mutation(): 430 | user = mixer.blend('auth.User') 431 | mut = schema.CreateMessageMutation() 432 | 433 | data = {'message': 'Test'} 434 | req = RequestFactory().get('/') 435 | req.user = AnonymousUser() 436 | res = mut.mutate(None, data, req, None) 437 | assert res.status == 403, 'Should return 403 if user is not logged in' 438 | 439 | req.user = user 440 | res = mut.mutate(None, {}, req, None) 441 | assert res.status == 400, 'Should return 400 if there are form errors' 442 | assert 'message' in res.formErrors, ( 443 | 'Should have form error for message field') 444 | 445 | res = mut.mutate(None, data, req, None) 446 | assert res.status == 200, 'Should return 200 if mutation is successful' 447 | assert res.message.pk == 1, 'Should create new message' 448 | ``` 449 | 450 | With these tests in place, we can implement the actual mutation: 451 | 452 | ```py 453 | # File: ./backend/simple_app/schema.py 454 | 455 | import json 456 | 457 | class CreateMessageMutation(graphene.Mutation): 458 | class Input: 459 | message = graphene.String() 460 | 461 | status = graphene.Int() 462 | formErrors = graphene.String() 463 | message = graphene.Field(MessageType) 464 | 465 | @staticmethod 466 | def mutate(root, args, context, info): 467 | if not context.user.is_authenticated(): 468 | return CreateMessageMutation(status=403) 469 | message = args.get('message', '').strip() 470 | # Here we would usually use Django forms to validate the input 471 | if not message: 472 | return CreateMessageMutation( 473 | status=400, 474 | formErrors=json.dumps( 475 | {'message': ['Please enter a message.']})) 476 | obj = models.Message.objects.create( 477 | user=context.user, message=message 478 | ) 479 | return CreateMessageMutation(status=200, message=obj) 480 | 481 | 482 | class Mutation(graphene.AbstractType): 483 | create_message = CreateMessageMutation.Field() 484 | ``` 485 | 486 | This new `Mutation` class is currently not hooked up in our main `schema.py` 487 | file, so let's add that: 488 | 489 | ```py 490 | # File: ./backend/backend/schema.py 491 | 492 | class Mutations( 493 | simple_app.schema.Mutation, 494 | graphene.ObjectType, 495 | ): 496 | pass 497 | 498 | [...] 499 | 500 | schema = graphene.Schema(query=Queries, mutation=Mutations) 501 | ``` 502 | 503 | > At this point you should be able to run `pytest` and get five passing tests. 504 | > You should also be able to browse to `graphiql` and run this mutation: 505 | 506 | ```graphql 507 | mutation { 508 | createMessage(message: "Test") { 509 | status, 510 | formErrors, 511 | message { 512 | id 513 | } 514 | } 515 | } 516 | ``` 517 | 518 | ## Add JWT-Authentication to Django 519 | 520 | One of the most common things that every web application needs is 521 | authentication. During my research I found [django-graph-auth](https://github.com/morgante/django-graph-auth), which is 522 | based on Django Rest Frameworks JWT plugin. There is als [pyjwt](https://pyjwt.readthedocs.io/en/latest/), which would allow you to 523 | implement your own endpoints. 524 | 525 | I didn't have the time to evaluate `django-graph-auth` yet, and I didn't have 526 | the confidence to run my own implementation. For that reason, I chose the 527 | practical approach and used what is tried and tested by a very large user base 528 | and added Django Rest Framework and 529 | [django-rest-framework-jwt](https://github.com/GetBlimp/django-rest-framework-jwt) 530 | to the project. 531 | 532 | We will also need to install [django-cors-headers](https://github.com/ottoyiu/django-cors-headers) because 533 | during local development, the backend and the frontend are served from 534 | different ports, so we need to enable to accept requests from all origins. 535 | 536 | ```bash 537 | cd ~/django-react-in-docker-microservices/backend/backend 538 | pip install djangorestframework 539 | pip install djangorestframework-jwt 540 | pip install django-cors-headers 541 | ``` 542 | 543 | Now we need to update our Django settings with new settings related to the 544 | `rest_framework` and `corsheaders` apps: 545 | 546 | ```py 547 | # File: ./backend/backend/settings.py** 548 | 549 | INSTALLED_APPS = [ 550 | 'django.contrib.admin', 551 | 'django.contrib.auth', 552 | 'django.contrib.contenttypes', 553 | 'django.contrib.sessions', 554 | 'django.contrib.messages', 555 | 'django.contrib.staticfiles', 556 | 'rest_framework', 557 | 'corsheaders', 558 | 'graphene_django', 559 | 'simple_app', 560 | ] 561 | 562 | REST_FRAMEWORK = { 563 | 'DEFAULT_PERMISSION_CLASSES': ( 564 | 'rest_framework.permissions.IsAuthenticated', 565 | ), 566 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 567 | 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 568 | 'rest_framework.authentication.SessionAuthentication', 569 | 'rest_framework.authentication.BasicAuthentication', 570 | ), 571 | } 572 | 573 | CORS_ORIGIN_ALLOW_ALL = True 574 | 575 | MIDDLEWARE_CLASSES = [ 576 | 'django.middleware.security.SecurityMiddleware', 577 | 'django.contrib.sessions.middleware.SessionMiddleware', 578 | 'corsheaders.middleware.CorsMiddleware', 579 | 'django.middleware.common.CommonMiddleware', 580 | 'django.middleware.csrf.CsrfViewMiddleware', 581 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 582 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 583 | 'django.contrib.messages.middleware.MessageMiddleware', 584 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 585 | ] 586 | ``` 587 | 588 | Now that Rest Framework is configured, we need to add a few URLs: 589 | 590 | ```py 591 | # File: ./backend/backend/urls.py 592 | 593 | [...] 594 | from rest_framework_jwt.views import obtain_jwt_token 595 | from rest_framework_jwt.views import refresh_jwt_token 596 | from rest_framework_jwt.views import verify_jwt_token 597 | 598 | 599 | urlpatterns = [ 600 | url(r'^admin/', admin.site.urls), 601 | url(r'^graphiql', csrf_exempt(GraphQLView.as_view(graphiql=True))), 602 | url(r'^gql', csrf_exempt(GraphQLView.as_view(batch=True))), 603 | url(r'^api-token-auth/', obtain_jwt_token), 604 | url(r'^api-token-refresh/', refresh_jwt_token), 605 | url(r'^api-token-verify/', verify_jwt_token), 606 | ] 607 | ``` 608 | 609 | > At this point you should be able to get a token by sending this request: `curl -X POST -d "username=admin&password=test1234" http://localhost:8000/api-token-auth/` 610 | 611 | # Part 2: The Frontend 612 | 613 | In Part 1, we create a Django backend that serves a GraphQL API. In this part 614 | we will create a ReactJS frontend that consumes that API. 615 | 616 | ## Create a new React Project 617 | 618 | Facebook has released a wonderful command line tool that kickstarts a new 619 | ReactJS project with a powerful webpack configuration. Let's use that: 620 | 621 | ```bash 622 | cd ~/django-react-in-docker-microservices/backend 623 | npm install -g create-react-app 624 | create-react-app frontend 625 | cd frontend 626 | yarn start 627 | ``` 628 | 629 | > At this point you should be able to run `yarn start` and the new ReactJS project should open up in a browser tab 630 | 631 | ## Add ReactRouter to React 632 | 633 | ```bash 634 | cd ~/django-react-in-docker-microservices/backend/frontend 635 | yarn add react-router-dom 636 | ``` 637 | 638 | First, we need to replace the example code in `App.js` with our own code: 639 | 640 | ```jsx 641 | import React, { Component } from 'react' 642 | import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom' 643 | import CreateView from './views/CreateView' 644 | import DetailView from './views/DetailView' 645 | import ListView from './views/ListView' 646 | import LoginView from './views/LoginView' 647 | import LogoutView from './views/LogoutView' 648 | 649 | class App extends Component { 650 | render() { 651 | return ( 652 | 653 |
654 |
    655 |
  • Home
  • 656 |
  • Create Message
  • 657 |
  • Login
  • 658 |
  • Logout
  • 659 |
660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 |
668 |
669 | ) 670 | } 671 | } 672 | 673 | export default App 674 | ``` 675 | 676 | You will notice that we imported a bunch of views that don't yet exist. Let's 677 | create them: 678 | 679 | ```bash 680 | cd ~/django-react-in-docker-microservices/backend/frontend/src/ 681 | mkdir views 682 | touch views/CreateView.js 683 | touch views/DetailView.js 684 | touch views/ListView.js 685 | touch views/LoginView.js 686 | touch views/LogoutView.js 687 | ``` 688 | 689 | Now fill each view with the following placeholder code, just change the class 690 | name and text in the div to the corresponding file-name. 691 | 692 | ```jsx 693 | // File: ./frontend/src/views/ListView.js 694 | 695 | import React from 'react' 696 | 697 | export default class ListView extends React.Component { 698 | render() { 699 | return
ListView
700 | } 701 | } 702 | ``` 703 | 704 | > At this point you should be able to run `yarn start` and see your projects. When you click at the links, the corresponding views should be rendered and the URL should change. 705 | 706 | ## Add Apollo to React 707 | 708 | We can now add Apollo to the mix. First we need to install it via yarn: 709 | 710 | ```bash 711 | cd ~/django-react-in-docker-microservices/backend/frontend/ 712 | yarn add react-apollo 713 | ``` 714 | 715 | Similar to react-router, we need to wrap our entire app in a higher order 716 | component that comes with Apollo. There are four steps involved: 717 | 718 | 1. Import relevant imports from `react-apollo` 719 | 2. Create a `networkInterface` that points to the GraphQL API 720 | 3. Instantiate `ApolloClient` using our `networkInterface` 721 | 4. Wrap entire app in `` component 722 | 723 | ```jsx 724 | // File: ./frontend/src/App.js 725 | 726 | [...] 727 | import { 728 | ApolloProvider, 729 | ApolloClient, 730 | createBatchingNetworkInterface, 731 | } from 'react-apollo' 732 | [...] 733 | 734 | const networkInterface = createBatchingNetworkInterface({ 735 | uri: 'http://localhost:8000/gql', 736 | batchInterval: 10, 737 | opts: { 738 | credentials: 'same-origin', 739 | }, 740 | }) 741 | 742 | const client = new ApolloClient({ 743 | networkInterface: networkInterface, 744 | }) 745 | 746 | class App extends Component { 747 | render() { 748 | return ( 749 | 750 | [...] 751 | 752 | ) 753 | } 754 | } 755 | 756 | export default App 757 | ``` 758 | 759 | In order to test if our Apollo installation is working properly, let's implement 760 | our ListView. There are some notable steps involved, too: 761 | 762 | 1. Import relevant imports from `react-apollo` 763 | 2. Create a `query` variable with the GraphQL query 764 | 3. Use `this.props.data.loading` to render "Loading" while query is in flight 765 | 4. Use `this.props.data.allMessages` to render all messages 766 | 5. Wrap `ListView` in `graphql` decorator 767 | 768 | ```jsx 769 | // File: ./frontend/src/views/ListView.js 770 | 771 | import React from 'react' 772 | import { Link } from 'react-router-dom' 773 | import { gql, graphql } from 'react-apollo' 774 | 775 | const query = gql` 776 | { 777 | allMessages { 778 | id, message 779 | } 780 | } 781 | ` 782 | 783 | class ListView extends React.Component { 784 | render() { 785 | let { data } = this.props 786 | if (data.loading || !data.allMessages) { 787 | return
Loading...
788 | } 789 | return ( 790 |
791 | {data.allMessages.map(item => ( 792 |

793 | 794 | {item.message} 795 | 796 |

797 | ))} 798 |
799 | ) 800 | } 801 | } 802 | 803 | ListView = graphql(query)(ListView) 804 | export default ListView 805 | ``` 806 | 807 | > At this point, you should be able to browse to `localhost:3000/` and see a list of messages. If you don't have any message in your database yet, add some at `localhost:8000/admin/` 808 | 809 | ## Add Query with Variables for DetailView 810 | 811 | We have now learned how to attach a simple query to a component, but what about 812 | queries that need some dynamic values. For example, when we click into the 813 | detail view of an item, we can't just query `allMessages`, we need to query 814 | the message with a certain ID. Let's implement that in our DetailView. The 815 | steps are slightly different here: 816 | 817 | 1. Our query is a name query (same name as component class name) 818 | 2. The named query defines a variable `$id` 819 | 3. We pass in `queryOptions` into the `graphql` decorator 820 | 4. `queryOptions` has a field `options` which is an anonymous function that 821 | accepts `props` as a parameter. 822 | 5. Thanks to react-router, we have access to `props.match.params.id` (id is the 823 | `/:id/` part of the path of our Route) 824 | 825 | ```jsx 826 | // File: ./frontend/src/views/DetailView.js 827 | 828 | import React from 'react' 829 | import { gql, graphql } from 'react-apollo' 830 | 831 | const query = gql` 832 | query DetailView($id: ID!) { 833 | message(id: $id) { 834 | id, creationDate, message 835 | } 836 | } 837 | ` 838 | 839 | class DetailView extends React.Component { 840 | render() { 841 | let { data } = this.props 842 | if (data.loading || !data.message) { 843 | return
Loading...
844 | } 845 | return ( 846 |
847 |

Message {data.message.id}

848 |

{data.message.creationDate}

849 |

{data.message.message}

850 |
851 | ) 852 | } 853 | } 854 | 855 | const queryOptions = { 856 | options: props => ({ 857 | variables: { 858 | id: props.match.params.id, 859 | }, 860 | }), 861 | } 862 | 863 | DetailView = graphql(query, queryOptions)(DetailView) 864 | export default DetailView 865 | ``` 866 | 867 | > At this point you should be able to browse to the list view and click at an item and see the DetailView with correct data. 868 | 869 | ## Add Token Middleware for Authentication 870 | 871 | A very common problem is "How to do authentication?". We will use JWT, so on 872 | the frontend, we need a way to make sure that we send the current token with 873 | every request. On the backend, we need to make sure, to attach the current user 874 | to the request, if a valid token has been sent. 875 | 876 | Let's start with the server: 877 | 878 | ```bash 879 | cd ~/django-react-in-docker-microservices/backend/backend/backend/ 880 | touch middleware.py 881 | ``` 882 | 883 | First, we will create a new middleware that attaches the current user to the 884 | request, if a valid token is given. We are standing on the shoulder of giants 885 | and re-use the implementation from django-rest-framework-jwt here: 886 | 887 | ```py 888 | # File: ./backend/backend/middleware.py 889 | 890 | from rest_framework_jwt.authentication import JSONWebTokenAuthentication 891 | 892 | 893 | class JWTMiddleware(object): 894 | def process_view(self, request, view_func, view_args, view_kwargs): 895 | token = request.META.get('HTTP_AUTHORIZATION', '') 896 | if not token.startswith('JWT'): 897 | return 898 | jwt_auth = JSONWebTokenAuthentication() 899 | auth = None 900 | try: 901 | auth = jwt_auth.authenticate(request) 902 | except Exception: 903 | return 904 | request.user = auth[0] 905 | ``` 906 | 907 | Next, we need to add this new middleware to our settings. Note: For the sake 908 | of simplicity, I'm setting `JWT_VERIFY_EXPIRATION = False`, which means that 909 | the token will never expire. In the real world, you will want to set some 910 | expiry time like two weeks here and then in your frontend store the token 911 | creation time together with your token in localStorage and whenever the token 912 | is close to the expiry date, call the `api-token-refresh` endpoint to get a 913 | new token. 914 | 915 | ```py 916 | # File: ./backend/backend/settings.py 917 | 918 | JWT_VERIFY_EXPIRATION = False 919 | 920 | MIDDLEWARE_CLASSES = [ 921 | 'django.middleware.security.SecurityMiddleware', 922 | 'django.contrib.sessions.middleware.SessionMiddleware', 923 | 'backend.middleware.JWTMiddleware', 924 | 'corsheaders.middleware.CorsMiddleware', 925 | 'django.middleware.common.CommonMiddleware', 926 | 'django.middleware.csrf.CsrfViewMiddleware', 927 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 928 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 929 | 'django.contrib.messages.middleware.MessageMiddleware', 930 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 931 | ] 932 | ``` 933 | 934 | Next, we need to update our schema so that we can query the current user. For 935 | sake of simplicity, I'm putting this new endpoint into the `simple_app`. In 936 | reality I would create a new app `user_profile` and add another schema file 937 | in that new app. As usual, we start with our tests: 938 | 939 | ```py 940 | # File: ./backend/simple_app/tests/test_schema.py 941 | 942 | def test_user_type(): 943 | instance = schema.UserType() 944 | assert instance 945 | 946 | 947 | def test_resolve_current_user(): 948 | q = schema.Query() 949 | req = RequestFactory().get('/') 950 | req.user = AnonymousUser() 951 | res = q.resolve_current_user(None, req, None) 952 | assert res is None, 'Should return None if user is not authenticated' 953 | 954 | user = mixer.blend('auth.User') 955 | req.user = user 956 | res = q.resolve_current_user(None, req, None) 957 | assert res == user, 'Should return the current user if is authenticated' 958 | ``` 959 | 960 | Now we can write our implementation: 961 | 962 | ```py 963 | # File: ./backend/simple_app/schema.py 964 | [...] 965 | from django.contrib.auth.models import User 966 | [...] 967 | 968 | 969 | class UserType(DjangoObjectType): 970 | class Meta: 971 | model = User 972 | 973 | [...] 974 | 975 | class Query(graphene.AbstractType): 976 | current_user = graphene.Field(UserType) 977 | 978 | def resolve_current_user(self, args, context, info): 979 | if not context.user.is_authenticated(): 980 | return None 981 | return context.user 982 | 983 | [...] 984 | ``` 985 | 986 | > At this point, you shold be able to browse to `localhost:8000/graphiql` and run the following query: `{ currentUser { id } }`. If you were previously logged in to the Django admin, it should return your admin user as the current user. 987 | 988 | Finally, we need to make sure that every request has the token included. Apollo 989 | has the option to add middlewares to manipulate the requests that are being 990 | sent to the GraphQL API. We will try to read the JWT token from the 991 | `localStorage` and attach it to the request headers: 992 | 993 | ```jsx 994 | // File: ./frontend/src/App.js 995 | 996 | [...] 997 | 998 | const networkInterface = createBatchingNetworkInterface({ 999 | uri: 'http://localhost:8000/gql/', 1000 | batchInterval: 10, 1001 | opts: { 1002 | credentials: 'same-origin', 1003 | }, 1004 | }) 1005 | 1006 | // Add this new part: 1007 | networkInterface.use([ 1008 | { 1009 | applyBatchMiddleware(req, next) { 1010 | if (!req.options.headers) { 1011 | req.options.headers = {} 1012 | } 1013 | 1014 | const token = localStorage.getItem('token') 1015 | ? localStorage.getItem('token') 1016 | : null 1017 | req.options.headers['authorization'] = `JWT ${token}` 1018 | next() 1019 | }, 1020 | }, 1021 | ]) 1022 | 1023 | const client = new ApolloClient({ 1024 | networkInterface: networkInterface, 1025 | }) 1026 | 1027 | [...] 1028 | ``` 1029 | 1030 | Right now, it's a bit hard to test in our frontend if all this is working, 1031 | so let's use the `currentUser` endpoint in our `CreateView` and if the user 1032 | is not logged in, let's redirect to the `/login/` view: 1033 | 1034 | ```jsx 1035 | // File: ./frontend/src/views/CreateView.js 1036 | 1037 | import React from 'react' 1038 | import { gql, graphql } from 'react-apollo' 1039 | 1040 | const query = gql` 1041 | { 1042 | currentUser { 1043 | id 1044 | } 1045 | } 1046 | ` 1047 | 1048 | class CreateView extends React.Component { 1049 | componentWillUpdate(nextProps) { 1050 | if (!nextProps.data.loading && nextProps.data.currentUser === null) { 1051 | window.location.replace('/login/') 1052 | } 1053 | } 1054 | 1055 | render() { 1056 | let { data } = this.props 1057 | if (data.loading) { 1058 | return
Loading...
1059 | } 1060 | return
CreateView
1061 | } 1062 | } 1063 | 1064 | CreateView = graphql(query)(CreateView) 1065 | export default CreateView 1066 | ``` 1067 | 1068 | > At this point, you should be able to click at the `Create Message` link and be redirected to the `/login/` view. 1069 | 1070 | ## Add Login / Logout Views 1071 | 1072 | Now we want to be able to actually login and receive a valid JWT token, so 1073 | lets implement the LoginView. Here are some notable steps: 1074 | 1075 | 1. We use the `fetch` api to send the request to the `api-token-auth` endpoint 1076 | 1. If we provide correct username and password, we save the token to 1077 | localStorage and refresh the page. In the real world, you would want to 1078 | check if there is a `?next` parameter in the URL so that you can redirect 1079 | back to the URL that triggered the login view 1080 | 1. We use `let data = new FormData(this.form)` to collect all current values 1081 | from all input elements that are inside the form 1082 | 1. With a neat little React trick `
this.form = ref}>` we are 1083 | able to get a reference to our form anywhere in our code via `this.form` 1084 | 1085 | ```jsx 1086 | // File: ./frontend/src/views/LoginView.js 1087 | 1088 | import React from 'react' 1089 | 1090 | export default class LoginView extends React.Component { 1091 | handleSubmit(e) { 1092 | e.preventDefault() 1093 | let data = new FormData(this.form) 1094 | fetch('http://localhost:8000/api-token-auth/', { 1095 | method: 'POST', 1096 | body: data, 1097 | }) 1098 | .then(res => { 1099 | res.json().then(res => { 1100 | if (res.token) { 1101 | localStorage.setItem('token', res.token) 1102 | window.location.replace('/') 1103 | } 1104 | }) 1105 | }) 1106 | .catch(err => { 1107 | console.log('Network error') 1108 | }) 1109 | } 1110 | 1111 | render() { 1112 | return ( 1113 |
1114 |

LoginView

1115 | (this.form = ref)} 1117 | onSubmit={e => this.handleSubmit(e)} 1118 | > 1119 |
1120 | 1121 | 1122 |
1123 |
1124 | 1125 | 1126 |
1127 | 1128 | 1129 |
1130 | ) 1131 | } 1132 | } 1133 | ``` 1134 | 1135 | > At this point you should be able to click at `Login`, provide username and password, then go back to `Create Message`. 1136 | 1137 | When we are at it, let's also create our `LogoutView`. That one is pretty 1138 | simple, we just remove the token from localStorage and trigger a page refresh. 1139 | 1140 | ```jsx 1141 | import React from 'react' 1142 | 1143 | export default class LogoutView extends React.Component { 1144 | handleClick() { 1145 | localStorage.removeItem('token') 1146 | window.location.replace('/') 1147 | } 1148 | 1149 | render() { 1150 | return ( 1151 |
1152 |

Logout

1153 | 1154 |
1155 | ) 1156 | } 1157 | } 1158 | ``` 1159 | 1160 | > At this point you should be able to login, browse to `Create Message` view and to logout again. 1161 | 1162 | ## Add Mutation for CreateView 1163 | 1164 | Since we are now able to login and visit the CreateView, it is time to add a 1165 | mutation to the CreateView that allows us to write new messages into the DB. 1166 | 1167 | Notable steps involved: 1168 | 1169 | 1. Create a `const mutation` that is named after the class and needs one variable 1170 | 1. Create a `submitHandler` that calls `this.props.mutate` and provides the 1171 | required variable 1172 | 1. Wrap the class in another `graphql` decorator so that the mutation gets 1173 | registered and is available via `this.props.mutate` 1174 | 1175 | ```jsx 1176 | // File: ./frontend/src/views/CreateView.js 1177 | 1178 | import React from 'react' 1179 | import { gql, graphql } from 'react-apollo' 1180 | 1181 | // This is new: 1182 | const mutation = gql` 1183 | mutation CreateView($message: String!) { 1184 | createMessage(message: $message) { 1185 | status, 1186 | formErrors, 1187 | message { 1188 | id 1189 | } 1190 | } 1191 | } 1192 | ` 1193 | 1194 | const query = gql` 1195 | { 1196 | currentUser { 1197 | id 1198 | } 1199 | } 1200 | ` 1201 | 1202 | class CreateView extends React.Component { 1203 | componentWillUpdate(nextProps) { 1204 | if (!nextProps.data.loading && nextProps.data.currentUser === null) { 1205 | window.location.replace('/login/') 1206 | } 1207 | } 1208 | 1209 | // This is new: 1210 | handleSubmit(e) { 1211 | e.preventDefault() 1212 | let data = new FormData(this.form) 1213 | this.props 1214 | .mutate({ variables: { message: data.get('message') } }) 1215 | .then(res => { 1216 | if (res.status === 200) { 1217 | window.location.replace('/') 1218 | } 1219 | }) 1220 | .catch(err => { 1221 | console.log('Network error') 1222 | }) 1223 | } 1224 | 1225 | render() { 1226 | // This is new: 1227 | let { data } = this.props 1228 | if (data.loading || data.currentUser === null) { 1229 | return
Loading...
1230 | } 1231 | return ( 1232 |
1233 |

Create Message

1234 |
(this.form = ref)} 1236 | onSubmit={e => this.handleSubmit(e)} 1237 | > 1238 |
1239 | 1240 |