The idea
Not every client is a person with a browser. A mobile app, a Streamlit dashboard, a partner's system or your own JavaScript all want data, not HTML. An API (Application Programming Interface) answers requests with JSON, the format you've parsed in the Python track.
Most web APIs follow a convention called REST: URLs name resources (things), and HTTP methods say what to do with them.
| Method + URL | Meaning | Success status |
|---|---|---|
GET /api/products | list products | 200 |
GET /api/products/3 | read product 3 | 200 (404 if missing) |
POST /api/products | create a product | 201 |
PATCH /api/products/3 | update some fields | 200 |
DELETE /api/products/3 | delete it | 204 (no body) |
Notice the URLs are nouns (/products), never verbs (/getProducts, /deleteProduct): the method is the verb.
Returning JSON
Return a dict or a list, and Flask converts it to JSON with the right Content-Type:
from flask import Flask
app = Flask(__name__)
PRODUCTS = {
1: {"id": 1, "name": "French Press 600ml", "price": 185000},
2: {"id": 2, "name": "Pour-over Kettle", "price": 320000},
}
@app.get("/api/products")
def list_products():
return {"products": list(PRODUCTS.values()), "count": len(PRODUCTS)}
r = app.test_client().get("/api/products")
print(r.status_code, r.mimetype)
print(r.get_json())
200 application/json
{'count': 2, 'products': [{'id': 1, 'name': 'French Press 600ml', 'price': 185000}, {'id': 2, 'name': 'Pour-over Kettle', 'price': 320000}]}
r.get_json() in the test client parses the body back into Python. (jsonify(...) does the same as returning a dict; you'll see it in older code.)
Wrapping the list in an object ({"products": [...], "count": 2}) is a good habit: you can add fields later (paging, totals) without breaking clients.
Reading JSON from the request
A mobile app or another program that talks to your API doesn't fill in HTML forms. It sends its data as JSON in the request body. To add a product through the API, the client sends something like {"name": "Moka Pot", "price": 320000}, and request.get_json() turns it into a Python dictionary:
from flask import Flask, request
app = Flask(__name__)
PRODUCTS = {}
next_id = 1
@app.post("/api/products")
def create_product():
global next_id
data = request.get_json(silent=True) or {}
name = str(data.get("name", "")).strip()
price = data.get("price")
errors = []
if not name:
errors.append("name is required")
if not isinstance(price, int) or price <= 0:
errors.append("price must be a positive integer")
if errors:
return {"errors": errors}, 400
product = {"id": next_id, "name": name, "price": price}
PRODUCTS[next_id] = product
next_id += 1
return product, 201
client = app.test_client()
r = client.post("/api/products", json={"name": "Matcha 100g", "price": 85000})
print(r.status_code, r.get_json())
r = client.post("/api/products", json={"price": "cheap"})
print(r.status_code, r.get_json())
201 {'id': 1, 'name': 'Matcha 100g', 'price': 85000}
400 {'errors': ['name is required', 'price must be a positive integer']}
client.post(..., json={...})sends a JSON body (and the matching Content-Type).request.get_json(silent=True)returnsNoneinstead of raising when the body isn't valid JSON, so you can answer with your own 400.- JSON has real types:
85000arrives as anint,"85000"as a string. Check the type, not just the value.
Read, update, delete
A product API usually needs the full set of operations: look at one product, change it, and remove it. Each is the same URL, /api/products/<id>, with a different HTTP method saying what to do:
from flask import Flask, request, abort
app = Flask(__name__)
PRODUCTS = {1: {"id": 1, "name": "French Press 600ml", "price": 185000}}
def get_or_404(product_id):
return PRODUCTS.get(product_id) or abort(404)
@app.get("/api/products/<int:product_id>")
def read_product(product_id):
return get_or_404(product_id)
@app.patch("/api/products/<int:product_id>")
def update_product(product_id):
product = get_or_404(product_id)
data = request.get_json(silent=True) or {}
if "price" in data:
if not isinstance(data["price"], int) or data["price"] <= 0:
return {"errors": ["price must be a positive integer"]}, 400
product["price"] = data["price"]
return product
@app.delete("/api/products/<int:product_id>")
def delete_product(product_id):
get_or_404(product_id)
del PRODUCTS[product_id]
return "", 204
client = app.test_client()
print(client.patch("/api/products/1", json={"price": 175000}).get_json())
print(client.delete("/api/products/1").status_code)
print(client.get("/api/products/1").status_code)
{'id': 1, 'name': 'French Press 600ml', 'price': 175000}
204
404
204 No Content means "done, nothing to send back". A small helper like get_or_404 keeps every route that needs "this product or a 404" to one line.
JSON errors, not HTML errors
By default, abort(404) returns an HTML error page, which an API client can't use. Register an error handler that returns JSON:
from flask import Flask, abort
app = Flask(__name__)
@app.errorhandler(404)
def not_found(error):
return {"error": "not found"}, 404
@app.get("/api/products/<int:product_id>")
def read_product(product_id):
abort(404)
r = app.test_client().get("/api/products/99")
print(r.status_code, r.mimetype, r.get_json())
404 application/json {'error': 'not found'}
The error-handling module goes deeper. For now: an API should answer every request, including failures, in JSON.
A good API is predictable: nouns for URLs, methods for actions, JSON in and out, correct status codes, and errors that are JSON objects with a clear message.
Why this matters for clean code
An API is a contract other people's code depends on. Consistency (same shapes, same error format, same status codes everywhere) matters more than cleverness. Small helpers (get_or_404, a validation function per resource) keep each route short, and the test client lets you pin the contract down with tests, which is the next-but-several module.
Recap
- REST: nouns in URLs, HTTP methods as verbs;
GETlist/read,POSTcreate (201),PATCHupdate,DELETE(204). - Return a dict/list to send JSON; wrap lists in an object.
request.get_json(silent=True)reads a JSON body; validate types and values; answer 400 with all errors.- Use a
get_or_404helper for "this item or 404". - Register
@app.errorhandler(404)to return JSON errors in an API.