DocsBuild
Recipes
Three things almost every app needs, as complete files you can copy. Every code block below with a
filename is written into a fresh project and exercised by the test suite, so what you read is what
runs.
They use plain API handlers; the same functions work behind a page in either mode.
Sign in with a session
A visitor posts a name and a password, the server remembers them in a signed cookie, and everything
under /api/account is closed to anyone else.
Turn sessions on. They need itsdangerous (uv add itsdangerous):
from nexy.core.models import NexyConfigModel
class NexyConfig(NexyConfigModel):
useSession = {"secret_key": "change-me-in-production"}Sign in and out:
from pydantic import BaseModel
from nexy.http import HTTPException, Request
USERS = {"ada": "lovelace"}
class Credentials(BaseModel):
name: str
password: str
async def POST(request: Request, body: Credentials):
if USERS.get(body.name) != body.password:
raise HTTPException(status_code=401, detail="Wrong name or password")
request.session["user"] = body.name
return {"user": body.name}from nexy.http import Request
async def POST(request: Request):
request.session.clear()
return {"signed_out": True}Guard a whole folder with one dependencies.py:
from nexy.http import HTTPException, Request
def current_user(request: Request) -> str:
if not request.session.get("user"):
raise HTTPException(status_code=401, detail="Sign in first")
return request.session["user"]
dependencies = [current_user]from nexy.http import Request
async def GET(request: Request):
return {"user": request.session["user"]}A JSON API with create, read and delete
A store in a module, a collection route and an item route. Replace the dictionary with a database when the data must survive a restart.
from pydantic import BaseModel
class Note(BaseModel):
id: int
title: str
NOTES: dict[int, Note] = {}from pydantic import BaseModel
from src.notes import NOTES, Note
class NewNote(BaseModel):
title: str
async def GET() -> list[Note]:
return list(NOTES.values())
async def POST(body: NewNote) -> Note:
note = Note(id=max(NOTES, default=0) + 1, title=body.title)
NOTES[note.id] = note
return notefrom nexy.http import HTTPException
from src.notes import NOTES, Note
async def GET(id: int) -> Note:
if id not in NOTES:
raise HTTPException(status_code=404, detail="No such note")
return NOTES[id]
async def DELETE(id: int) -> dict[str, bool]:
if NOTES.pop(id, None) is None:
raise HTTPException(status_code=404, detail="No such note")
return {"deleted": True}A body that does not fit NewNote is refused with 422 before your code runs, and the return
annotation becomes the documented response. See
validation and errors.
Receive a file
Uploads need python-multipart (uv add python-multipart). Check the size yourself, and never use
the client's file name as a path:
from pathlib import Path
from nexy.http import HTTPException, UploadFile
MAX_BYTES = 1_000_000
async def POST(file: UploadFile) -> dict[str, int | str]:
data = await file.read()
if len(data) > MAX_BYTES:
raise HTTPException(status_code=413, detail="File too large")
name = Path(file.filename or "upload").name
return {"name": name, "size": len(data)}Path(...).name drops any folder the client put in the name, such as ../../etc/passwd. Save the
bytes under a name you generate, not under this one.
Next steps
- Actions: typed server functions for forms and buttons
- Middleware and security: CORS, hosts, sessions
- Testing: test your own handlers the same way