Hooks
FluidKit hooks let you run Python code at server lifecycle points and intercept every remote
function call — without writing any TypeScript. They mirror SvelteKit's hooks.server.ts behavior but live entirely in Python.
Lifecycle
@hooks.init
Runs once when the server starts. Async or sync, no parameters. One per application —
registering a second from the same module replaces the first with a warning, from a different
module raises RuntimeError.
from fluidkit import hooks
@hooks.init
async def setup():
global db
db = await Database.connect("postgresql://...")@hooks.cleanup
Runs once when the server shuts down. Async or sync, no parameters. One per application.
@hooks.cleanup
async def teardown():
await db.close()@hooks.lifespan
Paired setup and teardown via a generator. Code before yield runs at startup, code
after runs at shutdown. Async or sync generator, yields exactly once. One per application.
@hooks.lifespan
async def manage_redis():
global redis
redis = await aioredis.from_url("redis://localhost")
yield
await redis.close()Request middleware — @hooks.handle
Runs before every remote function call. Receives (event, resolve). Must return await resolve(event) to continue, or return early to short-circuit.
from fluidkit import hooks, error
@hooks.handle
async def auth(event, resolve):
token = event.cookies.get("access_token")
if not token:
error(401, "Unauthorized")
event.locals["user"] = await verify_token(token)
return await resolve(event)Multiple @hooks.handle hooks are allowed and execute in source order by default:
@hooks.handle
async def logging(event, resolve):
import time
start = time.time()
result = await resolve(event)
print(f"{event.method} {event.url} took {time.time() - start:.2f}s")
return resultOrdering
Use hooks.sequence() to set explicit execution order. Each function must already be
decorated with @hooks.handle. Calling it from the same module replaces the previous
order. Calling it from a different module raises RuntimeError.
hooks.sequence(auth, logging)HookEvent reference
The event object passed to every @hooks.handle handler:
| Field | Type | Description |
|---|---|---|
event.url | str | Full request URL |
event.method | str | HTTP method |
event.headers | dict[str, str] | Incoming request headers |
event.cookies | Cookies | Shared with the remote function handler. Reads and writes collected together. |
event.locals | dict | Shared with the remote function handler. Serializable values forwarded to SvelteKit. |
event.is_remote | bool | True for remote function calls, False for page-level requests. |
Sharing data via event.locals
event.locals and event.cookies are the same instances shared with RequestEvent inside the remote function. A value set in a hook is immediately
visible inside the handler. Serializable values in locals are forwarded to
SvelteKit automatically.
from fluidkit import hooks, query, get_request_event
@hooks.handle
async def auth(event, resolve):
token = event.cookies.get("access_token")
event.locals["user"] = await verify_token(token)
return await resolve(event)
@query
async def get_profile() -> dict:
event = get_request_event()
user = event.locals.get("user") # set by auth hook above
if not user:
error(401, "Unauthorized")
return await db.get_profile(user["id"])Error hooks
Error hooks fire for unexpected exceptions only. error() and redirect() are intentional control flow and never reach these hooks.
@hooks.handle_error
Fires for TypeError (status 400), ValueError (400 in @form, 500 elsewhere), and any other unhandled exception (status 500). Must accept (error, event, status, message). Must return a dict with at minimum {"message": str}
@hooks.handle_error
async def on_error(error, event, status, message):
error_id = str(uuid4())
logger.exception(error, extra={"error_id": error_id})
return {"message": "Something went wrong", "error_id": error_id}@hooks.handle_validation_error
Fires when a remote function parameter fails Pydantic schema validation (status 400). Must
accept (issues, event) where issues is Pydantic's e.errors() list. Must return a dict with at minimum {"message": str}.
@hooks.handle_validation_error
async def on_validation_error(issues, event):
first = issues[0] if issues else {}
field = first.get("loc", ("input",))[-1]
return {"message": f"Invalid value for field: {field}"}One of each per application. If either hook itself raises, the default error response is used silently.
Generated src/hooks.server.ts
When any hooks are registered, FluidKit automatically generates src/hooks.server.ts. Do not edit it — FluidKit overwrites it on every dev and build. If you need additional SvelteKit handle logic, use
SvelteKit's sequence() helper in a separate file.
If no hooks are registered and this file was previously generated by FluidKit, it is removed automatically.
Deprecated API
@on_startup, @on_shutdown, and @lifespan imported
directly from fluidkit still work but emit DeprecationWarning.
| Deprecated | Replacement |
|---|---|
@on_startup | @hooks.init |
@on_shutdown | @hooks.cleanup |
@lifespan | @hooks.lifespan |
# Before
from fluidkit import on_startup, on_shutdown
@on_startup
async def setup():
global db
db = await Database.connect("postgresql://...")
@on_shutdown
async def teardown():
await db.close()
# After
from fluidkit import hooks
@hooks.init
async def setup():
global db
db = await Database.connect("postgresql://...")
@hooks.cleanup
async def teardown():
await db.close()