How the fuck do I build a Django, Django REST Framework, Angular 1.1.x and Webpack project?

Datetime:2016-08-23 00:26:58          Topic: AngularJS  Webpack  Django           Share

I've had enough of the bullshit: the JS community sucks balls. All I wanted to do is build a simple, replicable Angular project with a Django back-end, but NO! That's not so simple when the community guiding your learning barely has any grasp on what it means to standardize.

If you're struggling, here's a guide to building a basic Angular app driven by Django and organized by Webpack.

Problem

I want to set up an Angular 1.1.x project and feed it data from a Django server. I'd like to use Django REST Framework (DRF) to build a RESTful API. I also want to bundle my JavaScript assets. For now, I plan to run the site on a single server.

> Pre-reqs

  • Python 2.x
  • Django 1.9.x
  • npm 2.15.8+
  • Webpack 1.13.x ( sudo npm i -g webpack )
  • ESLint 2.13.1+ ( sudo npm i -g eslint )
  • NodeJS 4.4.7+

Summary

  1. Scaffold your project. Create your initial directories.
  2. Scaffold your Django project.
  3. Set environment variables needed to run your Django server.
  4. Install Django REST Framework and configure Django using your environment variables.
  5. Run your Django server using dev settings.
  6. Initialize your npm package and install your front-end JS dependencies.
  7. Create an Angular entry-point and load initial dependencies.
  8. Tell Django to load your app.
  9. Create the Angular app base template.
  10. Write a home component.
  11. Write Angular routes leading to your home component and a 404 page.
  12. Add angular-router directives to the app entrypoint template.
  13. Try out your REST API in your Angular app.

Go!

Let's begin.

0. Set up your Python virtual environment

mkvirtualenv mysite

1. Scaffold your project. Create your initial directories.

We want to focus on modularity throughout development. Therefore, there are lots of directories we'll end up utilizing. We want our directory tree to look like this initially:

mysite  
├── backend
│   ├── docs
│   ├── requirements
└── frontend
    ├── app
    │   ├── components
    │   └── shared
    ├── assets
    │   ├── css
    │   ├── img
    │   ├── js
    │   └── libs
    ├── config
    ├── dist
        └── js

Do it:

mkdir -p mysite \  
         backend/docs/ backend/requirements/ \ 
         frontend/app/shared/ \
         frontend/app/components/ \
         frontend/config \
         frontend/assets/img/ frontend/assets/css/ \
         frontend/assets/js/ frontend/assets/libs/ \ 
         frontend/dist/js/

*Note: This project structure was inspired by a few different projects. I find this organization ideal, but you don't have to. As you walk through this guide, you should stick to this structure so you don't encounter hiccups.

2. Scaffold your Django project.

Inside of the backend/ directory, create a Django project:

$ python django-project.py startproject mysite

Also create your requirements.txt here:

pip freeze > requirements.txt

Within the backend/mysite/ directory (your Django project), scaffold the directories in which your API will live:

touch applications/__init__.py applications/api/__init__.py \  
        applications/api/v1/__init__.py applications/api/v1/routes.py \
        applications/api/v1/serializers.py applications/api/v1/viewsets.py

Now, create your settings directory structure:

mkdir mysite/settings && touch mysite/settings/__init__.py \  
        mysite/settings/base.py mysite/settings/dev.py mysite/settings/prod.py
        mysite/dev.env mysite/prod.env

3. Set environment variables needed to run your Django server.

In this step, I prefer to use django-environ in order to manage my environment variables. There are many ways to do this, but the django-environ package simplified this process for me tremendously, so I use it in all of my projects.

Install django-environ :

pip install django-environ

In mysite/dev.env , add the following:

DATABASE_URL=sqlite:///mysite.db  
DEBUG=True  
FRONTEND_ROOT=path/to/mysite/frontend/  
SECRET_KEY=_some_secret_key

We're going to use these environment variables in our settings. The benefit of having our environment-specific variables in separate files is mostly that such a setup provides ease as we switch between environments. In our case, the dev.env file is a list of variables we'd use in our local development environment. On the same token, our prod.env file contains production-specific variables.

*Note: The SECRET_KEY can be taken from the settings.py that was generated by django-admin.py startproject .

4. Install Django REST Framework and configure Django using your environment variables.

Install DRF:

pip install djangorestframework

Populate settings/base.py with the following:

Tell Django where to find environment variables.

import environ

project_root = environ.Path(__file__) - 3  
env = environ.Env(DEBUG=(bool, False),)  
CURRENT_ENV = 'dev' # 'dev' is the default environment

