<?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>Forem: Pete Letkeman</title>
    <description>The latest articles on Forem by Pete Letkeman (@pbaletkeman).</description>
    <link>https://forem.com/pbaletkeman</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1234322%2Fee63d7e8-0339-4890-8e4b-ca26dfa04662.png</url>
      <title>Forem: Pete Letkeman</title>
      <link>https://forem.com/pbaletkeman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/pbaletkeman"/>
    <language>en</language>
    <item>
      <title>AI-Powered Resume Generator: Architecture &amp; Implementation</title>
      <dc:creator>Pete Letkeman</dc:creator>
      <pubDate>Sat, 24 Jan 2026 01:18:09 +0000</pubDate>
      <link>https://forem.com/pbaletkeman/ai-powered-resume-generator-architecture-implementation-2748</link>
      <guid>https://forem.com/pbaletkeman/ai-powered-resume-generator-architecture-implementation-2748</guid>
      <description>&lt;h1&gt;
  
  
  Building an AI-Powered Resume Generator: Architecture &amp;amp; Implementation
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;I've been working on a full-stack application that leverages LLMs to generate polished, professional resume content. This post is a technical walkthrough of the architecture, integration points, and key implementation details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Java 21, Spring Boot 3.x, Gradle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React 19, TypeScript, Vite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM Integration:&lt;/strong&gt; OpenAI API / Ollama (OpenAI-compatible endpoint)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Format:&lt;/strong&gt; JSON-driven resume model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Tooling:&lt;/strong&gt; Gradle (backend), Node (frontend)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/pbaletkeman/java-resumes" rel="noopener noreferrer"&gt;https://github.com/pbaletkeman/java-resumes&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐
│   React UI  │
│ (TypeScript)│
└──────┬──────┘
       │ HTTP/REST
       ↓
