<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: bigrivi</title>
    <description>The latest articles on DEV Community by bigrivi (@_340a11d0e3d75cd9d691d).</description>
    <link>https://dev.to/_340a11d0e3d75cd9d691d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4069598%2Fabd82e40-7967-47b3-a3f7-2fe9327e6e63.png</url>
      <title>DEV Community: bigrivi</title>
      <link>https://dev.to/_340a11d0e3d75cd9d691d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_340a11d0e3d75cd9d691d"/>
    <language>en</language>
    <item>
      <title>One Python Agent Core, Four Ways to Run It: Nova on Server, Web, TUI, and Desktop</title>
      <dc:creator>bigrivi</dc:creator>
      <pubDate>Sat, 12 Sep 2026 13:29:11 +0000</pubDate>
      <link>https://dev.to/_340a11d0e3d75cd9d691d/one-python-agent-core-four-ways-to-run-it-nova-on-server-web-tui-and-desktop-m9f</link>
      <guid>https://dev.to/_340a11d0e3d75cd9d691d/one-python-agent-core-four-ways-to-run-it-nova-on-server-web-tui-and-desktop-m9f</guid>
      <description>&lt;p&gt;Nova is an open-source personal AI agent runtime for developers. One Python core is available through terminal, web, desktop, and API, so you get a single local workspace for model providers, tools, sessions, memory, MCP, and sub-agents.&lt;/p&gt;

&lt;p&gt;The practical problem it addresses is familiar: you start an agent in the terminal to fix a bug, then you want the same setup for a longer task you check from a browser, then you want an API you can script against, then something clickable on the desktop. Without a shared runtime, that becomes four tools, four configs, and four ways for behavior to drift.&lt;/p&gt;

&lt;p&gt;Nova's answer, in its README's words, is "Your open source AI agent on desktop, terminal, web, and API." The same agent core in &lt;code&gt;nova/&lt;/code&gt; drives the TUI, server, frontend, and desktop. What changes is the surface. What stays the same is the agent loop, the tool registry, and the SQLite store.&lt;/p&gt;

&lt;h2&gt;
  
  
  One shared runtime
&lt;/h2&gt;

&lt;p&gt;All four surfaces use the same pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same agent loop decides what to do next, calls tools, and streams back text, reasoning blocks, tool calls, and tool results.&lt;/li&gt;
&lt;li&gt;The same tool registry provides 21 built-in tools, plus whatever your connected MCP servers add at runtime.&lt;/li&gt;
&lt;li&gt;The same SQLite store keeps sessions, messages, agents, and memories under &lt;code&gt;~/.nova/nova.db&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The runtime home lives at &lt;code&gt;~/.nova/&lt;/code&gt;, with &lt;code&gt;config.json&lt;/code&gt;, &lt;code&gt;nova.db&lt;/code&gt;, &lt;code&gt;logs/nova.log&lt;/code&gt;, &lt;code&gt;skills/&lt;/code&gt;, &lt;code&gt;workspace/&lt;/code&gt;, and &lt;code&gt;agents/&lt;/code&gt; alongside it. You can point it elsewhere with &lt;code&gt;NOVA_HOME&lt;/code&gt; if you keep dotfiles or checkouts isolated.&lt;/p&gt;

&lt;p&gt;That shared store is what makes switching surfaces uneventful. A session you start in the terminal is stored in the same SQLite file the web UI and desktop read from. Pick a model with &lt;code&gt;/models&lt;/code&gt; in the TUI or with the model selector in the web UI. Add persona files like &lt;code&gt;IDENTITY.md&lt;/code&gt;, &lt;code&gt;SOUL.md&lt;/code&gt;, &lt;code&gt;USER.md&lt;/code&gt;, or &lt;code&gt;MEMORY.md&lt;/code&gt; and they land in the system prompt no matter which surface you open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four interfaces, same agent
&lt;/h2&gt;

