Skip to main content
If your Pixeltable app has hand-written FastAPI endpoints that call pxt.get_table(), run queries, and manually serialize results, you can replace most of them with FastAPIRouter routes. The result: fewer lines of code, automatic request/response schemas, built-in media serving, and background job support.

Concept Mapping


Side by Side: Query Endpoint

What Changes


Step-by-Step Migration

1. Define queries in router files

Every read pattern becomes a @pxt.query function defined in the router file where it’s used. Each one maps to a single add_query_route call.
@pxt.query eagerly evaluates the function body at decoration time. Tables must exist before the router module is imported. Run python schema.py before starting the app.

2. Replace APIRouter with FastAPIRouter

FastAPIRouter is a subclass of APIRouter, so hand-written @router.post() endpoints coexist on the same instance. Mount routers in main.py:

4. Convert uploads

The multipart form field name must match the column name.

5. Convert deletes

6. Delete old Pydantic models

FastAPIRouter auto-generates request/response schemas from column types. Delete hand-written Pydantic models for any endpoint that is now declarative. Keep only models for remaining hand-written endpoints.

Eliminating Insert-Then-Query with return_rows=True

A common hand-written pattern is inserting a row, then immediately querying to read back computed columns. Use return_rows=True instead:
status.rows contains one dict per inserted row with all columns (including computed). Use model_validate() with extra="ignore" for typed access to the subset you need. Also works with update() and batch_update().

When to Keep a Hand-Written Endpoint

Not everything fits the declarative model. Keep @router.post() when:
  • Multi-table operations: inserting into one table then conditionally writing to another
  • Conditional logic: different behavior based on intermediate results
  • Custom response shapes: aggregating across multiple tables into a single response
  • Side effects: sending emails, webhooks, or other non-Pixeltable actions
Since FastAPIRouter extends APIRouter, hand-written and declarative routes coexist on the same router instance.

Gotchas

@pxt.query runs at decoration time

The function body executes when Python hits the @pxt.query decorator, not when you call the function. If the tables don’t exist yet, you get an error. Run python schema.py before starting the app so tables exist when router modules are imported.

UUID parameters need uuid.UUID annotations

Pixeltable UUID columns require uuid.UUID objects. Use the type annotation and let Pydantic parse strings automatically:
Do not call UUID() inside the query body. The parameter is an expression proxy, not a string.

Media columns serialize as URL strings

FastAPIRouter serializes pxt.Document, pxt.Image, pxt.Video columns as URL paths (e.g., /api/data/media/path/to/file.pdf). The client receives a string, not binary data.

Query routes default to POST

add_query_route defaults to method="post" (JSON body). For parameter-free list endpoints, set method="get":

Delete routes use POST, not HTTP DELETE

add_delete_route uses POST with a JSON body ({"uuid": "..."}), not the HTTP DELETE method. Update client fetch calls accordingly.

Query responses wrap results in { "rows": [...] }

All add_query_route responses return {"rows": [...]}, not a flat array. Client code must unwrap .rows.

Name your embedding indexes explicitly

add_embedding_index(if_exists="ignore") can create duplicates without an explicit name. Always pass idx_name:

Best Practices

  1. Start with @pxt.query for all read patterns. Each one becomes a single add_query_route.
  2. One table per route. Let the client merge results from granular endpoints.
  3. Use FastAPIRouter as your base router. You get media serving and background jobs for free.
  4. Keep complex orchestration hand-written. Multi-table inserts and conditional logic stay as @router.post().
  5. Type your @pxt.query parameters (uuid.UUID, int, float, str). Pydantic coerces the incoming JSON; Pixeltable handles the rest.
  6. Prototype with TOML first. pxt serve with [[tool.pixeltable.service.routes]] in pyproject.toml is the fastest way to test a new route before wiring it into your app.

Next Steps

HTTP Serving Guide

TOML config, CLI, Python API, background jobs

Deployment Overview

Full backend, batch processing, and declarative serving

Production Operations

Concurrency, error handling, sync endpoints

AI Applications

End-to-end multimodal app patterns
Last modified on June 24, 2026