Sunday, February 23, 2025

Unfolding an Angular App

When Google launched App Engine, their first Google Cloud product, in 2008, I created an unfolding sequence for a web application. 

It can be found here: 

https://www.corememory.org/passing-keys/single-page-list-and-items 

The idea of an "unfolding sequence" is to show the ideal growth of an application, from a small program to the final program in a series of steps. I describe the general idea here:


The steps of an unfolding sequence are not conventional steps. The idea is to inspire good software development behavior at each step in a number of important ways. 


At the end of each step, there is a working program. It gets to an end-to-end working program quickly. 


Each step adds something essential, and global, that makes the final application work, and improves the whole.


If the sequence is good, the essentials added at each step should be ordered by dependencies. That is: the most important essential, global, decisions come earliest in the sequence, because the subsequent, more detailed steps, depend upon them. 


I'm making an attempt here to create another working unfolding sequence, with working code at each step, which has an unfolding quality similar to the one described in my article, and in my description above. It’s the same application, with the same set of features, but for a simple Python / Flask / Datastore / HTML / CSS / JavaScript / AngularJS 1.x-style webapp, on Google App Engine.

Again, the goal is to illustrate how you can start from a tiny working app and grow it to a more complex, fully functional one, with each step building upon the previous one (respecting dependencies) and preserving a working state at every stage.


Overview of the Steps

  1. Minimal “Hello World” on GAE with Flask

  2. Serve a Static HTML Page

  3. Integrate Basic AngularJS on the Client

  4. Set Up a Simple In-Memory Item List

  5. Create a Simple JSON API Endpoint for Items

  6. Display Items in AngularJS from the API

  7. Add New Items via a Form (In-Memory)

  8. Connect to the Google Cloud Datastore

  9. Store and Retrieve Items from Datastore

  10. Refactor for a Separate AngularJS Controller

  11. Add Edit/Update Feature

  12. Delete Items Feature

  13. Add Basic CSS for Styling

  14. Final Cleanup & Deployment Instructions

We’ll keep each step’s code as minimal as possible but still complete enough to be self-contained. The directory structure will also evolve slightly. This is just a toy program: in a real project, you might break things out more granularly (multiple modules for Python code, separate Angular components, etc.), but here I'll focus on clarity of the incremental steps.


Step 1: Minimal “Hello World” on GAE with Flask

What We Do

  • Create a very minimal Flask application that runs on Google App Engine Standard Environment.

  • Show a “Hello World” text to confirm everything is wired up.

Project Structure

myapp/

  ├─ app.yaml

  ├─ main.py

  └─ requirements.txt


app.yaml (minimal config for Python 3 on GAE)

runtime: python310


# If you are using the older Python 2.7 environment or Python 3.7, adjust accordingly.

# If you are using a specific entrypoint, you can set it like so:

# entrypoint: gunicorn -b :$PORT main:app



requirements.txt

Flask==2.2.2



main.py

from flask import Flask


app = Flask(__name__)


@app.route('/')

def hello():

    return "Hello World from Flask on GAE!"


# For local testing using "python main.py"

if __name__ == '__main__':

    app.run(host='127.0.0.1', port=8080, debug=True)


Test It

  • Install dependencies: pip install -r requirements.txt

  • Run locally: python main.py

  • Go to http://localhost:8080

  • Deploy to GAE if desired: gcloud app deploy

At this point, we have a minimal but working GAE app.


Step 2: Serve a Static HTML Page

What We Do

  • Serve a static HTML page instead of returning raw text.

  • Demonstrates rendering HTML from Flask, or simply serving a static file.

Changes

  • Create a templates/ folder and an index.html in it.

  • Return that HTML from the root route.

Updated Project Structure

myapp/

  ├─ app.yaml

  ├─ main.py

  ├─ requirements.txt

  └─ templates/

      └─ index.html


templates/index.html

<!DOCTYPE html>

<html>

  <head>

    <meta charset="utf-8" />

    <title>Step 2 - Static HTML</title>

  </head>

  <body>

    <h1>Hello from a Static HTML Page</h1>

    <p>We are still on Google App Engine!</p>

  </body>

</html>


main.py (only showing the changed parts)

from flask import Flask, render_template


app = Flask(__name__)


@app.route('/')

def root():

    return render_template('index.html')


