FastAPI Integration

Overview

The FastAPI bridge provides a simple way to serve wilco components from any FastAPI application. It exposes API endpoints for listing, fetching, and getting metadata for components.

Installation

Install wilco with FastAPI support using the optional extra:

pip install wilco[fastapi]

This installs wilco with FastAPI (>= 0.115.0). For development, you’ll also want uvicorn:

pip install wilco[fastapi] uvicorn

Quick start

Here’s a minimal example that serves components from a directory:

from pathlib import Path
from fastapi import FastAPI
from wilco import ComponentRegistry
from wilco.bridges.fastapi import create_router

app = FastAPI()

# Create a registry pointing to your components
registry = ComponentRegistry(Path("./components"))

# Mount the wilco router
app.include_router(create_router(registry), prefix="/api")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8301)

This creates three endpoints:

  • GET /api/bundles - List available components

  • GET /api/bundles/{name}.js - Get bundled JavaScript

  • GET /api/bundles/{name}/metadata - Get component metadata

API reference

create_router

from wilco.bridges.fastapi import create_router

def create_router(
    registry: ComponentRegistry,
    build_dir: Path | None = None,
) -> APIRouter:
    """Create an APIRouter with component serving endpoints.

    Args:
        registry: The component registry to serve components from.
        build_dir: Optional path to pre-built bundles directory.
            When provided, serves pre-built bundles from the manifest
            and falls back to live bundling for missing components.

    Returns:
        A FastAPI APIRouter that can be mounted on any app.
    """

The returned router provides the following endpoints:

GET /bundles

List all available component bundles.

Response:

[
    {"name": "counter"},
    {"name": "product_card"},
    {"name": "ui.button"}
]

GET /bundles/{name}.js

Get the bundled JavaScript for a component.

Parameters:

  • name: Component name (e.g., counter, ui.button)

Response:

  • Content-Type: application/javascript

  • Cache-Control: public, max-age=31536000, immutable

The response contains the bundled ESM JavaScript with inline source maps.

Errors:

  • 404: Component not found

  • 422: Invalid component name

  • 500: Bundle generation failed

GET /bundles/{name}/metadata

Get metadata for a component, including its JSON schema and content hash.

Parameters:

  • name: Component name

Response:

{
    "title": "Counter",
    "description": "A simple counter component",
    "version": "1.0.0",
    "properties": {
        "initialValue": {"type": "number", "default": 0},
        "step": {"type": "number", "default": 1}
    },
    "hash": "abc123def456"
}

The hash field can be used for cache busting on the frontend.

Errors:

  • 404: Component not found

  • 422: Invalid component name

Component registry

Multiple sources

You can register components from multiple directories:

registry = ComponentRegistry()

# Add components from multiple sources
registry.add_source(Path("./shared_components"))
registry.add_source(Path("./app_components"), prefix="app")

app.include_router(create_router(registry), prefix="/api")

With a prefix, components are namespaced:

  • ./shared_components/button/ becomes button

  • ./app_components/header/ becomes app:header

Caching

The FastAPI bridge returns long-lived cache headers for component bundles:

Cache-Control: public, max-age=31536000, immutable

To enable cache busting:

  1. Fetch the metadata endpoint to get the hash

  2. Append the hash as a query parameter: /api/bundles/counter.js?abc123

The hash changes whenever the component source changes.

Full example

Here’s a complete example with CORS and development server:

from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from wilco import ComponentRegistry
from wilco.bridges.fastapi import create_router

app = FastAPI(title="My Component Server")

# Enable CORS for frontend development
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # Vite dev server
    allow_methods=["GET"],
    allow_headers=["*"],
)

# Set up component registry
registry = ComponentRegistry()
registry.add_source(Path("./components"))

# Mount API routes
app.include_router(create_router(registry), prefix="/api")

@app.get("/")
def root():
    return {"message": "Component server running"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8301, reload=True)

Frontend integration

From the frontend, you can load components using the wilco loader:

import { useComponent } from '@wilcojs/react';

function App() {
    const Counter = useComponent('counter');

    return (
        <Suspense fallback={<div>Loading...</div>}>
            <Counter initialValue={10} step={2} />
        </Suspense>
    );
}

See the Component Specification documentation for more details on component composition.

Example application