# read the .env file associated with the settings that're loaded
env.read_env('./mysite/{}.env'.format(CURRENT_ENV))

Establish a database. In this case, we're going to use django-environ built-in SQLite settings.

DATABASES = {  
    'default': env.db()
}

Specify a SECRET_KEY and the DEBUG state of the app.

SECRET_KEY = env('SECRET_KEY')  
DEBUG = env('DEBUG')

Add DRF to the pool of apps Django needs to utilize.

# Application definition
INSTALLED_APPS = [

    ...

    # Django Packages
    'rest_framework',
]

Our URLs will live in URLs module generated with our base Django project.

ROOT_URLCONF = 'mysite.urls'

Tell Django where to find all of our templates and other static assets.

STATIC_URL = '/static/'  
STATICFILES_FINDERS = [  
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
STATICFILES_DIRS = [  
    env('FRONTEND_ROOT')
]

TEMPLATES = [  
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [env('FRONTEND_ROOT')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

As per the TEMPLATES setting, Django should search for templates inside of your frontend/ directory. That's where your Angular app will live. We're only using Django to serve up the template within which our Angular app loads, which will be done via an entry-point directive. If you don't know what I mean, keep reading...

Populate settings/dev.py :

from mysite.settings.base import *


CURRENT_ENV = 'dev'

Here, we tell Django that this settings file inherits settings from base.py and overrides the CURRENT_ENV constant string found in base.py . We're saying, "use this value instead of the value found in the module being inherited from."

5. Create an API.

We need something to test out our Angular services with, so let's build a small API. You can skip this, but I don't recommend you do that. Knowing that the Angular app setup worked entirely in terms of its potential to facilitate HTTP requests is important.

Generate a Django app.

manage.py startapp games

Create a Django model in games/models.py

class Game(models.model):  
    title = models.CharField(max_length=255)
    description = models.CharField(max_length=750)

Create a DRF serializer for the Game model in applications/api/v1/serializers.py

from rest_framework.serializers import ModelSerializer  
from applications.games.models import Game


class GameSerializer(ModelSerializer):

    class Meta:
        model = Game

Create a DRF viewset for your model in applications/api/v1/viewsets.py

from rest_framework import viewsets  
from applications.games.models import Game  
from applications.api.v1.serializers import GameSerializer


class GameViewSet(viewsets.ModelViewSet):  
    queryset = Game.objects.all()
    serializer_class = GameSerializer

In applications/api/v1/routes.py , register routes using DRF's router registration features.

from rest_framework import routers  
from applications.api.v1.viewsets import GameViewSet


api_router = routers.SimpleRouter()  
api_router.register('games', GameViewSet)

Map URLs to registered DRF route inside of mysite/urls.py

from django.contrib import admin  
from django.conf.urls import include, url  
from applications.api.v1.routes import api_router

urlpatterns = [  
    url(r'^admin/', admin.site.urls),

    # API:V1
    url(r'^api/v1/', include(api_router.urls)),
]

6. Run your Django server using dev settings.

manage.py runserver --DJANGO_SETTINGS_MODULE=mysite.settings.dev

By passing the DJANGO_SETTINGS_MODULE into runserver , we're telling Django to serve using specific settings.

If everything works, you should be able to go to localhost:8000/api/v1/games and see a DRF response. If you do, it's time to build an Angular app. If not, address your issues. If you're stuck, leave a comment and I'll help you out.

7. Initialize your npm package and install your front-end JS dependencies.

Your Angular app won't work the way we want it to without the right dependencies installed. It's time to install the minimum packages you'll need to get up and running.

Initialize your NPM package. From within frontend/ , run

npm init --yes

By passing the --yes flag into init , you're telling NPM to generate a package.json using NPM defaults. Otherwise, if you don't pass that in, you'll have to answer questions... Boring.

Install dev dependencies.

$ npm install --save-dev eslint eslint-loader

Install general dependencies.

npm install --save eslint eslint-loader angular angular-resource angular-route json-loader mustache-loader lodash

Your package.json file in frontend/ should look something like this:

{
  "name": "my-app",
  "version": "0.0.1",
  "description": "This is my first angular app.",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "eslint": "^2.13.1",
    "eslint-loader": "^1.4.0"
  },
  "dependencies": {
    "angular": "^1.5.7",
    "angular-resource": "^1.5.7",
    "angular-route": "^1.5.7",
    "eslint": "^3.0.0",
    "eslint-loader": "^1.4.0",
    "hogan.js": "^3.0.2",
    "json-loader": "^0.5.4",
    "lodash": "~3.10.1",
    "mustache-loader": "^0.3.1",
    "webpack": "^1.13.1"
  }
}

Here's what you just installed:

  • eslint - a fancy linter, so we can keep our JavaScript neat (consistent).
  • eslint-loader - for running eslint via Webpack. I'll explain the concept of "loaders" in a bit.
  • angular - an MVC framework. If you didn't know that, you should consider leaving this page.
  • angular-resource - our (Angular) HTTP library of choice. It's an abstraction of $http .
  • json-loader - a loader (again, used by Webpack) for extracting JSON from .json files via require() throughout our app.
  • mustache-loader - a loader we'll use for parsing mustache templates. Mustache templates are fun.

It's safe for me to assume that you don't know how all of these packages will play together. Don't worry, bruh.

8. Create an Angular entry-point, declare initial dependencies, declare initial globals.

In frontend/app/app.js , add the following:

/* Libs */
require("angular/angular");  
require("angular-route/angular-route");  
require("angular-resource/angular-resource");

/* Globals */
_ = require("lodash");  
_urlPrefixes = {  
  API: "api/v1/",
  TEMPLATES: "static/app/"
};

/* Components */

/* App Dependencies */
angular.module("myApp", [  
  "ngResource",
  "ngRoute",
]);

/* Config Vars */
var routesConfig = require("./routes");

/* App Config */
angular.module("myApp").config(routesConfig);

app.js is where Webpack will look for modules to bundle together. I personally appreciate this organization of declarations and method calls, but such an organization isn't necessary. There are 6 sections:

  • Libs - general libraries used throughout our Angular app
  • Globals - reserved global variables we can use throughout our app.
  • Components - project-specific modules
  • App Dependencies - the declaration of our app entrypoint name and its dependencies
  • Config Vars - variables where configurations are stored, such as our route config
  • App Config - inject configs into our app using the stored configs from the previous section.

In order for the globals to work, you need to tell ESLint about which variables are global.

In config/eslint.json add the following:

{
    "env": {
        "node": true
    },
    "extends": "eslint:recommended",
    "rules": {
        "indent": [
            "error",
            2
        ],
        "linebreak-style": [
            "error",
            "unix"
        ],
        "quotes": [
            "error",
            "double"
        ],
        "semi": [
            "error",
            "always"
        ],
        "no-console": 0
    },
    "globals": {
        "_": true,
        "_urlPrefixes": true,
        "angular": true,
        "inject": true,
        "window": true
    },
    "colors": true
}

There are a few globals we've warned ESLint about here:

  • _ to represent lodash.
  • _urlPrefixes which is an object we'll use throughout our app for URLs. I'll explain later.
  • angular to represent the AngularJS object driving our entire application.
  • inject which will be used for Angular dependency injections.
  • window which just represents the JavaScript window object, which is a representation of the DOM.

9. Configure Webpack.

Now that we've laid out most of our app dependencies, we can build our Webpack config file. Webpack will consolidate all of our dependencies, as well as app-specific modules we build, into one file, a bundle .

In frontend/webpack.config.js add the following.

module.exports = {  
    entry: "./app/app.js",
    output: {
        path: "./dist/js/",
        filename: "bundle.js",
        sourceMapFilename: "bundle.js.map",
    },

    watch: true,

    // eslint config
    eslint: {
      configFile: './config/eslint.json'
    },

    module: {
        preLoaders: [{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "eslint-loader"
        }],
        loaders: [
          { test: /\.css$/, loader: "style!css" },
          { test: /\.html$/, loader: "mustache-loader" },
          { test: /\.json$/, loader: "json-loader" }]
    },

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

In order to have Webpack bundle our static dependencies together, we need to tell it where to look for those dependencies, which dependencies to process and how to manage them prior to bundling.

Let's have a quick look at what you're telling Webpack via webpack.config.js :

  • entry is the path to what Webpack needs to start bundling. This can either be a full path or a path that's relative to where webpack.config.js is located. In this case, it's the latter.
  • output is an object containing a path , which is the directory wherein the bundled dependencies should be placed; the filename is the name of the bundle; and, in this case, we decided to use sourceMapFilename to indicate what our source map should be called (which'll also be placed where our bundle is).
  • watch tells Webpack to monitor file changes while it's running. If this isn't set to true , Webpack will run through its bundling process once and exit.
  • eslint contains ESLint-specific settings, which are used by eslint-loader .
  • module tells Webpack what to do with the modules it touches.
  • module.preLoaders say what to do before bundling. In this case, we want to run our modules (excluding modules installed by npm) through eslint.
  • module.loaders is where a loader sequence is specified. In our case, we only set test and loader , where test tells Webpack which modules to run a loader on (by matching the filename with a regex pattern), and loader tells Webpack which loader to use on the modules that match the regex pattern in test . Each loader is listed in a string and separated using an exclamation mark. Ex: loader!another_loader!yet_another_loader
  • module.preLoaders say which preLoaders to run modules through. The settings we use are just like the settings we wrote in module.loaders .

But Greg, what the fuck's the difference between preLoaders and loaders? I'm glad you asked, my swearing friend!!

A loadertells Webpack how to bundle require() d files. A loader looks at a module, says, "Hey, as you pack this into one file as a string, this is how it should be transformed for the bundle."

A preLoaderprocesses code prior to loaders, for instance, to lint your JavaScript modules.

A postLoaderis a Webpack plugin that processes code after it's already been bundled. We haven't specified any postLoaders for the sake of simplicity.

10. Tell Django to load your app.

Right now, all you've managed to do is tell Webpack what to create and how to create what needs to be created. (Hell, at this point, I'd be surprised if you tried to run Webpack and it runs without error. If it does, I'm the fuckin' man.)

Since Django imposes its own URL processor on our application, we're mostly at the mercy of Django for managing what's entered into a user's browser location bar. However, we're building a single-page application using a totally different framework, and we want that application to have full control of the user's input. All we need Django for is to serve the single page on which our SPA is running. Therefore, ...

In backend/mysite/mysite/urls.py add the following in the urlpatterns list

# Web App Entry
url(r'^$', TemplateView.as_view(template_name="app/index.html"), name='index'),

That says that when a user goes to mysite.com/ , env('FRONTEND_ROOT') + app/index.html will be searched for by STATICFILES_FINDERS in order to render a generic-looking HTML template.

11. Create the Angular app base template.

Our frontend/app/components/app/index.html template should look like a regular Django template.

In frontend/app/index.html , add the following:

{% load staticfiles %}
<!DOCTYPE html>  
<html ng-app="myApp">  
  <head>
    <title>My Site</title>
    <script src="{% static 'dist/js/bundle.js' %}"></script>
  </head>
  <body>
  </body>
</html>

At this point, you should be able to run Webpack. If you run your Django server and go to localhost:8000 , you should see a blank page. If not, let me know.

12. Write a home component.

Let's write our first component. It'll display text on our page when a user goes to localhost:8000 .

Create a component directory and base files. In frontend/app/components/ :

mkdir home && touch home/home-controller.js home/home.js home/home.html

In frontend/app/components/home/home.html , add the following:

<div ng-controller="HomeController as ctrl">  
    <div>
        <h1>Home!</h1>
    </div>
</div>

Now, add the following to frontend/app/components/home/home-controller.js :

function HomeController() {  
  var that = this;
  that.foo = "Foo!";
  console.log(that); // should print out the controller object
}

angular.module("Home")  
  .controller("HomeController", [
    HomeController
  ]);

Your Angular module definition should be declared in home.js :

angular.module("Home", []);

require("./home-controller");

Now, we can refer to "Home" in a module definition's dependency space. Let's do that.

In app/app.js add the following:

/* Components */
require("./components/home/home");

/* App Dependencies */
angular.module("myApp", [  
  "Home", // this is our component
  "ngResource",
  "ngRoute"
]);

13. Write Angular routes leading to your home component and a 404 page.

We need to configure our first route. When a user goes to localhost:8000 , Angular should take control upon loading the Django template that's rendered. To do this, we're going to leverage angular-router .

In frontend/app/routes.js , write the following:

function routesConfig($routeProvider) {  
  $routeProvider
    .when("/", {
      templateUrl: _urlPrefixes.TEMPLATES + "components/home/home.html",
      label: "Home"
    })
    .otherwise({
      templateUrl: _urlPrefixes.TEMPLATES + "404.html"
    });
}

routesConfig.$inject = ["$routeProvider"];

module.exports = routesConfig;

If we don't add _urlPrefixes.TEMPLATES , angular-router will assume that components/home/home.html is an actual URL the server recognizes. Because of STATIC_URL in our Django settings, assuming localhost:8000/components/home/home.html will work is incorrect.

Also, if you haven't already, you'll notice otherwise({...}) in the routes code. That's how 404s will be handled.

In frontend/app/404.html , put the following:

<h1>NOT FOUND</h1>

14. Add angular-router directives to the app entrypoint template.

Now, we need to tell Angular where the switching of views will happen as a user navigates. In order to do this, we use more of angular-router 's power.

In the <head> tags of frontend/app/index.html , add:

<base href="/">

Now, in the <body> tags, add:

<div ng-view></div>

Your index.html should look like this:

{% load staticfiles %}
<!DOCTYPE html>  
<html ng-app="myApp">  
  <head>
    <title>My Site</title>
      <script src="{% static 'dist/js/bundle.js' %}" ></script>
      <base href="/">
  </head>
  <body>
    <div>
      <div ng-view></div>
    </div>
  </body>
</html>

Run Webpack. Go to localhost:8000 . You should see what's in home/home.html . (If not, let me know!)

15. Try out your REST API in your Angular app.

If everything's gone right up to this point, you should be able to write Angular services for your Django API. Let's build a small component to see if we can do that. This component should list games. I assume you've already populated your database so that an HTTP request to localhost:8000/api/v1/games will return a list of games.

Create a component scaffold in frontend/app/components/ :

mkdir -p game/list/ && touch game/list/game-list-controller.js game/list/game-list-controller_test.js game/game-service.js game/game.js game/game.html

This component should list games. I assume you've already populated your database so that an HTTP request to localhost:8000/api/v1/games will return a list of games.

In game/game-service.js :

function GameService($resource) {  
  /**
   * @name GameService
   *
   * @description
   * A service providing game data.
   */

  var that = this;

  /**
   * A resource for retrieving game data.
   */
  that.GameResource = $resource(_urlPrefixes.API + "games/:game_id/");

  /**
   * A convenience method for retrieving Game objects.
   * Retrieval is done via a GET request to the ../games/ endpoint.
   * @param {object} params - the query string object used for a GET request to ../games/ endpoint
   * @returns {object} $promise - a promise containing game-related data
   */
  that.getGames = function(params) {
    return that.GameResource.query(params).$promise;
  };
}

angular.module("Game")  
  .service("GameService", ["$resource", GameService]);

Notice the reference to $resource , which is what we're using in order to set up our HTTP mechanisms in our service.

In game/list/game-list-controller.js :

function GameListController(GameService) {  
  var that = this;

  /* Stored game objects. */
  that.games = [];

  /**
   * Initialize the game list controller.
   */
  that.init = function() {
    return GameService.getGames().then(function(games) {
      that.games = games;
    });
  };
}

angular.module("Game")  
  .controller("GameListController", [
    "GameService",
    GameListController
  ]);

In game/game.html :

<div ng-controller="GameListController as ctrl" ng-init="ctrl.init()">  
    <div>
        <h1>Games</h1>
        <!-- Test List -->
        <ul>
          <li ng-repeat="game in ctrl.games">{{ game.title }}</li>
        </ul>
    </div>
</div>

In game/game.js :

angular.module("Game", []);

require("./list/game-list-controller");  
require("./game-service");

Next, refer to the component in app.js :

/* Components */
require("./components/game/game");

/* App Dependencies */
angular.module("myApp", [  
  "Home",
  "Game",
  "ngResource",
  "ngRoute"
]);

Finally, we're going to have to configure routes for our list of games, so in frontend/app/routes.js , add the following to the $routeProvider object:

.when("/game", {
      templateUrl: _urlPrefixes.TEMPLATES + "components/game/list/game-list.html",
      label: "Games"
    })

Run Webpack again. Everything should compile correctly. If not, let me know.

Go to localhost:8000/#/games and you should see a list of games.

Done

That's that.

Concerns/Thoughts

There are some concerns here:

  • Environment variables can fuck you up if you don't know how to manage them. Although the way we're working with environment variables here works locally, it may not be the right way to go in production. You can make it work, though, as the method described is fairly robust, AFAIK. Just, don't throw secrets into your .env files.
  • Your Angular app is tightly coupled with Django . This won't make your project composable between the front-end and back-end. If you rip Django out of the picture, your routes will have to change, unless you configure your new backend to behave the same way that staticfiles works. You'll also need to change the index.html file where your Angular entrypoint lives. There isn't much work involved when breaking away from Django on a small project. Larger projects with a lot more coupling could make for a bad time, though. My advice: the only coupling between your Angular app and Django server should be the index entry-point.
  • Deployment should be done the same way any Django app deployment should be.

That's about it. If you have absolutely ANY questions or you encounter some sorta hiccup, please, please, please let me know in the comments below. That way, I can improve this guide for future readers.

>>> Cheat <<<

I'll share a GitHub repo with all of this code in it soon...

I miss you, Renee. Let's go.





About List