Now you’ll see the HTML page at your root URL.


Step 3: Integrate Basic AngularJS on the Client

What We Do

  • Add AngularJS (1.x) to the static HTML page via a <script> tag.

  • Confirm that a simple Angular binding works.

Changes

  • We embed a small AngularJS script.

templates/index.html

<!DOCTYPE html>

<html ng-app="myApp">

  <head>

    <meta charset="utf-8" />

    <title>Step 3 - Basic AngularJS</title>

    <!-- AngularJS from a CDN (older 1.x version) -->

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>

  </head>

  <body>

    <h1>Flask + AngularJS</h1>


    <div ng-controller="MainController">

      <input type="text" ng-model="message">

      <p>You typed: {{ message }}</p>

    </div>


    <script>

      angular.module("myApp", [])

        .controller("MainController", function($scope){

          $scope.message = "Hello Angular!";

        });

    </script>

  </body>

</html>


Everything else is the same as Step 2. You should see the two-way binding in action (typing in the input updates the paragraph).


Step 4: Set Up a Simple In-Memory Item List

What We Do

  • We introduce a simple in-memory “items” list in the AngularJS controller.

  • We display the items on the page.

templates/index.html

<!DOCTYPE html>

<html ng-app="myApp">

  <head>

    <meta charset="utf-8" />

    <title>Step 4 - In-Memory Items</title>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>

  </head>

  <body>

    <h1>In-Memory Item List</h1>


    <div ng-controller="MainController">

      <ul>

        <li ng-repeat="item in items track by $index">

          {{ item }}

        </li>

      </ul>

    </div>


    <script>

      angular.module("myApp", [])

        .controller("MainController", function($scope) {

          // In-memory array of items

          $scope.items = ["Apple", "Banana", "Cherry"];

        });

    </script>

  </body>

</html>


No server changes yet, but we have a working item list on the client side.


Step 5: Create a Simple JSON API Endpoint for Items

What We Do

  • Provide a Flask route, e.g., /api/items, that returns JSON.

  • Start with a Python in-memory list so we can see how the front end can fetch it.

Changes in main.py

from flask import Flask, render_template, jsonify


app = Flask(__name__)


# In-memory items list on the server

ITEMS = ["Apple", "Banana", "Cherry"]


@app.route('/')

def root():

    return render_template('index.html')


@app.route('/api/items', methods=['GET'])

def get_items():

    return jsonify(ITEMS)


templates/index.html
Same as step 4 except we remove the hardcoded array in $scope.items and load from /api/items using Angular’s $http.

<!DOCTYPE html>

<html ng-app="myApp">

  <head>

    <meta charset="utf-8" />

    <title>Step 5 - JSON API</title>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>

  </head>

  <body>

    <h1>Item List from /api/items</h1>


    <div ng-controller="MainController">

      <ul>

        <li ng-repeat="item in items track by $index">

          {{ item }}

        </li>

      </ul>

    </div>


    <script>

      angular.module("myApp", [])

        .controller("MainController", function($scope, $http){

          $scope.items = [];


          // Fetch items from the server

          $http.get('/api/items')

               .then(function(response) {

                 $scope.items = response.data;

               })

               .catch(function(err){

                 console.error("Error fetching items:", err);

               });

        });

    </script>

  </body>

</html>


Now the items are coming from the Flask endpoint as JSON.


Step 6: Display Items in AngularJS from the API (Already Done)

In the prior step, we already displayed items from the new /api/items endpoint. So we can consider that as Step 6 done. Essentially, we have a minimal single-page read-only list.


Step 7: Add New Items via a Form (In-Memory)

What We Do

  • Add a POST endpoint in Flask that allows adding new items to the in-memory list.

  • Use AngularJS $http.post to call this endpoint from a simple form.

Changes in main.py

from flask import Flask, render_template, jsonify, request


app = Flask(__name__)


ITEMS = ["Apple", "Banana", "Cherry"]


@app.route('/')

def root():

    return render_template('index.html')


@app.route('/api/items', methods=['GET'])

def get_items():

    return jsonify(ITEMS)


@app.route('/api/items', methods=['POST'])

def add_item():

    data = request.get_json()

    new_item = data.get('item')

    if new_item:

        ITEMS.append(new_item)

    return jsonify({"success": True, "items": ITEMS})


