Skip to content

CRUD

kokage-ui can generate a full CRUD (Create, Read, Update, Delete) interface from a Pydantic model with a single method call.

Quick Start

from fastapi import FastAPI
from pydantic import BaseModel, Field
from kokage_ui import KokageUI, InMemoryStorage

app = FastAPI()
ui = KokageUI(app)

class Todo(BaseModel):
    id: str = ""
    title: str = Field(min_length=1, max_length=200)
    completed: bool = False

storage = InMemoryStorage(Todo)

ui.crud("/todos", model=Todo, storage=storage)

This registers the following routes:

Method Path Description
GET /todos List page with search and pagination
GET /todos/_list Table fragment (htmx)
GET /todos/new Create form page
POST /todos/new Create handler
GET /todos/{id} Detail page
GET /todos/{id}/edit Edit form page
POST /todos/{id}/edit Update handler
DELETE /todos/{id} Delete handler

ui.crud() Parameters

Parameter Type Description
prefix str URL prefix (e.g., "/todos")
model type[BaseModel] Pydantic model class
storage Storage Storage backend
id_field str Name of the ID field (default: "id")
per_page int Items per page (default: 20)
title str | None Display title (defaults to ModelName + "s")
exclude_fields list[str] | None Fields to exclude from all views
table_exclude list[str] | None Fields to exclude from table view
form_exclude list[str] | None Fields to exclude from forms
page_wrapper Callable | None callable(content, title) → Page for custom layout
layout Layout | None Layout instance (used as page_wrapper if page_wrapper not set)
theme str DaisyUI theme name (default: "light")

Storage

Storage is an abstract base class that defines the interface for data persistence.

from kokage_ui import Storage

class Storage(ABC, Generic[T]):
    async def list(self, *, skip=0, limit=20, search=None) -> tuple[list[T], int]: ...
    async def get(self, id: str) -> T | None: ...
    async def create(self, data: T) -> T: ...
    async def update(self, id: str, data: T) -> T | None: ...
    async def delete(self, id: str) -> bool: ...

All methods are async. The list method returns a tuple of (items, total_count) for pagination.

InMemoryStorage

A built-in dict-based storage implementation with auto-generated UUIDs:

from kokage_ui import InMemoryStorage

storage = InMemoryStorage(Todo)

# With initial data
storage = InMemoryStorage(Todo, initial=[
    Todo(id="1", title="First"),
    Todo(id="2", title="Second"),
])

InMemoryStorage supports full-text search across all fields via the search parameter in list().

Custom Storage

Implement the Storage ABC to connect to any database:

from kokage_ui import Storage

class PostgresStorage(Storage[User]):
    def __init__(self, pool):
        self.pool = pool

    async def list(self, *, skip=0, limit=20, search=None):
        # Your SQL queries here
        ...

    async def get(self, id: str):
        ...

    async def create(self, data):
        ...

    async def update(self, id: str, data):
        ...

    async def delete(self, id: str):
        ...

Using Layout with CRUD

Wrap CRUD pages in a consistent layout:

from kokage_ui import Layout, NavBar, A

layout = Layout(
    navbar=NavBar(
        start=A("My App", cls="btn btn-ghost text-xl", href="/"),
        end=A("New Todo", cls="btn btn-primary btn-sm", href="/todos/new"),
    ),
    title_suffix=" - My App",
    include_toast=True,
)

ui.crud("/todos", model=Todo, storage=storage, layout=layout)

Or use a custom page_wrapper function:

from kokage_ui import Page

def my_wrapper(content, title):
    return Page(
        NavBar(start=A("App", href="/")),
        content,
        title=f"{title} - App",
    )

ui.crud("/todos", model=Todo, storage=storage, page_wrapper=my_wrapper)

Pagination

The Pagination component is used internally by CRUD but can also be used standalone:

from kokage_ui import Pagination

Pagination(
    current_page=2,
    total_pages=10,
    base_url="/items/_list",
    target="#table-container",
    search="query",
)

SQLModel Storage

For database persistence, use SQLModelStorage instead of InMemoryStorage:

from sqlalchemy.ext.asyncio import create_async_engine
from kokage_ui import SQLModelStorage, create_tables
from sqlmodel import SQLModel, Field

class Todo(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str = Field(min_length=1, max_length=200)
    completed: bool = False

engine = create_async_engine("sqlite+aiosqlite:///app.db")

@app.on_event("startup")
async def startup():
    await create_tables(engine)

storage = SQLModelStorage(Todo, engine)
ui.crud("/todos", model=Todo, storage=storage)

See SQL Storage for details.

Features

  • Search — Built-in SearchFilter with debounce on the list page
  • Pagination — htmx-powered page navigation
  • Validation — Pydantic validation errors shown inline on forms
  • Toast notifications — Success/error messages after create, update, delete
  • Confirm delete — JavaScript confirmation dialog before deletion
  • htmx integration — Form submissions and table updates use htmx for smooth UX

See Also

  • DataGrid — Advanced table with sorting, filtering, bulk actions
  • Admin Dashboard — Auto-generated admin panel using CRUD + DataGrid