Files
docs/openapi-first/wiki/02_components/index.html

1165 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="prev" href="../01_overview/">
<link rel="next" href="../03_use_cases/01_quickstart/">
<link rel="icon" href="../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.6.23">
<title>Components - openapi_first</title>
<link rel="stylesheet" href="../assets/stylesheets/main.84d31ad4.min.css">
<link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"JetBrains Mono"}</style>
<link rel="stylesheet" href="https://unpkg.com/dracula-prism/dist/css/dracula-prism.css">
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="blue-grey" data-md-color-accent="teal">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#components-what-ships-in-the-box" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href=".." title="openapi_first" class="md-header__button md-logo" aria-label="openapi_first" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7H5a2 2 0 0 0-2 2v8h2v-4h2v4h2V9a2 2 0 0 0-2-2m0 4H5V9h2m7-2h-4v10h2v-4h2a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2m0 4h-2V9h2m6 0v6h1v2h-4v-2h1V9h-1V7h4v2Z"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
openapi_first
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Components
</span>
</div>
</div>
</div>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href=".." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="../01_overview/" class="md-tabs__link">
Overview
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="./" class="md-tabs__link">
Components
</a>
</li>
<li class="md-tabs__item">
<a href="../03_use_cases/01_quickstart/" class="md-tabs__link">
Use Cases
</a>
</li>
<li class="md-tabs__item">
<a href="../04_design/" class="md-tabs__link">
Design
</a>
</li>
<li class="md-tabs__item">
<a href="../05_security/" class="md-tabs__link">
Security
</a>
</li>
<li class="md-tabs__item">
<a href="../06_error_handling/" class="md-tabs__link">
Error Handling
</a>
</li>
<li class="md-tabs__item">
<a href="../07_testing/" class="md-tabs__link">
Testing
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted md-nav--integrated" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href=".." title="openapi_first" class="md-nav__button md-logo" aria-label="openapi_first" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7H5a2 2 0 0 0-2 2v8h2v-4h2v4h2V9a2 2 0 0 0-2-2m0 4H5V9h2m7-2h-4v10h2v-4h2a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2m0 4h-2V9h2m6 0v6h1v2h-4v-2h1V9h-1V7h4v2Z"/></svg>
</a>
openapi_first
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href=".." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../01_overview/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Components
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Components
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#1-module-map" class="md-nav__link">
<span class="md-ellipsis">
🗂️ 1. Module Map
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-loader-load-validate" class="md-nav__link">
<span class="md-ellipsis">
⚙️ 2. loader — Load &amp; Validate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#3-app-the-application-bootstrap" class="md-nav__link">
<span class="md-ellipsis">
🧬 3. app — The Application Bootstrap
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#4-binder-spec-route-binding" class="md-nav__link">
<span class="md-ellipsis">
🔗 4. binder — Spec → Route Binding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#5-client-the-other-side-of-the-contract" class="md-nav__link">
<span class="md-ellipsis">
📡 5. client — The Other Side of the Contract
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#6-errors-explicit-error-hierarchy" class="md-nav__link">
<span class="md-ellipsis">
💥 6. errors — Explicit Error Hierarchy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#7-security-auth-from-the-spec" class="md-nav__link">
<span class="md-ellipsis">
🛡️ 7. security — Auth from the Spec
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#8-codegen-cli-build-time-tooling" class="md-nav__link">
<span class="md-ellipsis">
🏭 8. codegen &amp; cli — Build-Time Tooling
</span>
</a>
<nav class="md-nav" aria-label="🏭 8. codegen &amp; cli — Build-Time Tooling">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#openapi-first-models-spec-o-file" class="md-nav__link">
<span class="md-ellipsis">
openapi-first models &lt;spec&gt; -o &lt;file&gt;
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#openapi-first-routes-spec-o-dir-use-models-models-module-models" class="md-nav__link">
<span class="md-ellipsis">
openapi-first routes &lt;spec&gt; -o &lt;dir&gt; [--use-models] [--models-module models]
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#openapi-first-scaffold-template-path" class="md-nav__link">
<span class="md-ellipsis">
openapi-first scaffold &lt;template&gt; [path]
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#9-templates-copyable-applications" class="md-nav__link">
<span class="md-ellipsis">
🧱 9. templates — Copyable Applications
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#related" class="md-nav__link">
<span class="md-ellipsis">
🔗 Related
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_4" >
<label class="md-nav__link" for="__nav_4" id="__nav_4_label" tabindex="0">
<span class="md-ellipsis">
Use Cases
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_4_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_4">
<span class="md-nav__icon md-icon"></span>
Use Cases
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../03_use_cases/01_quickstart/" class="md-nav__link">
<span class="md-ellipsis">
Quickstart
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../03_use_cases/02_templates/" class="md-nav__link">
<span class="md-ellipsis">
Templates
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../03_use_cases/03_client/" class="md-nav__link">
<span class="md-ellipsis">
Client
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../03_use_cases/04_codegen/" class="md-nav__link">
<span class="md-ellipsis">
Codegen
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../04_design/" class="md-nav__link">
<span class="md-ellipsis">
Design
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../05_security/" class="md-nav__link">
<span class="md-ellipsis">
Security
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../06_error_handling/" class="md-nav__link">
<span class="md-ellipsis">
Error Handling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../07_testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="components-what-ships-in-the-box">Components — What Ships in the Box</h1>
<p>The library is deliberately small. Everything you need to run an OpenAPI-first service and talk to it with a strict client fits in a handful of modules.</p>
<hr />
<h2 id="1-module-map">🗂️ 1. Module Map</h2>
<table>
<thead>
<tr>
<th>Module</th>
<th>Responsibility</th>
<th>Import</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>openapi_first.loader</code></td>
<td>Load/validate the spec, resolve <code>{ENV_VAR}</code></td>
<td><code>load_openapi</code></td>
</tr>
<tr>
<td><code>openapi_first.app</code></td>
<td>OpenAPI-first FastAPI application bootstrap</td>
<td><code>OpenAPIFirstApp</code></td>
</tr>
<tr>
<td><code>openapi_first.binder</code></td>
<td>Spec → route binding via <code>operationId</code></td>
<td><code>bind_routes</code></td>
</tr>
<tr>
<td><code>openapi_first.client</code></td>
<td><code>operationId</code>-driven HTTP client</td>
<td><code>OpenAPIClient</code></td>
</tr>
<tr>
<td><code>openapi_first.errors</code></td>
<td>Explicit error hierarchy</td>
<td><code>OpenAPIFirstError</code>, <code>OpenAPIClientError</code>, <code>MissingOperationHandler</code></td>
</tr>
<tr>
<td><code>openapi_first.security</code></td>
<td>Security-dependency construction from the spec</td>
<td><code>parse_security_schemes</code>, <code>make_security_dependencies</code></td>
</tr>
<tr>
<td><code>openapi_first.codegen</code></td>
<td>Pydantic model generation (build-time)</td>
<td><code>generate_models</code></td>
</tr>
<tr>
<td><code>openapi_first.codegen_routes</code></td>
<td>Route-handler stub generation (build-time)</td>
<td><code>generate_routes</code></td>
</tr>
<tr>
<td><code>openapi_first.cli</code></td>
<td><code>scaffold</code> / <code>models</code> / <code>routes</code> command surface</td>
<td>CLI entry point</td>
</tr>
<tr>
<td><code>openapi_first.templates</code></td>
<td>Copyable application templates (NOT library API)</td>
<td><code>openapi-first scaffold</code></td>
</tr>
</tbody>
</table>
<hr />
<h2 id="2-loader-load-validate">⚙️ 2. <code>loader</code> — Load &amp; Validate</h2>
<p><code>load_openapi(path: str | Path) -&gt; dict[str, Any]</code> (in <code>openapi_first/loader.py</code>).</p>
<p><code>loader.py</code> ensures a spec is real, readable, parseable, and schema-valid <em>before</em> anything else runs — a golden rule of fail-fast.</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
<span class="normal"><a href="#__codelineno-0-2">2</a></span>
<span class="normal"><a href="#__codelineno-0-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.loader</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_openapi</span>
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a>
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"openapi.yaml"</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>Behavior:</p>
<ul>
<li>Accepts <code>.json</code>, <code>.yaml</code>, <code>.yml</code> (parsed by extension).</li>
<li>Runs strict OpenAPI 3.x validation (<code>openapi-spec-validator</code>) at load time.</li>
<li>Raises <code>OpenAPISpecLoadError</code> on: missing file, unparseable content, or spec-validation failure.</li>
<li>Does <strong>not</strong> modify, coerce, or "fix" the spec — it's a read-only gate.</li>
</ul>
<p><strong>Env-var resolution</strong> lives at the <em>security</em> layer rather than the loader (see <a href="../05_security/">Security</a>).</p>
<hr />
<h2 id="3-app-the-application-bootstrap">🧬 3. <code>app</code> — The Application Bootstrap</h2>
<p><code>OpenAPIFirstApp</code> (in <code>openapi_first/app.py</code>) is a FastAPI subclass that replaces manual route registration with OpenAPI-driven binding.</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
<span class="normal"><a href="#__codelineno-1-2">2</a></span>
<span class="normal"><a href="#__codelineno-1-3">3</a></span>
<span class="normal"><a href="#__codelineno-1-4">4</a></span>
<span class="normal"><a href="#__codelineno-1-5">5</a></span>
<span class="normal"><a href="#__codelineno-1-6">6</a></span>
<span class="normal"><a href="#__codelineno-1-7">7</a></span>
<span class="normal"><a href="#__codelineno-1-8">8</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.app</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAPIFirstApp</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="kn">import</span><span class="w"> </span><span class="nn">routes</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a><span class="n">app</span> <span class="o">=</span> <span class="n">OpenAPIFirstApp</span><span class="p">(</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">"openapi.yaml"</span><span class="p">,</span>
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6"></a> <span class="n">routes_module</span><span class="o">=</span><span class="n">routes</span><span class="p">,</span>
</span><span id="__span-1-7"><a id="__codelineno-1-7" name="__codelineno-1-7"></a> <span class="n">title</span><span class="o">=</span><span class="s2">"My Service"</span><span class="p">,</span>
</span><span id="__span-1-8"><a id="__codelineno-1-8" name="__codelineno-1-8"></a><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p><strong>Startup pipeline (fail-fast, in this order):</strong></p>
<ol>
<li><strong>Load</strong> the spec (<code>.yaml</code>/<code>.json</code>).</li>
<li><strong>Validate</strong> it against OpenAPI 3.x schema.</li>
<li><strong>Parse</strong> <code>securitySchemes</code> and per-operation <code>security</code>.</li>
<li><strong>Build</strong> per-route security dependencies.</li>
<li><strong>Bind</strong> every path/method → handler by <code>operationId</code>; a missing handler, a missing <code>operationId</code> on a declared operation, or an unbound operation raises at startup.</li>
</ol>
<p><strong>Guarantees:</strong></p>
<ul>
<li>Every route has a spec declaration (no undocumented routes).</li>
<li>Every spec operation has a handler (no unhandled operations).</li>
<li>Auth enforcement is spec-driven, not hand-wired.</li>
<li><code>/openapi.json</code> + Swagger UI always reflect the provided spec.</li>
</ul>
<p>Keyword arguments beyond <code>openapi_path</code> / <code>routes_module</code> pass straight through to <code>fastapi.FastAPI</code> (it's a subclass — <code>title</code>, <code>version</code>, middleware, lifespan, … all work).</p>
<hr />
<h2 id="4-binder-spec-route-binding">🔗 4. <code>binder</code> — Spec → Route Binding</h2>
<p><code>bind_routes(app, spec, routes_module, security_deps=None) -&gt; None</code> (in <code>openapi_first/binder.py</code>).</p>
<p>This is the heart of the OpenAPI-first guarantee. For each <code>path</code> + HTTP method in the spec:</p>
<ol>
<li>Reads the operation's <code>operationId</code>.</li>
<li>Looks up <code>routes_module.&lt;operationId&gt;</code> — a <strong>plain callable</strong>.</li>
<li>Registers a FastAPI <code>APIRoute</code> bound to that handler, injecting <code>Depends(...)</code> for any matching security requirements.</li>
</ol>
<p>Failures are explicit and early:</p>
<table>
<thead>
<tr>
<th>Condition</th>
<th>Raised</th>
</tr>
</thead>
<tbody>
<tr>
<td>No <code>operationId</code> on an operation</td>
<td><code>MissingOperationHandler</code></td>
</tr>
<tr>
<td><code>operationId</code> has no handler function</td>
<td><code>MissingOperationHandler</code></td>
</tr>
<tr>
<td>A path/method declared but unbound</td>
<td><code>MissingOperationHandler</code></td>
</tr>
</tbody>
</table>
<p>Handlers stay framework-agnostic: they're plain functions <code>(payload, id, response)</code> named after <code>operationId</code>s — no decorators, no routing metadata.</p>
<hr />
<h2 id="5-client-the-other-side-of-the-contract">📡 5. <code>client</code> — The Other Side of the Contract</h2>
<p><code>OpenAPIClient(spec, base_url=None, client=None)</code> (in <code>openapi_first/client.py</code>).</p>
<p>The same spec that builds the server builds its client — one callable per <code>operationId</code>, keyed by name:</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-2-1">1</a></span>
<span class="normal"><a href="#__codelineno-2-2">2</a></span>
<span class="normal"><a href="#__codelineno-2-3">3</a></span>
<span class="normal"><a href="#__codelineno-2-4">4</a></span>
<span class="normal"><a href="#__codelineno-2-5">5</a></span>
<span class="normal"><a href="#__codelineno-2-6">6</a></span>
<span class="normal"><a href="#__codelineno-2-7">7</a></span>
<span class="normal"><a href="#__codelineno-2-8">8</a></span>
<span class="normal"><a href="#__codelineno-2-9">9</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.loader</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_openapi</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.client</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAPIClient</span>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"openapi.yaml"</span><span class="p">)</span>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5"></a><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAPIClient</span><span class="p">(</span><span class="n">spec</span><span class="p">)</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6"></a>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_health</span><span class="p">()</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_user</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">"user_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">})</span>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">create_user</span><span class="p">(</span><span class="n">body</span><span class="o">=</span><span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"Ada"</span><span class="p">})</span>
</span></code></pre></div></td></tr></table></div>
<p>How operations become methods:</p>
<ul>
<li>Each <code>operationId</code> → a dynamically-built callable on the client.</li>
<li><strong>Path parameters</strong><code>path_params={"user_id": 1}</code>.</li>
<li><strong>Request body</strong><code>body={...}</code> (JSON) or raw content for non-JSON media types.</li>
<li><strong>Query/headers</strong><code>query=</code> / <code>headers=</code>.</li>
<li>Returns the raw <code>httpx.Response</code> (no implicit deserialization, no hidden schema inference).</li>
</ul>
<p>Client-construction guarantees (fail-fast, same philosophy as the app):</p>
<ul>
<li>Spec must declare at least one <code>servers</code> entry (<code>base_url</code> falls back to it).</li>
<li>Spec must have <code>paths</code>.</li>
<li>Every operation must have a <strong>unique</strong> <code>operationId</code>.</li>
<li>Required params must match the spec at call time.</li>
</ul>
<p>Construction errors raise <code>OpenAPIClientError</code>; request-time errors surface as <code>httpx</code> exceptions with <code>operationId</code> context.</p>
<hr />
<h2 id="6-errors-explicit-error-hierarchy">💥 6. <code>errors</code> — Explicit Error Hierarchy</h2>
<p>Everything raised by <code>openapi-first</code> derives from <code>OpenAPIFirstError</code> (in <code>openapi_first/errors.py</code>), so callers can handle first-party failures with a single <code>except</code>:</p>
<ul>
<li><code>OpenAPIFirstError</code> — base (comparable / aims to be picklable share).</li>
<li><code>OpenAPIClientError</code> — client-side contract violations.</li>
<li><code>MissingOperationHandler</code> — spec declares an operation whose handler is missing or unresolvable; carries <code>path</code>, <code>method</code>, and optional <code>operationId</code>.</li>
</ul>
<p>See <a href="../06_error_handling/">Error Handling</a> for the full table.</p>
<hr />
<h2 id="7-security-auth-from-the-spec">🛡️ 7. <code>security</code> — Auth from the Spec</h2>
<p><code>security.py</code> turns an OpenAPI <code>securitySchemes</code> section into FastAPI <code>Depends(...)</code> objects — <strong>no manual middleware</strong>.</p>
<ul>
<li><code>parse_security_schemes(spec) -&gt; dict[str, dict]</code> — collects schemes, resolving <code>{ENV_VAR}</code> placeholders.</li>
<li><code>make_security_dependencies(spec, security_schemes) -&gt; dict[str, list[Depends]]</code> — builds <code>METHOD:/path</code> → dependency list from per-operation <code>security</code>, falling back to top-level <code>security</code>.</li>
</ul>
<p>Supported scheme types (extensible):</p>
<ul>
<li><code>type: http, scheme: bearer</code><code>OpenAPIFirstSecurityDependency(HTTPBearer)</code>. With <code>x-introspect-path</code>/<code>x-server-url</code>, it introspects the JWT against an auth service; optionally sets <code>request.state.user</code>. Without introspection, it validates token presence and stores it on <code>request.state.token</code>.</li>
<li>API-key style schemes via the same resolution path.</li>
</ul>
<p>Key properties: resolution is <strong>per-operation</strong>, env placeholders resolve once at startup, and dependencies are injected by <code>binder</code> — your handlers never mention security.</p>
<hr />
<h2 id="8-codegen-cli-build-time-tooling">🏭 8. <code>codegen</code> &amp; <code>cli</code> — Build-Time Tooling</h2>
<p>Generation is strictly <strong>build-time</strong> — spec → code, run once by a developer, committed:</p>
<h3 id="openapi-first-models-spec-o-file"><code>openapi-first models &lt;spec&gt; -o &lt;file&gt;</code></h3>
<p>Generates Pydantic models from spec schemas (wraps <code>datamodel_code_generator</code>):</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-3-1">1</a></span>
<span class="normal"><a href="#__codelineno-3-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1"></a><span class="c1"># cli path</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2"></a><span class="n">openapi</span><span class="o">-</span><span class="n">first</span> <span class="n">models</span> <span class="n">openapi</span><span class="o">.</span><span class="n">yaml</span> <span class="o">-</span><span class="n">o</span> <span class="n">app</span><span class="o">/</span><span class="n">models</span><span class="o">.</span><span class="n">py</span>
</span></code></pre></div></td></tr></table></div>
<h3 id="openapi-first-routes-spec-o-dir-use-models-models-module-models"><code>openapi-first routes &lt;spec&gt; -o &lt;dir&gt; [--use-models] [--models-module models]</code></h3>
<p>Generates one <code>routes_&lt;resource&gt;.py</code> stub file per resource, with <code>NotImplementedError</code> handlers bound to <code>operationId</code>s (optionally importing your generated models):</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-4-1">1</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1"></a><span class="n">openapi</span><span class="o">-</span><span class="n">first</span> <span class="n">routes</span> <span class="n">openapi</span><span class="o">.</span><span class="n">yaml</span> <span class="o">-</span><span class="n">o</span> <span class="n">app</span><span class="o">/</span><span class="n">routes</span> <span class="o">--</span><span class="n">use</span><span class="o">-</span><span class="n">models</span>
</span></code></pre></div></td></tr></table></div>
<h3 id="openapi-first-scaffold-template-path"><code>openapi-first scaffold &lt;template&gt; [path]</code></h3>
<p>Copies a bundled application template verbatim (see <a href="../03_use_cases/02_templates/">Templates</a>).</p>
<hr />
<h2 id="9-templates-copyable-applications">🧱 9. <code>templates</code> — Copyable Applications</h2>
<p>Four bundled, runnable applications live under <code>openapi_first/templates/</code>: <code>health_app</code>, <code>crud_app</code>, <code>model_app</code>, and <code>vet_app</code>. They are <strong>not</strong> part of the library API — no lint/format/type gates, never imported at runtime. They exist to be copied via:</p>
<div class="language-bash highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-5-1">1</a></span>
<span class="normal"><a href="#__codelineno-5-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1"></a>openapi-first<span class="w"> </span>scaffold<span class="w"> </span>&lt;template&gt;<span class="w"> </span><span class="o">[</span>target-dir<span class="o">]</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2"></a>openapi-first<span class="w"> </span>scaffold<span class="w"> </span>--list
</span></code></pre></div></td></tr></table></div>
<hr />
<h2 id="related">🔗 Related</h2>
<ul>
<li><a href="../01_overview/">01 Overview</a> · <a href="../03_use_cases/02_templates/">02 Templates</a> · <a href="../05_security/">03 Security</a> · <a href="../04_design/">04 Design</a></li>
</ul>
</article>
</div>
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"base": "..", "features": ["navigation.sections", "navigation.expand", "navigation.top", "navigation.instant", "navigation.tracking", "navigation.indexes", "content.code.copy", "content.code.annotate", "content.tabs.link", "content.action.edit", "search.highlight", "search.share", "search.suggest", "navigation.tabs", "toc.integrate", "header.autohide", "announce.dismiss", "footer.social", "content.code.select", "content.code.line_numbers", "content.tooltips"], "search": "../assets/javascripts/workers/search.973d3a69.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../assets/javascripts/bundle.f55a23d4.min.js"></script>
</body>
</html>