Lesson 1 — an application that answers HTTP¶
What you will build¶
The application from the Fundamentals path, answering requests. Two routes, one of them using a service, and the adapter that turns route records into an ASGI application a server can run.
What you need¶
Xyberos with the http extra, and an ASGI server when you want to run it for real:
The rule worth remembering before any code: the runtime owns composition, Starlette owns ASGI
mechanics. Route records live in core — that is why @app.get works with no extra installed — and the
adapter compiles them.
The code¶
"""Lesson 1 — an application that answers HTTP.
Run it with:
python -m learn_traditional.traditional01_http
The test client below is the same client the test suite uses, so you can follow along without
starting a server. The lesson page shows the server command as well.
"""
from __future__ import annotations
from typing import Annotated
from starlette.applications import Starlette
from xyberos import App, Inject
from xyberos.http import HttpAdapter
from xyberos.http.testing import create_test_client
class Greeter:
"""The service the first route uses."""
def greet(self, name: str) -> str:
"""Return a greeting."""
return f"Hello, {name}!"
def create_app() -> App:
"""Build the application: services first, then routes."""
app = App()
app.provide(Greeter)
@app.get("/")
async def hello(greeter: Annotated[Greeter, Inject()]) -> dict[str, str]:
return {"message": greeter.greet("world")}
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
return app
def build_asgi_app(app: App) -> Starlette:
"""Build the ASGI application an ASGI server can run."""
return HttpAdapter(app).asgi_app
def app_factory() -> Starlette:
"""Build the example ASGI application for an ASGI server.
Usage: ``uvicorn --app-dir examples/learn_traditional --factory traditional01_http:app_factory``
"""
return build_asgi_app(create_app())
def main() -> None:
with create_test_client(HttpAdapter(create_app())) as client:
response = client.get("/")
print("status:", response.status_code)
print("json:", response.json())
print("health:", client.get("/health").json())
print("correlation id set:", bool(response.headers.get("X-Request-ID")))
if __name__ == "__main__":
main()
Three things in that file are the whole lesson:
| Part | What it does |
|---|---|
@app.get("/") |
registers a route record on the application; nothing has been served yet |
HttpAdapter(app).asgi_app |
compiles the records into a Starlette application, validating every handler signature |
create_test_client(HttpAdapter(...)) |
the client the test suite uses, so this lesson needs no running server |
How to run it¶
To run it for real, hand the ASGI application to a server:
--factory is not decoration: app_factory is called by the server to build the application, and
that function is the only place in this file that mentions Starlette. The suite checks that the quoted
command names a factory that really builds an ASGI application.
What it prints¶
The last line is worth a second look: every response carries an X-Request-ID, either the one the
client sent or a new one. It is the first thing to look for in a log — and it is untrusted display
data, never an identity.
A handler's parameters are its whole interface¶
| Parameter | Where the value comes from |
|---|---|
Annotated[T, Inject()] |
the provider graph |
| a name that appears in the route path | the URL, converted to the declared type |
request: Request |
Starlette |
Anything else is refused when the adapter is constructed, which is why a handler with a mistake fails at startup rather than on the first request.
What a handler may return¶
| Handler returns | The client receives |
|---|---|
a Starlette Response |
it, unchanged |
None |
204 No Content |
| anything else | 200 with the value as JSON |
Common mistakes¶
- Forgetting the adapter.
app.routesis a list of route records; nothing serves them untilHttpAdapter(app).asgi_appexists.xyberos checkworks without it, which is a useful way to separate "my graph is wrong" from "my server is wrong". - Importing Starlette in a service. The adapter is the one module that should mention it; a service that does cannot be tested without the extra.
- Building the adapter once at import time in a test.
create_test_clientruns the application's lifespan, so build the application inside thewithblock or pass a freshly built one, as this lesson does.
Exercise¶
Add a route /hello/{name} that greets whoever is in the URL.
Show the answer
"""Lesson 1, exercise — a route that greets whoever is in the URL.
The answer to the lesson's exercise. The only new idea is a path parameter: it is a handler argument
whose name appears in the route, and the adapter converts it to the declared type.
"""
from __future__ import annotations
from typing import Annotated
from xyberos import App, Inject
from xyberos.http import HttpAdapter
from xyberos.http.testing import create_test_client
class Greeter:
"""The service the route uses."""
def greet(self, name: str) -> str:
"""Return a greeting."""
return f"Hello, {name}!"
def create_app() -> App:
"""Build the application with one route that takes a path parameter."""
app = App()
app.provide(Greeter)
@app.get("/hello/{name}")
async def hello_name(name: str, greeter: Annotated[Greeter, Inject()]) -> dict[str, str]:
return {"message": greeter.greet(name)}
return app
def main() -> None:
"""Ask the route for a greeting."""
with create_test_client(HttpAdapter(create_app())) as client:
print(client.get("/hello/Ada").json())
if __name__ == "__main__":
main()
Next¶
Lesson 2 reads more of the request — query strings, bodies, and the errors that say "no" with a status code. Until it is published, the roadmap shows where each lesson stands.