β±οΈ Estimated Reading Time: 15 minutes
Hey there! Iβm Emmanuel Michael, and in this guide, weβll build two progressive Flask APIs that will help you think and code like a backend engineer.
Weβll start simple with an in-memory Stock Manager, then evolve it into a more advanced Task Manager that handles users, tasks, validation, and more.
If youβve ever wanted to understand how to design clean, modular Flask APIs β without getting lost in frameworks or databases β this guide is for you.
- Why Flask and In-Memory APIs?
- Project 1: Stock Manager API
- Project 2: Task Manager API
- Shared Utilities: validators.py & response.py
- Testing Your APIs (curl & Postman)
- Folder Structure & Setup
- Try It Yourself Challenges
- Next-Level Improvements
- Closing Reflection
- About the Author
Before we touch code, letβs understand why this project matters.
Flask is perfect for learning and prototyping because itβs:
- Lightweight πͺΆ
- Flexible π§
- Beginner-friendly πΆ
- Production-capable π
And in-memory APIs? Theyβre APIs that store data temporarily in Python lists or dictionaries instead of a database.
Itβs great for learning because you can focus on API design, validation, and logic β without worrying about SQL yet.
Letβs start with something simple: managing stock items.
Each stock item has:
- A name
- Quantity
- Unit price
- Description
From these, weβll compute a total_price and allow CRUD operations.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/item/add |
Add a new item |
GET |
/api/v1/item/all |
Get all items |
GET |
/api/v1/item/<id> |
Get single item |
PUT |
/api/v1/item/<id>/update |
Update an item |
DELETE |
/api/v1/item/<id>/delete |
Delete an item |
@app.route('/api/v1/item/add', methods=['POST'])
def add_item():
data = request.get_json()
if not data:
return bad_request_response("Invalid payload")
# Validation example
error = validate_required_fields(data, 'name')
if error:
return bad_request_response(error)
new_item = {
"id": len(items) + 1,
"name": data["name"],
"quantity": data.get("quantity", 0),
"unit_price": data.get("unit_price", 0),
"description": data.get("description", ""),
"total_price": data.get("quantity", 0) * data.get("unit_price", 0)
}
items.append(new_item)
return success_response("Item added successfully", new_item, 201)π‘ Pro Tip:
Always validate input data before adding to your data store. It prevents inconsistent or missing values.
curl -X POST http://127.0.0.1:5000/api/v1/item/add -H "Content-Type: application/json" -d '{
"name": "Rice",
"quantity": 10,
"unit_price": 65000,
"description": "50kg bag of rice"
}'{
"status": "success",
"message": "Item added successfully",
"data": {
"id": 1,
"name": "Rice",
"quantity": 10,
"unit_price": 65000,
"total_price": 650000,
"description": "50kg bag of rice"
},
"timestamp": "2025-10-05T09:00:00Z"
}
Donβt forget to use request.get_json() β using request.form will break your JSON body.
Now letβs level up! π―
Weβll build a Task Manager that supports both users and tasks β showing relationships between data.
Each user can have multiple tasks.
Each task can have statuses like pending, in-progress, or completed.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/user/add |
Create user |
GET |
/api/v1/user/all |
Get all users |
POST |
/api/v1/task/add |
Add task |
PUT |
/api/v1/task/<id>/assign/<user_id> |
Assign a task |
PUT |
/api/v1/task/<id>/status/update |
Update status |
@app.route('/api/v1/user/add', methods=['POST'])
def create_user():
data = request.get_json()
payload_error = validate_payload(data)
if payload_error:
return bad_request_response(payload_error)
# Validate fields
error = (
validate_required_fields(data, 'firstName') or
validate_required_fields(data, 'lastName') or
validate_required_fields(data, 'email') or
validate_required_fields(data, 'phone')
)
if error:
return bad_request_response(error)
if not validate_email(data['email']):
return bad_request_response("Invalid email format")
if not validate_phone(data['phone']):
return bad_request_response("Invalid phone number")
new_user = {
"id": len(users) + 1,
"firstName": data["firstName"],
"lastName": data["lastName"],
"email": data["email"],
"phone": data["phone"]
}
users[new_user["id"]] = new_user
return success_response("User created successfully", new_user, 201)π‘ Pro Tip:
Notice how every step validates data before proceeding. This pattern helps when you move to real databases later.
curl -X POST http://127.0.0.1:5000/api/v1/user/add -H "Content-Type: application/json" -d '{
"firstName": "Emmanuel",
"lastName": "Michael",
"email": "emmanuel@example.com",
"phone": "08123456789"
}'{
"status": "success",
"message": "User created successfully",
"data": {
"id": 1,
"firstName": "Emmanuel",
"lastName": "Michael",
"email": "emmanuel@example.com",
"phone": "08123456789"
}
}Both APIs use the same helper modules β because reusability is key.
def validate_payload(payload):
if payload is None:
return "Payload is missing"
if not isinstance(payload, dict):
return "Payload must be a valid JSON object"
if not payload:
return "Payload cannot be empty"
return None
def validate_required_fields(data, field):
if field not in data or not str(data[field]).strip():
return f"{field.capitalize()} is required"
return None
def validate_email(email):
return "@" in email and "." in email
def validate_phone(phone):
return phone.isdigit() and len(phone) == 11π‘ Pro Tip:
By keeping validators separate, you avoid repetition and make your app easier to test.
from datetime import datetime
from flask import jsonify
def success_response(message, data=None, status_code=200):
return jsonify({
"status": "success",
"message": message,
"data": data,
"timestamp": datetime.now().isoformat()
}), status_code
def bad_request_response(error_message, status_code=400):
return jsonify({
"status": "error",
"message": error_message,
"timestamp": datetime.now().isoformat()
}), status_code
Avoid repeating jsonify logic inside every route β centralize responses like this.
You can use Postman or curl for testing.
π‘ Example β Fetch all items:
curl http://127.0.0.1:5000/api/v1/item/allπ‘ Example β Fetch all users:
curl http://127.0.0.1:5000/api/v1/user/allπ‘ Example β Assign a task to a user:
curl -X PUT http://127.0.0.1:5000/api/v1/task/1/assign/1project_root/
βββ app.py # Stock Manager API
βββ taskManagerApp.py # Task Manager API
βββ validators.py # Input validation helpers
βββ response.py # Response format helpers
βββ README.md # This guide
βββ .venv/ # Virtual environment (optional)
# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install Flask
pip install flask
# Run Stock Manager
flask --app app.py run
# Run Task Manager
flask --app taskManagerApp.py runπ§° Challenge 1: Add an endpoint /api/v1/item/search/<name> to find an item by name.
π‘ Hint: Use list comprehensions and .lower() for case-insensitive search.
π§° Challenge 2: Extend Task Manager to support deadlines and completion timestamps.
π§° Challenge 3: Create a global counter that tracks how many requests the API has handled.
Once youβre comfortable with this setup, try upgrading your API:
| Goal | Description |
|---|---|
| π§± Add SQLite | Replace lists with persistent storage |
| π JWT Authentication | Secure user and task routes |
| π§© Flask Blueprints | Split routes into modules |
| π§ͺ Testing | Use pytest for automated tests |
| π§Ύ Logging | Track actions and errors |
| π Deployment | Deploy to Render or Railway |
At this point, youβve done more than just build two Flask apps β
youβve learned how to think like a backend engineer.
You now understand:
- How to structure APIs cleanly
- How to validate and sanitize input
- How to make reusable code modules
- How to design endpoints with purpose
π¬ βKeep building, keep experimenting β thatβs how you grow as a developer.β
β Emmanuel Michael
Emmanuel Michael is a Backend Engineer passionate about building scalable APIs and helping others grow into confident developers.
He loves teaching through practical projects and turning ideas into real, working systems.