┌─────────────────────────────────┐
│   Spring Boot REST API          │
│  (Java 21, Gradle 8.10)         │
│                                 │
│  ├─ ResumeController            │
│  ├─ FilesStorageService         │
│  └─ ApiService (LLM gateway)    │
└──────────┬──────────────────────┘
           │
      ┌────┴─────┐
      ↓          ↓
  ┌────────┐  ┌────────────┐
  │ Ollama │  │ OpenAI API │
  │(local) │  │  (cloud)   │
  └────────┘  └────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Components &amp;amp; Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;REST API Layer (Spring Boot)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The backend exposes endpoints for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File uploads&lt;/strong&gt; (multipart/form-data)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume optimization&lt;/strong&gt; (async background processing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File retrieval&lt;/strong&gt; (results polling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File management&lt;/strong&gt; (list, download, delete)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Endpoint Pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/api/upload"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResponseMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;optimizeResume&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"optimize"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;optimizeJson&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resume"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;MultipartFile&lt;/span&gt; &lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"job"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;MultipartFile&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Validate inputs&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BAD_REQUEST&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResponseMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No file/invalid file provided"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Spawn background thread for LLM processing (non-blocking)&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BackgroundResume&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Return 202 Accepted immediately&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ACCEPTED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResponseMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"generating"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this pattern?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM API calls are slow (2-30+ seconds)&lt;/li&gt;
&lt;li&gt;HTTP connections timeout if we wait for LLM&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;202 Accepted&lt;/code&gt; signals async processing to the client&lt;/li&gt;
&lt;li&gt;Frontend polls &lt;code&gt;/api/files&lt;/code&gt; until results appear&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Async Background Processing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;BackgroundResume&lt;/code&gt; class handles long-running operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BackgroundResume&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Runnable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Optimize&lt;/span&gt; &lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// 1. Load LLM configuration&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;configStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Utility&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readFileAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"config.json"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;Config&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gson&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configStr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// 2. Build LLM request&lt;/span&gt;
            &lt;span class="nc"&gt;ChatBody&lt;/span&gt; &lt;span class="n"&gt;chatBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createChatBody&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// 3. Call LLM (OpenAI-compatible API)&lt;/span&gt;
            &lt;span class="nc"&gt;LLMResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;produceFiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEndpoint&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getApikey&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getModel&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// 4. Save results (Markdown + PDF)&lt;/span&gt;
            &lt;span class="nc"&gt;FilesStorageService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

            &lt;span class="no"&gt;LOGGER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Resume optimization completed"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="no"&gt;LOGGER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Background task failed: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why background threads instead of async/await?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple, synchronous model&lt;/li&gt;
&lt;li&gt;No need for reactive framework overhead&lt;/li&gt;
&lt;li&gt;Easy to reason about error handling&lt;/li&gt;
&lt;li&gt;Works well for moderate concurrency&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;LLM Integration (OpenAI-Compatible API)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ApiService&lt;/code&gt; class abstracts LLM provider differences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;LLMResponse&lt;/span&gt; &lt;span class="nf"&gt;produceFiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Optimize&lt;/span&gt; &lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Build OpenAI-compatible request&lt;/span&gt;
        &lt;span class="nc"&gt;ChatBody&lt;/span&gt; &lt;span class="n"&gt;chatBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChatBody&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;chatBody&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;chatBody&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMessages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createPrompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;chatBody&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTemperature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTemperature&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="c1"&gt;// Send to LLM&lt;/span&gt;
        &lt;span class="nc"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newHttpClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jsonRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gson&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatBody&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/v1/chat/completions"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BodyPublishers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonRequest&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BodyHandlers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofString&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Parse response&lt;/span&gt;
        &lt;span class="nc"&gt;LLMResponse&lt;/span&gt; &lt;span class="n"&gt;llmResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gson&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;LLMResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;llmResponse&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why OpenAI-compatible format?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works with Ollama (local models)&lt;/li&gt;
&lt;li&gt;Works with OpenAI (cloud models)&lt;/li&gt;
&lt;li&gt;Works with Azure OpenAI, Together.ai, etc.&lt;/li&gt;
&lt;li&gt;Single integration code path&lt;/li&gt;
&lt;li&gt;Easy to swap providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration (config.json):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apikey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ollama"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mistral:7b"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Frontend Architecture (React + TypeScript)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Hook: &lt;code&gt;useApi&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Centralized API communication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&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;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  File Upload &amp;amp; Polling Pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MainContentTab&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;generatedFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGeneratedFiles&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&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;await&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 1. Upload resume + job description&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadForOptimization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// 2. Start polling for results&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 5 minutes max&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Poll every 5s&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uploadTime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setGeneratedFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newFiles&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;++&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="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;// UI for upload and display results&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;strong&gt;Why polling instead of WebSockets?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simpler client/server contract&lt;/li&gt;
&lt;li&gt;Works through corporate proxies/firewalls&lt;/li&gt;
&lt;li&gt;No need for persistent connection&lt;/li&gt;
&lt;li&gt;Acceptable for batch processing workflows&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Data Model: Optimize DTO
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Optimize&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;promptType&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// ["Resume", "CoverLetter", "Skills"]&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// 0.0-1.0 (creativity level)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// Model identifier from config&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// Target company name&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jobTitle&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Target job title&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jobDescription&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Full job posting text&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// User's current resume&lt;/span&gt;

    &lt;span class="c1"&gt;// Getters/setters...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This DTO drives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prompt construction&lt;/strong&gt; - What content to generate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM parameters&lt;/strong&gt; - Temperature, model selection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output filtering&lt;/strong&gt; - Which sections to include&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Key Implementation Challenges &amp;amp; Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: LLM Response Time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; API calls can take 10-30+ seconds&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Return &lt;code&gt;202 Accepted&lt;/code&gt; immediately&lt;/li&gt;
&lt;li&gt;Process async in background thread&lt;/li&gt;
&lt;li&gt;Frontend polls for completion&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Challenge 2: File Format Conversion
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; LLM outputs plain text; need PDF with formatting&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert Markdown → HTML (CommonMark parser)&lt;/li&gt;
&lt;li&gt;Convert HTML → PDF (Flying Saucer library)&lt;/li&gt;
&lt;li&gt;Save both Markdown + PDF for flexibility&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Challenge 3: Local vs Cloud LLM
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Different APIs for Ollama vs OpenAI&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use OpenAI-compatible format (both support it)&lt;/li&gt;
&lt;li&gt;Config-driven endpoint selection&lt;/li&gt;
&lt;li&gt;Single integration point&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Challenge 4: Test Isolation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Tests failing due to state dependencies (file existence)&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@BeforeEach&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;uploadsPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Paths&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uploads"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDirectories&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploadsPath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Create dummy files for delete tests, etc.&lt;/span&gt;
    &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploadsPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resume.pdf"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"dummy"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deployment Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Local Development
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1: Start Ollama&lt;/span&gt;
ollama serve
ollama pull mistral:7b

&lt;span class="c"&gt;# Terminal 2: Run backend&lt;/span&gt;
./gradlew bootRun  &lt;span class="c"&gt;# Listens on :8080&lt;/span&gt;

&lt;span class="c"&gt;# Terminal 3: Run frontend&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev  &lt;span class="c"&gt;# Listens on :5173&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cloud Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# application.properties
&lt;/span&gt;&lt;span class="py"&gt;server.port&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;upload.path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/data/uploads&lt;/span&gt;
&lt;span class="c"&gt;# Spring will detect OpenAI config from environment
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;80%+ Coverage Target:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Controller Tests&lt;/strong&gt; - HTTP layer with MockMvc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Tests&lt;/strong&gt; - Business logic, mocked LLM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration Tests&lt;/strong&gt; - Full request flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Tests&lt;/strong&gt; - DTO serialization/validation
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew &lt;span class="nb"&gt;test&lt;/span&gt;                    &lt;span class="c"&gt;# Run all tests&lt;/span&gt;
./gradlew &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--tests&lt;/span&gt; ClassName  &lt;span class="c"&gt;# Run specific test&lt;/span&gt;
./gradlew checkstyleMain          &lt;span class="c"&gt;# Code quality (100% compliance)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance &amp;amp; Scalability Notes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Scaling:&lt;/strong&gt; Add more backend instances behind load balancer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate Limiting:&lt;/strong&gt; Implement per-user quotas for LLM API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching:&lt;/strong&gt; Cache LLM responses for identical inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async Queue:&lt;/strong&gt; For high volume, use message queue (RabbitMQ, Kafka)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Storage:&lt;/strong&gt; Consider cloud storage (S3, Azure Blob) vs local filesystem&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  ⚠️ Important Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LLM Hallucination Risk
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; LLMs can generate plausible-sounding but inaccurate content. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fabricated job experiences&lt;/li&gt;
&lt;li&gt;Incorrect technical skills&lt;/li&gt;
&lt;li&gt;Made-up company names or achievements&lt;/li&gt;
&lt;li&gt;Dates and timelines that don't align with reality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always proofread generated content&lt;/strong&gt; before using it&lt;/li&gt;
&lt;li&gt;Cross-check facts against source documents&lt;/li&gt;
&lt;li&gt;Verify all claims in the resume&lt;/li&gt;
&lt;li&gt;Consider this tool as a &lt;strong&gt;content enhancement tool&lt;/strong&gt;, not a replacement for human review&lt;/li&gt;
&lt;li&gt;Use it to refine and polish verified information, not to generate unverified content&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Processing Time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; File generation is NOT instant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local models (Ollama):&lt;/strong&gt; 30 seconds to 5+ minutes depending on model size (7B models are faster, 13B+ models take longer)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud models (OpenAI):&lt;/strong&gt; 5-30 seconds typically, but can vary with load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large job descriptions:&lt;/strong&gt; Processing time increases with input size&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network latency:&lt;/strong&gt; Slower connections add to total time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Frontend Polling:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Default: polls every 5 seconds for up to 5 minutes (60 attempts)&lt;/span&gt;
&lt;span class="c1"&gt;// For longer processing, increase attempts or polling interval&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Adjust this for longer waits&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 5 seconds&lt;/span&gt;
  &lt;span class="c1"&gt;// ... check for files&lt;/span&gt;
  &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="o"&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;strong&gt;User Experience:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display a progress indicator during processing&lt;/li&gt;
&lt;li&gt;Show estimated wait time based on model selection&lt;/li&gt;
&lt;li&gt;Allow users to check back later via job ID&lt;/li&gt;
&lt;li&gt;Consider implementing email notifications when complete&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Code Quality Standards
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checkstyle:&lt;/strong&gt; 100% compliance (120 char line limit)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Coverage:&lt;/strong&gt; 80%+ target&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java Version:&lt;/strong&gt; Java 21 LTS with modern features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Boot:&lt;/strong&gt; Version 3.5.1 with latest practices&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Potential improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] WebSocket support for real-time updates&lt;/li&gt;
&lt;li&gt;[ ] Template system for different resume formats&lt;/li&gt;
&lt;li&gt;[ ] Batch processing for multiple candidates&lt;/li&gt;
&lt;li&gt;[ ] Integration with LinkedIn/job boards&lt;/li&gt;
&lt;li&gt;[ ] A/B testing for LLM prompt optimization&lt;/li&gt;
&lt;li&gt;[ ] Cost analytics for OpenAI usage&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Async by Default&lt;/strong&gt; - HTTP endpoints should never block on slow operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embrace Standards&lt;/strong&gt; - OpenAI-compatible API is a superpower&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple Patterns &amp;gt; Complex Frameworks&lt;/strong&gt; - Background threads work great for this use case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Independence&lt;/strong&gt; - Always set up required state in @BeforeEach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config Over Code&lt;/strong&gt; - Keep LLM provider flexible via configuration&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/pbaletkeman/java-resumes" rel="noopener noreferrer"&gt;https://github.com/pbaletkeman/java-resumes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Start:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/pbaletkeman/java-resumes
&lt;span class="nb"&gt;cd &lt;/span&gt;java-resumes
./gradlew clean build
./gradlew bootRun
&lt;span class="c"&gt;# Visit http://localhost:8080/spotlight/index.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;Special thanks to &lt;strong&gt;Shaw Talebi&lt;/strong&gt; for his excellent &lt;a href="https://www.youtube.com/watch?v=R5WXaxmb6m4" rel="noopener noreferrer"&gt;tutorial on building resume optimization tools&lt;/a&gt;, which served as the inspiration and starter foundation for this project.&lt;/p&gt;




&lt;p&gt;Have you built LLM integrations in Java? What patterns did you use? Drop a comment!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discussion Topics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async patterns for LLM integrations&lt;/li&gt;
&lt;li&gt;Local vs cloud LLM trade-offs&lt;/li&gt;
&lt;li&gt;Resume optimization strategies&lt;/li&gt;
&lt;li&gt;Full-stack Java + React workflows&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>ollama</category>
      <category>springboot</category>
      <category>llm</category>
    </item>
    <item>
      <title>Secure Python Litestar Site With OpenID/OIDC Using Okta Hosted Login</title>
      <dc:creator>Pete Letkeman</dc:creator>
      <pubDate>Fri, 16 Feb 2024 22:09:18 +0000</pubDate>
      <link>https://forem.com/pbaletkeman/secure-python-litestar-site-with-openidoidc-using-okta-hosted-login-38nf</link>
      <guid>https://forem.com/pbaletkeman/secure-python-litestar-site-with-openidoidc-using-okta-hosted-login-38nf</guid>
      <description>&lt;p&gt;Here is a tutorial on how to setup &lt;a href="https://www.okta.com"&gt;OKTA&lt;/a&gt; as a OpenID/OIDC security provider for a site programmed in Python 3.11.x using &lt;a href="https://www.litestar.dev"&gt;Litestar&lt;/a&gt;.&lt;br&gt;
This is done with 'Authorization Code flow'&lt;/p&gt;
&lt;h4&gt;
  
  
  About the Authorization Code grant
&lt;/h4&gt;

&lt;p&gt;The Authorization Code flow is the recommended method for controlling access to web applications capable of securely storing secrets, see &lt;a href="https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/"&gt;https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  OKTA Setup
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;
After you have created your OKTA account open `Applications` =&amp;gt; `Applications` as seen on the right.
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t2oqsbt35lsghlo293ya.png"&gt;
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2oqsbt35lsghlo293ya.png" width="308" height="630"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Step 2
&lt;/h3&gt;

&lt;p&gt;Click &lt;code&gt;Create App Integration&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvzikq1c8p66ntdl8xp7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvzikq1c8p66ntdl8xp7.png" alt="Image description" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3
&lt;/h3&gt;

&lt;p&gt;Choose &lt;code&gt;OIDC&lt;/code&gt; - Open Connect with a description of&lt;br&gt;
'Token-based OAuth 2.0 authentication for Single Sign-On (SSO) through API endpoints. Recommended if you intend to build a custom app integration with the Okta Sign-In Widget.'&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7ire96qy769y2tc4o8z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7ire96qy769y2tc4o8z.png" alt="Image description" width="800" height="703"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then choose &lt;code&gt;Web Application&lt;/code&gt; with a description of &lt;br&gt;
'Server-side applications where authentication and tokens are handled on the server (for example, Go, Java, ASP.Net, Node.js, PHP)'&lt;/p&gt;

&lt;p&gt;And Click &lt;code&gt;Next&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4
&lt;/h3&gt;

&lt;p&gt;Checked the following (&lt;em&gt;may not matter&lt;/em&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client Credentials&lt;/li&gt;
&lt;li&gt;Refresh Token&lt;/li&gt;
&lt;li&gt;Implicit (hybrid)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6czzrrbq7lk411l3643i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6czzrrbq7lk411l3643i.png" alt="Image description" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5
&lt;/h3&gt;

&lt;p&gt;Ensure that both &lt;code&gt;Sign-in redirect URIs&lt;/code&gt; and &lt;code&gt;Sign-out redirect URIs (Optional) have the correct values&lt;/code&gt;. By default Litestar brings up the web site on port 8000.&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnl1eaa565ocxxtwytxmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnl1eaa565ocxxtwytxmu.png" alt="Image description" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;Assignments&lt;/code&gt; select &lt;code&gt;Allow everyone in your organization to access&lt;/code&gt;.&lt;br&gt;
In &lt;code&gt;Enable immediate access (Recommended)&lt;/code&gt; select &lt;code&gt;Enable immediate access with *Federation Broker Model*&lt;/code&gt;&lt;br&gt;
Click &lt;code&gt;Save&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pqduqiox4v6sb5hp8g6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pqduqiox4v6sb5hp8g6.png" alt="Image description" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 7
&lt;/h3&gt;

&lt;p&gt;Create a text file named '&lt;strong&gt;.env&lt;/strong&gt;' and from the &lt;code&gt;General&lt;/code&gt; tab, copy both &lt;code&gt;Client ID&lt;/code&gt; and &lt;code&gt;Client Secret&lt;/code&gt; to the file to have something like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
OKTA_DOMAIN=dev-123456789.okta.com
REDIRECT_URL=http://localhost:8000/authorization-code/callback
OKTA_PROMPT=consent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And provide the you value for &lt;code&gt;OKTA_DOMAIN&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
You do not to supply a value for &lt;code&gt;OKTA_PROMPT&lt;/code&gt; or even include it if you choose not to. For more details about regarding this parameter see &lt;a href="https://developer.okta.com/docs/reference/api/oidc/#parameter-details"&gt;https://developer.okta.com/docs/reference/api/oidc/#parameter-details&lt;/a&gt;.&lt;br&gt;
Using OKTA_PROMPT is very helpful when testing multiple user accounts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Python Code
&lt;/h2&gt;

&lt;p&gt;Github.com repository found here &lt;a href="https://github.com/pbaletkeman/litestarOpenID"&gt;https://github.com/pbaletkeman/litestarOpenID&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy the .env file from the previous step to the root directory of your Python project.&lt;/p&gt;

&lt;p&gt;For the most part the code is well documented, however here are some notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It uses Mako templates, &lt;a href="https://www.makotemplates.org/"&gt;https://www.makotemplates.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;/profile is not secured, and is used to show you JWT Bearer Token which is needed to access /read-items and other secure routes, this is set in the callback method&lt;/li&gt;
&lt;li&gt;More user properties are found in the &lt;code&gt;user_response&lt;/code&gt; object located in the callback method&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Additionality PKCE
&lt;/h2&gt;

&lt;p&gt;You can use Authorization Code Grant with Proof Key for Code Exchange (PKCE) which you can read about here:&lt;br&gt;
&lt;a href="https://developer.okta.com/docs/guides/implement-grant-type/authcodepkce/main/#about-the-authorization-code-grant-with-pkce"&gt;https://developer.okta.com/docs/guides/implement-grant-type/authcodepkce/main/#about-the-authorization-code-grant-with-pkce&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To enforce PKCE you will have to select 'Require PKCE as additional verification' as seen in the image below.&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4o3728k2cl5hdxh69mu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4o3728k2cl5hdxh69mu.png" alt="Image description" width="753" height="403"&gt;&lt;/a&gt;&lt;br&gt;
You can still use PKCE without requiring it.&lt;/p&gt;

&lt;p&gt;Here is the Python code for this,&lt;br&gt;
&lt;a href="https://github.com/pbaletkeman/okta-litestar-oidc-pkce"&gt;https://github.com/pbaletkeman/okta-litestar-oidc-pkce&lt;/a&gt;&lt;br&gt;
Which is a lot like the none PKCE code.&lt;br&gt;
Note that the following have been added/changed:&lt;/p&gt;

&lt;p&gt;lines: 181, 182, 192, 193&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'code_challenge': code_challenge,
'code_challenge_method': 'S256',
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;line: 231&lt;br&gt;
&lt;code&gt;query_params = {'grant_type': 'authorization_code', 'code': code,&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;line: 185, 197 &lt;br&gt;
And the &lt;code&gt;response_mode&lt;/code&gt; has been changed from 'query' to 'form_post' which tells OKTA to send the data as a form post instead of using the query string. Query string is still valid and can be used if you choose to.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>Secure Litestar APIs with OKTA</title>
      <dc:creator>Pete Letkeman</dc:creator>
      <pubDate>Sat, 10 Feb 2024 20:08:09 +0000</pubDate>
      <link>https://forem.com/pbaletkeman/secure-litestar-apis-with-okta-570l</link>
      <guid>https://forem.com/pbaletkeman/secure-litestar-apis-with-okta-570l</guid>
      <description>&lt;p&gt;I borrowed a lot from &lt;a href="https://developer.okta.com/blog/2020/12/17/build-and-secure-an-api-in-python-with-fastapi"&gt;https://developer.okta.com/blog/2020/12/17/build-and-secure-an-api-in-python-with-fastapi&lt;/a&gt; thanks to &lt;a href="https://developer.okta.com/blog/authors/karl-hughes/"&gt;Karl Hughes&lt;/a&gt; for this article.&lt;/p&gt;

&lt;p&gt;Litestar framework for Python 3.x creating APIs.&lt;br&gt;
For more information about Litestar visit &lt;a href="https://litestar.dev/"&gt;https://litestar.dev/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  OKTA Setup
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;p&gt;After creating a developer account with okta click 'Applications' then 'Create App Integration' as shown in the image&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5n3pswjhkulldplf9f5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5n3pswjhkulldplf9f5t.png" alt="Image description" width="790" height="334"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2
&lt;/h3&gt;

&lt;p&gt;Choose 'API Services' with a description of&lt;br&gt;
'Interact with Okta APIs using the scoped OAuth 2.0 access tokens for machine-to-machine authentication.'&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvchz84nydn97hs2qcqbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvchz84nydn97hs2qcqbt.png" alt="Image description" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3
&lt;/h3&gt;

&lt;p&gt;Give the integration a name&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu47f1u2gbkf5g3w55e5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu47f1u2gbkf5g3w55e5i.png" alt="Image description" width="800" height="233"&gt;&lt;/a&gt;Result Screen&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjdcun93vr6utbvggl65.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjdcun93vr6utbvggl65.png" alt="Image description" width="765" height="632"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4
&lt;/h3&gt;

&lt;p&gt;In the directory for the Python project create a file named .env with the following:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OKTA_CLIENT_ID=Value From The Previous Step&lt;br&gt;
OKTA_CLIENT_SECRET=Value From The Previous Step&lt;br&gt;
OKTA_ISSUER="{oktaDomain}/oauth2/default"&lt;br&gt;
OKTA_TOKEN="{oktaDomain}/oauth2/default/v1/token"&lt;br&gt;
OKTA_INTROSPECTION="{oktaDomain}/oauth2/default/v1/introspect"&lt;br&gt;
OKTA_AUDIENCE="api://default"&lt;br&gt;
OKTA_SCOPE="items"&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5
&lt;/h3&gt;

&lt;p&gt;Now go to 'Security' -&amp;gt; 'API' shown below:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fts27wqi0ndy1r9nytsau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fts27wqi0ndy1r9nytsau.png" alt="Image description" width="800" height="529"&gt;&lt;/a&gt;&lt;br&gt;
Note that values for Audience and Issuer URI are in .env file.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6
&lt;/h3&gt;

&lt;p&gt;Click on the edit icon you should see something like this:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh66p8dilmrnj0f12y94u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh66p8dilmrnj0f12y94u.png" alt="Image description" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 7
&lt;/h3&gt;

&lt;p&gt;Try the 'Metadata URI' in a new tab in you web browser to see something like:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zzqizbwsayqfcwdjd50.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zzqizbwsayqfcwdjd50.png" alt="Image description" width="746" height="349"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 8
&lt;/h3&gt;

&lt;p&gt;Click 'Scopes' -&amp;gt; 'Add Scope'&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flr2kmr2njvxrx1sc3rgz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flr2kmr2njvxrx1sc3rgz.png" alt="Image description" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 9
&lt;/h3&gt;

&lt;p&gt;Add a scope such as what is shown below:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofusyayh5tdaunblyt42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofusyayh5tdaunblyt42.png" alt="Image description" width="728" height="742"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 10
&lt;/h3&gt;

&lt;p&gt;Complete the .env file with the correct information including replacing &lt;code&gt;{oktaDomain}&lt;/code&gt; with your okta domain and having the correct value for &lt;code&gt;OKTA_SCOPE="items"&lt;/code&gt;, if you changed yours that is and note that OKTA_SCOPE is important for remote validation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Python Code
&lt;/h2&gt;

&lt;p&gt;You can find the final code for this here &lt;a href="https://github.com/pbaletkeman/litestarOKTA"&gt;https://github.com/pbaletkeman/litestarOKTA&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Large segments of this code came from &lt;a href="https://docs.litestar.dev/latest/usage/security/jwt.html"&gt;https://docs.litestar.dev/latest/usage/security/jwt.html&lt;/a&gt;.&lt;br&gt;
To learn more about Litestar security go here &lt;a href="https://docs.litestar.dev/latest/usage/security/index.html"&gt;https://docs.litestar.dev/latest/usage/security/index.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Download the files to your local system and copy them to your Python project directory.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 11
&lt;/h3&gt;

&lt;p&gt;Install the required Python libraries using&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Note, that this includes all of litestar[full] which may be more than you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 12
&lt;/h3&gt;

&lt;p&gt;Libraries of note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;base64&lt;/code&gt; is used to encode your &lt;code&gt;client id&lt;/code&gt; and &lt;code&gt;client secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;httpx&lt;/code&gt; is used to connect to the OKTA server and return the JWT token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;okta_jwt.jwt&lt;/code&gt; is used to validate the JWT token locally, which is quicker, but less secure than JWT token validation on the OKTA server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;starlette.config&lt;/code&gt; is used to import the .env file into your application, you may want to use &lt;code&gt;python-dotenv&lt;/code&gt; instead&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 13
&lt;/h3&gt;

&lt;p&gt;In this sample project we only ever will have one user/account which means that we can do this&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;API_USER_DB: dict[str, OAuthSchema] = {}&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
 but if you have more than one account you should rethink how your database of users is stored.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 14
&lt;/h3&gt;

&lt;p&gt;This project includes a method for local JWT token validation, but it is not used, the remote JWT token validate is used instead.&lt;br&gt;
Remote validation is more secure, takes longer and increases system resource usage as this makes a call to the &lt;a href="https://developer.okta.com/docs/reference/api/oidc/#introspect"&gt;Introspection&lt;/a&gt; endpoint.&lt;br&gt;
The local validation method is &lt;code&gt;def validate(token: str) -&amp;gt; bool:&lt;/code&gt; and the remote validation method is &lt;code&gt;def validate_remote(token: str) -&amp;gt; bool:&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;
&lt;h4&gt;
  
  
  Note 1
&lt;/h4&gt;

&lt;p&gt;When running this project you should have the following endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/login

&lt;ul&gt;
&lt;li&gt;uses Authorization header to login&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;/form-login

&lt;ul&gt;
&lt;li&gt;uses data from HTML Form to login&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;/json-login

&lt;ul&gt;
&lt;li&gt;uses data from JSON body to login&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;as shown below:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgujinm6u6o9mx95a8vju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgujinm6u6o9mx95a8vju.png" alt="Image description" width="496" height="297"&gt;&lt;/a&gt;&lt;br&gt;
Each endpoint has it's purpose and it's up to you to choose the appropriate one.&lt;/p&gt;
&lt;h4&gt;
  
  
  Note 2
&lt;/h4&gt;

&lt;p&gt;In &lt;code&gt;json_login_handler&lt;/code&gt; and &lt;code&gt;form_login_handler&lt;/code&gt; there is some funky magic happening here:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;auth_header = 'Basic ' + str(base64.b64encode((data.client_id + ':' + data.client_secret).encode('ascii')))[2:-1]&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;To get the token we must send a authorization header of 'basic' a base 64 encoded version of the username:password. However Python prepends the letter "b" and then surrounds the value with a single quote which is why we are using [2:-1]&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>api</category>
      <category>security</category>
    </item>
  </channel>
</rss>
