DocsFullstack frameworks
From SvelteKit to Nexy
In Nexy a SvelteKit project keeps its Svelte pages and layouts as plain .svelte files, while
+server.ts, +page.server.ts loads, form actions and hooks move to Python files next to them.
Each section below shows a SvelteKit habit, then the same thing in Nexy.
Pick the mode first
| Your SvelteKit app is | Nexy mode | Pages are |
|---|---|---|
| an app with client navigation | client mode, useCSR = True |
.svelte files under src/routes/ |
| mostly content, a few interactive parts | server mode, the default | .nexy pages with Svelte islands |
from nexy.core.models import NexyConfigModelfrom nexy.frontend import svelte class NexyConfig(NexyConfigModel): useFF = svelte() useCSR = True useAliases = {"@": "src"}The sections below use client mode unless they say otherwise.
+page.svelte
<h1>About</h1>A page is the file itself, named after its URL:
<h1>About</h1>src/routes/+page.svelte is src/routes/index.svelte.
Dynamic routes and params
<script lang="ts"> import { page } from "$app/state";</script> <h1>{page.params.slug}</h1>The segments reach the page as props:
<script lang="ts"> let { slug }: { slug: string } = $props();</script> <h1>{slug}</h1>In any other component, the params store of @nexy/client holds them: $params.slug.
[...rest] and (group) folders work as in SvelteKit.
+layout.svelte
<script lang="ts"> let { children } = $props();</script> <header>My app</header><main>{@render children()}</main>There is no app.html: the root layout.svelte owns the document.
<script lang="ts"> import type { Snippet } from "svelte"; let { children }: { children?: Snippet } = $props();</script> <html> <body> <header>My app</header> <main>{@render children?.()}</main> </body></html>+server.ts
import { json } from "@sveltejs/kit"; export function GET({ params }) { return json({ id: Number(params.id) });}The same GET export, in Python:
async def GET(id: int): return {"id": id}from pydantic import BaseModel, Field from nexy.http import JSONResponse class NewItem(BaseModel): name: str price: float = Field(gt=0) async def POST(item: NewItem): return JSONResponse(item.model_dump(), status_code=201)error()
import { error, json } from "@sveltejs/kit"; export function GET({ params, url }) { if (Number(params.id) > 100) error(404, "No such order"); return json({ id: Number(params.id), currency: url.searchParams.get("currency") ?? "EUR" });}from nexy.http import HTTPException async def GET(id: int, currency: str = "EUR"): if id > 100: raise HTTPException(status_code=404, detail="No such order") return {"id": id, "currency": currency}load in +page.server.ts
export async function load() { return { users: await db.user.findMany() };}<script lang="ts"> let { data } = $props();</script> <ul> {#each data.users as user (user.id)} <li>{user.name}</li> {/each}</ul>In client mode the page asks a handler for its data:
async def GET(): return [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Alan"}]<script lang="ts"> let users = $state<{ id: number; name: string }[]>([]); $effect(() => { fetch("/api/users").then((response) => response.json()).then((list) => (users = list)); });</script> <ul> {#each users as user (user.id)} <li>{user.name}</li> {/each}</ul>In server mode the frontmatter of a .nexy page is the load function, and it runs while the server
renders:
users = [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Alan"}]<ul> {% for user in users %} <li>{{ user.name }}</li> {% endfor %}</ul>Form actions and use:enhance
export const actions = { default: async ({ request }) => { const data = await request.formData(); await db.todo.create({ title: data.get("title") }); },};<script lang="ts"> import { enhance } from "$app/forms";</script> <form method="POST" use:enhance> <input name="title" /> <button>Add</button></form>An action is a Python function with a generated, typed client:
from pydantic import BaseModel from nexy.decorators import Action, revalidate class Todo(BaseModel): title: str done: bool = False @Actionasync def add_todo(title: str) -> Todo: revalidate("/todos") return Todo(title=title)<script lang="ts"> import { useAction } from "@nexy/client"; import { add_todo } from "@nexy/actions"; let title = $state(""); const save = useAction(add_todo, { onSuccess: () => (title = "") });</script> <form onsubmit={(e) => { e.preventDefault(); save.run({ title }); }}> <input bind:value={title} /> <button disabled={$save.pending}>Add</button></form>revalidate("/todos") plays the part of invalidateAll(): the page fetches itself again.
goto, links and $app/state
<script lang="ts"> import { goto } from "$app/navigation"; import { page } from "$app/state";</script> <a href="/about">About</a><span>{page.url.pathname}</span><button onclick={() => goto("/dashboard")}>Dashboard</button><script lang="ts"> import Link from "@nexy/Link.svelte"; import { pathname, router } from "@nexy/client";</script> <Link href="/about" activeClass="active">About</Link><span>{$pathname}</span><button onclick={() => router.push("/dashboard")}>Dashboard</button>hooks.server.ts
export async function handle({ event, resolve }) { if (event.url.pathname.startsWith("/dashboard") && !event.cookies.get("session")) { return new Response(null, { status: 307, headers: { location: "/login" } }); } const response = await resolve(event); response.headers.set("x-app", "demo"); return response;}The path test is the folder. A root middleware.py wraps everything, a dashboard/middleware.py
only the routes below it:
from nexy.http import Request async def middleware(request: Request, call_next): response = await call_next(request) response.headers["x-app"] = "demo" return responsefrom nexy.http import RedirectResponse, Request async def middleware(request: Request, call_next): if "session" not in request.cookies: return RedirectResponse("/login", status_code=307) return await call_next(request)async def GET(): return {"visits": 12}prerender and entries
export const prerender = true; export function entries() { return [{ slug: "hello" }, { slug: "world" }];}Export getStaticParams from the page for one pre-rendered document per value:
<script lang="ts" module> export function getStaticParams() { return [{ slug: "hello" }, { slug: "world" }]; }</script> <script lang="ts"> let { slug }: { slug: string } = $props();</script> <h1>{slug}</h1>In server mode, revalidate = 60 in the frontmatter of a .nexy page caches it per URL instead:
revalidate = 60<h1>News</h1><svelte:head>
<svelte:head> <title>Blog</title></svelte:head>In client mode, write the tags in the root layout.svelte. In server mode a .nexy page sets
them, and its layout writes them with {{ useMetadata().html() }}:
metadata = {"title": "Blog", "description": "Posts about Nexy"}<h1>Blog</h1>Svelte components in server pages
<script lang="ts"> let { start = 0 }: { start?: number } = $props(); let count = $state(start);</script> <button onclick={() => count++}>{count}</button>In server mode the component is an island of a .nexy page:
from "@/components/Counter.svelte" import Counter<h1>Static heading</h1><Counter start=3 directive="client:visible" />+error.svelte
src/routes/notfound.svelte answers unknown URLs in client mode, notfound.nexy in server mode,
and API clients get {"detail": ...}.
$env/static/private
import { DATABASE_URL } from "$env/static/private";import os DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///dev.db")Running
| SvelteKit | Nexy |
|---|---|
npx sv create |
nx new, template web-fbr-svelte |
vite dev |
nx dev |
vite build with an adapter, then node build |
nx build, then nx start |
static/ |
public/, served at / |
src/lib/ and $lib |
src/components/ and @ |