<?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: itsuki</title>
    <description>The latest articles on DEV Community by itsuki (@seballiot).</description>
    <link>https://dev.to/seballiot</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%2F2477061%2F2de41c2a-9349-49ef-bc0f-1db5a7fead31.png</url>
      <title>DEV Community: itsuki</title>
      <link>https://dev.to/seballiot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seballiot"/>
    <language>en</language>
    <item>
      <title>How Reading a Design Patterns Book Led Me to Audit My Rust Framework with UML and Merise</title>
      <dc:creator>itsuki</dc:creator>
      <pubDate>Wed, 22 Jul 2026 23:20:33 +0000</pubDate>
      <link>https://dev.to/seballiot/how-reading-a-design-patterns-book-led-me-to-audit-my-rust-framework-with-uml-and-merise-45dj</link>
      <guid>https://dev.to/seballiot/how-reading-a-design-patterns-book-led-me-to-audit-my-rust-framework-with-uml-and-merise-45dj</guid>
      <description>&lt;p&gt;I was reading a design patterns book. I got to the UML class diagrams — and I stopped.&lt;/p&gt;

&lt;p&gt;Because the diagram on the page was describing something Rust already says in its own syntax. A &lt;code&gt;struct&lt;/code&gt; is a class. &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt; is a &lt;code&gt;0..1&lt;/code&gt; association. &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; is &lt;code&gt;1..N&lt;/code&gt;. Ownership is composition. The formalism I was looking at, drawn as boxes and diamonds, was the same information the compiler already had — just in a different notation.&lt;/p&gt;

&lt;p&gt;Which led to an uncomfortable thought. If the type system already encodes what UML draws, then translating my code into UML shouldn't tell me anything new. It should be redundant. Unless the translation &lt;em&gt;didn't line up&lt;/em&gt; — and everywhere it failed to line up, there would be a bug hiding in the gap.&lt;/p&gt;

&lt;p&gt;So I tried it. I took Runique — my Rust web framework, v2.1.21 — and I modeled the framework itself, module by module, in UML for structure and Merise for the data. The intuition was mine; I used Claude to help verify each translation against the actual code, line by line. We found twenty-plus latent bugs. Three of them were security-relevant. None of them had shown up in &lt;code&gt;cargo test&lt;/code&gt;, in &lt;code&gt;tracing&lt;/code&gt;, or in the compiler.&lt;/p&gt;

&lt;p&gt;This is what the gaps looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I couldn't see in my own code
&lt;/h2&gt;

&lt;p&gt;Runique is a Django-inspired framework: Axum, SeaORM, Tera, edition 2024. It works. It's powered a real restaurant's booking site and runique.io itself in production for months — that's my dogfooding. But the audit I'm describing was on the framework, not on those sites. And that's exactly the problem — "it works" is the state in which bugs are hardest to see. The compiler is happy. The tests are green. &lt;code&gt;tracing&lt;/code&gt; shows nothing because nothing is throwing.&lt;/p&gt;

&lt;p&gt;The tools I reach for every day are all &lt;em&gt;local&lt;/em&gt;. &lt;code&gt;cargo check&lt;/code&gt; sees one function. A unit test sees one path. &lt;code&gt;tracing&lt;/code&gt; sees one request. None of them step back and show you the shape of the whole thing at once — and some bugs only exist in the shape. A field that's &lt;code&gt;pub&lt;/code&gt; when it should be &lt;code&gt;pub(crate)&lt;/code&gt;. A file written one step too early in a sequence. A type that diverges from the type it's supposed to match, three files away, behind a feature flag.&lt;/p&gt;

&lt;p&gt;I needed an outside view of the code — without leaving the code. Not a rewrite, not a new tool, not a linter. A different &lt;em&gt;representation&lt;/em&gt; of the same thing, precise enough that the discrepancies would be mechanical to spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three notations, one structure
&lt;/h2&gt;

&lt;p&gt;Here's the idea the book handed me, made explicit.&lt;/p&gt;

