Let’s start with the big picture: the project structure.

development files

Open the backend folder. As a Flask developer you will immediately see a familiar pattern:

  • a templates with your HTML Jinja templates
  • our Flask application in app.py
  • a requirements.txt file for our python dependencies
  • a gunicorn_config.py file which configures our Gunicorn WSGI
  • a Dockerfile to containarize the application for easier development and deployment
  • a static folder holding the bundled .js and .css files, they will be imported in our HTML
  • a serverless.yml file which defines our IAS (Infrastructure As Code), we will use it later to deploy a Lambda function

secrets

One thing is missing though: the .env file. You will see an .env.example file with keys and empty values. Rename it to .env.local and fill it with the according values througout the rest of this documentation. When you are ready to deploy to production, you will create a .env file and copy the info there.

Open app.py, you will see that if we run the file locally with bash local.sh, we will use the .env.local file, otherwise we use the default .env file.

app.py
if __name__ == "__main__":
    dotenv.load_dotenv(".env.local")
    print("Loading development environment", flush=True)
else:
    dotenv.load_dotenv(".env")
    print("Loading production environment", flush=True)

Over the course I will refer to both as the .env file, use the one according to your current stage. At the top of your .env file will be a “SECRET_KEY”, used by your Flask application. Make sure to change to something secure, like a UUID4 from here.

bundling

Now let’s look at the most interesting part: the bundling. Open package.json and take a look at:

package.json
  "scripts": {
    "watch-js": "node build.mjs",
    "watch-css": "postcss ./src/css/*.css --dir ./backend/static/css --watch",
    "build-css": "postcss ./src/css/*.css --dir ./backend/static/css",
    "watch": "npm-run-all --parallel watch-css watch-js"
  },

You can run watch-css, watch-js or watch to watch and bundle the corresponding file types. As you can see we use PostCSS and a build.mjs file to transpile our content from our src to our static folder. Open postcss.config.js, here you can see the plugins that we use to:

  • allow importing other .css files
  • reduce our bundle size to a minimum
  • use modern CSS features while making the bundle usable by legacy browsers
postcss.config.js
module.exports = {
    plugins: [
        require('postcss-import'),
        require('cssnano')({ // https://github.com/cssnano/cssnano
            preset: 'default',
        }),
        require('postcss-preset-env')({ // https://preset-env.cssdb.org/features/
            stage: 2
        })
    ],
};

Now take a look at the build.mjs file. Here we loop over each file in our src/js directory and start watching them for changes, when one occurs we create bundle with the same name.

build.mjs
import * as esbuild from 'esbuild'
import fs from "fs";

const srcDir = "./src/js/"
const distDir = "./backend/static/js/"
const entryPoints = fs.readdirSync(srcDir);
console.log(entryPoints);

for (const entryPoint of entryPoints) {
  if (entryPoint.endsWith(".jsx") || entryPoint.endsWith(".js")) {
    let ctx = await esbuild.context({
      entryPoints: [srcDir + entryPoint],
      outfile: `${distDir}${entryPoint.split('.')[0]}.js`,
      bundle: true,
    });
  
    await ctx.watch();
    console.log(`Watching ${entryPoint}...`);
  } else {
    console.log("Skipping " + entryPoint)
  }
}

All of this combined allows you to write advanced JS and CSS and see it in realtime.

deployment files

Besides the Dockerfile in backend we have a few more files, which we will need for deploying our Docker containers in a provisioned way, for example the nginx folder, which holds all the Configuration for our web server. In the root dir you can see the Dockerfile for that. When building, Docker copies the static files and serves them through Nginx instead of our Flask application, since this is more efficient.

Besides that we have a docker-compose.yml file, for managing multiple Docker containers and making the communicate.

Requirements

While developing you will need:

For deployment install:

Running the application

Once we are done with configuration, you will have two choices to start the app:

  • WHEN WORKING LOCALLY: bash local.sh will start a local (!DEVELOPMENT!) server which listens on Port 8080 or
  • WHEN DEPLOYING: bash docker.sh will run the same application with a propper WSGI and a production-ready Nginx web-server

Before we can do that, there is just a bit more configuration to do:

  • Setup and connect our database
  • Configure Stripe
  • Create an OAuth application for authentification
  • setup emails

Continue reading the next section to learn how to do this