Skip to main content

Nexy - The Python fullstack meta-framework: FastAPI backend, Vite frontend, zero config

Nexy

The fullstack Python meta-framework. FastAPI on the server, Vite on the client, one file per page. Sub-second startup, HMR in under 100 ms.

PyPI version Python versions CI GitHub stars License Last commit

Why Nexy

A route is a file. Put a .nexy file under src/routes/ and you get a page: Python for the logic, Jinja2 for the markup, an optional React, Vue, Svelte, Solid or Preact component for interactivity. Put a .py file there and you get an API handler: the function names are the HTTP methods.

The server is FastAPI and Starlette. The client is Vite. No second process, no second origin, no glue project between the frontend and the backend.

You get Instead of
Sub-second startup 10-30 s waiting for Next.js, Rails, Django
One file = server logic, markup, interactive island Separate API, UI and build-tooling projects
Zero-config Vite integration Hand-wiring Webpack, Vite or Parcel
Any supported client framework, or none Being locked into React or Vue
SSR, ISR and parallel SSG built in SSG bolted on afterwards

Quickstart

uvx nexy new my-app
cd my-app
uvx nexy dev

Open http://localhost:3000. uvx downloads Nexy and runs it, so nothing is installed globally. Install the CLI once with uv tool install nexy to drop the prefix and use nx dev (or nexy dev).

new asks four questions: file-based router or modular, web or API, client framework or none, ORM or none. The answers decide what the scaffold writes, including the Node package manager it reuses: a committed lockfile wins when that tool is installed, otherwise bun, pnpm then npm by priority.

Change a file, the page reloads. nx dev compiles the routes you touched and proxies Vite through its own port, so the browser only ever talks to 3000.

Requirements: Python 3.12+, uv, and Node.js 18+ only when you use a client framework.

A page, an endpoint, an island

A page

Python on top, HTML in the middle, CSS at the bottom.

---
from nexy import usePathname

title: prop[str] = "Hello"
pathname = usePathname()
---
<article class="post">
  <h1>{{ title }}</h1>
  <p>You are on {{ pathname }}</p>
</article>

<style>
  .post { max-width: 60ch; }
</style>

Everything between the --- lines is Python. Imports resolve at compile time; the rest runs on every request. prop[type] declares a property, = value gives it a default.