templates/index.html

<!DOCTYPE html>

<html ng-app="myApp">

  <head>

    <meta charset="utf-8" />

    <title>Step 7 - Add Items</title>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>

  </head>

  <body>

    <h1>Add Items to the In-Memory List</h1>


    <div ng-controller="MainController">

      <ul>

        <li ng-repeat="item in items track by $index">

          {{ item }}

        </li>

      </ul>


      <input type="text" ng-model="newItem" placeholder="Enter new item" />

      <button ng-click="addItem()">Add</button>

    </div>


    <script>

      angular.module("myApp", [])

        .controller("MainController", function($scope, $http) {

          $scope.items = [];

          $scope.newItem = '';


          // Initial load

          $http.get('/api/items')

               .then(function(response) {

                 $scope.items = response.data;

               });


          $scope.addItem = function() {

            if(!$scope.newItem) return;

            $http.post('/api/items', { item: $scope.newItem })

                 .then(function(response) {

                   $scope.items = response.data.items;

                   $scope.newItem = '';

                 })

                 .catch(function(err){

                   console.error("Error adding item:", err);

                 });

          };

        });

    </script>

  </body>

</html>


We now have a working form that can add new items in memory on the server.


Step 8: Connect to the Google Cloud Datastore

What We Do

  • Move away from the in-memory Python list to a real Datastore model.

  • For simplicity, we use the google-cloud-datastore client library.

Changes

  • Add google-cloud-datastore to requirements.txt

  • In main.py, initialize the Datastore client.

requirements.txt

Flask==2.2.2

google-cloud-datastore==2.11.0


main.py (showing the relevant additions)

import os

from flask import Flask, render_template, jsonify, request

from google.cloud import datastore


app = Flask(__name__)


# Initialize the Datastore client

# This will automatically pick up GCP credentials when deployed on App Engine

datastore_client = datastore.Client()


@app.route('/')

def root():

    return render_template('index.html')


@app.route('/api/items', methods=['GET'])

def get_items():

    query = datastore_client.query(kind='Item')

    # We'll sort by creation timestamp or something similar later

    results = list(query.fetch())

    # Extract the "name" property from each entity

    items = [ entity.get('name') for entity in results ]

    return jsonify(items)


@app.route('/api/items', methods=['POST'])

def add_item():

    data = request.get_json()

    new_item = data.get('item')

    if new_item:

        key = datastore_client.key('Item')

        entity = datastore.Entity(key=key)

        entity.update({

            'name': new_item

        })

        datastore_client.put(entity)

    # Return updated list

    query = datastore_client.query(kind='Item')

    results = list(query.fetch())

    items = [ entity.get('name') for entity in results ]

    return jsonify({"success": True, "items": items})


On local development, you’ll need to configure the Google Cloud SDK to emulate or connect to a real project. When deployed on GAE, the datastore.Client() picks up credentials automatically.


Step 9: Store and Retrieve Items from Datastore

At this point (step 8), we already are storing and retrieving items from the Datastore.

  • If you deploy to GAE, your items will persist across requests (unlike the old in-memory approach).

  • Confirm that the “Add” button in the UI now populates Datastore.

This step is effectively complete with the code introduced in step 8.


Step 10: Refactor for a Separate AngularJS Controller File

What We Do

  • Move our inline AngularJS code to its own file for clarity.

  • Introduce a static/ folder to host JavaScript, CSS, etc.

New Project Structure

myapp/

  ├─ app.yaml

  ├─ main.py

  ├─ requirements.txt

  ├─ static/

  │   ├─ app.js

  │   └─ style.css

  └─ templates/

      └─ index.html


static/app.js


angular.module("myApp", [])

  .controller("MainController", function($scope, $http) {

    $scope.items = [];

    $scope.newItem = '';


    // Initial load of items

    $http.get('/api/items')

         .then(function(response) {

           $scope.items = response.data;

         });


    $scope.addItem = function() {

      if(!$scope.newItem) return;

      $http.post('/api/items', { item: $scope.newItem })

           .then(function(response) {

             $scope.items = response.data.items;

             $scope.newItem = '';

           })

           .catch(function(err){

             console.error("Error adding item:", err);

           });

    };

  });


templates/index.html (note we load app.js from /static/)

