Typorithm · medium

Static and dynamic routes have an order

pythonfastapiindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Static and dynamic routes have an order

0%

Concept and contract

Use APIRouter without letting a generic identifier consume a named operation.

A router groups related operations, but it does not eliminate route matching rules. A generic path such as /roles/{role_id} can match text intended for a named endpoint. If that route appears first, later validation can fail before the intended static operation is considered.

The fixture registers /roles/stats before /roles/{role_id}. The first returns an observed fixture count; the second accepts an integer identifier. The count describes the owned example only and does not claim to represent all vacancies on a live job market.

Mechanism and worked example

APIRouter supplies a common prefix, and include_router attaches its routes to the application.

APIRouter supplies a common prefix, and include_router attaches its routes to the application. A static path is registered first so stats reaches its own endpoint. A numeric segment reaches the dynamic operation, where FastAPI applies the integer annotation.

The helper sends exact local paths and reports status and body. This makes route selection observable. A nonnumeric unknown segment matches the dynamic shape and then fails integer parsing; it is not necessarily the same result as a completely unmatched URL.

Complete worked example

python

from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient

def inspect_example(path):
    app = FastAPI()
    router = APIRouter(prefix="/roles")
    def stats():
        return {"observed": 2}
    def read(role_id: int):
        return {"id": role_id}
    router.add_api_route("/stats", stats, methods=["GET"])
    router.add_api_route("/{role_id}", read, methods=["GET"])
    app.include_router(router)
    with TestClient(app) as client:
        response = client.get(path)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
APIRouter supplies a common prefix, and include_router attaches its routes to the application. A static path is registered first so stats reaches its own endpoint. A numeric segment reaches the dynamic operation, where FastAPI applies the integer annotation. The helper sends exact local paths and reports status and body. This makes route selection observable. A nonnumeric unknown segment matches the dynamic shape and then fails integer parsing; it is not necessarily the same result as a completely unmatched URL.

Focused decision: use the complete example for context

python

router = APIRouter(prefix="/roles")
router.add_api_route("/stats", stats, methods=["GET"])
router.add_api_route("/{role_id}", read, methods=["GET"])
app.include_router(router)
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The controlled mistake reverses registration order.

The controlled mistake reverses registration order. The earlier dynamic path accepts the stats segment structurally, then its int conversion rejects that text. The presence of a later static route does not retroactively choose it after validation fails.

Do not fix this problem by accepting any string as an identifier or by suppressing all validation errors. That changes the dynamic contract without solving the route design clearly. Keep named operations explicit and test them alongside representative identifiers whenever routes are added or reorganized.

Controlled mistake: observe the actual wrong behavior

python

from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient

def inspect_example(path):
    app = FastAPI()
    router = APIRouter(prefix="/roles")
    def stats():
        return {"observed": 2}
    def read(role_id: int):
        return {"id": role_id}
    router.add_api_route("/{role_id}", read, methods=["GET"])
    router.add_api_route("/stats", stats, methods=["GET"])
    app.include_router(router)
    with TestClient(app) as client:
        response = client.get(path)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The controlled mistake reverses registration order. The earlier dynamic path accepts the stats segment structurally, then its int conversion rejects that text. The presence of a later static route does not retroactively choose it after validation fails. Do not fix this problem by accepting any string as an identifier or by suppressing all validation errors. That changes the dynamic contract without solving the route design clearly. Keep named operations explicit and test them alongside representative identifiers whenever routes are added or reorganized.

Independent practice: one bounded change

Rename the static operation from stats to summary.

Rename the static operation from stats to summary. Preserve its registration before the dynamic operation, keep the numeric route and let the former stats path follow the existing dynamic parsing behavior. This is a focused route evolution task, not a request to support every spelling.

Check the new summary path, one numeric ID and the old stats path. Consider whether a real public API should retain a redirect or alias for compatibility, but do not add it silently to this exercise. The acceptance cases define the intended new shape.

Step 1: Rename the named operation to summary while preserving route ordering and numeric validation. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient

def inspect_example(path):
    app = FastAPI()
    router = APIRouter(prefix="/roles")
    def stats():
        return {"observed": 2}
    def read(role_id: int):
        return {"id": role_id}
    router.add_api_route("/stats", stats, methods=["GET"])
    router.add_api_route("/{role_id}", read, methods=["GET"])
    app.include_router(router)
    with TestClient(app) as client:
        response = client.get(path)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}

# Independent task: Rename the named operation to summary while preserving route ordering and numeric validation.
Reasoning
Rename the named operation to summary while preserving route ordering and numeric validation.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution changes the static path declaration while retaining ordering.

The solution changes the static path declaration while retaining ordering. The named summary operation returns the same observed count. Numeric reads are unchanged, and the former static name no longer resolves as that named operation.

Route organization also affects generated documentation, URL construction and client caches. A prefix is useful for grouping but should not be confused with a versioning or authorization policy. Router-level dependencies can add checks, while each object operation may still need an ownership decision.

Practice solution and acceptance checks

python

from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient

def inspect_example(path):
    app = FastAPI()
    router = APIRouter(prefix="/roles")
    def stats():
        return {"observed": 2}
    def read(role_id: int):
        return {"id": role_id}
    router.add_api_route("/summary", stats, methods=["GET"])
    router.add_api_route("/{role_id}", read, methods=["GET"])
    app.include_router(router)
    with TestClient(app) as client:
        response = client.get(path)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The solution changes the static path declaration while retaining ordering. The named summary operation returns the same observed count. Numeric reads are unchanged, and the former static name no longer resolves as that named operation. Route organization also affects generated documentation, URL construction and client caches. A prefix is useful for grouping but should not be confused with a versioning or authorization policy. Router-level dependencies can add checks, while each object operation may still need an ownership decision.

Interview reasoning and next step

Explain why the old path produces a validation failure rather than the statistics response in the reversed-order counterexample.

Explain why the old path produces a validation failure rather than the statistics response in the reversed-order counterexample. Identify the difference between structural path matching and typed parameter validation. This helps diagnose seemingly inexplicable 422 responses.

The next lesson adds authorization. A correctly matched and validated path still says nothing about who can read the selected object. Keep identity resolution and ownership checks explicit rather than relying on the shape of an identifier.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track fastapi, then edit the working exercise baseline and run --practice fa-routing. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Use APIRouter without letting a generic identifier consume a named operation.