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 APIRouter included 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/.

nexyconfig.py
from nexy.core.models import NexyConfigModel  class NexyConfig(NexyConfigModel):    pass  # file-based: no useRouter, the files of src/routes/ are the routes

The modular router is the root module of src/apps/:

nexyconfig.py
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:

nexyconfig.py
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 is

Choosing 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:

src/
routes/
blog/[slug].nexya route (/blog/{slug}) whose file is a page
api/posts.pya route whose handlers answer two endpoints
components/
Like.tsxa component: never served on its own
src/routes/blog/[slug].nexy
---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.

src/routes/api/posts.py
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:

src/
apps/
app_module.pythe root module: lists the controllers
posts_controller.py a route (/api/posts) whose handlers answer two endpoints
components/
Like.tsxa component: never served on its own
src/apps/posts_controller.py
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}
src/apps/app_module.py
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:

src/legacy.py
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.

src/routes/sales.nexy
---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:

src/apps/sales_view.nexy
---from "@/components/Chart.tsx" import Chart---<h1>Sales</h1><Chart directive="client:visible" />
src/apps/sales_controller.py
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:

src/actions/newsletter.py
from nexy.decorators import Action  @Actionasync def subscribe(email: str) -> bool:    return True
src/routes/index.nexy
<form method="post" action="{{ __actions_path['subscribe'] }}">  <input name="email" type="email">  <button type="submit">Subscribe</button></form>
src/apps/home_view.nexy
<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:

src/routes/account/dependencies.py
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:

src/apps/account_controller.py
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:

src/account.py
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

Edit on GitHub

Enter opens the result, Esc closes.