&lt;p&gt;Rust's type system, UML class diagrams, and Merise data models are three notations for the same underlying facts: entities, their relationships, their cardinalities, their invariants. The GoF patterns are just the recurring &lt;em&gt;shapes&lt;/em&gt; that show up across all three. Rust encodes them in the type system, where the compiler enforces them. UML and Merise encode them in a separate graphical formalism, where a human reads them. The translation between the two is almost mechanical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Rust&lt;/th&gt;
&lt;th&gt;UML / Merise&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;struct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Class / Entity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mandatory 1..1&lt;/td&gt;
&lt;td&gt;field by value: &lt;code&gt;T&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;cardinality &lt;code&gt;1..1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional 0..1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cardinality &lt;code&gt;0..1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1..N&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; / &lt;code&gt;HashMap&amp;lt;K,V&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;cardinality &lt;code&gt;1..N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composition (owned)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;struct A { b: B }&lt;/code&gt; or &lt;code&gt;Box&amp;lt;B&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;filled diamond&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregation (shared)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;amp;B&lt;/code&gt; / &lt;code&gt;Arc&amp;lt;B&amp;gt;&lt;/code&gt; / &lt;code&gt;Rc&amp;lt;B&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;hollow diamond&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refinement&lt;/td&gt;
&lt;td&gt;AST → HIR → MIR&lt;/td&gt;
&lt;td&gt;MCD → MLD → MPD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One caveat, because Rustaceans will (rightly) pounce otherwise: the ownership-to-composition mapping isn't perfect. &lt;code&gt;Box&amp;lt;B&amp;gt;&lt;/code&gt; still &lt;em&gt;owns&lt;/em&gt;, so it's composition, not aggregation; aggregation — a shared reference with an independent lifetime — is &lt;code&gt;&amp;amp;B&lt;/code&gt;, &lt;code&gt;Arc&amp;lt;B&amp;gt;&lt;/code&gt;, &lt;code&gt;Rc&amp;lt;B&amp;gt;&lt;/code&gt;. The analogy is a lens, not a law. But it's a sharp enough lens that when the code and the diagram disagree, the disagreement is worth investigating.&lt;/p&gt;

&lt;p&gt;That's the whole method in one sentence: &lt;strong&gt;model the code in a formalism the type system doesn't natively speak, and audit the places where the two representations refuse to agree.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Merise deserves a note here, because it's the unfamiliar one for most English-speaking readers. It's a French systems-modeling method from the 1970s, still taught in French CS programs today. It splits a data model into conceptual (MCD), logical (MLD), and physical (MPD) levels — and crucially, at the logical level it forces you to write down &lt;em&gt;both&lt;/em&gt; the cardinality &lt;em&gt;and&lt;/em&gt; the concrete type of every association. That second demand is what caught one of the bugs below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method: three lenses, no sampling
&lt;/h2&gt;

&lt;p&gt;I didn't spot-check. The point of an audit is coverage, so I went module by module across the whole framework, with three lenses, each matched to what it's good at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merise for the data&lt;/strong&gt; — an MCD and an MLD of every &lt;code&gt;eihwaz_*&lt;/code&gt; framework table, with cardinalities, foreign keys, and physical types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UML class diagrams for the structure&lt;/strong&gt; — one per layer: engine, forms, admin, auth, middleware, migration, config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequence diagrams for the dynamics&lt;/strong&gt; — the request/CSRF/upload flow, login/session, admin CRUD. This is the lens that shows you &lt;em&gt;order&lt;/em&gt;, which neither the code nor a class diagram makes visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is Mermaid in Markdown, versioned next to the code. No binary tool, no separate app — the diagrams render on GitHub and travel with the repo. Each one ends with an "anomalies" section, consolidated into a single &lt;code&gt;anomalies.md&lt;/code&gt; with severity and a &lt;code&gt;file:line&lt;/code&gt; for every finding.&lt;/p&gt;

&lt;p&gt;Now the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1 — UML: a &lt;code&gt;pub&lt;/code&gt; field that quietly bypassed CSRF
&lt;/h2&gt;

&lt;p&gt;I'll open with the most &lt;em&gt;interesting&lt;/em&gt; one — not the most severe, the most interesting, because it's the bug that best shows what this whole exercise is really about.&lt;/p&gt;

&lt;p&gt;I was drawing the UML class diagram for &lt;code&gt;Prisme&lt;/code&gt;, the type that holds a request's parsed-and-CSRF-checked body. The diagram made me write down, for each field, its visibility. And there it was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prisme
  + data: StrMap        ← public
  + csrf_valid: bool
  + checked_data() -&amp;gt; Option&amp;lt;&amp;amp;StrMap&amp;gt;   ← fail-closed accessor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;data&lt;/code&gt; was &lt;code&gt;pub&lt;/code&gt;. &lt;code&gt;checked_data()&lt;/code&gt; — the accessor that returns the body &lt;em&gt;only if CSRF passed&lt;/em&gt; — was right next to it. The moment those two sat in the same box on the diagram, the problem was obvious: why build a fail-closed gate and then leave the raw field public next to it?&lt;/p&gt;

