This task is to build a safe RESTful API that extends the Flask PWA - Programming for the Web Task. From the parent task, students will abstract the database and management to an REST API with key authentication. The PWA will then be retooled to GET request the data from the REST API and POST request data to the REST API. The PWA UI for the API will be rapidly prototyped using the Bootstrap frontend framework.
The API instructions focus on modelling how to build and test an API incrementally. The PWA instructions focus on using the Bootstrap frontend framework to prototype an enhanced UI/UX frontend rapidly using Bootstrap components and classes.
Note
The template for this project has been pre-populated with assets from the Flask PWA task, including the logo, icons and database. Students can migrate their own assets if they wish.
- VSCode or GitHub Codespaces
- Python 3.x
- GIT 2.x.x +
- SQLite3 Editor
- Start git-bash 6.Thunder Client
- pip/pip3 installs
pip install Flask
pip install flask_cors
pip install flask_limiter
pip install flask_wtf
pip install flask_csp
pip install jsonschema
pip install requestsImportant
MacOS and Linux users may have a pip3 soft link instead of pip, run the below commands to see what path your system is configured with and use that command through the project. If neither command returns a version, then likely Python 3.x needs to be installed.
pip show pip
pip3 show pipNote
Your repository is already cloned and Git is configured. This section focuses on the essential Git workflow for tracking your development progress.
- Verify you're in the correct directory:
pwd- Check what files have changed:
git statusAs you complete each step in the instructions, commit your changes:
- Stage files you've modified:
git add api.py- Or stage multiple files:
git add api.py database_manager.py- Commit with a descriptive message:
git commit -m "Add basic API route for extensions"If you want to experiment without affecting your main code:
- Create and switch to a new branch:
git checkout -b feature/my-experiment- Work on your changes, then commit:
git add .
git commit -m "Test new feature"- Switch back to main branch:
git checkout main- If your experiment worked, merge it:
git merge feature/my-experiment- Push your commits to GitHub:
git push- If pushing a new branch:
git push -u origin feature/my-experimentUse clear, descriptive commit messages:
git commit -m "feat: add POST endpoint for new extensions"
git commit -m "fix: resolve JSON validation error"
git commit -m "docs: update README with API examples"For detailed commits, add a description:
git commit -m "feat: implement API authentication
- Add API key validation
- Configure rate limiting
- Add security logging"See what you've accomplished:
git log --onelineOr view the last 5 commits:
git log --oneline -5Tip
Commit regularly! A good rule is to commit whenever you complete a working feature or step in the instructions. This makes it easy to track your progress and undo changes if needed.
In your previous Flask PWA, everything ran in one application:
main.pyhandled both database queries AND served web pages- This works fine for small projects, but has limitations:
- Hard to reuse data in other applications
- Can't control who accesses your data
- Difficult to update frontend without touching backend
The API Architecture:
- API Server (
api.pyon port 3000): Manages data & database - PWA Server (
main.pyon port 5000): Serves web pages & handles user interface - They communicate using HTTP requests (like talking over the internet)
Real-World Analogy: Think of a restaurant:
- Kitchen (API) = Prepares food (data)
- Dining room (PWA) = Where customers interact
- Waiters (HTTP requests) = Carry orders and food between them
This means multiple "dining rooms" (mobile app, website, desktop app) can all use the same "kitchen" (API).
Watch: Build a Flask API in 12 Minutes
Note
The video uses Postman, this tutorial uses Thunder Client a VS Code extension that has similar functionality, it does not work in a Codespace, you will need to use it in desktop version of VS Code.
Students can create files as they are needed. This structure defines the correct directory structure for all files. As students touch each file, they should refer to this structure to ensure the file path is correct.
├── database
│ └─── data_source.db
├── static
│ ├── css
│ │ ├──bootstrap.min.css
│ │ └──style.css
│ ├── icons
│ │ ├──desktop_screenshot.png
│ │ ├──icon-128x128.png
│ │ ├──icon-192x192.png
│ │ ├──icon-384x384.png
│ │ ├──icon-512x512.png
│ │ └──mobile_screenshot.png
│ ├── images
│ │ ├──favicon.png
│ │ └──logo.png
│ ├─── js
│ │ ├──app.js
│ │ ├──bootstrap.bundle.min.js
│ │ └──serviceWorker.js
│ └── manifest.json
├── templates
│ ├── partials
│ │ ├──footer.html
│ │ └──menu.html
│ ├──index.html
│ ├──layout.html
│ └──privacy.html
├── api.py
├── database_manager.py
├── LICENSE
└── main.py
This Python implementation in 'api.py':
- Imports all the required dependencies for the whole project.
- Configure the 'Cross Origin Request' policy.
- Configure the rate limiter.
- Configure a route for the root
/with a GET method to return stub data and a 200 response. - Configure a route to /add_extension with a POST method to return stub data and a 201 response.
Understanding CORS - Cross-Origin Resource Sharing:
By default, browsers block requests between different "origins" (different ports/domains) for security.
- Your PWA runs on
http://localhost:5000 - Your API runs on
http://localhost:3000 - These are different origins!
Why do we need CORS? Without CORS, when your PWA tries to request data from the API, the browser will block it saying: "These are different servers, this might be dangerous!"
The CORS Configuration:
CORS(api)= "Allow requests from other origins"CORS_HEADERS= "Allow Content-Type header" (needed for JSON)
In Production: You'd limit CORS to only allow YOUR specific PWA domain, not all origins.
Understanding Rate Limiting:
Rate limiting protects your API from abuse by limiting how many requests each user can make.
Without Rate Limiting:
- Someone could write a script to call your API millions of times
- This could crash your server or fill your database with junk
- Called a "Denial of Service" (DoS) attack
How This Configuration Works:
default_limits: Everyone gets max 200 requests/day, 50/hour@limiter.limit("3/second"): This specific endpoint allows only 3 requests per secondget_remote_address: Tracks limits per IP addressstorage_uri="memory://": Stores counters in RAM (resets when server restarts)
What Happens When Limit Exceeded?
User receives 429 Too Many Requests error.
from flask import Flask
from flask import request
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import logging
import database_manager as dbHandler
api = Flask(__name__)
cors = CORS(api)
api.config["CORS_HEADERS"] = "Content-Type"
limiter = Limiter(
get_remote_address,
app=api,
default_limits=["200 per day", "50 per hour"],
storage_uri="memory://",
)
@api.route("/", methods=["GET"])
@limiter.limit("3/second", override_defaults=False)
def get():
return ("API Works"), 200
@api.route("/add_extension", methods=["POST"])
@limiter.limit("1/second", override_defaults=False)
def post():
data = request.get_json()
return data, 201
if __name__ == "__main__":
api.run(debug=True, host="0.0.0.0", port=3000)# Verify your API is working:
python api.py
# Should show: * Running on all addresses (0.0.0.0)
# Should show: * Running on http://127.0.0.1:3000
# Test in Thunder Client:
# 1. GET request to http://localhost:3000 → Should return "API Works" with status 200
# 2. POST request to http://localhost:3000/add_extension with JSON body → Should echo back your JSON with status 201API not starting? Check that Flask, flask_cors, and flask_limiter are installed:
pip list | grep -i flaskHaving issues? See đź”§ Troubleshooting - API Server Issues
Every API response includes a status code that tells the client what happened.
Common Status Codes You'll Use:
| Code | Name | Meaning | When to Use |
|---|---|---|---|
| 200 | OK | Request succeeded | GET requests that return data |
| 201 | Created | New resource created | POST requests that add to database |
| 400 | Bad Request | Client sent invalid data | JSON validation fails |
| 401 | Unauthorised | Missing/invalid authentication | Wrong API key |
| 404 | Not Found | Resource doesn't exist | URL endpoint not found |
| 429 | Too Many Requests | Rate limit exceeded | User hits rate limit |
| 500 | Internal Server Error | Server crashed | Your code has a bug |
Why They Matter:
- Allows client (PWA) to handle different situations appropriately
- Standard way for all web services to communicate success/failure
- Makes debugging easier (specific error codes)
Example:
return ("API Works"), 200 # Tell client: "Success! Here's your data"
return {"error": "Invalid JSON"}, 400 # Tell client: "You sent bad data"
return {"error": "Unauthorised"}, 401 # Tell client: "You're not allowed"Understanding jsonify() in APIs:
In your previous PWA, you returned Python data directly to Jinja2 templates. In an API, you must return JSON text for HTTP responses.
Without jsonify:
return {"name": "test"}
# Response: Python object (only works within Python)With jsonify:
return jsonify({"name": "test"})
# Response: '{"name": "test"}' (JSON text string)
# + Sets Content-Type: application/json header
# + Properly escapes special charactersWhat's the Difference?
| Aspect | Python Dict | JSON String (via jsonify) |
|---|---|---|
| Data Type | dict object |
Text string |
| Use | Within Python | Over HTTP/network |
| Format | Python-specific | Universal standard |
| Headers | None | Content-Type: application/json |
Extend the get(): method in api.py to get data from the database via the dbHandler and return it to the request with a status 200.
def get():
content = dbHandler.extension_get("%")
return (content), 200This Python implementation in 'database_manager.py'
- Imports all the required dependencies for the project
- Connects to the SQLite3 database
- Executes a query
- Converts the query data to a JSON structure
- Returns the JSON data
Why Build Dictionaries from Rows?
Instead of returning tuples like (1, 'name', 'link'), we build dictionaries for JSON:
# Without dictionaries (tuple):
data = cur.fetchall() # Returns: [(1, 'name', 'link'), (2, 'name2', 'link2')]
# With dictionaries:
migrate_data = [
dict(extID=row[0], name=row[1], hyperlink=row[2], ...)
for row in cur.fetchall()
]
# Returns: [{"extID": 1, "name": "name", "hyperlink": "link"}, ...]Why?
- Self-documenting: JSON keys show what each value means
- Order-independent: Can access
data["name"]instead ofdata[1] - Flexible: Can add/remove fields without breaking client code
from flask import jsonify
import sqlite3 as sql
from jsonschema import validate
from flask import current_app
def extension_get(lang):
con = sql.connect("database/data_source.db")
cur = con.cursor()
cur.execute("SELECT * FROM extension")
migrate_data = [
dict(
extID=row[0],
name=row[1],
hyperlink=row[2],
about=row[3],
image=row[4],
language=row[5],
)
for row in cur.fetchall()
]
return jsonify(migrate_data)# Verify your GET endpoint returns database data:
# 1. Ensure api.py is running: python api.py
# 2. Test in Thunder Client:
# GET request to http://localhost:3000
# Should return JSON array with all extensions from database
# 3. Check the response format is valid JSON
# 4. Verify all database fields appear in the response (extID, name, hyperlink, about, image, language)No data returning? Check that database_manager.py is in the same directory and data_source.db exists in the database folder.
Having issues? See đź”§ Troubleshooting - Database Connection Issues
Understanding Query Parameters:
Query parameters are data sent in the URL after a ? symbol. They're perfect for filtering, searching, and sorting.
Example:
http://127.0.0.1:3000?lang=python
└─ Query parameter: lang=python
Use Query Parameters for:
- Filtering data (search, sort, filter)
- Data visible in URL
- GET requests
- Bookmarkable URLs
Security Note: We validate that lang contains only alphabetic characters using .isalpha() to prevent SQL injection attacks!
Extend the get(): method in api.py to either get all data or data that matches a language parameter from the database by
- Validating the argument is "lang" and that the "lang" is only alpha characters for security.
- Passing the language request to the dbHandler.
- If no language is specified, the wildcard
%will be passed. - Return the data from dbHandler to the request.
- Return a status
200.
def get():
# For security data is validated on entry
if request.args.get("lang") and request.args.get("lang").isalpha():
lang = request.args.get("lang")
lang = lang.upper()
content = dbHandler.extension_get(lang)
else:
content = dbHandler.extension_get("%")
return (content), 200Extend the database query in the extension_get(): method in the database_manager.py to filter the SQL query based on the argument parameter and return it as JSON data where:
- If no valid parameter is passed, the function will return the entire database in a JSON format because of the
%wildcard. - If a valid parameter is passed, the database will be queried with a `WHERE language LIKE' SQL query, and all matching languages (if any) will be returned in JSON format.
def extension_get(lang):
con = sql.connect("database/data_source.db")
cur = con.cursor()
cur.execute("SELECT * FROM extension WHERE language LIKE ?;", [lang])
migrate_data = [
dict(
extID=row[0],
name=row[1],
hyperlink=row[2],
about=row[3],
image=row[4],
language=row[5],
)
for row in cur.fetchall()
]
return jsonify(migrate_data)# Verify your filtered GET endpoint works:
# 1. Test in Thunder Client:
# GET http://localhost:3000?lang=python
# Should return only Python extensions
# 2. Test with different languages:
# GET http://localhost:3000?lang=bash
# GET http://localhost:3000?lang=html
# 3. Test without parameter:
# GET http://localhost:3000
# Should return ALL extensions
# 4. Verify case insensitivity (lowercase 'python' returns results)Filtering not working? Check that your language values in the database match the ENUM values (uppercase: PYTHON, CPP, BASH, etc.)
Having issues? See đź”§ Troubleshooting - Query Parameter Issues
Extend the /add_extension route in api.py to pass the POST data to the 'dbHandler' and set up a driver to return the response with a 201 status code.
def post():
data = request.get_json()
response = dbHandler.extension_add(data)
return responseExtend the extension_add(): method in the database_manager.py to be a driver that returns the received data to the POST request.
def extension_add(response):
data = response
return data, 200# Verify your POST endpoint echoes data back:
# 1. Test in Thunder Client:
# POST to http://localhost:3000/add_extension
# Body (JSON): {"name": "test", "hyperlink": "https://test.com", "about": "test", "image": "https://test.jpg", "language": "PYTHON"}
# Should return the same JSON with status 200
# 2. Verify the response matches what you sent
# 3. Note: Data is NOT saved to database yet (coming in next steps)POST not working? Ensure you set Content-Type to "application/json" in Thunder Client headers.
Having issues? See đź”§ Troubleshooting - POST Request Issues
Understanding JSON Schema Validation:
In your previous PWA, you trusted user input and inserted it directly into the database. This is dangerous! Users could:
- Send malicious code (XSS attacks)
- Send wrong data types that crash your app
- Send extra fields you don't expect
JSON Schema Solution: Define strict rules for what valid JSON looks like, then reject anything that doesn't match.
Update the extension_add(): method in database_manager.py to validate the JSON and return a message and response code. The schema provided validates the JSON with the following rules:
- All 5 properties are required.
- No extra properties are allowed.
- The data type for all 5 properties is string.
- The hyperlink pattern enforces the URL to start with
https://marketplace.visualstudio.com/items?itemName=, and the characters<and>are not allowed to prevent XXS attacks. - The image pattern requires https:// but
<and>are not allowed to prevent XXS attacks. - Languages must be enumerated with the list of languages.
Breaking Down the JSON Schema:
schema = {
"type": "object", # Must be a JSON object {...}
"required": ["name", "hyperlink", ...], # These 5 fields MUST exist
"additionalProperties": False, # Extra fields = REJECTED
"properties": {
"name": {"type": "string"}, # Must be text
"hyperlink": {
"type": "string",
"pattern": r"^https:\/\/marketplace\.visualstudio\.com..."
},
}
}Understanding the Hyperlink Pattern:
Let's break down the regex pattern piece by piece:
^https:\/\/marketplace\.visualstudio\.com\/items\?itemName=
└─ MUST start with this exact URL
(?!.*[<>])
└─ FORBIDS < and > characters (prevents XSS attacks)
[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]*$
└─ ALLOWS only safe URL characters
Why Block < and >? These characters can inject HTML/JavaScript:
<script>alert('hacked!')</script>Testing Your Patterns: Use https://regex101.com/ to test and understand regex patterns.
Important
You can use https://regex101.com/ to design and test patterns for your database design. Regular expressions in Python require a raw string (with the r prefix) due to the way characters need to be escaped.
if validate_json(data):
return {"message": "Extension added successfully"}, 201
else:
return {"error": "Invalid JSON"}, 400
schema = {
"type": "object",
"validationLevel": "strict",
"required": [
"name",
"hyperlink",
"about",
"image",
"language",
],
"properties": {
"name": {"type": "string"},
"hyperlink": {
"type": "string",
"pattern": r"^https:\/\/marketplace\.visualstudio\.com\/items\?itemName=(?!.*[<>])[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]*$",
},
"about": {"type": "string"},
"image": {
"type": "string",
"pattern": r"^https:\/\/(?!.*[<>])[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]*$",
},
"language": {
"type": "string",
"enum": ["PYTHON", "CPP", "BASH", "SQL", "HTML", "CSS", "JAVASCRIPT"],
},
},
"additionalProperties": False,
}
def validate_json(json_data):
try:
validate(instance=json_data, schema=schema)
return True
except:
return FalseSample JSON data for you to test the API:
{"name": "test", "hyperlink": "https://marketplace.visualstudio.com/items?itemName=123.html", "about": "This is a test", "image": "https://test.jpg", "language": "BASH"}
# Verify your JSON validation works:
# 1. Test VALID JSON in Thunder Client:
# POST to http://localhost:3000/add_extension
# Body: {"name": "test", "hyperlink": "https://marketplace.visualstudio.com/items?itemName=123.html", "about": "This is a test", "image": "https://test.jpg", "language": "BASH"}
# Should return: {"message": "Extension added successfully"} with status 201
#
# 2. Test INVALID JSON (missing field):
# Body: {"name": "test", "hyperlink": "https://test.com"}
# Should return: {"error": "Invalid JSON"} with status 400
#
# 3. Test INVALID URL pattern:
# Body with hyperlink: "https://wrong-domain.com"
# Should return: {"error": "Invalid JSON"} with status 400
#
# 4. Test XSS attack prevention:
# Body with image: "https://test.com/<script>alert('xss')</script>"
# Should return: {"error": "Invalid JSON"} with status 400Validation always passing? Check that jsonschema is installed:
pip install jsonschemaHaving issues? See đź”§ Troubleshooting - JSON Validation Issues
Update the extension_add(): method in database_manager.py to INSERT the JSON data into the database. The extID is not required as it has been configured to auto increment in the database table.
def extension_add(data):
if validate_json(data):
con = sql.connect("database/data_source.db")
cur = con.cursor()
cur.execute(
"INSERT INTO extension (name, hyperlink, about, image, language) VALUES (?, ?, ?, ?, ?);",
[
data["name"],
data["hyperlink"],
data["about"],
data["image"],
data["language"],
],
)
con.commit()
con.close()
return {"message": "Extension added successfully"}, 201
else:
return {"error": "Invalid JSON"}, 400# Verify data is being saved to database:
# 1. Test POST with valid JSON in Thunder Client
# 2. Check response: {"message": "Extension added successfully"} with status 201
# 3. Verify data in database:
# - Open database/data_source.db in SQLite3 Editor
# - Run query: SELECT * FROM extension ORDER BY extID DESC LIMIT 1;
# - Should see your newly added extension
# 4. Test GET endpoint to confirm new data appears:
# GET http://localhost:3000Data not saving? Check database file permissions and that the connection path is correct:
database/data_source.dbHaving issues? See đź”§ Troubleshooting - Database Insert Issues
Understanding API Authentication:
Currently, ANYONE can POST data to your API and add extensions to your database!
Authentication Types:
| Type | Description | Use Case |
|---|---|---|
| API Key (We use this) | Shared secret string | App-to-app communication |
| User Auth (OAuth, JWT) | Individual user credentials | User-specific data |
| No Auth | Public access | Read-only public data |
Why API Keys for This Project?
- We're not authenticating individual users
- We're authenticating the PWA application itself
- Only authorized applications can add extensions
How It Works:
- Server side (api.py):
auth_key = "4L50v92nOgcDCYUM" # Secret key (like a password)
if request.headers.get("Authorisation") == auth_key:
# Key matches = allow request
else:
return {"error": "Unauthorised"}, 401 # Wrong key = reject- Client side (main.py) - coming later:
app_header = {"Authorisation": "4L50v92nOgcDCYUM"} # Include key in request
response = requests.post(
"http://127.0.0.1:3000/add_extension",
json=data,
headers=app_header # Send key with request
)Security Considerations:
⚠️ NEVER commit API keys to GitHub (use environment variables in production)⚠️ NEVER expose API keys in client-side JavaScript (they're in main.py server-side only)⚠️ Use HTTPS in production (or keys can be intercepted)- ✅ Generate unique keys using https://acte.ltd/utils/randomkeygen
Analogy: API key = Building access card
- Only people with the card can enter
- If card is stolen, change the lock code
API Key Authorisation is a common method for authorising an application, site, or project. In this scenario, the API is not authorising a specific user. This is a very simple implementation of API Key Authorisation.
Extend the api.py to store the key as a variable. Students will need to generate a unique basic 16 secret key with https://acte.ltd/utils/randomkeygen.
auth_key = "4L50v92nOgcDCYUM"Extend the def post(): method in api.py to request the authorisation attribute from the post head, compare it to the auth_key, and process the appropriate response.
def post():
if request.headers.get("Authorisation") == auth_key:
data = request.get_json()
response = dbHandler.extension_add(data)
return response
else:
return {"error": "Unauthorised"}, 401# Verify API key authentication works:
# 1. Test POST WITHOUT auth header in Thunder Client:
# POST to http://localhost:3000/add_extension
# Body: (valid JSON)
# Headers: (none)
# Should return: {"error": "Unauthorised"} with status 401
#
# 2. Test POST WITH WRONG auth key:
# Headers: Authorisation: wrongkey123
# Should return: {"error": "Unauthorised"} with status 401
#
# 3. Test POST WITH CORRECT auth key:
# Headers: Authorisation: 4L50v92nOgcDCYUM (or your generated key)
# Should return: {"message": "Extension added successfully"} with status 201
#
# 4. Verify only authenticated requests can add dataAuthentication not blocking requests? Check the header name is exactly "Authorisation" (not "Authorization") to match the code.
Having issues? See đź”§ Troubleshooting - Authentication Issues
Extend the api.py with the implementation below, which should be inserted directly below the imports. This will configure the logger to log to a file for security analysis.
api_log = logging.getLogger(__name__)
logging.basicConfig(
filename="api_security_log.log",
encoding="utf-8",
level=logging.DEBUG,
format="%(asctime)s %(message)s",
)# Verify your complete API is working:
# 1. Test GET all extensions: http://localhost:3000
# 2. Test GET filtered: http://localhost:3000?lang=python
# 3. Test POST with auth: Should add to database
# 4. Test POST without auth: Should return 401
# 5. Test POST with invalid JSON: Should return 400
# 6. Check api_security_log.log file was created
# 7. Restart API and verify everything still worksReady to move on? Your API is complete! Next, you'll build the PWA interface to interact with this API.
Note
This implementation uses the Bootstrap frontend CSS & JS design framework. Version 5.3.3 has been included in the static files.
├── templates
│ ├── partials
│ │ ├──footer.html
│ │ └──menu.html
│ ├──index.html
│ ├──layout.html
│ └──privacy.html
This Jinga2/HTML implementation in layout.html:
- Security features are defined in the head.
- The menu and footer are defined in a partial for easy maintenance.
- The body will be defined by the block content when the
layout.htmlis inherited. - Bootstrap components (CSS & JavaScript) are linked.
- JS Components, including the PWA service worker, are linked.
<!DOCTYPE html>
<html lang="en">
<head>
<meta
http-equiv="Content-Security-Policy"
content="base-uri 'self'; default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' *; media-src 'self'; font-src 'self'; connect-src 'self'; object-src 'self'; worker-src 'self'; frame-src 'none'; form-action 'self'; manifest-src 'self'"
/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="static/css/style.css" />
<title>VS Code Extensions for Software Engineering</title>
<link rel="manifest" href="static/manifest.json" />
<link rel="icon" type="image/x-icon" href="static/images/favicon.png" />
<meta name="theme-color" content="" />
<link href="static/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
{% include "partials/menu.html" %}
<main>{% block content %}{% endblock %}</main>
{% include "partials/footer.html" %}
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/serviceWorker.js"></script>
<script src="static/js/app.js"></script>
</body>
</html>This HTML implementation provides a full-width horizontal rule and a Bootstrap column containing a link to the privacy page.
<div class="container-fluid">
<hr />
</div>
<div class="container">
<div class="row">
<div class="col-12">
<a href="privacy.html">Privacy Policy</a>
</div>
</div>
</div>This HTML implementation is an adaption of the basic Bootstrap Navbar.
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<img src="static/images/logo.png" alt="logo" height="80" />
</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" href="/" aria-current="page">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/add.html">Add Extension</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/privacy.html">Privacy</a>
</li>
</ul>
<form class="d-flex" role="search" id="search-form">
<input
class="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
id="search-input"
/>
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>Extend the app.js with this script that toggles the active class and the aria-current="page" attribute for the current page menu item. The active class improves UX by styling the current page in the menu differently and adding the aria-current attribute to the current page which improves the context understanding of screen readers for enhanced accessibility.
document.addEventListener("DOMContentLoaded", function () {
const navLinks = document.querySelectorAll(".nav-link");
const currentUrl = window.location.pathname;
navLinks.forEach((link) => {
const linkUrl = link.getAttribute("href");
if (linkUrl === currentUrl) {
link.classList.add("active");
link.setAttribute("aria-current", "page");
} else {
link.classList.remove("active");
link.removeAttribute("aria-current");
}
});
});Extend the app.js with this script that adds basic search functionality to the search button in the menu by searching the current page and highlighting matching words.
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("search-form");
const input = document.getElementById("search-input");
form.addEventListener("submit", function (event) {
event.preventDefault();
const searchTerm = input.value.trim().toLowerCase();
if (searchTerm) {
highlightText(searchTerm);
}
});
function highlightText(searchTerm) {
const mainContent = document.querySelector("main");
removeHighlights(mainContent);
highlightTextNodes(mainContent, searchTerm);
}
function removeHighlights(element) {
const highlightedElements = element.querySelectorAll("span.highlight");
highlightedElements.forEach((el) => {
el.replaceWith(el.textContent);
});
}
function highlightTextNodes(element, searchTerm) {
const regex = new RegExp(`(${searchTerm})`, "gi");
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while ((node = walker.nextNode())) {
const parent = node.parentNode;
if (
parent &&
parent.nodeName !== "SCRIPT" &&
parent.nodeName !== "STYLE"
) {
const text = node.nodeValue;
const highlightedText = text.replace(
regex,
'<span class="highlight">$1</span>'
);
if (highlightedText !== text) {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = highlightedText;
while (tempDiv.firstChild) {
parent.insertBefore(tempDiv.firstChild, node);
}
parent.removeChild(node);
}
}
}
}
});Extend style.css to add the class required by the search script.
.highlight {
background-color: yellow;
border-radius: 20px;
border: 1px yellow solid;
}Insert the basic HTML into index.html.
{% extends 'layout.html' %} {% block content %}
<div class="container">
<div class="row"></div>
<div class="row"></div>
</div>
{% endblock %}This Python Flask implementation in main.py
- Imports all dependencies required for the whole project.
- Set up CSRFProtect to provide asynchronous keys that protect the app from a CSRF attack. Students will need to generate a unique basic 16 secret key with https://acte.ltd/utils/randomkeygen.
- Defines the head attribute for authorising a POST request to the API.
- Define a secure Content Secure Policy (CSP) head.
- Configures the Flask app.
- Redirect /index.html to the domain root for a consistent user experience.
- Renders the index.html for a GET app route.
- Provide an endpoint to log CSP violations for security analysis.
from flask import Flask
from flask import redirect
from flask import render_template
from flask import request
import requests
from flask_wtf import CSRFProtect
from flask_csp.csp import csp_header
import logging
# Generate a unique basic 16 key: https://acte.ltd/utils/randomkeygen
app = Flask(__name__)
csrf = CSRFProtect(app)
app.secret_key = b"6HlQfWhu03PttohW;apl"
app_header = {"Authorisation": "4L50v92nOgcDCYUM"}
@app.route("/index.html", methods=["GET"])
def root():
return redirect("/", 302)
@app.route("/", methods=["GET"])
@csp_header(
{
"base-uri": "self",
"default-src": "'self'",
"style-src": "'self'",
"script-src": "'self'",
"img-src": "*",
"media-src": "'self'",
"font-src": "self",
"object-src": "'self'",
"child-src": "'self'",
"connect-src": "'self'",
"worker-src": "'self'",
"report-uri": "/csp_report",
"frame-ancestors": "'none'",
"form-action": "'self'",
"frame-src": "'none'",
}
)
def index():
return render_template("/index.html")
@app.route("/csp_report", methods=["POST"])
@csrf.exempt
def csp_report():
app.logger.critical(request.data.decode())
return "done"
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)python main.py# Verify your PWA application works:
# 1. Start the PWA server: python main.py
# 2. Open browser to http://localhost:5000
# 3. Should see:
# - Logo and menu at top
# - "VS Code Extensions" heading
# - Empty container (no data yet)
# - Footer with privacy policy link
# 4. Check browser console (F12) for any errors
# 5. Verify menu links work (Home, Add Extension, Privacy)PWA not loading? Check that Flask, flask_wtf, flask_csp, and requests are installed:
pip list | grep -i flaskHaving issues? See đź”§ Troubleshooting - Flask Application Issues
{% extends 'layout.html' %} {% block content %}
<div class="container">
<div class="row">
<h1 class="display-1">Privacy Policy</h1>
<p>Policy here...</p>
</div>
<div class="row"></div>
</div>
{% endblock %}Extend main.py to include an app route to privacy.html
@app.route("/privacy.html", methods=["GET"])
def privacy():
return render_template("/privacy.html")Ensure your page renders correctly with the test cases:
- The page renders correctly
- The privacy menu item is darker than the other menu items
- A search for "priv" highlights the correct letters in the main body.
# Verify navigation and search features:
# 1. Test menu navigation:
# - Click "Home" → Should go to /
# - Click "Add Extension" → Should go to /add.html (will show template error - that's OK for now)
# - Click "Privacy" → Should go to /privacy.html
# 2. Test active menu styling:
# - On each page, verify the current page menu item is highlighted/darker
# 3. Test search functionality:
# - Go to privacy page
# - Type "priv" in search box and click Search
# - Should see "priv" highlighted in yellow on the page
# 4. Check browser console for JavaScript errorsSearch not highlighting? Check that app.js is properly linked in layout.html and the highlight CSS class exists in style.css.
Having issues? See đź”§ Troubleshooting - JavaScript Issues
From Direct Database Access to HTTP Requests:
In your previous PWA, main.py directly called the database:
# Previous approach:
def index():
data = dbHandler.listExtension() # Direct database query
return render_template('/index.html', content=data)In this API architecture, main.py requests data from the API via HTTP:
# API approach:
def index():
url = "http://127.0.0.1:3000"
response = requests.get(url) # HTTP GET request
data = response.json() # Parse JSON response
return render_template("index.html", data=data)What Changed?
| Previous Approach | API Approach |
|---|---|
| Direct function call | HTTP request over network |
| Instant response | Network delay (milliseconds) |
| Python data structures | JSON text format |
| No authentication needed | API key required |
| Tight coupling | Loose coupling |
Why Use requests Library?
requests.get()= Send HTTP GET request (retrieve data)requests.post()= Send HTTP POST request (send data).json()= Parse JSON text into Python dictionary.raise_for_status()= Raise exception if error status code
Error Handling is Now Critical:
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
except requests.exceptions.RequestException as e:
# API might be down, network error, etc.
data = {"error": "Failed to retrieve data from the API"}What Could Go Wrong?
- API server not running
- Network connection lost
- API returns error status
- Invalid JSON response
Extend the index(): method in main.py so it requests data from the API and handles the exception that the API did not respond with an error message.
def index():
url = "http://127.0.0.1:3000"
try:
response = requests.get(url)
response.raise_for_status() # Raise an exception for HTTP errors
data = response.json()
except requests.exceptions.RequestException as e:
data = {"error": "Failed to retrieve data from the API"}
return render_template("index.html", data=data)Replace the test html in 'index.html` template that:
- Implements a Bootstrap jumbotron heading.
- Implements a Bootstrap button group that will later allow users to filter the extensions by language.
- Implements the database items as Bootstrap cards in a responsiveBootstrap Column Layout.
- Provides API error feedback to the user that is styled by the Bootstrap color utility.
- Apply Bootstrap sizing and Bootstrap spacing utilities to layout the cards.
{% extends 'layout.html' %} {% block content %}
<div class="container py-4"">
<div class="p-4 bg-body-tertiary rounded-3">
<div class="container-fluid py-2">
<h1 class="display-4">VS Code Extensions for Software Engineering</h1>
<p class="lead">
This is a collection of Visual Studio Code extensions that are useful
for software engineering.
</p>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="btn-group" role="group" aria-label="Filter by language">
<button type="button" class="btn btn-primary" id="all">All</button>
<button type="button" class="btn btn-primary" id="python">Python</button>
<button type="button" class="btn btn-primary" id="c++">C++</button>
<button type="button" class="btn btn-primary" id="bash">BASH</button>
<button type="button" class="btn btn-primary" id="sql">SQL</button>
<button type="button" class="btn btn-primary" id="html">HTML</button>
<button type="button" class="btn btn-primary" id="css">CSS</button>
<button type="button" class="btn btn-primary" id="js">JAVASCRIPT</button>
</div>
</div>
</div>
<div class="container pt-4">
<div class="row">
<div class="error"><h2 class="text-danger">{{ data.error }}</h2></div>
{% if data.error is not defined %}
{% for row in data %}
<div class="col-sm-12 col-lg-4 mb-4">
<div class="card h-100" style="width: 18rem">
<img
src="{{ row.image }}"
class="card-img-top"
alt="Product image for the {{ row.name }} VSCode extension."
/>
<div class="card-body">
<h5 class="card-title">{{ row.name }}</h5>
<p class="card-text">{{ row.about }}</p>
<a href="{{ row.hyperlink }}" class="btn btn-primary">Read More</a>
</div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}# Verify PWA is retrieving data from API:
# 1. Ensure BOTH servers are running:
# Terminal 1: python api.py (port 3000)
# Terminal 2: python main.py (port 5000)
#
# 2. Open browser to http://localhost:5000
# - Should see Bootstrap jumbotron header
# - Should see filter buttons (All, Python, C++, etc.)
# - Should see Bootstrap cards displaying extensions from database
#
# 3. Test API connection:
# - Stop the API server (Terminal 1)
# - Refresh browser → Should see error message "Failed to retrieve data from the API"
# - Restart API server
# - Refresh browser → Data should load again
#
# 4. Test filter buttons:
# - Click "Python" button → URL should change to ?lang=python
# - Should see only Python extensions
# - Click "All" → Should see all extensions againNo data showing? Check both servers are running and the API URL in main.py is correct:
http://127.0.0.1:3000Having issues? See đź”§ Troubleshooting - API Integration Issues
IMPORTANT: This project requires TWO servers running at the same time!
Terminal 1 - API Server:
python api.py
# Running on http://127.0.0.1:3000Terminal 2 - PWA Server:
python main.py
# Running on http://127.0.0.1:5000Why Two Servers?
- API (port 3000): Manages data & database
- PWA (port 5000): Serves web pages to users
- PWA makes HTTP requests to API to get/send data
Communication Flow:
User Browser (localhost:5000)
↓ Views web page
PWA Server (main.py port 5000)
↓ HTTP GET request
API Server (api.py port 3000)
↓ Query database
Database (data_source.db)
↑ Return data
API Server
↑ JSON response
PWA Server
↑ Render HTML with data
User Browser
Testing Checklist:
- âś… Start API server (Terminal 1):
python api.py - âś… Test API directly: Visit
http://localhost:3000→ Should see JSON - ✅ Start PWA server (Terminal 2):
python main.py - âś… Test PWA: Visit
http://localhost:5000→ Should see web page with data
Troubleshooting:
- If PWA shows "Failed to retrieve data from API" → API server not running
- If you see "Address already in use" → Server already running, kill it first
Understanding Request Data: Form Data vs JSON:
Form Data (HTML forms):
# Client-side (HTML form):
<form method="POST">
<input name="email" value="user@example.com">
</form>
# Server-side (main.py):
email = request.form["email"] # Gets "user@example.com" from formUse for:
- Submitting new data from HTML forms
- Sensitive data (passwords)
- POST requests
- Large amounts of data
JSON Data (API communication):
# Client-side (PWA):
data = {"name": "test", "language": "PYTHON"}
requests.post(url, json=data)
# Server-side (API):
data = request.get_json() # Gets entire JSON object
name = data["name"] # Access by keyUse for:
- API communication
- Complex structured data
- App-to-app communication
The HTML Implementation in add.html
- Provides error and message feedback to the user that is styled by the Bootstrap color utility.
- Uses Bootstrap Forms to layout a data entry form.
- Uses Form attributes type, place holder & pattern to improve user experience in entering the correct data.
{% extends 'layout.html' %} {% block content %}
<div class="container">
<div class="row">
<h1>Add an Extension</h1>
<div class="error">
<h2>
<span class="text-danger">{{ data.error }}</span
><span class="text-success">{{ data.message }}</span>
</h2>
</div>
</div>
</div>
<div class="container">
<div class="row">
<form action="/add.html" method="POST" class="box">
<div class="col-auto">
<label for="name" class="form-label">Extension name</label>
<textarea
id="name"
name="name"
class="form-control"
rows="1"
autocomplete="off"
></textarea>
</div>
<div class="col-auto">
<label for="hyperlink" name="hyperlink" class="form-label"
>Hyperlink to extension</label
>
<input
id="hyperlink"
name="hyperlink"
type="url"
class="form-control"
placeholder="https://marketplace.visualstudio.com/items?itemName="
pattern="^https:\/\/marketplace\.visualstudio\.com\/items\?itemName=(?!.*[<>])[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]*$"
/>
</div>
<div class="col-auto">
<label for="about" class="form-label">About</label>
<textarea
id="about"
name="about"
class="form-control"
rows="3"
placeholder="A brief description of the extension"
></textarea>
</div>
<div class="col-auto">
<label for="name" name="image" class="form-label">URL to Icon</label>
<input
id="image"
name="image"
type="url"
class="form-control"
pattern="^https:\/\/(?!.*[<>])[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]*$"
placeholder="https://"
/>
</div>
<div class="col-auto">
<label for="language" name="language" class="form-label"
>Programming language</label
>
<select
id="language"
name="language"
class="form-select"
aria-label="Default select language"
>
<option selected>Select a language from this menu</option>
<option value="PYTHON">PYTHON</option>
<option value="CPP">CPP</option>
<option value="BASH">BASH</option>
<option value="SQL">SQL</option>
<option value="HTML">HTML</option>
<option value="CSS">CSS</option>
<option value="JAVASCRIPT">JAVASCRIPT</option>
</select>
</div>
<br />
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-3">Submit</button>
</div>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>
</div>
</div>
{% endblock %}Extend main.py' to provide a route with POST and GET methods for add.html` that
- Renders
add.htmlon GET requests. - On a POST method, read the form in add.html and construct a JSON.
- Then, POST the JSON with the header that includes the Authentication key to the API.
- Render
add.htmlwith any errors or messages from the API.
@app.route("/add.html", methods=["POST", "GET"])
def form():
if request.method == "POST":
name = request.form["name"]
hyperlink = request.form["hyperlink"]
about = request.form["about"]
image = request.form["image"]
language = request.form["language"]
data = {
"name": name,
"hyperlink": hyperlink,
"about": about,
"image": image,
"language": language,
}
app.logger.critical(data)
try:
response = requests.post(
"http://127.0.0.1:3000/add_extension",
json=data,
headers=app_header,
)
data = response.json()
except requests.exceptions.RequestException as e:
data = {"error": "Failed to retrieve data from the API"}
return render_template("/add.html", data=data)
else:
return render_template("/add.html", data={})Extend app.js with a script to provide functionality to the home page buttons.
document.addEventListener("DOMContentLoaded", function () {
if (window.location.pathname === "/") {
const buttons = [
{ id: "all", url: "/" },
{ id: "python", url: "?lang=python" },
{ id: "cpp", url: "?lang=cpp" },
{ id: "bash", url: "?lang=bash" },
{ id: "sql", url: "?lang=sql" },
{ id: "html", url: "?lang=html" },
{ id: "css", url: "?lang=css" },
{ id: "js", url: "?lang=javascript" },
];
buttons.forEach((button) => {
const element = document.getElementById(button.id);
if (element) {
element.addEventListener("click", function () {
window.location.href = button.url;
});
}
});
}
}); url = "http://127.0.0.1:3000"
if request.args.get("lang") and request.args.get("lang").isalpha():
lang = request.args.get("lang")
url += f"?lang={lang}"# Verify ALL features work together:
# 1. Ensure BOTH servers are running (api.py and main.py)
#
# 2. Test complete workflow:
# a) Go to http://localhost:5000
# b) Click filter buttons → Data should filter by language
# c) Click "Add Extension" → Form should load
# d) Submit new extension → Success message should appear
# e) Return to home → New extension should appear in cards
#
# 3. Test search functionality:
# - Search for extension names on home page
# - Should highlight matching text
#
# 4. Test navigation:
# - All menu links work
# - Active menu item is highlighted on each page
#
# 5. Test error handling:
# - Stop API server
# - Refresh home page → Should show error message
# - Try to add extension → Should show error message
# - Restart API → Everything works again
#
# 6. Verify security logs:
# - main_security_log.log exists and contains entries
# - api_security_log.log exists and contains entriesEverything working? Great! Your PWA is complete and communicating with the API. Next: Step 15 for final logging setup.
Extend the main.py with the implementation below, which should be inserted directly below the imports. This will configure the logger to log to a file for security analysis.
app_log = logging.getLogger(__name__)
logging.basicConfig(
filename="main_security_log.log",
encoding="utf-8",
level=logging.DEBUG,
format="%(asctime)s %(message)s",
### 15: Configure the logger to log to main_security_log.log
Extend the `main.py` with the implementation below, which should be inserted directly below the `imports`. This will configure the logger to log to a file for security analysis.
```python
app_log = logging.getLogger(__name__)
logging.basicConfig(
filename="main_security_log.log",
encoding="utf-8",
level=logging.DEBUG,
format="%(asctime)s %(message)s",
)
# Final verification checklist:
#
# API (port 3000):
# âś… GET all extensions works
# âś… GET filtered by language works
# âś… POST with authentication works
# âś… POST without authentication blocked (401)
# âś… POST with invalid JSON rejected (400)
# âś… Rate limiting configured
# âś… CORS enabled
# âś… api_security_log.log created
#
# PWA (port 5000):
# âś… Home page displays extension cards
# âś… Filter buttons work
# âś… Add extension form works
# âś… Form validation works
# âś… Search functionality works
# âś… Menu navigation works
# âś… Active menu highlighting works
# âś… Privacy page renders
# âś… Error handling displays messages
# âś… main_security_log.log created
# âś… CSRF protection enabled
# âś… CSP headers configured
#
# Integration:
# âś… PWA successfully requests data from API
# âś… PWA successfully posts data to API
# âś… Authentication headers working
# âś… Both servers run simultaneously
# ✅ Error handling when API is down🎉 Congratulations! You've built a complete RESTful API with authentication and a PWA that consumes it!
- Create a get_languages method that returns all the languages in the database.
- Improve exception handling of the
add_extensionendpoint to give more detailed feedback to the user. - Use the new get-languages method to define the content that renders in the PWA.
- Implement a sort extension by function
Error: ModuleNotFoundError: No module named 'flask_cors' or similar
Solution:
# Install all required packages:
pip install Flask flask_cors flask_limiter
# or try:
pip3 install Flask flask_cors flask_limiterError: Various import or syntax errors Solution:
- Check you're in the correct directory:
pwd - Verify api.py exists:
ls -la - Check Python syntax in api.py
- Ensure all imports are correct
- Verify database_manager.py exists in same directory
Error: OSError: [Errno 48] Address already in use
Solution:
# Kill processes using port 3000:
lsof -ti:3000 | xargs kill -9
# Or use a different port in api.py:
api.run(debug=True, host='0.0.0.0', port=3001)Error: Access to XMLHttpRequest has been blocked by CORS policy
Solution:
- Verify CORS is imported:
from flask_cors import CORS - Check CORS is applied:
cors = CORS(api) - Ensure API server is running on port 3000
- Try restarting both servers
Error: sqlite3.OperationalError: unable to open database file
Solution:
- Check database file exists:
ls database/data_source.db - Verify path in database_manager.py:
database/data_source.db - Check file permissions:
ls -la database/ - Ensure you're in project root directory
Error: sqlite3.OperationalError: no such table: extension
Solution:
- Open database in SQLite3 Editor
- Run CREATE TABLE query from instructions
- Verify table was created:
SELECT * FROM extension; - Check table name matches exactly (case-sensitive)
Error: sqlite3.OperationalError: near "...": syntax error
Solution:
- Check for typos in SQL commands
- Ensure proper quotes around text values
- Verify column names match exactly
- Check for missing commas or parentheses
- Use SQLite3 Editor query validator
Error: All extensions returned regardless of ?lang= parameter Solution:
- Check language values in database are uppercase: PYTHON, BASH, etc.
- Verify SQL query uses LIKE with parameter:
WHERE language LIKE ? - Test with uppercase in URL:
?lang=PYTHON - Check
.upper()is applied to lang variable in api.py
Solution:
- Verify language exists in database
- Check spelling matches exactly
- Test without parameter - should return all data
- Check SQL wildcard
%is used for "all"
Error: 415 error in Thunder Client Solution:
- Set Content-Type header to
application/json - Ensure body is valid JSON format
- Use "JSON" body type in Thunder Client, not "Text"
Error: {"error": "Invalid JSON"}
Solution:
- Validate JSON structure at https://jsonlint.com/
- Check all required fields are present: name, hyperlink, about, image, language
- Verify URLs match regex patterns
- Check language is in ENUM list
- Remove any extra fields not in schema
Solution:
- Verify Content-Type header is set to
application/json - Check body contains valid JSON
- Ensure using POST method, not GET
- Try
request.get_json(force=True)temporarily for debugging
Error: All valid JSON rejected with 400 Solution:
- Check jsonschema is installed:
pip install jsonschema - Verify schema definition matches in database_manager.py
- Test with exact sample JSON from instructions
- Check regex patterns are raw strings (r"pattern")
- Print data before validation to see what's being received
Solution:
- Test pattern at https://regex101.com/
- Ensure raw string prefix:
r"^https:\/\/..." - Check for typos in pattern
- Verify escape characters:
\/for/
Solution:
- Check URL doesn't contain
<or>characters - Ensure
(?!.*[<>])pattern is in regex - Test with simple URL first:
https://test.com - Add complexity gradually
Error: No error, but data doesn't appear Solution:
- Check
con.commit()is called after INSERT - Verify
con.close()is called - Check database file permissions
- Open database in SQLite3 Editor and run SELECT query
- Ensure validation is passing (returns True)
Error: sqlite3.IntegrityError: UNIQUE constraint failed
Solution:
# Add error handling in database_manager.py:
try:
cur.execute("INSERT INTO extension...")
con.commit()
except sqlite3.IntegrityError:
return jsonify({"error": "Extension already exists"}), 400Error: {"error": "Unauthorised"} even with correct key
Solution:
- Check header name is exactly
Authorisation(British spelling) - Verify key matches in both api.py and request
- Check for extra spaces in key value
- Try copying key directly, don't type it
- Check header is being sent in request
Solution:
- Verify if statement is checking header:
request.headers.get("Authorisation") - Check
auth_keyvariable is defined - Ensure comparison is
==not= - Test with wrong key to confirm it blocks
Note: The code uses British spelling "Authorisation" - this is intentional and must match exactly in:
- api.py:
request.headers.get("Authorisation") - main.py:
app_header = {"Authorisation": "..."} - Thunder Client: Header name must be
Authorisation
Error: Various import or syntax errors Solution:
- Check you're in the correct directory:
pwd - Verify main.py exists:
ls -la - Check all required packages are installed:
pip install Flask flask_wtf flask_csp requests- Verify imports at top of main.py
Solution:
# Kill processes using port 5000:
lsof -ti:5000 | xargs kill -9
# Or use a different port in main.py:
app.run(debug=True, host='0.0.0.0', port=5001)Error: jinja2.exceptions.TemplateNotFound: index.html
Solution:
- Check templates folder exists:
ls templates/ - Verify file names match exactly (case-sensitive)
- Check file is in correct location:
templates/index.html - Ensure render_template path starts with
/:render_template("/index.html")
Error: 400 Bad Request when submitting forms Solution:
- Check CSRF token in form:
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> - Verify CSRFProtect is initialized:
csrf = CSRFProtect(app) - Ensure app.secret_key is set
- Check form method is POST
Solution:
- Verify API server is running:
curl http://localhost:3000 - Check API is on port 3000, PWA on port 5000
- Test API endpoint directly in browser
- Check for firewall blocking
- Verify URL in main.py:
http://127.0.0.1:3000
Solution:
- Test each server independently first
- Check CORS is enabled on API
- Verify requests library is installed:
pip install requests - Check browser console for CORS errors
- Ensure correct ports in code
Solution:
- Check response format is JSON
- Verify
response.json()is called in main.py - Check for exceptions in try/except block
- Print response status code:
print(response.status_code) - Verify data structure matches template expectations
Solution:
- Check app.js is linked in layout.html
- Verify highlight CSS class exists in style.css
- Open browser console for JavaScript errors
- Check search form has correct ID:
id="search-form" - Verify input has ID:
id="search-input"
Solution:
- Check button IDs match JavaScript:
id="python", etc. - Verify script checks pathname:
if (window.location.pathname === "/") - Open browser console for errors
- Check event listeners are attached
- Verify buttons exist in HTML
Solution:
- Check
aria-current="page"attribute toggle logic - Verify CSS for
.activeclass exists - Check nav-link class on menu items
- Confirm currentUrl matches href values exactly
- Check browser console for errors
Note: Thunder Client doesn't work in browser-based Codespaces Solution:
Use curl instead:
# GET request:
curl http://localhost:3000
# POST request:
curl -X POST http://localhost:3000/add_extension \
-H "Content-Type: application/json" \
-H "Authorisation: 4L50v92nOgcDCYUM" \
-d '{"name":"test","hyperlink":"https://marketplace.visualstudio.com/items?itemName=test","about":"test","image":"https://test.jpg","language":"PYTHON"}'Solution:
- Open two terminal windows/tabs
- Run api.py in Terminal 1
- Run main.py in Terminal 2
- Both should show "Running on..." messages
- Keep both terminals open while testing
When encountering issues, run these commands to gather information:
# Check Python version
python --version
python3 --version
# Check Flask installation
pip show flask
pip list | grep -i flask
# Check current directory and files
pwd
ls -la
# Test if API responds
curl -I http://localhost:3000
# Test if PWA responds
curl -I http://localhost:5000
# Check database
sqlite3 database/data_source.db ".tables"
sqlite3 database/data_source.db "SELECT COUNT(*) FROM extension;"
# Check for running processes on ports
lsof -i:3000
lsof -i:5000
# View recent log entries
tail -n 20 api_security_log.log
tail -n 20 main_security_log.logIf you're still stuck after trying these solutions:
- Read the error message carefully - it often tells you exactly what's wrong
- Use browser DevTools (F12) to check Console and Network tabs
- Test each component separately - API, then PWA, then together
- Check the checkpoints - verify each step completed successfully
- Review previous tutorial - ensure foundational knowledge is solid
- Ask a classmate or teacher - explain what you've tried already
- Search for the specific error message online
| Error Pattern | Likely Cause | Solution Section |
|---|---|---|
ModuleNotFoundError |
Missing Python package | API Server Issues |
Address already in use |
Port conflict | API/Flask Application Issues |
sqlite3.OperationalError |
Database problem | Database Connection Issues |
CORS policy |
Cross-origin blocked | API Server Issues |
401 Unauthorised |
Auth problem | Authentication Issues |
400 Bad Request |
Invalid data | JSON Validation Issues |
TemplateNotFound |
Missing HTML file | Flask Application Issues |
Failed to retrieve data |
API communication | API Integration Issues |
Flask PWA API Extension Task Source and Flask PWA API Extension Task Template by Ben Jones is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International







