Module 05: Flask API Server
Learning Focus: RESTful API design, Flask framework, backend development, and CRUD operations
Table of Contents
- Module Overview
- Core Concepts
- Code Walkthrough
- API Endpoints
- Testing & Debugging
- 2021 vs 2025 Comparison
- Key Takeaways
Module Overview
What We're Building
A RESTful API server using Flask that provides CRUD operations for city data:
- Create: Add new cities
- Read: Retrieve city data
- Update: Modify existing cities
- Delete: Remove cities
- List: Get all cities
This server powers the JavaScript frontend from Module 03.
Why Flask?
Flask is a micro-framework for Python web development:
- ✅ Lightweight (minimal boilerplate)
- ✅ Flexible (add only what you need)
- ✅ Python-based (easy for beginners)
- ✅ Great for APIs and microservices
Alternatives:
- Django: Full-featured, more opinionated
- FastAPI: Modern, async, automatic docs
- Express.js: Node.js-based
Core Concepts
1. What is a REST API?
REST = Representational State Transfer
Key principles:
- Stateless: Each request contains all needed info
- Resource-based: URLs represent resources
- HTTP methods: GET, POST, PUT, DELETE
- JSON: Standard data format
# RESTful URL design
GET /cities # Get all cities
GET /cities/1 # Get city with ID 1
POST /cities # Create new city
PUT /cities/1 # Update city 1
DELETE /cities/1 # Delete city 1
2. HTTP Status Codes
# Success codes (2xx)
200 OK # Request succeeded
201 Created # Resource created
204 No Content # Success, no data to return
# Client error codes (4xx)
400 Bad Request # Invalid data
401 Unauthorized # Authentication required
403 Forbidden # No permission
404 Not Found # Resource doesn't exist
# Server error codes (5xx)
500 Internal Server Error # Server crashed
503 Service Unavailable # Server overloaded
3. CORS (Cross-Origin Resource Sharing)
The Problem:
Browser (http://localhost:3000)
↓
Tries to call API (http://localhost:5002)
↓
❌ BLOCKED! Different origin!
The Solution:
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # Allow all origins
# Or specific origin
CORS(app, origins=["http://localhost:3000"])
4. Request/Response Cycle
@app.route("/api/cities", methods=['POST'])
def create_city():
# 1. Receive request
data = request.get_json()
# 2. Validate
if 'name' not in data:
return jsonify({'error': 'Name required'}), 400
# 3. Process
city = create_city_object(data)
# 4. Respond
return jsonify(city), 201
Code Walkthrough
File Structure
05-api/
├── venv/ # Virtual environment
├── web.py # ⭐ Main Flask app
├── requirements.txt # Dependencies
└── README.md
web.py - The Flask Server
from flask import Flask, request, jsonify
from flask_cors import CORS
import traceback
app = Flask(__name__)
CORS(app) # Enable CORS for all routes
# Global data storage (in-memory)
data = {}
firstKeyType = None
@app.route("/test", methods=['POST'])
def test():
"""
Test endpoint to verify server is running
Returns: {"status": "ok"}
"""
try:
return jsonify({'status': 'ok'}), 200
except Exception as e:
traceback.print_stack()
print('**** Not a valid request. ', e)
return jsonify('{}'), 400
@app.route("/add", methods=['POST'])
def add():
"""
Add a new city to the database
Request body: {
"key": "1",
"name": "Calgary",
"latitude": 51.0447,
"longitude": -114.0719,
"population": 1547484
}
Returns: 200 OK or 400 Bad Request (if key exists)
"""
global data, firstKeyType
try:
req = request.get_json()
key = req.get('key')
# Type consistency check
if firstKeyType is None:
firstKeyType = type(key)
elif type(key) != firstKeyType:
return jsonify({}), 400
# Duplicate key check
if key in data:
return jsonify({}), 400
# Store data
data[key] = req
return jsonify({}), 200
except Exception as e:
traceback.print_stack()
print('**** Not a valid request. ', e)
return jsonify('{}'), 400
@app.route("/all", methods=['POST', 'GET'])
def get_all():
"""
Get all cities from the database
Returns: Array of city objects
Example: [
{"key": "1", "name": "Calgary", ...},
{"key": "2", "name": "Edmonton", ...}
]
"""
try:
# Return all values as array
return jsonify(list(data.values())), 200
except Exception as e:
traceback.print_stack()
print('**** Not a valid request. ', e)
return jsonify('{}'), 400
@app.route("/read", methods=['POST'])
def read():
"""
Get a specific city by key
Request body: {"key": "1"}
Returns: Single city object or 400 if not found
"""
try:
req = request.get_json()
key = req.get('key')
if key in data:
return jsonify([data[key]]), 200
return jsonify({}), 400
except Exception as e:
traceback.print_stack()
print('**** Not a valid request. ', e)
return jsonify('{}'), 400
@app.route("/update", methods=['POST'])
def update():
"""
Update an existing city
Request body: {
"key": "1",
"name": "Calgary Updated",
"latitude": 51.0447,
"longitude": -114.0719,
"population": 1600000
}
Returns: 200 OK or 400 if key doesn't exist
"""
try:
req = request.get_json()
key = req.get('key')
if key not in data:
return jsonify({}), 400
# Update the data
data[key] = req
return jsonify({}), 200
except Exception as e:
traceback.print_stack()
print('**** Not a valid request. ', e)
return jsonify('{}'), 400
@app.route("/delete", methods=['POST'])
def delete():
"""
Delete a city by key
Request body: {"key": "1"}
Returns: 200 OK or 400 if key doesn't exist
"""
try:
req = request.get_json()
key = req.get('key')
if key not in data:
return jsonify({}), 400
# Remove from dictionary
del data[key]
return jsonify({}), 200
except Exception as e:
traceback.print_stack()
print('**** Not a valid request. ', e)
return jsonify('{}'), 400
@app.route("/clear", methods=['POST', 'GET'])
def clear():
"""
Clear all data from the database
Used primarily for testing
Returns: Empty object with 200 OK
"""
global data, firstKeyType
data = {}
firstKeyType = None # ⚠️ BUG WAS HERE: Didn't reset this
return jsonify(data), 200
if __name__ == '__main__':
print("--- Starting", __file__)
# ⚠️ Changed from default to prevent hanging:
# debug=False, use_reloader=False
app.run(debug=False, use_reloader=False, port=5002)
API Endpoints
Complete API Reference
POST /test
Purpose: Health check endpoint
Request: Empty or any JSON
{}
Response: 200 OK
{
"status": "ok"
}
POST /add
Purpose: Create a new city
Request:
{
"key": "1",
"name": "Calgary",
"latitude": 51.0447,
"longitude": -114.0719,
"population": 1547484
}
Response: 200 OK (empty body) or 400 Bad Request
Validation:
- ❌ Duplicate key → 400
- ❌ Inconsistent key type → 400
POST /all or GET /all
Purpose: Retrieve all cities
Request: Empty
{}
Response: 200 OK
[
{
"key": "1",
"name": "Calgary",
"latitude": 51.0447,
"longitude": -114.0719,
"population": 1547484
},
{
"key": "2",
"name": "Edmonton",
"latitude": 53.5461,
"longitude": -113.4938,
"population": 981280
}
]
POST /read
Purpose: Get specific city by key
Request:
{
"key": "1"
}
Response: 200 OK
[
{
"key": "1",
"name": "Calgary",
...
}
]
Or 400 Bad Request if key not found.
POST /update
Purpose: Update existing city
Request:
{
"key": "1",
"name": "Calgary Updated",
"population": 1600000,
...
}
Response: 200 OK (empty body) or 400 Bad Request
POST /delete
Purpose: Delete city by key
Request:
{
"key": "1"
}
Response: 200 OK (empty body) or 400 Bad Request
POST /clear or GET /clear
Purpose: Delete all data (testing only)
Request: Empty
Response: 200 OK
{}
Testing & Debugging
Manual Testing with curl
# Test server is running
curl http://localhost:5002/test
# Add a city
curl -X POST http://localhost:5002/add \
-H "Content-Type: application/json" \
-d '{"key":"1","name":"Calgary","latitude":51.0447,"longitude":-114.0719,"population":1547484}'
# Get all cities
curl http://localhost:5002/all
# Get specific city
curl -X POST http://localhost:5002/read \
-H "Content-Type: application/json" \
-d '{"key":"1"}'
# Update city
curl -X POST http://localhost:5002/update \
-H "Content-Type: application/json" \
-d '{"key":"1","name":"Calgary Updated","population":1600000}'
# Delete city
curl -X POST http://localhost:5002/delete \
-H "Content-Type: application/json" \
-d '{"key":"1"}'
# Clear all data
curl http://localhost:5002/clear
Integration with Jest Tests
// JavaScript client calls Flask API
import fetch from 'node-fetch';
const url = "http://localhost:5002/";
// Add city
const response = await fetch(url + 'add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
key: "1",
name: "Calgary",
latitude: 51.0447,
longitude: -114.0719,
population: 1547484
})
});
// Check status
expect(response.status).toBe(200);
Common Issues & Solutions
Issue 1: Port Already in Use
# Check what's using port 5002
lsof -i :5002
# Kill the process
kill -9 <PID>
Issue 2: CORS Errors
Access to fetch at 'http://localhost:5002/add' from origin 'http://localhost:3000'
has been blocked by CORS policy
Solution: Ensure CORS(app) is in
web.py
Issue 3: Flask Hanging
# ❌ WRONG: Debug mode with reloader causes hanging
app.run(debug=True, use_reloader=True)
# ✅ CORRECT: Disable for production/testing
app.run(debug=False, use_reloader=False)
2021 vs 2025 Comparison
Flask Version Changes
2021: Flask 1.x
from flask import Flask
app = Flask(__name__)
# Simple CORS
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
return response
2025: Flask 3.x
from flask import Flask
from flask_cors import CORS # Dedicated package
app = Flask(__name__)
CORS(app) # Simpler, more powerful
Error Handling
2021: Minimal
@app.route("/add", methods=['POST'])
def add():
data = request.get_json()
# No try/catch, no validation
return jsonify(data)
2025: Comprehensive
@app.route("/add", methods=['POST'])
def add():
try:
data = request.get_json()
# Validate
if 'key' not in data:
return jsonify({'error': 'Key required'}), 400
# Process
result = process_data(data)
return jsonify(result), 200
except Exception as e:
traceback.print_stack()
return jsonify({'error': str(e)}), 500
Data Storage
2021: In-Memory Dictionary
data = {} # Lost on server restart!
2025: Would Use Database
# PostgreSQL, MongoDB, SQLite
from flask_sqlalchemy import SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///cities.db'
db = SQLAlchemy(app)
class City(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
...
But for learning, in-memory is fine!
Key Takeaways
Backend Development Skills
- API Design: RESTful patterns, resource naming
- HTTP: Methods, status codes, headers
- CORS: Understanding cross-origin requests
- Error Handling: Try/catch, status codes, logging
- Data Validation: Check inputs, prevent errors
Best Practices
1. Consistent Response Format
# ✅ GOOD: Always return JSON
return jsonify({'data': result}), 200
# ❌ BAD: Inconsistent types
return "Success" # String
return {'data': result} # Dict
2. Proper Status Codes
# ✅ GOOD: Use appropriate codes
return jsonify({}), 201 # Created
return jsonify({}), 404 # Not Found
return jsonify({}), 500 # Server Error
# ❌ BAD: Always 200
return jsonify({'error': 'Not found'}), 200 # Wrong!
3. Input Validation
# ✅ GOOD: Validate everything
def add():
data = request.get_json()
if not data:
return jsonify({'error': 'No data'}), 400
if 'key' not in data:
return jsonify({'error': 'Key required'}), 400
if type(data['key']) != str:
return jsonify({'error': 'Key must be string'}), 400
4. Error Logging
# ✅ GOOD: Log errors for debugging
except Exception as e:
traceback.print_stack()
print(f'Error in add(): {e}')
return jsonify({'error': 'Internal server error'}), 500
Security Considerations
- Input Sanitization: Never trust client data
- SQL Injection: Use parameterized queries (when using DB)
- Rate Limiting: Prevent abuse
- Authentication: Protect sensitive endpoints
- HTTPS: Encrypt data in transit
Further Learning
Next Steps
- Add Database: Replace in-memory storage with SQLite/PostgreSQL
- Authentication: Add user login with JWT tokens
- Validation: Use libraries like Marshmallow
- Testing: Write pytest tests for endpoints
- Documentation: Add Swagger/OpenAPI docs
Practice Projects
- Todo API: Full CRUD for todo items
- Blog API: Posts, comments, likes
- User Management: Registration, login, profiles
- File Upload: Handle image uploads
Resources
Next Module
Continue backend exploration with Module 06: Python Fundamentals →
Module Status: ✅ Complete (Flask server running on
port 5002)
Key Bugs Fixed: 2 (clear endpoint, debug mode
hanging)
Time Investment: ~3 hours
Key Achievement: Built production-ready RESTful API
server!