&lt;p&gt;The CSRF enforcement in Runique doesn't live in the middleware for HTML forms — it lives at the point of access. &lt;code&gt;req.form()&lt;/code&gt; checks &lt;code&gt;csrf_valid&lt;/code&gt; and refuses to hand you validated data if the token is bad. But if &lt;code&gt;Prisme::data&lt;/code&gt; is &lt;code&gt;pub&lt;/code&gt;, any handler — or, more to the point, any &lt;em&gt;third-party code built on Runique&lt;/em&gt; — can read the raw body directly and skip the gate entirely, without ever knowing it did.&lt;/p&gt;

&lt;p&gt;I want to be exact about the severity, because the honest version is more interesting than the scary one. My own code was clean: admin, login, and &lt;code&gt;form()&lt;/code&gt; all check CSRF before touching the body. This was never an exploited hole in Runique itself. It was a &lt;strong&gt;footgun in the public API&lt;/strong&gt; — a shape that let someone building on the framework bypass CSRF &lt;em&gt;by accident&lt;/em&gt;. That's a framework-author's bug, not a break-in.&lt;/p&gt;

&lt;p&gt;The fix is one keyword, and it's the whole point of the article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Prisme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// Parsed body/query data. **Crate-private**: user code cannot read the raw&lt;/span&gt;
    &lt;span class="cd"&gt;/// body without going through the CSRF gate (anomaly C2). External access only&lt;/span&gt;
    &lt;span class="cd"&gt;/// via checked_data() (fail-closed) or req.form().&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StrMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;csrf_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Prisme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// Fail-closed accessor: returns body data only if CSRF is valid.&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;checked_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;StrMap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.csrf_valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="p"&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;p&gt;&lt;code&gt;pub&lt;/code&gt; → &lt;code&gt;pub(crate)&lt;/code&gt;. The Rust visibility system now &lt;em&gt;structurally guarantees&lt;/em&gt; that the only doors to the request body are the two that check CSRF first. It's not a convention, not a lint, not a code-review rule someone has to remember. The compiler will reject the bypass. &lt;strong&gt;Visibility as a security mechanism&lt;/strong&gt; — that's what the UML diagram surfaced, because a class diagram forces you to write down the one property (&lt;code&gt;pub&lt;/code&gt; vs &lt;code&gt;pub(crate)&lt;/code&gt;) that the code lets you skim past.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2 — Sequence diagram: a file written before it was allowed to be
&lt;/h2&gt;

&lt;p&gt;The second lens is the one that shows time. Here's the sequence diagram for a multipart form POST, as I drew it from the real pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant MW as csrf_middleware
    participant P as prisme_pipeline
    participant AG as parse_multipart
    participant H as Handler

    C-&amp;gt;&amp;gt;MW: POST multipart (csrf_token as a field, no header)
    Note over MW: mutating method, no X-CSRF-Token,&amp;lt;br/&amp;gt;form content-type → LET THROUGH&amp;lt;br/&amp;gt;("Prisme will validate")
    MW-&amp;gt;&amp;gt;P: next.run()
    P-&amp;gt;&amp;gt;AG: parse_multipart(req)
    AG-&amp;gt;&amp;gt;AG: stream file → tmp (size cap, NO extension check)
    AG-&amp;gt;&amp;gt;AG: rename tmp → MEDIA_ROOT/uuid.ext (COMMIT)
    Note over AG: file committed to its final,&amp;lt;br/&amp;gt;servable location BEFORE CSRF,&amp;lt;br/&amp;gt;validation, and the handler
    AG--&amp;gt;&amp;gt;P: final paths
    P-&amp;gt;&amp;gt;P: check_csrf() → csrf_valid (flag only)
    P--&amp;gt;&amp;gt;H: Request
    H-&amp;gt;&amp;gt;H: if form.is_valid() { ... } else { reject }
    Note over H: file already on disk in MEDIA_ROOT,&amp;lt;br/&amp;gt;never removed on rejection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read top to bottom and the bug reads itself. The file gets renamed into &lt;code&gt;MEDIA_ROOT&lt;/code&gt; — its final, potentially publicly-served location — &lt;em&gt;before&lt;/em&gt; CSRF is checked, before the form validates, and before the extension filter (which lives later, in &lt;code&gt;FileField::validate&lt;/code&gt;) ever runs. On any public endpoint that extracts a &lt;code&gt;Request&lt;/code&gt; and accepts multipart, that meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unauthenticated file writes.&lt;/strong&gt; The file lands in &lt;code&gt;MEDIA_ROOT&lt;/code&gt; no matter what happens next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No extension filter at that stage&lt;/strong&gt; — only a size cap. An &lt;code&gt;.html&lt;/code&gt;, &lt;code&gt;.svg&lt;/code&gt;, or &lt;code&gt;.js&lt;/code&gt; could land in a served directory. If &lt;code&gt;MEDIA_ROOT&lt;/code&gt; is served statically, that's a path to stored XSS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphans on rejection.&lt;/strong&gt; CSRF fails, validation fails, honeypot trips — the file is already written, and nothing cleans it up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No unit test caught this because no unit test is &lt;em&gt;shaped&lt;/em&gt; like the request lifecycle. The class diagram didn't catch it either — every individual method is correct in isolation. Only the temporal view, the one that lays out &lt;em&gt;order&lt;/em&gt;, made "written before validated" visible as a single glance.&lt;/p&gt;

