Crash course: coming from React, Vue or Django¶
You already know how the web works: requests, components, state, an API, a database. This page maps what you know onto Rational Web, and calls out the three ideas that are genuinely different. Reading it takes ten minutes; the code you write is real and runs as printed.
Install and run¶
$ python -m pip install "rational[db]"
$ python app.py --dev # hot reload, live browser reload, readable error pages
There is no build step, no package.json, no bundler, no CORS. Pages are served at /,
your JSON API at /api (interactive docs at /api/docs), framework assets at
/_rational — one origin, one process.
The mental model in three sentences¶
Your components are server-side Python objects. Their
render()produces HTML on the server, andStateattributes are reactivity roots: writing one marks the components that read it dirty, and the server sends back just their HTML.The browser is a thin delivery mechanism. A ~4 KiB runtime wires events to the server, applies the HTML patches (preserving focus and caret), and runs the small closed set of
js.*operations locally. There is no client-side component tree, no hydration, no virtual DOM, and no client router.The network boundary is explicit. Every event handler is a server round trip you can see in the network tab. Only methods marked
@remoteare addressable as HTTP; nothing else is callable from the browser, by accident or otherwise.
Translation table¶
What you know |
What Rational Web does |
|---|---|
|
|
props |
annotated class attributes ( |
|
|
|
|
|
|
controlled input |
|
re-render + diff/hydration |
server re-render of dirty components + DOM morph patch |
context / store (Redux, Pinia) |
|
CSS-in-JS / Tailwind classes |
|
|
|
|
|
Express/FastAPI/Laravel controllers |
|
ORM models |
SQLAlchemy 2.0 async models (no second query language) |
A component, for orientation¶
from rational import Button, Card, Component, State, Text
class Tagger(Component):
tags = State(factory=list)
def add(self, value: str):
self.tags = [*self.tags, value] # plain Python: assign a new value
def render(self):
return Card(
Text(" ".join(f"#{t}" for t in self.tags) or "No tags yet"),
Button("Add #python", on_click=lambda: self.add("python")),
)
Clicking the button POSTs to the server, add runs on the server, and the response
carries only the card’s new HTML. It feels like client-side reactivity; the mechanism is a
round trip costing well under a millisecond of server time (see Benchmarks).
Two rules that prevent most confusion:
render()must not change state — do that in handlers ormount().Components in a list need
key=so instances keep their state across re-renders — same idea as React keys, same failure mode when omitted.
Data: services instead of fetch calls¶
In a JS stack your component fetches an endpoint. Here, a service owns the rules and the queries, and both pages and the JSON API call it:
from rational import FieldError
from rational.backend import Service, remote
class TodoService(Service):
@remote(method="GET")
async def list_todos(self) -> list[TodoOut]:
"""Every to-do for the current user."""
...
@remote
async def add(self, title: str) -> TodoOut:
if not title.strip():
raise FieldError("title", "Give it a name") # shown under the field
...
The same method is two things at once: self.use(TodoService).add(title) from any component,
and a validated, documented POST /api/todo/add with an OpenAPI schema — the endpoint you’d
otherwise hand-write. FieldError places the message under the right form field, exactly
where a client-side validation library would.
Forms are validated from handler annotations (Pydantic): pass a form model as the handler’s parameter and invalid input never reaches your code.
Styling without a preprocessor¶
from rational import Style
CARD = Style(padding="md", shadow="lg", viewport={"narrow": Style(shadow="none")})
hero = Style(background="primary", color="on_primary") | Style(padding_y="xl")
Style values are immutable and compose with |. The compiler emits one hashed class per
element — no specificity wars, no unused CSS, no purge step. Themes (light/dark/system) are CSS
custom properties; viewport= is your media query and container= your container query.
Anything it can’t express goes through the raw= escape hatch as plain CSS.
Escape hatches (use them without guilt)¶
h("iframe", src=...)— any HTML element.Style(raw={...})— any CSS.A FastAPI
APIRoutermounted as-is — the blog example serves RSS this way.Hand-written JavaScript if you truly need it; pages without handlers ship zero JS.
What to read next¶
Components, state and events — props, state scopes, identity, the event system,
js.*operations.Services, the API and databases — services,
@remote, databases, transactions.Structuring a project — the
app.py/settings.py/models/services/viewslayout every example uses.Examples — the
todosandsocial-networkwalkthroughs read well for a second pass;marketplaceshows sessions and transactions.Design — the full decision record (updates, transport, identity) if you want the why.