LESSON 75 · ADVANCED4 min read

Project: A Flask micro web app

Build a small Flask JSON service with an app factory, validated requests, honest status codes, and tests that never start a live server.

Python 3.12+Free to readModule 17 of 19
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Explain the request/response contract of a small web service.
  • Organize a Flask app with a factory and a blueprint.
  • Validate JSON at the boundary and return consistent errors.
  • Test routes with Flask's test client.

01A web app is a request/response contract

A route maps a method and a path to a handler. The handler reads the request, applies a rule, and returns a response with a status code. GET reads, POST creates, PUT or PATCH updates, and DELETE removes. The status code is part of the contract: 201 for a created resource, 404 for a missing one, and 422 for a well-formed request with invalid fields.

Design the resource before the code. A task has an id, a title, and a completed flag. Decide which fields are accepted on create, which are returned, and what an error body looks like. Consistency matters more than cleverness: a client should be able to handle every failure the same way.

http_contract.txtShell
GET    /tasks        -> 200 [task, ...]POST   /tasks        -> 201 task | 400/422 errorGET    /tasks/<id>   -> 200 task | 404 errorPATCH  /tasks/<id>   -> 200 task | 404/422 errorDELETE /tasks/<id>   -> 204 no body | 404 error

02Use an app factory

An app factory is a function that creates and configures the Flask application. It accepts configuration, registers blueprints, and returns the app. Tests can call the factory with a test configuration instead of importing a globally configured app, which keeps test state isolated.

A blueprint groups related routes. The factory registers the blueprint under a URL prefix. Keep the entry point thin: create the app and run it only under the main guard, or let a production server import the factory.

app_factory.pyPython
from flask import Flask, Blueprint, current_app, jsonify, requestfrom validation import parse_createapi = Blueprint("api", __name__)@api.get("/tasks")def list_tasks():    return jsonify(list(current_app.extensions["tasks"].values()))@api.post("/tasks")def create_task():    payload = request.get_json(silent=True)    fields, error = parse_create(payload)    if error is not None:        return error    store = current_app.extensions["tasks"]    task_id = current_app.extensions["next_id"]    current_app.extensions["next_id"] = task_id + 1    task = {"id": task_id, **fields}    store[task_id] = task    return jsonify(task), 201def create_app(config=None):    app = Flask(__name__)    if config:        app.config.update(config)    app.extensions["tasks"] = {}    app.extensions["next_id"] = 1    app.register_blueprint(api, url_prefix="/api")    return appif __name__ == "__main__":    create_app().run(debug=True)

03Validate JSON at the boundary

Use request.get_json(silent=True) so malformed JSON becomes a controlled 400 rather than an HTML error page. Check that the body is a dictionary, that required keys are present, and that each value has the expected type. Return a 422 with a field-level message when the shape is valid but the content is not.

Do not trust a client-supplied id or completed flag on create. Assign the id on the server and set the initial state from your own rule. This prevents a caller from choosing an identifier that collides with an existing record or marking a new task complete before it exists.

validation.pyPython
from flask import jsonifydef parse_create(payload):    if not isinstance(payload, dict):        return None, (jsonify(error="body must be a JSON object"), 400)    title = payload.get("title")    if not isinstance(title, str) or not title.strip():        return None, (jsonify(error="title is required"), 422)    return {"title": title.strip(), "completed": False}, None

04Test with the test client

Flask's test client sends requests to the app without opening a port. A pytest fixture creates a fresh app for each test, so a task created in one test does not leak into another. Assert the status code and the JSON body, not just that the request succeeded.

Cover the happy path, a validation failure, and a missing resource. Those three tests catch most boundary regressions. When the service grows, add a test for duplicate creation and for an unsupported method.

test_api.pyPython
import pytestfrom app_factory import create_app@pytest.fixturedef client():    app = create_app({"TESTING": True})    with app.test_client() as test_client:        yield test_clientdef test_list_is_empty(client):    response = client.get("/api/tasks")    assert response.status_code == 200    assert response.get_json() == []def test_create_rejects_blank_title(client):    response = client.post("/api/tasks", json={"title": "  "})    assert response.status_code == 422    assert "title" in response.get_json()["error"]
LET IT CLICK

The ideas worth keeping.

  • A route is a method, a path, a handler, and a status code.
  • The app factory keeps configuration and test state isolated.
  • Validate at the boundary and test with the client, not a live server.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Add a read-one route

Add GET /tasks/<int:task_id> that returns the task or a 404 JSON error. Use the same error shape as the other routes.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
from flask import current_app, jsonify@api.get("/tasks/<int:task_id>")def get_task(task_id):    task = current_app.extensions["tasks"].get(task_id)    if task is None:        return jsonify(error="task not found"), 404    return jsonify(task)

The converter <int:task_id> rejects non-numeric paths before the handler runs. The handler still checks existence because a valid integer can identify a missing record.

02

Test a validation failure

Write a test that posts a task with a numeric title and asserts a 422 response with a field-level error. Use the test client fixture.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
def test_create_rejects_non_string_title(client):    response = client.post("/api/tasks", json={"title": 42})    assert response.status_code == 422    body = response.get_json()    assert body["error"] == "title is required"

A numeric title is syntactically valid JSON but violates the field contract. The test documents the difference between malformed JSON (400) and invalid fields (422).

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Which status code means a resource was created?
02Why use an app factory?
03What is the difference between a 400 and a 422?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Add a read-one route

Add GET /tasks/<int:task_id> that returns the task or a 404 JSON error. Use the same error shape as the other routes.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
main.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.