&lt;p&gt;The fix moves the commit to the end. &lt;code&gt;parse_multipart&lt;/code&gt; now writes to a non-served staging directory, and &lt;code&gt;FileField::finalize&lt;/code&gt; — running &lt;em&gt;after&lt;/em&gt; CSRF and validation — is the only thing allowed to commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// No commit here: files stay in staging. FileField::finalize (the only committer)&lt;/span&gt;
&lt;span class="c1"&gt;// moves them to their served destination after CSRF + validation. On rejection,&lt;/span&gt;
&lt;span class="c1"&gt;// staging is purged by sweep_stale_staging (best-effort, TTL). Errors are logged,&lt;/span&gt;
&lt;span class="c1"&gt;// never swallowed.&lt;/span&gt;
&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One committer, one gate, running in the right order — and a TTL sweep for staging that never got promoted, with every failure logged rather than dropped on the floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3 — Merise: a foreign key that didn't match its target
&lt;/h2&gt;

&lt;p&gt;The last one is the bug Merise's "write down the type" discipline caught, and it's a clean demonstration of why a separate formalism earns its keep.&lt;/p&gt;

&lt;p&gt;Here's the MLD fragment — the logical model — for the junction table between users and groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;erDiagram
    USER ||--o{ USER_GROUPE : "belongs to"
    GROUPE ||--o{ USER_GROUPE : "groups"
    USER {
        pk id "int OR bigint under big-pk"
    }
    USER_GROUPE {
        pk user_id FK "→ users.id"
        int groupe_id FK
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merise made me write two things next to &lt;code&gt;user_id&lt;/code&gt;: its cardinality (a mandatory FK into &lt;code&gt;users.id&lt;/code&gt;) &lt;em&gt;and&lt;/em&gt; its physical type. And &lt;code&gt;users.id&lt;/code&gt; has a footnote — it's &lt;code&gt;integer&lt;/code&gt; normally, but &lt;code&gt;bigint&lt;/code&gt; under the &lt;code&gt;big-pk&lt;/code&gt; feature flag. So I went to check that the FK column followed the same rule. It didn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// user_id was hardcoded .integer() — it must follow eihwaz_users.id, which becomes&lt;/span&gt;
&lt;span class="c1"&gt;// BIGINT under the big-pk feature, or the FK is a type mismatch and fails to create.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;user_id_col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ColumnDef&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Alias&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;user_id_col&lt;/span&gt;&lt;span class="nf"&gt;.integer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// ← always integer, even when users.id is bigint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sessions&lt;/code&gt;, &lt;code&gt;history&lt;/code&gt;, and &lt;code&gt;reset_tokens&lt;/code&gt; all propagated the feature flag to their &lt;code&gt;user_id&lt;/code&gt; columns. This one junction table didn't. Under &lt;code&gt;big-pk&lt;/code&gt;, on a strict database like Postgres, the foreign key is a type mismatch and fails to create — a broken migration that only shows up for the subset of users who flip that flag. The fix is mechanical once you've seen it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;user_id_col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ColumnDef&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Alias&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nd"&gt;#[cfg(feature&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"big-pk"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;user_id_col&lt;/span&gt;&lt;span class="nf"&gt;.big_integer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nd"&gt;#[cfg(not(feature&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"big-pk"&lt;/span&gt;&lt;span class="nd"&gt;))]&lt;/span&gt;
&lt;span class="n"&gt;user_id_col&lt;/span&gt;&lt;span class="nf"&gt;.integer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;user_id_col&lt;/span&gt;&lt;span class="nf"&gt;.not_null&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler could never have caught this — both branches compile fine; they're just describing a database that won't build. Merise caught it because it made the type an explicit thing I had to write down and compare, instead of something buried in a &lt;code&gt;ColumnDef&lt;/code&gt; builder call three files away from its counterpart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real discovery wasn't the bugs
&lt;/h2&gt;

&lt;p&gt;Here's the twist I didn't expect. The bugs were the &lt;em&gt;goal&lt;/em&gt; of the exercise. But they weren't the most valuable thing it produced.&lt;/p&gt;

&lt;p&gt;The most valuable thing was the map.&lt;/p&gt;

&lt;p&gt;By the end I had an architectural understanding of Runique that I — its author — did not have when I started. Not a vague "I know my codebase" feeling, but a precise, drawn, versioned model of every layer and every data flow. The bugs were a byproduct of building that map. The map is the asset.&lt;/p&gt;

&lt;p&gt;Two things convinced me of that. The first was the &lt;strong&gt;verified false positives&lt;/strong&gt;. I didn't just log the real bugs; I logged every hypothesis I was sure of that turned out to be &lt;em&gt;wrong&lt;/em&gt;. I was convinced &lt;code&gt;makemigrations&lt;/code&gt; couldn't handle &lt;code&gt;ALTER COLUMN&lt;/code&gt; — false, it uses a full schema diff I'd forgotten I wrote. I was convinced two session-write paths produced a divergent &lt;code&gt;session_id&lt;/code&gt; — false, the &lt;code&gt;on_conflict&lt;/code&gt; clause makes it deterministic. Documenting the things you were wrong about is what separates an audit from a victory lap, and it's the part most people would quietly delete.&lt;/p&gt;

&lt;p&gt;The second was the &lt;strong&gt;transverse themes&lt;/strong&gt;. Once the bugs were on one page, they stopped looking like twenty separate bugs and started looking like four repeated &lt;em&gt;anti-patterns&lt;/em&gt;: errors swallowed in silence; two sources of truth for one fact; security operations in the wrong order; a feature flag that didn't propagate everywhere it needed to. That's the jump from "fix the bug" to "extract the rule" — and you can only make it when you can see all the instances at once. The map is what let me see them at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to do this to your own project
&lt;/h2&gt;

&lt;p&gt;If you want to try it, the method compresses to a few rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with one module, one layer.&lt;/strong&gt; Don't model the whole thing on day one; model the part you've started to distrust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the lens to the question.&lt;/strong&gt; Data integrity → Merise. Structure and visibility → UML class diagrams. Ordering and lifecycle → sequence diagrams. Most bugs live in exactly one of those three views and are invisible in the other two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the tooling trivial.&lt;/strong&gt; Mermaid in Markdown, committed with the code. It renders on GitHub and it diffs like source, which means the diagrams stay honest instead of rotting in a wiki.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log your false positives as carefully as your bugs.&lt;/strong&gt; The discipline is the whole value. An audit that only records wins is marketing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one honest warning: this takes real time. It is not worth it on a weekend project. It's worth it on a production codebase you've started to distrust — the one that works, that passes its tests, and that you've nonetheless got a quiet feeling about. That feeling is usually right, and this is how you find out what it's pointing at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The loop back to the book that started it is this. When I modeled Runique, I found patterns already in it — Builder, Template Method, Composite, Strategy — that I had never deliberately placed. They'd emerged on their own, because they're the shapes a problem like this pushes you toward. The book didn't teach me to add them. It gave me the vocabulary to &lt;em&gt;see&lt;/em&gt; the ones that were already there, and the modeling gave me the visual proof they existed.&lt;/p&gt;

&lt;p&gt;The full set of diagrams — every UML class diagram, the Merise data model, and the sequence flows, each with its own anomalies section — lives in the &lt;a href="https://github.com/seb-alliot/runique/tree/main/diagramme" rel="noopener noreferrer"&gt;&lt;code&gt;diagramme/&lt;/code&gt;&lt;/a&gt; folder on GitHub. Runique itself is on &lt;a href="https://crates.io/crates/runique" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; and &lt;a href="https://github.com/seb-alliot/runique" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, docs at &lt;a href="https://runique.io" rel="noopener noreferrer"&gt;runique.io&lt;/a&gt;. More of this audit is becoming its own series: security-through-visibility, a sweep of every swallowed error, and the full list of verified false positives.&lt;/p&gt;

&lt;p&gt;One question for the comments, because I genuinely want to know: &lt;strong&gt;have you ever used a modeling formalism to audit code you'd already written — and what did it reveal that the code itself was hiding?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>architecture</category>
      <category>showdev</category>
      <category>security</category>
    </item>
    <item>
      <title>I Built a Django-Inspired Web Framework in Rust — Here's What I Learned</title>
      <dc:creator>itsuki</dc:creator>
      <pubDate>Sun, 14 Jun 2026 14:18:48 +0000</pubDate>
      <link>https://dev.to/seballiot/i-built-a-django-inspired-web-framework-in-rust-heres-what-i-learned-5bbf</link>
      <guid>https://dev.to/seballiot/i-built-a-django-inspired-web-framework-in-rust-heres-what-i-learned-5bbf</guid>
      <description>&lt;p&gt;If you've ever used Django, you know the feeling: one command, and you have an admin panel, an ORM, form validation, middleware, session handling — everything just works. Then you try Rust web development, and you're back to assembling pieces yourself.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;strong&gt;Runique&lt;/strong&gt;: a batteries-included web framework for Rust, inspired by Django's philosophy, built on top of Axum and Tokio.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "Django for Rust"?
&lt;/h2&gt;

&lt;p&gt;Existing Rust frameworks are excellent at what they do — Axum is fast and composable, Actix-Web is a performance beast — but they're low-level by design. You bring your own ORM, your own session store, your own CSRF protection, your own admin interface. For many projects, that's the right tradeoff.&lt;/p&gt;

&lt;p&gt;Runique takes the opposite bet: &lt;strong&gt;convention over configuration&lt;/strong&gt;, with a structured, opinionated setup that gets you from zero to a production-ready app fast, without sacrificing Rust's safety and performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Builder: A Validated Construction Pipeline
&lt;/h2&gt;

&lt;p&gt;The part I'm most proud of is the application builder. You declare your components with a fluent API — in any order — and then a single &lt;code&gt;.build()&lt;/code&gt; call runs a &lt;strong&gt;fixed, validated construction pipeline&lt;/strong&gt; at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Validation → DB connection → Templates → Engine → Admin → Middleware → Static files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here's what a typical app setup looks like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;password_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;PasswordConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;auto_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Manual&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Argon2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;set_lang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;En&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RuniqueConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;RuniqueConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_env&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;db_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;DatabaseConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DatabaseConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_config&lt;/span&gt;&lt;span class="nf"&gt;.connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nn"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.with_log&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="nf"&gt;.dev&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;.host_validation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;WARN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.acme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;.routes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;url&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.with_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.with_mailer_from_env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="py"&gt;.with_password_reset&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BuiltinUserEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="nf"&gt;.forgot_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth/forgot_password.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.reset_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth/reset_password.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;.statics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.middleware&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="nf"&gt;.with_session_memory_limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.with_session_cleanup_interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.with_allowed_hosts&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="nf"&gt;.enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_debug&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                        &lt;span class="nf"&gt;.host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"runique.io"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;.host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"www.runique.io"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;.host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localhost:3000"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;.host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"127.0.0.1:3000"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="nf"&gt;.with_csp&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="nf"&gt;.policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SecurityPolicy&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                        &lt;span class="nf"&gt;.with_header_security&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;.with_upgrade_insecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_debug&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                        &lt;span class="nf"&gt;.scripts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"'strict-dynamic'"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;.with_admin&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="nf"&gt;.site_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Administration"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.sitemap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://runique.io/sitemap.xml"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RuniqueAdminAuth&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;.routes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;admins&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin-runique/"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;.templates&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="nf"&gt;.with_dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"admin/test_dashboard.html"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;.with_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;admins&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;admin_state&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;.with_rate_limiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.max_requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.retry_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;.with_login_guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LoginGuard&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.max_attempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.lockout_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;.page_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&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;The &lt;code&gt;.build()&lt;/code&gt; call validates every component — including cross-dependencies — before constructing anything. If your &lt;code&gt;SECRET_KEY&lt;/code&gt; is still the default insecure value in production, the build &lt;strong&gt;fails with a clear error and a fix suggestion&lt;/strong&gt;, before a single request is served:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Security] SECRET_KEY is using the default insecure value
  → Set SECRET_KEY to a random 32+ character string in your .env file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the Django &lt;code&gt;check&lt;/code&gt; framework equivalent, but enforced at startup — not discovered in production.&lt;/p&gt;


&lt;h2&gt;
  
  
  Security Included by Default
&lt;/h2&gt;

&lt;p&gt;Security in Runique isn't a plugin you add later. It's part of the construction pipeline itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSP (Content Security Policy)&lt;/strong&gt; — configurable profiles, per-request nonce, HTMX hash merging when the admin panel is enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSRF protection&lt;/strong&gt; — built into the middleware stack, with per-route exemptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host validation&lt;/strong&gt; — allowed hosts checked before routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusted proxies&lt;/strong&gt; — explicit configuration, not implicit trust&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security headers on static files&lt;/strong&gt; — &lt;code&gt;X-Content-Type-Options&lt;/code&gt;, &lt;code&gt;Strict-Transport-Security&lt;/code&gt;, &lt;code&gt;X-Frame-Options&lt;/code&gt;, &lt;code&gt;Referrer-Policy&lt;/code&gt; applied automatically
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You don't wire ServeDir yourself — `.statics()` does it,&lt;/span&gt;
&lt;span class="c1"&gt;// already wrapping every static/media route with security headers:&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="s"&gt;"path"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// → X-Content-Type-Options: nosniff&lt;/span&gt;
&lt;span class="c1"&gt;//   Strict-Transport-Security: max-age=31536000; includeSubDomains; preload&lt;/span&gt;
&lt;span class="c1"&gt;//   X-Frame-Options: DENY&lt;/span&gt;
&lt;span class="c1"&gt;//   Referrer-Policy: strict-origin-when-cross-origin&lt;/span&gt;
&lt;span class="c1"&gt;//   + cache-control&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The middleware stack uses &lt;strong&gt;numbered slots&lt;/strong&gt; to guarantee application order — you can't accidentally apply CSRF before sessions, because the slots enforce the correct sequence.&lt;/p&gt;


&lt;h2&gt;
  
  
  What's Included
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ORM&lt;/strong&gt; (via SeaORM, optional feature flag)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template engine&lt;/strong&gt; (Tera, with custom filters and a URL registry for named routes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin panel&lt;/strong&gt; (auto-generated from your models, merged before the middleware stack so it always has session/auth context)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form engine&lt;/strong&gt; (typed structs with validation, v2 in progress)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session handling&lt;/strong&gt; (memory-first store with database fallback, built on &lt;code&gt;tower-sessions&lt;/code&gt;, with periodic cleanup)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;i18n&lt;/strong&gt; (the FR/EN bilingual doc site is itself built with Runique)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middleware system&lt;/strong&gt; with ordered slots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password reset&lt;/strong&gt; (pluggable, routes registered automatically during build)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug error page&lt;/strong&gt; — a styled page with collapsible sections and copy-to-clipboard for stack traces&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  One Concrete Example: Admin Merge Order
&lt;/h2&gt;

&lt;p&gt;Here's a subtle design decision that illustrates Runique's philosophy. In Axum, &lt;code&gt;.layer()&lt;/code&gt; only applies to routes present at call time. This means if you merge your admin router after applying middleware, the admin routes run &lt;strong&gt;without&lt;/strong&gt; session, CSRF, or extension context — a silent, hard-to-debug failure.&lt;/p&gt;

&lt;p&gt;Runique handles this in the builder:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 4b: admin + password reset — merged BEFORE the middleware stack.&lt;/span&gt;
&lt;span class="c1"&gt;// `.layer()` in Axum only covers routes present at call time;&lt;/span&gt;
&lt;span class="c1"&gt;// merging after means admin routes run without Session/CSRF/Extensions.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.admin.enabled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="nf"&gt;.merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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="c1"&gt;// Step 5: middleware applied AFTER, covering everything including admin&lt;/span&gt;
&lt;span class="k"&gt;let&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;session_store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="nf"&gt;.apply_to_router&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;config&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;tera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You don't have to think about this. The pipeline handles it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;p&gt;Runique is at &lt;strong&gt;v2.1.x&lt;/strong&gt;, MIT licensed, with bilingual documentation (EN/FR) at &lt;a href="https://runique.io" rel="noopener noreferrer"&gt;runique.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The framework is used in production for a restaurant management system — dogfooding at its most direct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's missing&lt;/strong&gt; (being honest here, Django-style):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No async task queue yet (background jobs run on bare &lt;code&gt;tokio::spawn&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Email sending is partial&lt;/li&gt;
&lt;li&gt;OAuth is planned but not yet implemented&lt;/li&gt;
&lt;li&gt;Test coverage is at ~61% (growing)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Add the dependency to your &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;runique&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://runique.io/docs/en" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Read the Getting Started Guide&lt;/a&gt;
&lt;/p&gt;
&lt;h2&gt;
  
  
  Links &amp;amp; Resources
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/seb-alliot" rel="noopener noreferrer"&gt;
        seb-alliot
      &lt;/a&gt; / &lt;a href="https://github.com/seb-alliot/runique" rel="noopener noreferrer"&gt;
        runique
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A framework web base on Django/python
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Runique — the Django developer experience, in type-safe Rust&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/2da29fd375231aa4a1c7aa88bc1c9cf0f8dff75a6ef0f92cc18c7f804c8ca9c0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f727573742d312e39342532422d6f72616e6765"&gt;&lt;img src="https://camo.githubusercontent.com/2da29fd375231aa4a1c7aa88bc1c9cf0f8dff75a6ef0f92cc18c7f804c8ca9c0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f727573742d312e39342532422d6f72616e6765" alt="Rust"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/f9c93e573de524725c10bdeba536f89713262698b21c58ec380dffcb250299ca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d3233383025324225323070617373696e672d677265656e"&gt;&lt;img src="https://camo.githubusercontent.com/f9c93e573de524725c10bdeba536f89713262698b21c58ec380dffcb250299ca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d3233383025324225323070617373696e672d677265656e" alt="Tests passing"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e"&gt;&lt;img src="https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e" alt="License"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/5f655fe74b522c42e3a15379cb8c82db64c21617a9f629d20abf73f1399daeb7/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f76657273696f6e2d322e312e32312d626c7565"&gt;&lt;img src="https://camo.githubusercontent.com/5f655fe74b522c42e3a15379cb8c82db64c21617a9f629d20abf73f1399daeb7/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f76657273696f6e2d322e312e32312d626c7565" alt="Version"&gt;&lt;/a&gt;
&lt;a href="https://crates.io/crates/runique" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/2681e8d70b5efe4826f355b9f62a280f65466ed3b0605c911fa5b3082112b5c4/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f762f72756e69717565" alt="Crates.io"&gt;&lt;/a&gt;
&lt;a href="https://runique.io" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/96fcf63b7ce9ce7b9f5585864aa409f4ab1eea14f531e708f3be479d0ac07b9f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f52756e697175652d627269676874677265656e" alt="Runique"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Declare a model once — get the database table, the migration, a type-safe form &lt;em&gt;and&lt;/em&gt; a full admin panel.&lt;/strong&gt; Runique is a batteries-included web framework that brings Django's productivity to Rust, without giving up Rust's safety and performance. Built on Axum, SeaORM and Tera.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Status — honest:&lt;/strong&gt; active development. The framework crate (&lt;code&gt;runique&lt;/code&gt;) is the source of truth; &lt;code&gt;demo-app&lt;/code&gt; is a real validation app exercised against it. The admin is in &lt;strong&gt;beta&lt;/strong&gt;. Nothing below is overstated — see &lt;a href="https://github.com/seb-alliot/runique/blob/main/docs/en/PROJECT_STATUS.en.md" rel="noopener noreferrer"&gt;Project status&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;🌍 &lt;strong&gt;Languages&lt;/strong&gt;: English | &lt;a href="https://runique.io/readme/fr" rel="nofollow noopener noreferrer"&gt;Français&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Declarative macros, not boilerplate&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-rust notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;model&lt;/span&gt;&lt;span class="pl-en"&gt;!&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-v"&gt;Article&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    table&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-s"&gt;"articles"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    pk&lt;span class="pl-kos"&gt;:&lt;/span&gt; id =&amp;gt; &lt;span class="pl-v"&gt;Pk&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    enums&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;Status&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-v"&gt;Draft&lt;/span&gt;=&lt;span class="pl-s"&gt;"Draft"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-v"&gt;Published&lt;/span&gt;=&lt;span class="pl-s"&gt;"Published"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        title&lt;span class="pl-kos"&gt;:&lt;/span&gt;  text &lt;span class="pl-kos"&gt;[&lt;/span&gt;required&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
        slug&lt;span class="pl-kos"&gt;:&lt;/span&gt;   text &lt;span class="pl-kos"&gt;[&lt;/span&gt;unique&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/seb-alliot/runique" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Documentation: &lt;a href="https://runique.io" rel="noopener noreferrer"&gt;runique.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Crates.io: &lt;a href="https://crates.io/crates/runique" rel="noopener noreferrer"&gt;crates.io/crates/runique&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What do you think?
&lt;/h2&gt;

&lt;p&gt;I'm particularly curious to hear from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Django developers&lt;/strong&gt; — Does this structure feel familiar, or "too much" for the Rust ecosystem?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust experts&lt;/strong&gt; — How would you handle the middleware ordering differently?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's discuss in the comments!&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webdev</category>
      <category>django</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
