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 result

Ordering

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:

FieldTypeDescription
event.urlstrFull request URL
event.methodstrHTTP method
event.headersdict[str, str]Incoming request headers
event.cookiesCookiesShared with the remote function handler. Reads and writes collected together.
event.localsdictShared with the remote function handler. Serializable values forwarded to SvelteKit.
event.is_remoteboolTrue 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.

DeprecatedReplacement
@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()

Next steps

  • @query — read data, access cookies and locals
  • @command — write data, set cookies
  • @form — form handling with redirects and file uploads
FluidKit