DocsUnderstand the model
Core concepts
The words the rest of the docs use - route, page, view, component, handler, endpoint - each in one line, with a link to the page that goes deeper. New to Nexy? Start with What is Nexy?.
How to read these docs
| Level | The question it answers | Start with |
|---|---|---|
| Application architecture | which layer answers a request, and what it refuses to do | How Nexy works |
| Application structure | where the code lives, and what a file becomes | Project structure |
| Presentation and rendering | what the reader sees, and who produces the HTML | Views, Rendering |
| Application interaction | what the reader does, and what runs in response | Actions |
The levels are read in that order once. After that, a page you need is a lookup, not a course.
Application architecture
Three ways to decide which code answers a URL. They are alternatives, not layers:
- File-based routing (FBR): a file under
src/routes/is a route. The default. - Modular router: controllers and modules under
src/apps/, declared with decorators, for a NestJS-style codebase. - Custom router: an existing FastAPI
APIRouterincluded in the application.
The choice is one line of the NexyConfig class in nexyconfig.py.
File-based routing needs no line: without useRouter, Nexy reads src/routes/.
from nexy.core.models import NexyConfigModel class NexyConfig(NexyConfigModel): pass # file-based: no useRouter, the files of src/routes/ are the routesThe modular router is the root module of src/apps/:
from nexy.core.models import NexyConfigModel from src.apps.app_module import AppModule class NexyConfig(NexyConfigModel): useRouter = AppModule # modular: the root module of src/apps/A custom router is an APIRouter you already have:
from nexy.core.models import NexyConfigModel from src.legacy import api # an APIRouter you already have class NexyConfig(NexyConfigModel): useRouter = api # custom: your router, included as it isChoosing one of the three is one decision. Choosing a frontend framework is another, separate one: see Choose a router and Views.
Application structure
Five notions that a reader coming from Next.js usually merges into one. They are not the same thing, and the difference explains most "why is my page not rendering" questions.
| Notion | What it is | What it is not |
|---|---|---|
| Route | a URL pattern, produced by the path of a file | a file: one file can answer several URLs ([slug]) |
| Page | a route file that renders a view: .nexy, .md, .mdx, .tsx, .vue, .svelte |
an endpoint: it answers a document, not data |
| View | what produces the body of a page: Jinja2 and Python, Markdown, or a client component | a route: the same view can serve several pages |
| Component | a reusable piece a page imports | a route: in server mode a component is never served on its own |
| Route handler | the function of a .py route file, named after the HTTP method (GET, POST, …) |
a page: it returns JSON, a file or a Response |
| Endpoint | the pair (URL, method) that a handler answers | a handler: one file can answer several endpoints |
In one sentence: a page is a route file, a view is what renders it, a component is what a page reuses, a handler answers an endpoint.
All five in one small project:
from "@/components/Like.tsx" import Like slug: prop[str]<h1>{{ slug }}</h1><Like post="{{ slug }}" />The frontmatter and the template below it are the view of the page; Like is the component it
reuses.
async def GET(): # the endpoint GET /api/posts return {"posts": []} async def POST(): # the endpoint POST /api/posts return {"created": True}With the modular router, a route is the path of a controller, and a handler is one of its methods, named after the HTTP verb:
from nexy.decorators import Controller @Controller("/api/posts")class PostsController: async def get(self) -> dict: # the endpoint GET /api/posts return {"posts": []} async def post(self) -> dict: # the endpoint POST /api/posts return {"created": True}from nexy.decorators import Module from .posts_controller import PostsController @Module()class AppModule: controllers = [PostsController]A page is still a file of src/routes/, or a view a controller returns with useViews.
With a custom router, a route is a path of your APIRouter, and a handler is the function its
decorator registers:
from fastapi import APIRouter api = APIRouter() @api.get("/api/posts") # the endpoint GET /api/postsasync def list_posts(): return {"posts": []} @api.post("/api/posts") # the endpoint POST /api/postsasync def create_post(): return {"created": True}Pages are still files of src/routes/: a .nexy page answers next to the included routes.
Two more words that come back everywhere:
- Layout: a
layout.*file that wraps the pages below its folder; the root one owns the HTML document. See Layouts. - Dependency: a function that runs before a handler, applied per route or per folder. See Dependencies.
Presentation and rendering
Two independent decisions, often confused:
- The format decides which file renders the page: Views.
- The mode decides who produces the HTML and when: Rendering.
| Notion | One line |
|---|---|
| Server mode | the server renders .nexy and .md pages; components are islands (modes) |
| Client mode | useCSR = True: framework files are the routes, and navigation happens in the browser |
| SSR, ISR, CSR, islands | four rendering modes, decided per page except CSR, which is per project (rendering) |
| Hydration | the browser attaching a framework to HTML the server already sent (islands) |
| Props | the values a parent passes to a component, declared name: prop[type] in a .nexy file (.nexy files) |
A server-mode page with one island: the server renders the page, the browser hydrates the chart only when it scrolls into view.
from "@/components/Chart.tsx" import Chart<h1>Sales</h1><Chart directive="client:visible" />The island goes in the view, and the controller returns the view:
from "@/components/Chart.tsx" import Chart<h1>Sales</h1><Chart directive="client:visible" />from nexy.decorators import Controllerfrom nexy.hooks import useViews @Controller("/sales")class SalesController: async def get(self): return useViews("src/apps/sales_view.nexy")Application interaction
| Notion | One line |
|---|---|
| Action | a Python function marked @Action, served as a POST, callable from a form or as a typed function (actions) |
| Form | an HTML form that posts to an Action, with validation errors coming back to the page (actions) |
| Navigation | links that prefetch, and hooks that read the path, the query, the cookies and the session (hooks) |
| Data fetching | done by the page or the handler that needs it; Nexy ships no client data layer (handlers) |
An action and the form that posts to it, which works before any script loads:
from nexy.decorators import Action @Actionasync def subscribe(email: str) -> bool: return True<form method="post" action="{{ __actions_path['subscribe'] }}"> <input name="email" type="email"> <button type="submit">Subscribe</button></form><form method="post" action="{{ __actions_path['subscribe'] }}"> <input name="email" type="email"> <button type="submit">Subscribe</button></form>What Nexy does not do
Authentication, authorisation, database, internationalisation, background jobs, email, payments and search are your application's responsibility, not the framework's. Nexy gives the extension points and says where to plug each one: see What Nexy leaves to you.
Authentication, for example, plugs into a dependency of the folder it protects. Nexy runs it before every route below; who the user is stays your decision:
from nexy import unauthorizedfrom nexy.http import Request def require_login(request: Request) -> None: if not request.session.get("user"): unauthorized(scheme="Bearer") dependencies = [require_login]Authentication, for example, plugs into a guard on the controller it protects. Nexy runs it before every handler of the class; who the user is stays your decision:
from nexy import unauthorizedfrom nexy.decorators import Controller, UseGuardfrom nexy.http import Request def require_login(request: Request) -> None: if not request.session.get("user"): unauthorized(scheme="Bearer") @Controller("/account")@UseGuard(require_login)class AccountController: async def get(self) -> dict: return {"ok": True}Authentication, for example, plugs into a dependency of the router it protects, as in FastAPI; who the user is stays your decision:
from fastapi import APIRouter, Depends from nexy import unauthorizedfrom nexy.http import Request def require_login(request: Request) -> None: if not request.session.get("user"): unauthorized(scheme="Bearer") api = APIRouter(prefix="/account", dependencies=[Depends(require_login)])The reason is not minimalism for its own sake: a framework that picks your ORM or your auth library has decided something only your application can decide.
Next steps
- Project structure: every folder and file of a project
- Glossary: the same words, in one alphabetical index
- How Nexy works: the layers, who builds what, and what Nexy leaves to you