A complete example application is available in the examples/fastapi/ directory. It demonstrates:

  • A React SPA frontend (Vite + React 19)

  • SQLAdmin for product management with live preview

  • REST API endpoints for products

  • Component bundles served via the FastAPI bridge

  • ASGI middleware for injecting preview scripts

To run the example:

cd examples/fastapi
make setup   # Install deps, create database, load fixtures
make start   # Run backend + frontend (requires overmind)

Visit http://localhost:8300 for the store, http://localhost:8301/admin for the admin.

Live preview in SQLAdmin

The FastAPI example includes live preview functionality that shows real-time component updates as admin users edit forms.

Architecture

The live preview system consists of four parts:

  1. AdminPreviewMiddleware - ASGI middleware that injects preview scripts into admin pages

  2. admin-preview-inject.js - Creates the two-column layout UI

  3. live-loader-fastapi.js - Handles form validation and preview updates

  4. Validation endpoints - FastAPI routes for form validation returning component props

AdminPreviewMiddleware

This ASGI middleware intercepts HTML responses for /admin routes and injects the necessary scripts before </body>:

from starlette.types import ASGIApp, Message, Receive, Scope, Send

class AdminPreviewMiddleware:
    """Middleware to inject live preview scripts into SQLAdmin pages."""

    INJECT_SCRIPTS = """
    <script src="/wilco-static/wilco/loader.js" defer></script>
    <script src="/static/wilco/admin-preview-inject.js" defer></script>
    <script src="/static/wilco/live-loader-fastapi.js" defer></script>
    </body>"""

    def __init__(self, app: ASGIApp) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        path = scope.get("path", "")

        # Only inject into admin pages
        if not path.startswith("/admin"):
            await self.app(scope, receive, send)
            return

        # Buffer response and inject scripts before </body>
        # ... (full implementation in examples/fastapi/app/main.py)

Wrap your application after mounting admin:

app = FastAPI()
admin = Admin(app, engine, title="Admin")
admin.add_view(ProductAdmin)

# Wrap with preview middleware
app = AdminPreviewMiddleware(app)

Validation endpoint

Create validation endpoints that receive form data and return component props:

from fastapi import APIRouter, Depends, Request
from starlette.responses import JSONResponse
from sqlalchemy.orm import Session

preview_router = APIRouter()

@preview_router.post("/admin/product/preview")
async def validate_preview_create(request: Request, db: Session = Depends(get_db)):
    return await _validate_preview(request, db)

@preview_router.post("/admin/product/{product_id:int}/preview")
async def validate_preview_edit(request: Request, product_id: int, db: Session = Depends(get_db)):
    return await _validate_preview(request, db, product_id=product_id)

Important

Preview routes must be registered before SQLAdmin’s mount to take priority over SQLAdmin’s /admin catch-all route.

Static files setup

Serve the wilco loader script alongside your application’s static files:

from wilco.bridges.base import STATIC_DIR as WILCO_STATIC_DIR

app.mount("/wilco-static", StaticFiles(directory=str(WILCO_STATIC_DIR)), name="wilco_static")

Production deployment

For production, you can pre-compile component bundles to avoid runtime esbuild dependency and serve them as static files.

Pre-building bundles

Use the wilco build CLI to pre-compile components:

wilco build --output dist/wilco/ --components-dir ./components --prefix store

This generates:

  • dist/wilco/bundles/{name}.{hash}.js — hashed bundle files

  • dist/wilco/manifest.json — component-to-file mapping

Using pre-built bundles

Pass the build_dir parameter to create_router:

from pathlib import Path
from wilco.bridges.fastapi import create_router

build_dir = Path("./dist/wilco")

# Serves pre-built bundles when available, falls back to live bundling
app.include_router(create_router(registry, build_dir=build_dir), prefix="/api")

You can also set the WILCO_BUILD_DIR environment variable and resolve it programmatically:

from wilco.manifest import resolve_build_dir

build_dir = resolve_build_dir(Path("./dist/wilco"))
app.include_router(create_router(registry, build_dir=build_dir), prefix="/api")

Serving static bundles

In static mode, the API bundle endpoint returns 404 and clients load bundles from static file URLs instead. Mount the build output as a static directory:

from starlette.staticfiles import StaticFiles

app.mount("/static/wilco", StaticFiles(directory="dist/wilco"), name="wilco_bundles")