DataLens

Returning JSON and building a simple API

Build a small REST API that lists, reads, creates, updates and deletes resources with JSON, consistent URLs and correct status codes.

Level: Intermediate · 10 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

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 + URLMeaningSuccess status
GET /api/productslist products200
GET /api/products/3read product 3200 (404 if missing)
POST /api/productscreate a product201
PATCH /api/products/3update some fields200
DELETE /api/products/3delete it204 (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']}

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.

Key idea

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

Practise this module

The questions for this lesson run in your browser and check your answer straight away.