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
Minimal “Hello World” on GAE with Flask
Serve a Static HTML Page
Integrate Basic AngularJS on the Client
Set Up a Simple In-Memory Item List
Create a Simple JSON API Endpoint for Items
Display Items in AngularJS from the API
Add New Items via a Form (In-Memory)
Connect to the Google Cloud Datastore
Store and Retrieve Items from Datastore
Refactor for a Separate AngularJS Controller
Add Edit/Update Feature
Delete Items Feature
Add Basic CSS for Styling
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.
When we fetch items, we return a list of objects: [{ "id": 123, "name": "Apple" }, ...].
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]
From the myapp/ directory:
gcloud app deploy
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:
Get the minimal GAE + Flask skeleton working.
Serve a static page and confirm end-to-end.
Add AngularJS for interactive front-end.
Demonstrate an in-memory list for quick feedback.
Introduce JSON endpoints and hook them up.
Make the UI display data from the new endpoints.
Add form submission and server-side POST.
Migrate to Cloud Datastore.
Confirm store/retrieve is working.
Separate front-end code.
Add update (PUT).
Add delete (DELETE).
Add styling.
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.