<!DOCTYPE html>

<html ng-app="myApp">

  <head>

    <meta charset="utf-8" />

    <title>Step 10 - AngularJS Refactor</title>


    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>

    <script src="/static/app.js"></script>

  </head>

  <body>

    <h1>Refactored AngularJS App</h1>

    <div ng-controller="MainController">

      <ul>

        <li ng-repeat="item in items track by $index">

          {{ item }}

        </li>

      </ul>

      <input type="text" ng-model="newItem" placeholder="Enter new item" />

      <button ng-click="addItem()">Add</button>

    </div>

  </body>

</html>


Everything should still work exactly as before, but now we have a cleaner separation of concerns.


Step 11: Add Edit/Update Feature

What We Do

  • Let’s store an ID in Datastore to uniquely identify each item.

  • Provide an endpoint to update an existing item (by ID).

  • In the UI, allow a simple “Edit” mode.

Changes in main.py
We’ll store a random or auto-generated ID, or we can just rely on Datastore’s key ID. For clarity, we’ll store the item as an entity with .key.id as the unique ID, plus a “name” property.

  1. When we fetch items, we return a list of objects: [{ "id": 123, "name": "Apple" }, ...].

  2. Add a /api/items/<id> route with PUT to handle updates.

from flask import Flask, render_template, jsonify, request

from google.cloud import datastore


app = Flask(__name__)

datastore_client = datastore.Client()


@app.route('/')

def root():

    return render_template('index.html')


@app.route('/api/items', methods=['GET'])

def get_items():

    query = datastore_client.query(kind='Item')

    results = list(query.fetch())

    # Return objects with id and name

    items = []

    for entity in results:

        items.append({

            "id": entity.key.id,

            "name": entity.get('name')

        })

    return jsonify(items)


@app.route('/api/items', methods=['POST'])

def add_item():

    data = request.get_json()

    new_item = data.get('item')

    if new_item:

        key = datastore_client.key('Item')

        entity = datastore.Entity(key=key)

        entity.update({"name": new_item})

        datastore_client.put(entity)


    return jsonify({"success": True, "items": get_items_data()})


@app.route('/api/items/<int:item_id>', methods=['PUT'])

def update_item(item_id):

    data = request.get_json()

    updated_name = data.get('name')

    if updated_name:

        key = datastore_client.key('Item', item_id)

        entity = datastore_client.get(key)

        if entity:

            entity['name'] = updated_name

            datastore_client.put(entity)


    return jsonify({"success": True, "items": get_items_data()})


def get_items_data():

    # Helper to refetch items as an array

    query = datastore_client.query(kind='Item')

    results = list(query.fetch())

    items = []

    for entity in results:

        items.append({

            "id": entity.key.id,

            "name": entity.get('name')

        })

    return items


static/app.js (added an “edit” mode)

angular.module("myApp", [])

  .controller("MainController", function($scope, $http) {

    $scope.items = [];

    $scope.newItem = '';


    // Load items

    $http.get('/api/items').then(function(response) {

      $scope.items = response.data;

    });


    $scope.addItem = function() {

      if(!$scope.newItem) return;

      $http.post('/api/items', { item: $scope.newItem })

           .then(function(response) {

             $scope.items = response.data.items;

             $scope.newItem = '';

           });

    };


    $scope.editingId = null;   // track which item is being edited

    $scope.editedName = '';


    $scope.startEdit = function(item) {

      $scope.editingId = item.id;

      $scope.editedName = item.name;

    };


    $scope.saveEdit = function(item) {

      $http.put('/api/items/' + item.id, { name: $scope.editedName })

           .then(function(response) {

             $scope.items = response.data.items;

             $scope.editingId = null;

             $scope.editedName = '';

           });

    };

  });


templates/index.html (display edit form)

<!DOCTYPE html>

<html ng-app="myApp">

  <head>

    <meta charset="utf-8" />

    <title>Step 11 - Edit Items</title>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>

    <script src="/static/app.js"></script>

  </head>

  <body>

    <h1>Edit Items</h1>

    <div ng-controller="MainController">

      <ul>

        <li ng-repeat="item in items track by item.id">

          <span ng-if="editingId != item.id">

            {{ item.name }}

            <button ng-click="startEdit(item)">Edit</button>

          </span>

          <span ng-if="editingId == item.id">

            <input type="text" ng-model="editedName" />

            <button ng-click="saveEdit(item)">Save</button>

          </span>

        </li>

      </ul>


      <input type="text" ng-model="newItem" placeholder="Enter new item" />

      <button ng-click="addItem()">Add</button>

    </div>

  </body>

