Skip to content

Static files

A fast hunter still needs markings: CSS, JavaScript, images, fonts, favicons. VelociPy’s StaticFiles handler serves those assets directly from disk without dragging the main application into the work. It guards against directory traversal, caches filesystem metadata, and answers conditional requests with 304 Not Modified when the client already has the right file.

Use app.mount() to pin the handler under a URL prefix, then let the framework handle the rest.

Serving assets

Create a static/ directory and put your assets inside it:

static/
├── style.css
└── logo.png

Mount StaticFiles at /static:

from velocipy import VelociPy
from velocipy.routing.static import StaticFiles

app = VelociPy()
app.mount("/static", StaticFiles(directory="static"))

Now GET /static/style.css returns static/style.css with the correct content-type and a strong ETag.

Tip

StaticFiles can also be imported from velocipy directly: from velocipy import StaticFiles.

HTML mode for single-page and static sites

When html=True, a request for a trailing-slash path maps to index.html inside the matching directory. This is useful for static sites, documentation builds, or single-page applications that ship a prebuilt index.html.

from velocipy import VelociPy
from velocipy.routing.static import StaticFiles

app = VelociPy()

# Static assets at /static/<path>
app.mount("/static", StaticFiles(directory="static"))

# index.html fallback for directory requests at /
app.mount("/", StaticFiles(directory="static", html=True))

With this setup, GET / returns static/index.html if it exists, and GET /about/ returns static/about/index.html.

Warning

Without html=True, trailing slashes are stripped and the path must name a file directly. Directory requests return 404 Not Found.

How it works under the hood

StaticFiles keeps three claws sharp:

  1. Path resolution happens off the event loop with asyncio.to_thread, so a slow disk does not block the pack.
  2. Directory-traversal guard resolves the target path and ensures it stays inside the configured root.
  3. Metadata cache stores the resolved path, computed ETag, and guessed media type after the first request, so repeat hunts are cheap.

If the client sends If-None-Match matching the cached ETag, VelociPy returns 304 Not Modified without re-reading the file.

Multiple mounts and root fallback

You can mount several directories under different prefixes. A common pattern is /static for build assets plus / for an HTML entrypoint.

from pathlib import Path
from velocipy import VelociPy
from velocipy.routing.static import StaticFiles

app = VelociPy()

app.mount("/static", StaticFiles(directory="static"))
app.mount("/uploads", StaticFiles(directory="uploads"))
app.mount("/", StaticFiles(directory="public", html=True))

Because the router evaluates routes in order, put narrower prefixes such as /static before broader ones such as /.

Programmatic directory creation

If you generate the static directory at runtime, make sure it exists before constructing StaticFiles. The handler raises RuntimeError if the directory is missing.

from pathlib import Path
from velocipy import VelociPy
from velocipy.routing.static import StaticFiles

app = VelociPy()

static_dir = Path(__file__).parent / "static"
static_dir.mkdir(exist_ok=True)

app.mount("/static", StaticFiles(directory=static_dir))

At a glance

Gear What it does
StaticFiles(directory) Serves files from a local directory with traversal protection.
StaticFiles(directory, html=True) Serves index.html for trailing-slash directory requests.
app.mount("/static", ...) Pins the handler under a URL prefix.
ETag + If-None-Match Conditional responses save bandwidth with 304 Not Modified.
Metadata cache Filesystem work is done once per URL, then reused.

See examples/static_files.py for a runnable demo.