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 |
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 `