&lt;p&gt;Nova exposes the core in four ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nova serve                      &lt;span class="c"&gt;# HTTP server on http://127.0.0.1:8765&lt;/span&gt;
nova web                        &lt;span class="c"&gt;# built web UI in the browser&lt;/span&gt;
nova tui                        &lt;span class="c"&gt;# OpenTUI terminal client from any directory&lt;/span&gt;
./nova-tui                      &lt;span class="c"&gt;# equivalent source-checkout launcher&lt;/span&gt;
nova desktop                    &lt;span class="c"&gt;# desktop window&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nova serve&lt;/code&gt; runs a FastAPI backend with an SSE stream at &lt;code&gt;POST /api/chat/stream&lt;/code&gt;. &lt;code&gt;nova web&lt;/code&gt; serves the built frontend from the same backend address and opens your browser. &lt;code&gt;nova tui&lt;/code&gt; is a Bun plus React plus OpenTUI client that streams text, reasoning, tool calls, and inline diffs for &lt;code&gt;edit&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt;. &lt;code&gt;nova desktop&lt;/code&gt; hosts the built frontend in a PyWebView window with the backend on a background thread. Use &lt;code&gt;nova desktop --dev&lt;/code&gt; when working against the Vite dev server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7gc181pme81tfslfsdsn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7gc181pme81tfslfsdsn.png" alt="Nova empty chat screen with a message composer, Workspace selector, and model selector" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nova's web interface starts with a focused chat composer, a Workspace selector, and a model picker.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For frontend work with live reload, run the two halves separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nova serve
&lt;span class="nb"&gt;cd &lt;/span&gt;frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite proxies &lt;code&gt;/api/*&lt;/code&gt; to the backend, overridable with &lt;code&gt;NOVA_FRONTEND_PROXY_TARGET&lt;/code&gt; or &lt;code&gt;VITE_NOVA_API_BASE_URL&lt;/code&gt;. The &lt;a href="https://github.com/bigrivi/nova/blob/main/docs/getting-started/quickstart.md" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; has the full mapping.&lt;/p&gt;

&lt;p&gt;Pick the surface that fits the moment and keep the same agent underneath. Short fix in the TUI, long-running task in the web UI, scripted call over the API, casual use on desktop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays shared when you switch surfaces
&lt;/h2&gt;

&lt;p&gt;It helps to be precise about what "same agent" means here, because the four surfaces do not look or feel the same, and that is intentional.&lt;/p&gt;

&lt;p&gt;What stays shared is the state and the loop. Sessions, messages, agents, and memories live in the same SQLite file, under the same runtime home with its config, logs, skills, workspace, and agents folders. The agent loop, the tool registry including whatever MCP servers you connected, the per-session workspace semantics, and the persona files injected into the system prompt all behave the same no matter where you open Nova. That is why stopping a task in one place and continuing it in another needs no export step. The history is already there.&lt;/p&gt;

&lt;p&gt;What stays different is everything about interaction. The TUI is keyboard-driven with slash commands, Escape to interrupt, inline diffs for file changes, and tree-sitter highlighting. The web UI leans on a thread list, composer, workspace folder picker, memory manager, approval dialog, and language switcher. The desktop hosts that same built frontend in a PyWebView window with the backend on a background thread. The API has no UI at all and instead streams text, reasoning blocks, tool calls, and tool results over SSE for you to render however you like.&lt;/p&gt;

&lt;p&gt;The tradeoff is straightforward. You get continuity of state without uniformity of interface. Each surface keeps the controls that make sense for its setting, so there is still a small adjustment when you move. The benefit is that the adjustment is only about controls, not about reconfiguring providers, tools, or memory from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring your own model, including local models
&lt;/h2&gt;

&lt;p&gt;Model access lives in &lt;code&gt;~/.nova/config.json&lt;/code&gt;. Only &lt;code&gt;providers&lt;/code&gt; and optionally &lt;code&gt;mcp_servers&lt;/code&gt; sit at the top level. Aliases under &lt;code&gt;providers&lt;/code&gt; are yours to name.&lt;/p&gt;

&lt;p&gt;Nova supports four provider &lt;code&gt;type&lt;/code&gt; values: &lt;code&gt;ollama&lt;/code&gt;, &lt;code&gt;openai-compatible&lt;/code&gt;, &lt;code&gt;openai-response&lt;/code&gt;, and &lt;code&gt;anthropic&lt;/code&gt;. Ollama runs locally with no API key, &lt;code&gt;openai-response&lt;/code&gt; targets the Responses API, and Anthropic supports extended thinking.&lt;/p&gt;

&lt;p&gt;At the current checkout, &lt;code&gt;openai-response&lt;/code&gt; is configured through the config file or API rather than the frontend provider dropdown.&lt;/p&gt;

&lt;p&gt;If you already run Ollama locally, this minimal config from the documented quickstart is enough to start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ollama"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ollama"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"base_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qwen2.5:7b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen2.5:7b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap in whatever model you have pulled. The key part is &lt;code&gt;"tools": true&lt;/code&gt; so the agent can actually call tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools, MCP, and a workspace you control
&lt;/h2&gt;

&lt;p&gt;The built-in set is fixed and documented: &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;shell&lt;/code&gt;, &lt;code&gt;code_run&lt;/code&gt;, &lt;code&gt;glob&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;web_search&lt;/code&gt;, &lt;code&gt;web_fetch&lt;/code&gt;, &lt;code&gt;browser_use&lt;/code&gt;, &lt;code&gt;read_image&lt;/code&gt;, &lt;code&gt;todo_write&lt;/code&gt;, &lt;code&gt;ask_user&lt;/code&gt;, memory tools (&lt;code&gt;save_memory&lt;/code&gt;, &lt;code&gt;search_memory&lt;/code&gt;, &lt;code&gt;list_memories&lt;/code&gt;, &lt;code&gt;delete_memory&lt;/code&gt;), &lt;code&gt;delegate_to_agent&lt;/code&gt;, and skill tools (&lt;code&gt;list_skills&lt;/code&gt;, &lt;code&gt;load_skill&lt;/code&gt;, &lt;code&gt;install_skill&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Any MCP server you connect over stdio or SSE/HTTP shows up as extra tools too. &lt;code&gt;code_run&lt;/code&gt; executes inline Python with dependencies auto-installed to &lt;code&gt;~/.nova/site-packages/&lt;/code&gt;. Web fetch returns Markdown with a 5MB cap, alongside web search. &lt;code&gt;browser_use&lt;/code&gt; registers when Playwright imports. Image and document attachments ride on &lt;code&gt;POST /api/chat&lt;/code&gt;, with &lt;code&gt;read_image&lt;/code&gt; returning base64 plus extracted text.&lt;/p&gt;

&lt;p&gt;Set a per-session workspace folder and &lt;code&gt;shell&lt;/code&gt;, &lt;code&gt;code_run&lt;/code&gt;, &lt;code&gt;glob&lt;/code&gt;, and &lt;code&gt;grep&lt;/code&gt; respect it. Shell commands pass a three-tier approval gate of blocked, needs approval, and auto-run, with dangerous ones asking over SSE and an optional allowlist. For longer sessions, two-layer compaction trims old tool output to disk and summarizes older turns.&lt;/p&gt;

&lt;p&gt;The web interface keeps multi-step work visible rather than collapsing it into a single loading state. In the example below, Nova searches for recent open-source agent developments, opens first-party sources, cross-checks claims, and reports progress between rounds. It also catches a misdated OpenHands item before producing the final briefing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71az78knzkazkey19z9b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71az78knzkazkey19z9b.png" alt="Nova chat showing multi-round research, tool-call counts, progress updates, and source verification" width="800" height="895"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nova reports progress between research rounds, tracks tool calls, and surfaces corrections made during source verification.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three workflow patterns that show the architecture
&lt;/h2&gt;

&lt;p&gt;The shared runtime matters most once you see how different tasks pull on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminal code work with reviewable diffs.&lt;/strong&gt; Set a per-session workspace folder so file search and shell execution start from the checkout you mean, then work through reading, searching, and editing from the TUI. Successful file changes render as inline diffs you can read before moving on, and Escape interrupts a run that heads the wrong way. The workspace keeps everyday commands scoped to the task, while an explicit working directory still wins when you pass one, so treat it as a scoping aid that reduces mistakes rather than a boundary. This pattern fits tight fix loops where you stay in one repo and want quick review cycles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd2z1b928f6y8a0rgx3n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd2z1b928f6y8a0rgx3n.png" alt="Nova terminal TUI editing a file with an inline diff" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The TUI takes an instruction, reads a file, applies an edit shown as an inline diff, with model and context status visible.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-round web research with visible progress.&lt;/strong&gt; In the web UI, a research task becomes a series of search and fetch rounds with progress reported between them instead of one long silent wait. Fetched pages arrive as Markdown, the thread keeps tool-call counts and intermediate findings, and corrections surface in the open when a source does not check out. The implication is that verification work stays inspectable. You can follow which sources were opened, what was cross-checked, and where the final briefing diverged from an early lead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scripted use through the HTTP and SSE endpoint.&lt;/strong&gt; The same loop is available over HTTP for scripts, with chat streaming text, reasoning, tool calls, and results over SSE and attachments accepted alongside chat requests. Approval prompts for sensitive commands arrive over that stream and are answered through a dedicated approval endpoint, with an allowlist to remember routine approvals. The tradeoff here is control versus convenience. A script gets the full agent behavior including tools and memory, but it also takes on rendering progress, handling approvals, and deciding when to stop and retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sessions, memory, skills, and sub-agents
&lt;/h2&gt;

&lt;p&gt;Persistent sessions are the default. Threads, messages, agents, and memories live in SQLite, so you can stop a task in one surface and pick it up in another without exporting state.&lt;/p&gt;

&lt;p&gt;Memory covers &lt;code&gt;fact&lt;/code&gt;, &lt;code&gt;preference&lt;/code&gt;, &lt;code&gt;decision&lt;/code&gt;, and &lt;code&gt;context&lt;/code&gt; types across &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;project&lt;/code&gt;, and &lt;code&gt;session&lt;/code&gt; scopes, with search and optional AI reranking. The frontend includes a memory manager next to the thread list, composer, model selector, workspace folder picker, and approval dialog. The TUI covers &lt;code&gt;/new&lt;/code&gt;, &lt;code&gt;/sessions&lt;/code&gt;, &lt;code&gt;/clear&lt;/code&gt;, &lt;code&gt;/models&lt;/code&gt;, &lt;code&gt;/install-skill&lt;/code&gt;, and &lt;code&gt;/quit&lt;/code&gt;, with Escape to interrupt and inline diffs for &lt;code&gt;edit&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Skills live as &lt;code&gt;~/.nova/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt; files, scanned at startup and loaded on demand through &lt;code&gt;list_skills&lt;/code&gt; and &lt;code&gt;load_skill&lt;/code&gt;. &lt;code&gt;install_skill&lt;/code&gt; pulls from ClawHub only when you ask.&lt;/p&gt;

&lt;p&gt;When a task splits cleanly, &lt;code&gt;delegate_to_agent&lt;/code&gt; spawns a sub-agent with the hierarchy persisted in SQLite and surfaced over the API. Sub-agents run without MCP tools or further delegation, which keeps delegated work bounded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to extend it without forking it
&lt;/h2&gt;

&lt;p&gt;Nova leaves a few deliberate seams where your own setup slots in, and each one answers a different kind of change.&lt;/p&gt;

&lt;p&gt;Provider aliases are the first. The top-level config holds only providers and optionally MCP servers, and the names under providers are yours to choose. That means pointing Nova at a new account, endpoint, or local model is a config edit rather than a code change, and you can keep several named setups side by side for different tasks.&lt;/p&gt;

&lt;p&gt;The database layer is another extension seam. Nova's agent, session, memory, and configuration services depend on the &lt;code&gt;NovaRepository&lt;/code&gt; protocol instead of directly depending on SQLite. The current provider factory ships with &lt;code&gt;aiosqlite&lt;/code&gt; and in-memory implementations, but it can register another provider by name. A MySQL-backed deployment would implement the repository protocol, create it through a &lt;code&gt;DataSourceProvider&lt;/code&gt;, and register that provider with the factory. That is still real adapter work—the protocol covers sessions, messages, agents, compaction, and memory—but it keeps database-specific code behind one boundary instead of spreading SQL changes through the agent runtime.&lt;/p&gt;

&lt;p&gt;MCP servers are the second. Any server reachable over stdio or SSE and HTTP becomes extra tools at runtime, initialized in parallel with a per-server timeout. The practical effect is that new capabilities arrive as processes Nova talks to, not patches to the agent itself. If a server is slow or missing, only its tools are affected.&lt;/p&gt;

&lt;p&gt;Local skills are the third. A skill is a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; file under the runtime skills directory, scanned at startup and loaded only when the task calls for it. Fetching from ClawHub happens only when you ask for it. This suits repeatable procedures you want written down once and reused, like a review checklist or a repo-specific workflow, without baking them into every prompt.&lt;/p&gt;

&lt;p&gt;Persona files are the lightest touch. Short markdown files describing identity, background, user context, and retained notes are injected into the system prompt on every surface. They shape tone and defaults without touching tool wiring.&lt;/p&gt;

&lt;p&gt;Sub-agents are the most structured seam, and also the most bounded. Delegation persists the parent-child relationship and exposes it over the API, but the child runs without MCP tools and cannot delegate further. That bound is worth understanding before you lean on it. It keeps delegated work predictable and easy to trace, at the cost of ruling out recursive fan-out. Use it for cleanly separable chunks, not for open-ended chains.&lt;/p&gt;

&lt;h2&gt;
  
  
  What keeps longer runs manageable
&lt;/h2&gt;

&lt;p&gt;Longer tasks fail in familiar ways. They stall waiting on a risky command, loop on the same call, outgrow context, or forget a decision from an earlier session. Nova addresses each with a separate mechanism, and each asks something of you.&lt;/p&gt;

&lt;p&gt;Approval tiers handle the risky-command case. Shell input falls into blocked, needs approval, or auto-run by pattern, with sensitive prompts delivered over the stream and answered through an approval endpoint. A rememberable allowlist smooths repeated runs of commands you trust. The tradeoff is interruption. Tighter patterns mean more pauses, while a generous allowlist means fewer pauses and more responsibility for what you pre-approved.&lt;/p&gt;

&lt;p&gt;Repeated-call guardrails handle loops. The run halts after several identical calls or identical failures in a row, and warns after a run of read-only calls. This catches the agent re-reading the same files or retrying the same failing command instead of reconsidering. When you hit one, the fix is usually in the task framing rather than the limit.&lt;/p&gt;

&lt;p&gt;Two-layer compaction handles context growth. Older tool output is snipped to per-session files on disk while older turns are summarized, with tuning available for how aggressive each layer is. Snipped output stays retrievable rather than vanishing, which matters when you need to audit what the agent actually saw three rounds back.&lt;/p&gt;

&lt;p&gt;Persistent memory handles cross-session recall. Stored items carry a type like fact, preference, decision, or context, and a scope of user, project, or session, with search and optional reranking when you look something up. There is no per-turn prefetch, so memory does not silently steer every reply. The implication is direct. What you explicitly save and search for carries forward, and what you never write down does not. For longer projects that means building a small habit of saving decisions and preferences as they settle.&lt;/p&gt;

&lt;h2&gt;
  
  
  A starting point you can shape
&lt;/h2&gt;

&lt;p&gt;If building an agent from scratch sounds like too much plumbing, and taking a ready-made product as-is feels too rigid, Nova sits in the middle. The shared runtime, provider wiring, tool registry, SQLite persistence, four interfaces, MCP loading, skills, and sub-agent handling are already wired together, so you start from working code rather than an empty repo. As Apache-2.0 open source, you can inspect each part in &lt;code&gt;nova/&lt;/code&gt;, modify or replace what you need, and build your own setup on top while keeping the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local quickstart
&lt;/h2&gt;

&lt;p&gt;Nova needs Python 3.12 or newer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/bigrivi/nova.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nova
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;                &lt;span class="c"&gt;# Python 3.12+&lt;/span&gt;
playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium     &lt;span class="c"&gt;# only if you want the browser tools&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;nova&lt;/code&gt; is not found afterward, add your environment's &lt;code&gt;bin/&lt;/code&gt; directory to &lt;code&gt;PATH&lt;/code&gt;. The full walkthrough is in the &lt;a href="https://github.com/bigrivi/nova/blob/main/docs/getting-started/installation.md" rel="noopener noreferrer"&gt;installation guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then add a provider to &lt;code&gt;~/.nova/config.json&lt;/code&gt; as shown above, and start where you want to work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nova serve                      &lt;span class="c"&gt;# HTTP server on http://127.0.0.1:8765&lt;/span&gt;
nova web                        &lt;span class="c"&gt;# built web UI in the browser&lt;/span&gt;
nova tui                        &lt;span class="c"&gt;# OpenTUI terminal client from any directory&lt;/span&gt;
nova desktop                    &lt;span class="c"&gt;# desktop window&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick a model via &lt;code&gt;/models&lt;/code&gt; in the TUI or the model selector in the web UI, and you are running the same core everywhere.&lt;/p&gt;

&lt;p&gt;One practical note: &lt;code&gt;shell&lt;/code&gt; and &lt;code&gt;code_run&lt;/code&gt; execute locally as your user and are not sandboxed, so point Nova at repos and machines you can afford to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the source tree is organized
&lt;/h2&gt;

&lt;p&gt;The shared runtime lives in &lt;code&gt;nova/&lt;/code&gt;, with the terminal client in &lt;code&gt;tui/&lt;/code&gt;, the web UI in &lt;code&gt;frontend/&lt;/code&gt;, tests in &lt;code&gt;tests/&lt;/code&gt;, and guides in &lt;code&gt;docs/&lt;/code&gt;. Inside &lt;code&gt;nova/&lt;/code&gt;, each package owns one concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;agent/&lt;/code&gt; runs the agent loop that plans the next step, calls tools, and streams results.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/&lt;/code&gt; wires the runtime pieces together at startup.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config/&lt;/code&gt; handles runtime configuration under &lt;code&gt;~/.nova/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db/&lt;/code&gt; implements persistence behind the repository protocol, backed by SQLite.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;desktop/&lt;/code&gt; hosts the desktop window around the built frontend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llm/&lt;/code&gt; holds the model provider implementations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcp/&lt;/code&gt; loads connected MCP servers as extra tools.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory/&lt;/code&gt; stores and searches memory records across scopes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prompt/&lt;/code&gt; assembles the system prompt, including persona files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server/&lt;/code&gt; serves the HTTP backend and the chat stream.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session/&lt;/code&gt; manages session and thread state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skills/&lt;/code&gt; scans and loads local skills on demand.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/&lt;/code&gt; registers the built-in tool set.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;utils/&lt;/code&gt; holds shared helpers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__main__.py&lt;/code&gt; is the &lt;code&gt;nova&lt;/code&gt; console entry point for &lt;code&gt;serve&lt;/code&gt;, &lt;code&gt;web&lt;/code&gt;, &lt;code&gt;tui&lt;/code&gt;, and &lt;code&gt;desktop&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;settings.py&lt;/code&gt; parses &lt;code&gt;~/.nova/config.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a rough map: models go in &lt;code&gt;llm/&lt;/code&gt;, persistence in &lt;code&gt;db/&lt;/code&gt;, streaming and API behavior in &lt;code&gt;server/&lt;/code&gt;, prompt assembly in &lt;code&gt;prompt/&lt;/code&gt;, tools in &lt;code&gt;tools/&lt;/code&gt;, the terminal client in &lt;code&gt;tui/&lt;/code&gt;, and the web frontend in &lt;code&gt;frontend/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to contribute
&lt;/h2&gt;

&lt;p&gt;Setup follows the contributing guide. Install the Python package with dev tooling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frontend and TUI dependencies are separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bun &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the suite from the repo root. No &lt;code&gt;PYTHONPATH&lt;/code&gt; setup is needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a subset while iterating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest tests/test_server.py &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contributing guide currently reports 392 passing and 6 skipped. The skipped tests are the live Ollama end-to-end suite, which is opt-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUN_LIVE_OLLAMA_SERVER_E2E&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 pytest tests/e2e &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests use an internal &lt;code&gt;faker&lt;/code&gt; provider, so CI runs without real API keys. Browser tooling is optional: &lt;code&gt;playwright install chromium&lt;/code&gt; adds it, and no test requires it.&lt;/p&gt;

&lt;p&gt;For bigger changes, open an issue first to discuss the approach. Report vulnerabilities through the repo Security tab, not a public issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;Nova fits developers who want a local-first agent they can read end to end, who want to start with Ollama and no API key, who like sessions kept in one inspectable SQLite file, and who want to move between terminal, browser, desktop, and HTTP without switching agent implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clone it and run it
&lt;/h2&gt;

&lt;p&gt;Nova is Apache-2.0 licensed, and the code, docs, and issue tracker all live at &lt;a href="https://github.com/bigrivi/nova" rel="noopener noreferrer"&gt;bigrivi/nova&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If a single local runtime for providers, tools, sessions, memory, MCP, and sub-agents sounds useful, start with the &lt;a href="https://github.com/bigrivi/nova/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt;, clone the repo, run it locally with Ollama or your own key, and star it if it proves useful.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>fastapi-crudrouter is Dead. Here's How to Migrate to BetterCRUD</title>
      <dc:creator>bigrivi</dc:creator>
      <pubDate>Sun, 09 Aug 2026 07:54:31 +0000</pubDate>
      <link>https://dev.to/_340a11d0e3d75cd9d691d/fastapi-crudrouter-is-dead-heres-how-to-migrate-to-bettercrud-3gn9</link>
      <guid>https://dev.to/_340a11d0e3d75cd9d691d/fastapi-crudrouter-is-dead-heres-how-to-migrate-to-bettercrud-3gn9</guid>
      <description>&lt;h1&gt;
  
  
  fastapi-crudrouter is Dead. Here's How to Migrate to BetterCRUD
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/awtkns/fastapi-crudrouter" rel="noopener noreferrer"&gt;fastapi-crudrouter&lt;/a&gt; was the de-facto CRUD library for FastAPI — generating 6 CRUD routes from a model in a few lines. It's been &lt;strong&gt;unmaintained since November 2023&lt;/strong&gt;. No FastAPI 0.141+ support, no new features, and its SQLAlchemy backend doesn't keep up with SQLAlchemy 2.0 async best practices.&lt;/p&gt;

&lt;p&gt;If you're still on it, it's time to migrate. The good news: &lt;strong&gt;the route layout is nearly identical&lt;/strong&gt;, so the move is mostly drop-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why migrate?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;fastapi-crudrouter&lt;/th&gt;
&lt;th&gt;BetterCRUD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Maintained (2026)&lt;/td&gt;
&lt;td&gt;❌ stalled since Nov 2023&lt;/td&gt;
&lt;td&gt;✅ active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FastAPI 0.141+&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLAlchemy 2.0 async&lt;/td&gt;
&lt;td&gt;✅ (basic)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter operators&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ 27 operators&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagination modes&lt;/td&gt;
&lt;td&gt;❌ basic only&lt;/td&gt;
&lt;td&gt;✅ always/optional/disabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relationship queries &amp;amp; storage&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ joins, M2M, O2M, O2O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft delete + recover&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACL hooks&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle hooks&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom endpoints&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;@crud_action&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test coverage&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅ 99%+, 177 tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The migration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before (fastapi-crudrouter)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi_crudrouter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SQLAlchemyCRUDRouter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_session&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PetUpdate&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SQLAlchemyCRUDRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PetUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;create_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;update_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PetUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After (BetterCRUD)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;better_crud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;crud&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;better_crud.service.sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SqlalchemyCrudService&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PetUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PetPublic&lt;/span&gt;

&lt;span class="n"&gt;pet_router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SqlalchemyCrudService&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@crud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pet_router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetUpdate&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;serialize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetPublic&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetController&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PetService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole migration. Register &lt;code&gt;pet_router&lt;/code&gt; with &lt;code&gt;app.include_router(pet_router, prefix="/pet")&lt;/code&gt; and you're live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get for free after migrating
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Filtering you actually need
&lt;/h3&gt;

&lt;p&gt;fastapi-crudrouter had no filtering. BetterCRUD's &lt;code&gt;GET /pet&lt;/code&gt; supports 27 operators out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /pet?filter&lt;span class="o"&gt;=&lt;/span&gt;name||&lt;span class="nv"&gt;$cont&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;Re        &lt;span class="c"&gt;# contains&lt;/span&gt;
GET /pet?filter&lt;span class="o"&gt;=&lt;/span&gt;age||&lt;span class="nv"&gt;$between&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;1,5     &lt;span class="c"&gt;# range&lt;/span&gt;
GET /pet?filter&lt;span class="o"&gt;=&lt;/span&gt;species||&lt;span class="nv"&gt;$in&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;dog,cat  &lt;span class="c"&gt;# in list&lt;/span&gt;
GET /pet?s&lt;span class="o"&gt;={&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$or&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:[&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"age"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$gt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:3&lt;span class="o"&gt;}}&lt;/span&gt;,&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"species"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$eq&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"cat"&lt;/span&gt;&lt;span class="o"&gt;}}]}&lt;/span&gt;  &lt;span class="c"&gt;# nested logic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Pagination that fits your API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BetterCrudGlobalConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pagination_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;optional&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three modes — &lt;code&gt;always&lt;/code&gt;, &lt;code&gt;optional&lt;/code&gt;, &lt;code&gt;disabled&lt;/code&gt; — so small reference datasets can return plain arrays while large tables stay paginated.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Relationships, done properly
&lt;/h3&gt;

&lt;p&gt;Store nested payloads (many-to-many, one-to-many, one-to-one) automatically, and query them with &lt;code&gt;?load=&lt;/code&gt; / &lt;code&gt;?join=&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserBase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserProfileCreate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserTaskCreate&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Soft delete + recover
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@crud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;soft_delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow_recover&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DELETE&lt;/code&gt; becomes soft delete; &lt;code&gt;PATCH /pet/{id}/recover&lt;/code&gt; restores records.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Security hooks
&lt;/h3&gt;

&lt;p&gt;Every generated route exposes &lt;code&gt;feature&lt;/code&gt;/&lt;code&gt;action&lt;/code&gt; on the request state — wire your existing permission guards directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;better_crud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_action&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# your ACL logic
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Business logic without leaving the CRUD pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@crud_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{id}/adopt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adopt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;do_adopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registers &lt;code&gt;POST /pet/{id}/adopt&lt;/code&gt; with service injection, ACL, and response wrapping — no manual router wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;✅ Add &lt;code&gt;better_crud&lt;/code&gt; to requirements (remove &lt;code&gt;fastapi-crudrouter&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ Add a &lt;code&gt;PetService(SqlalchemyCrudService[Pet])&lt;/code&gt; class (thin, no logic needed initially)&lt;/li&gt;
&lt;li&gt;✅ Replace the &lt;code&gt;SQLAlchemyCRUDRouter(...)&lt;/code&gt; call with &lt;code&gt;@crud(...)&lt;/code&gt; on a controller class&lt;/li&gt;
&lt;li&gt;✅ Register the router — prefix stays the same, routes are identical&lt;/li&gt;
&lt;li&gt;✅ Point your frontend at the same endpoints — no client changes needed&lt;/li&gt;
&lt;li&gt;✅ Optionally enable soft delete, ACL, and custom actions as you go&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The route layout (&lt;code&gt;GET/POST /resource&lt;/code&gt;, &lt;code&gt;GET/PUT/DELETE /resource/{id}&lt;/code&gt;) is preserved, so &lt;strong&gt;your existing clients keep working&lt;/strong&gt; while the backend gains capabilities fastapi-crudrouter never had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href="https://bigrivi.github.io/better_crud/" rel="noopener noreferrer"&gt;https://bigrivi.github.io/better_crud/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/bigrivi/better_crud" rel="noopener noreferrer"&gt;https://github.com/bigrivi/better_crud&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;pip install better-crud&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Migrating? If BetterCRUD saves you time, give it a ⭐ on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>python</category>
      <category>migration</category>
      <category>backend</category>
    </item>
    <item>
      <title>Stop Writing CRUD Boilerplate: Generate a Complete FastAPI API From One Decorator</title>
      <dc:creator>bigrivi</dc:creator>
      <pubDate>Sun, 09 Aug 2026 07:50:07 +0000</pubDate>
      <link>https://dev.to/_340a11d0e3d75cd9d691d/stop-writing-crud-boilerplate-generate-a-complete-fastapi-api-from-one-decorator-3e6d</link>
      <guid>https://dev.to/_340a11d0e3d75cd9d691d/stop-writing-crud-boilerplate-generate-a-complete-fastapi-api-from-one-decorator-3e6d</guid>
      <description>&lt;h1&gt;
  
  
  Stop Writing CRUD Boilerplate: Generate a Complete FastAPI API From One Decorator
&lt;/h1&gt;

&lt;p&gt;Every FastAPI project needs the same endpoints. &lt;code&gt;GET /resource&lt;/code&gt;, &lt;code&gt;POST /resource&lt;/code&gt;, &lt;code&gt;GET/PUT/DELETE /resource/{id}&lt;/code&gt; — plus filtering, pagination, and sorting. And every project writes them by hand. Over. And over.&lt;/p&gt;

&lt;p&gt;I've seen codebases with 2,000 lines of nearly identical route handlers, where filtering is bolted on inconsistently, pagination is reinvented per endpoint, and permission checks are copy-pasted with subtle bugs.&lt;/p&gt;

&lt;p&gt;There's a better way. &lt;strong&gt;&lt;a href="https://github.com/bigrivi/better_crud" rel="noopener noreferrer"&gt;BetterCRUD&lt;/a&gt;&lt;/strong&gt; generates the entire CRUD layer from a single decorator — while keeping you in full control.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get from one decorator
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;better_crud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;crud&lt;/span&gt;

&lt;span class="n"&gt;pet_router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@crud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pet_router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetUpdate&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;serialize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetPublic&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetController&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PetService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. This generates &lt;strong&gt;8 routes&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;List with filtering, pagination, sorting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pet/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Get one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Create one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pet/bulk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Create many (atomic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pet/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;Update one (partial)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pet/{ids}/bulk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;Update many (atomic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pet/{ids}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;Delete many&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/pet/{id}/recover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;Soft-delete recover &lt;em&gt;(opt-in)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;First, a standard async SQLAlchemy setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# db.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.ext.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sessionmaker&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.pool&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NullPool&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite+aiosqlite:///crud.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SessionLocal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sessionmaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expire_on_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_session&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;SessionLocal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define your model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# model.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Integer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapped_column&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your schemas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# schema.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetPublic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PetBase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PetBase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PetBase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A thin service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# service.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;better_crud.service.sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SqlalchemyCrudService&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pet&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SqlalchemyCrudService&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And wire it all together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;better_crud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BetterCrudGlobalConfig&lt;/span&gt;

&lt;span class="n"&gt;BetterCrudGlobalConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;backend_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlalchemy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db_session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;get_session&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pet_router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/pet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a complete, documented (OpenAPI/Swagger) CRUD API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real power: everything comes free
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rich filtering
&lt;/h3&gt;

&lt;p&gt;The generated &lt;code&gt;GET /pet&lt;/code&gt; endpoint supports 27 filter operators out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# exact match&lt;/span&gt;
GET /pet?filter&lt;span class="o"&gt;=&lt;/span&gt;name||&lt;span class="nv"&gt;$eq&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;Rex

&lt;span class="c"&gt;# contains&lt;/span&gt;
GET /pet?filter&lt;span class="o"&gt;=&lt;/span&gt;name||&lt;span class="nv"&gt;$cont&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;Re

&lt;span class="c"&gt;# range&lt;/span&gt;
GET /pet?filter&lt;span class="o"&gt;=&lt;/span&gt;age||&lt;span class="nv"&gt;$between&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;1,5

&lt;span class="c"&gt;# in list&lt;/span&gt;
GET /pet?filter&lt;span class="o"&gt;=&lt;/span&gt;species||&lt;span class="nv"&gt;$in&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;dog,cat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And JSON search with nested logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /pet?s&lt;span class="o"&gt;={&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cont&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"Re"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$or&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:[&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"age"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$gt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:3&lt;span class="o"&gt;}}&lt;/span&gt;,&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"species"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$eq&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"cat"&lt;/span&gt;&lt;span class="o"&gt;}}]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pagination — three modes
&lt;/h3&gt;

&lt;p&gt;Control pagination behavior globally or per-route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BetterCrudGlobalConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pagination_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# "always" | "optional" | "disabled"
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;always&lt;/code&gt; — always return &lt;code&gt;{items, total, page, size, pages}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;optional&lt;/code&gt; &lt;em&gt;(default)&lt;/em&gt; — paginated only when &lt;code&gt;page&lt;/code&gt;/&lt;code&gt;size&lt;/code&gt; passed; otherwise a plain array&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;disabled&lt;/code&gt; — never paginate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect for small reference datasets that frontends need as a full array.&lt;/p&gt;

&lt;h3&gt;
  
  
  Relationship queries &amp;amp; storage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# One-to-many, many-to-many, one-to-one — all handled automatically
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserBase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserProfileCreate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserTaskCreate&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post a nested payload and BetterCRUD stores the relationships for you. Query them with &lt;code&gt;?load=&lt;/code&gt; and &lt;code&gt;?join=&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Soft delete + recover
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@crud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;soft_delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow_recover&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deletes become soft deletes; &lt;code&gt;PATCH /pet/{id}/recover&lt;/code&gt; brings records back.&lt;/p&gt;

&lt;h3&gt;
  
  
  ACL hooks
&lt;/h3&gt;

&lt;p&gt;Every generated route exposes its &lt;code&gt;feature&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt; on the request state, so permission guards slot in naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;better_crud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_action&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# e.g. "pet"
&lt;/span&gt;    &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# e.g. "read", "create", "update"
&lt;/span&gt;    &lt;span class="c1"&gt;# your permission logic
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lifecycle hooks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SqlalchemyCrudService&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_before_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pet_create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pet_create&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pet_create&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom endpoints for business logic
&lt;/h3&gt;

&lt;p&gt;CRUD doesn't cover everything. Attach business actions with &lt;code&gt;@crud_action&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@crud_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{id}/adopt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adopt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;adopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adopted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This registers &lt;code&gt;POST /pet/{id}/adopt&lt;/code&gt; &lt;strong&gt;inside&lt;/strong&gt; the CRUD ecosystem — with service injection, ACL, and response schema wrapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from fastapi-crudrouter
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/awtkns/fastapi-crudrouter" rel="noopener noreferrer"&gt;fastapi-crudrouter&lt;/a&gt; — the long-time de-facto CRUD library — has been &lt;strong&gt;unmaintained since November 2023&lt;/strong&gt;. If you're on it, the routes are nearly identical, so migration is mostly drop-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi_crudrouter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SQLAlchemyCRUDRouter&lt;/span&gt;
&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SQLAlchemyCRUDRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;update_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PetUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;better_crud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;crud&lt;/span&gt;
&lt;span class="n"&gt;pet_router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@crud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pet_router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetUpdate&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="n"&gt;serialize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetPublic&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PetController&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PetService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PetService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same REST semantics — but you gain 27 filter operators, pagination modes, ACL, soft delete, relationship storage, and an overridable service layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-ready by default
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;99%+ test coverage&lt;/strong&gt; with 177 passing tests&lt;/li&gt;
&lt;li&gt;Fully async (SQLAlchemy 2.0)&lt;/li&gt;
&lt;li&gt;Works with SQLAlchemy &lt;strong&gt;and&lt;/strong&gt; SQLModel&lt;/li&gt;
&lt;li&gt;Extensible: custom backends, custom response schemas, custom pagination models&lt;/li&gt;
&lt;li&gt;Class-based views &lt;strong&gt;and&lt;/strong&gt; functional views (&lt;code&gt;crud_generator&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;better-crud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full docs: &lt;a href="https://bigrivi.github.io/better_crud/" rel="noopener noreferrer"&gt;https://bigrivi.github.io/better_crud/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/bigrivi/better_crud" rel="noopener noreferrer"&gt;https://github.com/bigrivi/better_crud&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;Star the repo&lt;/strong&gt; on GitHub if BetterCRUD saves you time — it directly helps more developers find it&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Read the full docs&lt;/strong&gt; for deep dives: relationship storage, custom backends, response schemas, and more&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Report bugs or request features&lt;/strong&gt; via &lt;a href="https://github.com/bigrivi/better_crud/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Migrating from fastapi-crudrouter?&lt;/strong&gt; See the &lt;a href="https://dev.to/_340a11d0e3d75cd9d691d/fastapi-crudrouter-is-dead-heres-how-to-migrate-to-bettercrud-3gn9"&gt;migration guide&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If BetterCRUD saves you time, give it a ⭐ on GitHub — it helps more developers discover it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>python</category>
      <category>backend</category>
      <category>api</category>
    </item>
  </channel>
</rss>
