<?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: RDJ </title>
    <description>The latest articles on DEV Community by RDJ  (@rdj).</description>
    <link>https://dev.to/rdj</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%2F716773%2F665c240b-8ce4-4a25-abb2-e6a647f01faf.jpeg</url>
      <title>DEV Community: RDJ </title>
      <link>https://dev.to/rdj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rdj"/>
    <language>en</language>
    <item>
      <title>12 Cursor rules that stop AI assistants writing outdated FastAPI code</title>
      <dc:creator>RDJ </dc:creator>
      <pubDate>Sat, 17 Oct 2026 03:45:24 +0000</pubDate>
      <link>https://dev.to/rdj/12-cursor-rules-that-stop-ai-assistants-writing-outdated-fastapi-code-1bc2</link>
      <guid>https://dev.to/rdj/12-cursor-rules-that-stop-ai-assistants-writing-outdated-fastapi-code-1bc2</guid>
      <description>&lt;p&gt;AI coding assistants still reach for FastAPI patterns from a few years ago: &lt;code&gt;.dict()&lt;/code&gt;, &lt;code&gt;orm_mode&lt;/code&gt;, &lt;code&gt;session.query()&lt;/code&gt;, &lt;code&gt;HTTPException&lt;/code&gt; raised deep inside service code, SQLite in tests. The code runs, but it isn't the code you'd write today.&lt;/p&gt;

&lt;p&gt;Cursor's project rules fix most of this. A rule is a small &lt;code&gt;.mdc&lt;/code&gt; file in &lt;code&gt;.cursor/rules/&lt;/code&gt; with a glob, so it loads only when you edit matching files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FastAPI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;routers,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dependencies&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Pydantic&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;schemas"&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/app/**/router.py,src/app/**/schemas.py,src/app/**/deps.py&lt;/span&gt;
&lt;span class="na"&gt;alwaysApply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;One APIRouter(prefix="/&amp;lt;plural-resource&amp;gt;") per feature ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the 12 conventions that made the biggest difference for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routers and schemas
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pydantic v2 APIs only.&lt;/strong&gt; &lt;code&gt;model_validate&lt;/code&gt;, &lt;code&gt;model_dump&lt;/code&gt;, &lt;code&gt;ConfigDict&lt;/code&gt;, &lt;code&gt;field_validator&lt;/code&gt;. Never &lt;code&gt;.dict()&lt;/code&gt;, &lt;code&gt;parse_obj&lt;/code&gt;, &lt;code&gt;orm_mode&lt;/code&gt;, &lt;code&gt;class Config&lt;/code&gt; or the v1 &lt;code&gt;validator&lt;/code&gt; decorator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routers are thin and async.&lt;/strong&gt; Parse, call a service, return a schema. No SQL or business rules in routers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always declare &lt;code&gt;response_model&lt;/code&gt;&lt;/strong&gt; (or a return annotation) and an explicit &lt;code&gt;status_code&lt;/code&gt;: 201 for create, 204 for delete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schemas are named &lt;code&gt;XCreate&lt;/code&gt; / &lt;code&gt;XUpdate&lt;/code&gt; / &lt;code&gt;XRead&lt;/code&gt;.&lt;/strong&gt; Input models use &lt;code&gt;ConfigDict(extra="forbid")&lt;/code&gt; so a sneaky &lt;code&gt;"is_admin": true&lt;/code&gt; is rejected. Apply updates with &lt;code&gt;model_dump(exclude_unset=True)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Database
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SQLAlchemy 2.0 style only.&lt;/strong&gt; &lt;code&gt;Mapped[...]&lt;/code&gt;, &lt;code&gt;mapped_column()&lt;/code&gt;, &lt;code&gt;select()&lt;/code&gt;. Never &lt;code&gt;session.query()&lt;/code&gt; or &lt;code&gt;declarative_base()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services own the transaction&lt;/strong&gt; with &lt;code&gt;async with session.begin():&lt;/code&gt;. Repositories never commit. Use &lt;code&gt;async_sessionmaker(expire_on_commit=False)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships default to &lt;code&gt;lazy="raise"&lt;/code&gt;.&lt;/strong&gt; Load explicitly with &lt;code&gt;selectinload()&lt;/code&gt; or &lt;code&gt;joinedload()&lt;/code&gt;, so there's no hidden IO in async code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;MetaData(naming_convention=...)&lt;/code&gt; on your Base&lt;/strong&gt; so Alembic constraint names are deterministic. Never edit a migration that's already applied.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Errors, config and safety
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Services raise your own &lt;code&gt;AppError&lt;/code&gt; subclasses, never &lt;code&gt;HTTPException&lt;/code&gt;.&lt;/strong&gt; One exception handler maps them to &lt;code&gt;{"error": {"code": ..., "message": ...}}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings only through a cached &lt;code&gt;get_settings()&lt;/code&gt;&lt;/strong&gt; built on pydantic-settings. Never read &lt;code&gt;os.environ&lt;/code&gt; elsewhere, and keep secrets as &lt;code&gt;SecretStr&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never build SQL with f-strings.&lt;/strong&gt; Use bound parameters or &lt;code&gt;text(...).bindparams()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;pytest + pytest-asyncio + &lt;code&gt;httpx.AsyncClient(transport=ASGITransport(app=app))&lt;/code&gt;&lt;/strong&gt;, against a real Postgres with a rollback per test. Clear &lt;code&gt;app.dependency_overrides&lt;/code&gt; after each test. No SQLite stand-in.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using them
&lt;/h2&gt;

&lt;p&gt;Drop rules like these into &lt;code&gt;.cursor/rules/&lt;/code&gt;, one file per area, with globs that match your layout. For Claude Code, the same conventions work as a short &lt;code&gt;CLAUDE.md&lt;/code&gt; at the repo root.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I made a FastAPI rules pack with these conventions written out as 7 scoped &lt;code&gt;.mdc&lt;/code&gt; files plus a &lt;code&gt;CLAUDE.md&lt;/code&gt;, and I sell it for $12 at &lt;a href="https://codebaserules.com" rel="noopener noreferrer"&gt;codebaserules.com&lt;/a&gt;. Two of the files are a &lt;a href="https://codebaserules.com/packs/fastapi.html" rel="noopener noreferrer"&gt;free download&lt;/a&gt;, no email needed. Everything above works without buying anything. I'd like to hear which of these you'd change.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>python</category>
      <category>cursor</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