</html>


Now you can click “Edit” on an item, change its name, then “Save” to update it in Datastore.


Step 12: Delete Items Feature

What We Do

  • Provide a DELETE endpoint and add a “Delete” button in the UI.

Changes in main.py

@app.route('/api/items/<int:item_id>', methods=['DELETE'])

def delete_item(item_id):

    key = datastore_client.key('Item', item_id)

    datastore_client.delete(key)

    return jsonify({"success": True, "items": get_items_data()})


static/app.js (add a deleteItem function)

$scope.deleteItem = function(item) {

  $http.delete('/api/items/' + item.id)

       .then(function(response) {

         $scope.items = response.data.items;

       });

};


templates/index.html (add a “Delete” button)

<li ng-repeat="item in items track by item.id">

  <span ng-if="editingId != item.id">

    {{ item.name }}

    <button ng-click="startEdit(item)">Edit</button>

    <button ng-click="deleteItem(item)">Delete</button>

  </span>

  ...

</li>


This completes basic CRUD (Create, Read, Update, Delete).


Step 13: Add Basic CSS for Styling

What We Do

  • Create a static/style.css with some simple styling.

  • Load it in index.html.

static/style.css

body {

  font-family: sans-serif;

  margin: 20px;

}

h1 {

  color: #333;

}

ul {

  list-style: none;

  padding: 0;

}

li {

  margin-bottom: 5px;

}

input[type="text"] {

  padding: 5px;

  margin-right: 5px;

}

button {

  margin-right: 5px;

  padding: 5px 10px;

}


templates/index.html

<!DOCTYPE html>

<html ng-app="myApp">

  <head>

    <meta charset="utf-8" />

    <title>Step 13 - Basic CSS</title>

    <link rel="stylesheet" href="/static/style.css" />

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>

    <script src="/static/app.js"></script>

  </head>

  <body>

    <h1>Styled Item List</h1>

    <div ng-controller="MainController">

      <!-- content as before -->

    </div>

  </body>

</html>


We now have a slightly nicer look.


Step 14: Final Cleanup & Deployment Instructions

What We Do

  • Review all dependencies and ensure everything is coherent.

  • Make sure app.yaml references Python 3.

  • Confirm that the environment variables or other settings are correct for production.

  • Deploy with gcloud app deploy.

Final app.yaml

runtime: python310

# The default entrypoint for Flask apps on GAE might be:

# entrypoint: gunicorn -b :$PORT main:app


# If you want a flexible environment or specific scaling, you can add more config.


Deploying

Make sure you have the Google Cloud SDK installed and are authenticated:

gcloud auth login

gcloud config set project [YOUR_PROJECT_ID]

  1. From the myapp/ directory:

  2. gcloud app deploy

  3. Once deployed, visit https://[YOUR_PROJECT_ID].appspot.com to see your application in action.

At this point, you have a fully functional, end-to-end CRUD single-page application, built with:

  • Python/Flask on Google App Engine

  • Datastore as the database

  • AngularJS 1.x for the front-end

  • Basic HTML/CSS

  • A clean incremental progression from “Hello World” to a CRUD app.


Conclusion

This sequence shows how to unfold an application in meaningful, dependency-respecting steps:

  1. Get the minimal GAE + Flask skeleton working.

  2. Serve a static page and confirm end-to-end.

  3. Add AngularJS for interactive front-end.

  4. Demonstrate an in-memory list for quick feedback.

  5. Introduce JSON endpoints and hook them up.

  6. Make the UI display data from the new endpoints.

  7. Add form submission and server-side POST.

  8. Migrate to Cloud Datastore.

  9. Confirm store/retrieve is working.

  10. Separate front-end code.

  11. Add update (PUT).

  12. Add delete (DELETE).

  13. Add styling.

  14. Deploy to production.

At each step, the application remains in a working state, building your confidence that each piece is correct before moving on. This approach helps maintain a clear structure, fosters iterative refinement, points out important dependencies, and keeps the code alive.