The body is Jinja2 with the frontmatter variables in scope. Autoescaping is off: escape untrusted values with {{ value | e }}, and wrap JavaScript containing {{ or {% in {% raw %}...{% endraw %}. Declare props before using them; an undeclared variable renders as an empty string.

An endpoint

# src/routes/api/items.py  →  /api/items
from fastapi import Request
from pydantic import BaseModel


class Item(BaseModel):
    name: str


async def GET(request: Request):
    return {"items": []}


async def POST(request: Request, body: Item):
    return {"created": body.name}

Each function named after an HTTP method becomes an endpoint. Path segments, query strings, JSON bodies, Depends, headers and uploads keep their normal FastAPI behaviour, and every handler is published in the OpenAPI schema at /openapi.json and rendered by Scalar at /docs.

An island

Import a client component by path; the compiler mounts it and passes the attributes as props. This is not a client-only island: the HTML exists before the bundle runs, so the first paint does not wait for JavaScript.

---
from "@/components/Counter.tsx" import Counter
---
<Counter start="0" />

Project layout

my-app/
├─ src/
│  ├─ routes/            file-based routes (FBR)
│  │  ├─ index.nexy      → /
│  │  ├─ layout.nexy     shared layout, not a route
│  │  └─ api/hello.py    → /api/hello
│  ├─ apps/              modular router, decorators (optional, next to routes/)
│  ├─ components/        imported components (a convention, not enforced)
│  └─ globale.css        global styles (created by `nexy new`)
├─ public/               served at /
├─ nexyconfig.py         project configuration
├─ pyproject.toml
├─ package.json          only with a client framework
├─ vite.config.ts        only with a client framework
└─ __nexy__/             build output - generated, never edited

Routing

The URL is the file path relative to src/routes/, minus the extension.

File URL
index.nexy /
about.nexy /about
blog/index.nexy /blog
blog/[slug].nexy /blog/{slug}
docs/[...path].nexy /docs/{path:path}
(marketing)/pricing.nexy /pricing
api/users.py /api/users

index maps to its directory, not to /index. A directory in parentheses is a group: it organises files without appearing in the URL. Files and folders starting with _ or . are ignored, which is the place for partials and drafts.

These names are not routes: layout.nexy, loading.nexy, errors.nexy, notfound.nexy, dependencies.py, __init__.py. A layout.* wraps every page in its directory and below, and receives the page as children. notfound.* answers 404, errors.* answers 500, and dependencies.py applies its FastAPI dependencies to that subtree.

One file per URL. blog.nexy next to blog/index.nexy registers two routes for the same path; pick one.

Dynamic segments become page properties: [slug] gives a str, [...path] gives one string with the slashes included. In a .nexy page only int and float annotations are converted.

Routing reference →

Views

A view renders the page body. Pick one format per file.

Format File Server render Client framework
.nexy page Jinja2 + Python logic, streamed optional
.md pure Markdown cmarkgfm at request time, GFM, layout-wrapped optional via layout
.mdx Markdown with components framework SSR React or Preact
.tsx, .jsx component framework SSR / SSG React or Preact
.vue SFC framework SSR / SSG Vue
.svelte SFC framework SSR / SSG Svelte

Markdown pages get heading IDs and a generated table of contents, and support locale variants such as about.fr.md. The framework for .mdx is detected from the imports: Preact when the file imports preact, React otherwise.

# nexyconfig.py
from nexy.core.models import NexyConfigModel
from nexy.frontend import react


class NexyConfig(NexyConfigModel):
    useFF = react()  # vue(), svelte(), solid(), preact()

With a framework enabled, its extensions become active routes, and nx new sets it up with Vite and Tailwind. useVite defaults to True.

Views → · .nexy files → · Markdown →

Rendering modes

Mode How Behaviour
SSR default rendered per request by Jinja2, or by the framework at build time for framework routes
ISR revalidate = 60 in the frontmatter in-process, per-path cache; sends Cache-Control: public, s-maxage=60, stale-while-revalidate=60
CSR useCSR = True in nexyconfig.py first load returns HTML, then the client router navigates; .nexy routes are disabled
SSG nx build framework routes pre-rendered to HTML by a per-file esbuild pipeline, one file per worker

CSR requires a client framework and is incompatible with a modular useRouter. Only .tsx, .jsx, .vue and .svelte routes are navigable client-side; .md can use a CSR shell.

SSR → · CSR → · ISR →

More server features

WebSocket

Define SOCKET in a .py route file to register a WebSocket at the same path:

# src/routes/ws/chat.py
from fastapi import WebSocket


async def SOCKET(websocket: WebSocket):
    await websocket.accept()
    await websocket.send_text("hello")
    await websocket.close()

Modular routing

Files are the default, not the only option. Classes and decorators produce an APIRouter mounted with useRouter:

# src/apps/app_controller.py
from nexy.decorators import Controller
from nexy.core.types import JsonResponse


@Controller(prefix="/users")
class UserController:
    def get(self) -> JsonResponse:
        return {"users": []}
# nexyconfig.py
class NexyConfig(NexyConfigModel):
    useRouter = AppModule  # a module class or any APIRouter

Methods are matched by name, case-insensitively; duplicates raise at startup. @Injectable() classes are resolved into a controller's __init__, with Scope.SINGLETON, Scope.REQUEST and Scope.TRANSIENT lifetimes, and @UseGuard() / @Middleware() add FastAPI dependencies. File-based routing keeps working alongside a modular router.

Modular routing →

Actions

@Action turns a function into a POST endpoint with a generated path, so a page can call server code without declaring a route file:

# src/actions/user.py
from nexy.decorators import Action


@Action
async def create_user(name: str) -> dict:
    return {"name": name}
<form method="post" action="{{ __actions_path['create_user'] }}">
  <input name="name">
  <button type="submit">Create</button>
</form>

The path is deterministic (a hash of the module and function name). It keeps the endpoint out of casual discovery but is not a secret; do not rely on it for authorization.

Actions →

Hooks and components

Hooks read the current request from inside a .nexy page. Calling one outside a request raises RuntimeError.

Hook Returns
usePathname() request path, e.g. "/blog/post-1"
useSearchParams() query string as dict[str, str], first value per key
useQuery() dynamic path parameters, e.g. {"slug": "post-1"}
useRouter() {"path", "base_url", "url_for"}
useCookies() all cookies as dict[str, str]
useSession() the session dict, {} when sessions are not enabled
useToc(depth_range="2-6") table of contents HTML, "" when there is none
useViews(path, context=None) renders another view and returns an HTMLResponse

Built-in helpers return HTML strings, usable as a Jinja call or as a tag:

{{ Audio("/theme.mp3", controls=True) }}

<Audio src="/theme.mp3" controls=True />
Helper Purpose
Image <img> with lazy loading and explicit dimensions
Link <a data-nexy-link>, prefetched on hover and handled by the client router
Audio, Video media tags, with <source> children derived from sources
Import imports a component from Python, like the template syntax

Hooks → · Components →

Internationalization

Nexy ships no runtime i18n: an application picks the library it prefers. The command line itself is translated. See the i18n guide.

Config

Configuration is a class in nexyconfig.py at the project root. Nexy loads it from the directory it runs in; a project without the file uses the defaults.

from nexy.core.models import NexyConfigModel
from nexy.frontend import react


class NexyConfig(NexyConfigModel):
    useFF = react()
    usePort = 3000
    useHost = "0.0.0.0"
    useTitle = "My App"
    useAliases = {"@": "src/"}
    useCORS = {"allow_origins": ["https://example.com"]}
Area Fields
Server usePort, useHost, useSslKeyfile, useSslCertfile, useCSR
Routes useRouter, useAliases, excludeDirs, useLayoutExtensions, useNotFoundExtensions, useErrorsExtensions, useLoadingExtensions
Frontend useFF, useVite, useMarkdownExtensions, useWatchExtensions, useWatchExcludePatterns
API docs useDocs, useDocsUrl, useTitle
Middleware useMiddlewares, useCORS, useGZip, useTrustedHost, useSession, useAuth, useHTTPSRedirect
Markdown useTocDepth, useTocTitle, useTocAuto

Every field and its default →

CLI

The binary is nx, also installed as nexy. Without a global install, prefix with uvx nexy.

Command Alias What it does
nx dev nx d development server: compile on change, Vite, HMR
nx start nx s production server on the compiled output
nx build nx b production build
nx new nx n scaffold a project
nx init nx i add Nexy to an existing project
nx migrate nx m ORM migrations: autogenerate a revision, --init, --upgrade
nx check nx c type-check: mypy on Python, tsc on TypeScript
nx format nx f format Python, JS/TS and .nexy files
nx test - run pytest; --client adds vitest, --no-server skips pytest

--port and --host override usePort and useHost for dev and start; --check runs ruff and mypy after build, and reports instead of rewriting for format; --template / -t skips the new prompts.

All flags →

Build and deploy

nx build
nx start --host 0.0.0.0 --port 8000

nx build writes everything to __nexy__/: the compiled Python routes, the client bundle under __nexy__/client/, and the pre-rendered HTML for framework routes. nx start runs Uvicorn on that output - Python only, no Node process and no Vite process at runtime.

Deploy by copying the project with __nexy__/, or build inside the image. The build stage needs Node and Python; the runtime stage needs Python only.

FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project
COPY . .
RUN uv sync --frozen && uv run nx build

FROM python:3.13-slim
WORKDIR /app
COPY --from=build /app /app
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["nx", "start", "--host", "0.0.0.0", "--port", "8000"]

Static hosting is a deployment choice, not a build mode: there is no --static flag. Put static files in public/; they are served from / in development and production. Behind a reverse proxy, remember to forward WebSocket upgrade requests.

Deploy guide →

How it works

  • A .nexy file is compiled to a Python module in __nexy__/, mounted through a virtual file system, and rendered with Jinja2 at request time.
  • Framework routes are pre-rendered to HTML at build time by a per-file esbuild pipeline; the worker pool runs one file per worker, sized to the CPU count. A route whose server render fails is reported as client-only instead of failing the build.
  • In development, the watcher recompiles what changed and restarts Uvicorn cleanly - no importlib hacks, no stale modules. Vite runs on an internal port and Nexy proxies it through the app origin.
  • In production, one Uvicorn process serves the routes, the HTML and the assets from one origin.

Internals, request lifecycle and compiler pipeline: ARCHITECTURE.md.

How it compares

Feature Nexy Next.js Django + HTMX Remix
Language Python + any JS framework JS/TS only Python + HTMX JS/TS only
Server process Uvicorn + FastAPI, one process Node Python (ASGI/WSGI) Node
Startup < 1 s 10-30 s 3-8 s 8-15 s
HMR < 100 ms ~500 ms N/A ~300 ms
File format .nexy (Python + Jinja2 + UI) .tsx / .jsx .py + .html .tsx
SSR Built-in (esbuild) Built-in (React) Manual Built-in (React)
SSG Parallel worker pool next export Third-party Via Vite
Routing File-based + modular File-based Manual File-based

Framework support

Framework SSR SSG HMR
React ✓ ✓ ✓
Vue ✓ ✓ ✓
Svelte ✓ ✓ ✓
Solid ✓ ✓ ✓
Preact ✓ ✓ ✓
None (vanilla) ✓ - ✓

Documentation

This README is the front door. The pages below go deeper on one topic each, in plain Markdown, and are written from the code.

I want Read
to create a project and run it Getting started
the files and folders Project structure
the URL of a file Routing
dynamic segments, layouts, error pages Pages
the current path, query, cookies, session Hooks
to write a page .nexy files
Markdown or MDX Markdown
built-in HTML helpers Components
SSR, CSR, ISR SSR, CSR, ISR
a WebSocket WebSocket
controllers and modules Modular routing
to add an endpoint API
a form posted from a page Actions
translations i18n
a config field Config
the client build and Vite Frontend
every command and flag CLI
to test a project Testing
build output and Docker Deploy
rules for an AI agent Agents

llms.txt is the same map in one file, for tools that read plain text.

Contributing

Contributions are welcome - seasoned engineers and first-time PR authors alike.

Security issues: see SECURITY.md. Do not open a public issue for a vulnerability.

License

MIT - free forever, built in the open.

Metadata

Release files for nexy 2.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nexy 2.2.0
File Size Uploaded Provenance Actions
nexy-2.2.0.tar.gz 374.4 kB With provenance Details for nexy-2.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for nexy 2.2.0
File Interpreter ABI Platform Actions
nexy-2.2.0-py3-none-any.whl Python 3 none any Details for nexy-2.2.0-py3-none-any.whl

Total release size: 979.3 kB

Release files / nexy-2.2.0.tar.gz

Download URL nexy-2.2.0.tar.gz
Size 374.4 kB
Tags Source
SHA-256 checksum
How to use checksums
62b95c88608b187fad1072328b0facb45cc0764406021fb4014b5f7f618ac2db
BLAKE2b-256 checksum
How to use checksums
b78510f7688769c73741dbb6b4d6bd578d5c380cf51afdc45616ba1d24f20ffb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Release files / nexy-2.2.0-py3-none-any.whl

Download URL nexy-2.2.0-py3-none-any.whl
Size 604.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
374c10ddb86e50995f6d86e411b1f6ebe3d5c1ccb500b5881c034834f0e7b96f
BLAKE2b-256 checksum
How to use checksums
2cfd74dfe645e2b8a028c129a703e7a5a5002479096f5b81f5973f026f3ca566
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Security for nexy 2.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for nexy-2.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

2.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page