<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Code Craft by Hermann Kao | Fullstack Development Learning Journey & Tutorials]]></title><description><![CDATA[Following my journey through web and mobile development—sharing insights, challenges, and lessons learned as I build with React, SwiftUI, and more. Learning in public, one commit at a time.]]></description><link>https://itishermann.me</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1741871737384/29592155-3216-4be1-b496-9baf69ebf5a5.png</url><title>Code Craft by Hermann Kao | Fullstack Development Learning Journey &amp; Tutorials</title><link>https://itishermann.me</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 08 May 2026 14:44:04 GMT</lastBuildDate><atom:link href="https://itishermann.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Missing Piece: Why Claude Code Needs Serena's LSP Intelligence]]></title><description><![CDATA[The Problem Every Developer Faces with AI Coding Assistants
You've probably experienced this frustration: you ask Claude, Cursor, or any AI coding assistant to write a function, and it confidently generates code that looks perfect at first glance. Bu...]]></description><link>https://itishermann.me/the-missing-piece-why-claude-code-needs-serenas-lsp-intelligence</link><guid isPermaLink="true">https://itishermann.me/the-missing-piece-why-claude-code-needs-serenas-lsp-intelligence</guid><category><![CDATA[pythagorismes]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[lsp]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[llm]]></category><category><![CDATA[mcp]]></category><category><![CDATA[DX]]></category><category><![CDATA[codegeneration]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Mon, 06 Oct 2025 05:48:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/zghHYBLkyQQ/upload/1d08e31090366dee05d4eab09261385b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-problem-every-developer-faces-with-ai-coding-assistants">The Problem Every Developer Faces with AI Coding Assistants</h2>
<p>You've probably experienced this frustration: you ask Claude, Cursor, or any AI coding assistant to write a function, and it confidently generates code that looks perfect at first glance. But when you try to run it, you discover missing imports, undefined variables, or subtle syntax errors that your IDE would have caught immediately with those familiar red squiggles.</p>
<p><strong>The fundamental issue?</strong> AI assistants like Claude work with raw text. They don't have the semantic understanding of code that your IDE provides through Language Server Protocol (LSP). They can't see what symbols are actually defined in your codebase, which imports are missing, or where functions are actually located across your project files.</p>
<p>This is where <strong>Serena</strong> comes in—and it might just be the missing piece that transforms how AI assistants handle code.</p>
<h2 id="heading-what-makes-serena-different">What Makes Serena Different?</h2>
<p><a target="_blank" href="https://github.com/oraios/serena">Serena</a> is an open-source coding agent toolkit developed by Oraios AI that bridges this critical gap. Instead of treating code as plain text, Serena leverages the same Language Server Protocol that powers your IDE's intelligence features.</p>
<h3 id="heading-the-lsp-advantage">The LSP Advantage</h3>
<p>Think about what your IDE can do:</p>
<ul>
<li><p>Jump to definitions across multiple files</p>
</li>
<li><p>Find all references to a function or class</p>
</li>
<li><p>Auto-complete with context-aware suggestions</p>
</li>
<li><p>Show you errors before you even run the code</p>
</li>
<li><p>Navigate complex codebases with semantic understanding</p>
</li>
</ul>
<p>Serena brings these exact capabilities to AI assistants through LSP integration. When Claude uses Serena, it's no longer flying blind—it can actually "see" your codebase the way a seasoned developer using a professional IDE would.</p>
<h2 id="heading-how-serena-works">How Serena Works</h2>
<p>At its core, Serena implements the Model Context Protocol (MCP), which allows it to integrate seamlessly with Claude and other AI assistants. Here's what happens under the hood:</p>
<h3 id="heading-1-semantic-code-analysis">1. <strong>Semantic Code Analysis</strong></h3>
<p>Serena starts language servers for your project (supporting Python, TypeScript, JavaScript, Java, C#, Go, Rust, and more). These are the same language servers your IDE uses—like Pyright for Python or TypeScript Language Server for TypeScript.</p>
<h3 id="heading-2-symbol-aware-tools">2. <strong>Symbol-Aware Tools</strong></h3>
<p>Instead of basic text manipulation, Serena provides tools that understand your code structure:</p>
<ul>
<li><p><code>find_symbol</code>: Locate function definitions, classes, or variables across your entire project</p>
</li>
<li><p><code>find_references</code>: See where a symbol is used throughout your codebase</p>
</li>
<li><p><code>goto_definition</code>: Navigate to the exact location where something is defined</p>
</li>
<li><p>Smart editing tools that understand code context, not just string patterns</p>
</li>
</ul>
<h3 id="heading-3-project-indexing">3. <strong>Project Indexing</strong></h3>
<p>For large projects, Serena can index your codebase upfront, making semantic operations lightning-fast. This means Claude can efficiently explore massive codebases without getting lost.</p>
<h3 id="heading-4-memory-system">4. <strong>Memory System</strong></h3>
<p>Serena includes a memory system that allows it to learn about your project over time, storing insights that can be recalled in future sessions.</p>
<h2 id="heading-real-world-impact">Real-World Impact</h2>
<p>Let me illustrate with a concrete example:</p>
<p><strong>Without Serena:</strong></p>
<pre><code class="lang-plaintext">You: "Add error handling to the user authentication function"
Claude: *Writes code but has to guess where the function is located, 
        what it's called, what imports it needs, and how it's structured*
Result: You spend time correcting the details
</code></pre>
<p><strong>With Serena:</strong></p>
<pre><code class="lang-plaintext">You: "Add error handling to the user authentication function"  
Claude: *Uses find_symbol to locate authenticate_user() in auth/handlers.py,
        analyzes its current implementation using goto_definition,
        finds all references to understand usage patterns,
        makes precise edits with full context awareness*
Result: The changes work immediately because Claude knew exactly 
        what was there
</code></pre>
<h2 id="heading-getting-started-with-serena-claude">Getting Started with Serena + Claude</h2>
<p>The beauty of Serena is its simplicity. If you're using Claude Code or Claude Desktop, you can integrate Serena in minutes:</p>
<h3 id="heading-quick-installation">Quick Installation</h3>
<p>bash</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Using uvx (recommended)</span>
uvx --from git+https://github.com/oraios/serena serena start-mcp-server
</code></pre>
<h3 id="heading-for-claude-code">For Claude Code:</h3>
<p>bash</p>
<pre><code class="lang-bash">claude mcp add serena -- uvx --from git+https://github.com/oraios/serena \
  serena start-mcp-server --context ide-assistant --project $(<span class="hljs-built_in">pwd</span>)
</code></pre>
<h3 id="heading-for-claude-desktop">For Claude Desktop:</h3>
<p>Add this to your <code>claude_desktop_config.json</code>:</p>
<p>json</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"serena"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"/path/to/uvx"</span>,
      <span class="hljs-attr">"args"</span>: [
        <span class="hljs-string">"--from"</span>, 
        <span class="hljs-string">"git+https://github.com/oraios/serena"</span>, 
        <span class="hljs-string">"serena"</span>, 
        <span class="hljs-string">"start-mcp-server"</span>
      ]
    }
  }
}
</code></pre>
<p>Once configured, simply activate your project in conversation:</p>
<pre><code class="lang-plaintext">You: "Activate /path/to/my/project as a project using Serena"
</code></pre>
<h2 id="heading-beyond-basic-code-generation">Beyond Basic Code Generation</h2>
<p>What makes Serena truly powerful is how it enables sophisticated workflows:</p>
<h3 id="heading-code-exploration">Code Exploration</h3>
<p>Ask Claude to explore your codebase: "Show me all the database models" or "Find everywhere we make HTTP requests." With Serena's LSP tools, Claude can traverse your project's symbol graph.</p>
<h3 id="heading-refactoring">Refactoring</h3>
<p>"Rename this function and update all references"—Serena ensures nothing breaks because it knows about every usage.</p>
<h3 id="heading-debugging">Debugging</h3>
<p>Claude can trace execution paths, find function definitions, and understand call hierarchies to help diagnose issues.</p>
<h3 id="heading-documentation">Documentation</h3>
<p>Generate documentation that's accurate because Claude can verify what symbols actually exist and how they're used.</p>
<h2 id="heading-why-this-matters-for-the-future-of-ai-assisted-development">Why This Matters for the Future of AI-Assisted Development</h2>
<p>The difference between text-based AI coding and LSP-aware coding is like the difference between editing code in Notepad versus a modern IDE. It's a fundamental leap in capability.</p>
<p>Serena represents a shift in how we think about AI coding assistants:</p>
<ul>
<li><p><strong>From guessing to knowing</strong>: No more hallucinated imports or phantom functions</p>
</li>
<li><p><strong>From text to semantics</strong>: Understanding code structure, not just patterns</p>
</li>
<li><p><strong>From surface-level to deep</strong>: Navigating complex codebases with confidence</p>
</li>
</ul>
<h2 id="heading-the-open-source-advantage">The Open Source Advantage</h2>
<p>Being open source and free, Serena offers several benefits:</p>
<ol>
<li><p><strong>No vendor lock-in</strong>: Works with Claude's free tier and can integrate with other LLMs through Agno</p>
</li>
<li><p><strong>Transparency</strong>: You can see exactly what tools Claude is using and how</p>
</li>
<li><p><strong>Extensibility</strong>: Add custom tools or language servers as needed</p>
</li>
<li><p><strong>Community-driven</strong>: Actively developed with contributions from developers worldwide</p>
</li>
<li><p><strong>Cost-effective</strong>: No subscription fees on top of your Claude usage</p>
</li>
</ol>
<h2 id="heading-technical-deep-dive-supported-languages">Technical Deep Dive: Supported Languages</h2>
<p>Serena currently provides robust support for:</p>
<p><strong>Full Support:</strong></p>
<ul>
<li><p>Python (via Pyright/Jedi)</p>
</li>
<li><p>TypeScript/JavaScript (via TypeScript Language Server)</p>
</li>
<li><p>Java (via Eclipse JDT Language Server)</p>
</li>
<li><p>C# (via OmniSharp/Roslyn)</p>
</li>
<li><p>Go (via gopls)</p>
</li>
<li><p>Rust (via rust-analyzer)</p>
</li>
<li><p>C/C++ (via clangd)</p>
</li>
<li><p>Ruby (via Solargraph)</p>
</li>
<li><p>And more...</p>
</li>
</ul>
<p>Each language server brings IDE-level intelligence to Claude, adapted through Serena's unified interface.</p>
<h2 id="heading-configuration-and-customization">Configuration and Customization</h2>
<p>Serena is highly configurable through YAML files:</p>
<h3 id="heading-contexts">Contexts</h3>
<p>Different tool sets for different environments:</p>
<ul>
<li><p><code>ide-assistant</code>: For integration with IDEs like VS Code or Cursor</p>
</li>
<li><p><code>desktop-app</code>: For Claude Desktop usage</p>
</li>
<li><p><code>codex</code>: For command-line coding with Codex CLI</p>
</li>
<li><p><code>chatgpt</code>: For use with ChatGPT (via MCPO)</p>
</li>
</ul>
<h3 id="heading-modes">Modes</h3>
<p>Operational patterns:</p>
<ul>
<li><p><code>planning</code>: Focus on code exploration and analysis</p>
</li>
<li><p><code>editing</code>: Emphasis on code modification tools</p>
</li>
<li><p><code>interactive</code>: Balanced tool set for conversation</p>
</li>
<li><p><code>one-shot</code>: Optimized for single-task completions</p>
</li>
</ul>
<p>You can create custom contexts and modes by adding YAML files to <code>~/.serena/</code>.</p>
<h2 id="heading-security-considerations">Security Considerations</h2>
<p>For production or sensitive environments, Serena can be configured with security in mind:</p>
<p>yaml</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># In .serena/project.yml</span>
<span class="hljs-attr">excluded_tools:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">execute_shell_command</span>  <span class="hljs-comment"># Disable command execution</span>
<span class="hljs-attr">read_only:</span> <span class="hljs-literal">true</span>  <span class="hljs-comment"># Prevent file modifications</span>
</code></pre>
<p>This allows you to use Serena's analysis capabilities without granting write access.</p>
<h2 id="heading-performance-optimization">Performance Optimization</h2>
<p>For large codebases, indexing is crucial:</p>
<p>bash</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Index your project before first use</span>
uvx --from git+https://github.com/oraios/serena serena project index
</code></pre>
<p>This creates a symbolic index that dramatically speeds up symbol lookup and reference finding operations.</p>
<h2 id="heading-the-road-ahead">The Road Ahead</h2>
<p>Serena is under active development, with the team working on:</p>
<ul>
<li><p>Enhanced LSP features (debugging via DAP, better diagnostics)</p>
</li>
<li><p>Jetbrains extension for seamless IDE integration</p>
</li>
<li><p>Additional language server support</p>
</li>
<li><p>Performance optimizations for massive codebases</p>
</li>
</ul>
<p>The project is sponsored by Microsoft's Visual Studio Code team and GitHub Open Source, signaling strong industry interest in this approach.</p>
<h2 id="heading-conclusion-closing-the-perception-action-loop">Conclusion: Closing the Perception-Action Loop</h2>
<p>The "missing piece" that Serena provides isn't just about better code generation—it's about closing what the developers call the "cognitive perception-action loop."</p>
<p>When an AI assistant can perceive code the way a developer does (through semantic understanding) and act on it with precision (through LSP-aware tools), it transforms from a clever text generator into a genuine coding partner.</p>
<p>If you've been frustrated by AI assistants that generate plausible-but-broken code, if you've wished Claude could actually understand your project structure, or if you're tired of manually fixing imports and references—Serena might be exactly what you've been looking for.</p>
<p>And the best part? It's free, open source, and integrates with Claude in just a few commands.</p>
<hr />
<p><strong>Resources:</strong></p>
<ul>
<li><a target="_blank" href="https://github.com/oraios/serena">Serena GitHub Repository</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[I Built a Bulletproof Backup System for Passbolt in 30 Minutes (And You Can Too)]]></title><description><![CDATA[The Backstory
So there I was, staring at my shiny new Passbolt installation on Ubuntu 24.04, when that familiar dread crept in: "What happens when (not if) something goes wrong?" After one too many "I'll set up backups tomorrow" moments, I finally de...]]></description><link>https://itishermann.me/i-built-a-bulletproof-backup-system-for-passbolt-in-30-minutes-and-you-can-too</link><guid isPermaLink="true">https://itishermann.me/i-built-a-bulletproof-backup-system-for-passbolt-in-30-minutes-and-you-can-too</guid><category><![CDATA[passbolt]]></category><category><![CDATA[automation]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[System administration]]></category><category><![CDATA[Backup]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Fri, 18 Jul 2025 12:38:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/CflaSq74U5I/upload/a312fe37f25d6f017a26f0e6e769c05d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-backstory">The Backstory</h2>
<p>So there I was, staring at my shiny new Passbolt installation on Ubuntu 24.04, when that familiar dread crept in: "What happens when (not if) something goes wrong?" After one too many "I'll set up backups tomorrow" moments, I finally decided to build a proper backup system. Spoiler: it was easier than debugging a CSS flexbox issue.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>Passbolt stores critical password data across multiple locations:</p>
<ul>
<li><p>MySQL database (the crown jewels)</p>
</li>
<li><p>Configuration files in <code>/etc/passbolt</code></p>
</li>
<li><p>GPG keys and data in <code>/var/lib/passbolt</code></p>
</li>
<li><p>User avatars and uploads</p>
</li>
</ul>
<p>Manually backing up each component? That's a recipe for 3 AM disaster recovery panic attacks.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># The manual nightmare nobody wants</span>
mysqldump passbolt &gt; backup.sql
tar -czf config.tar.gz /etc/passbolt
<span class="hljs-comment"># ...20 more commands you'll forget in a crisis</span>
</code></pre>
<h2 id="heading-the-insight">The Insight</h2>
<p>Instead of reinventing the wheel, I created three scripts that work together like a well-oiled DevOps machine:</p>
<ol>
<li><p><strong>Backup Script</strong>: Handles all components with proper error handling</p>
</li>
<li><p><strong>Restore Script</strong>: One-command restoration with safety checks</p>
</li>
<li><p><strong>Systemd Timer</strong>: Automated backups every 6 hours</p>
</li>
</ol>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
<span class="hljs-comment"># The magic happens here</span>
BACKUP_DIR=<span class="hljs-string">"/srv/backup/passbolt"</span>
RETENTION_DAYS=<span class="hljs-string">"<span class="hljs-variable">${PASSBOLT_BACKUP_RETENTION_DAYS:-30}</span>"</span>

<span class="hljs-comment"># Backup everything atomically</span>
mysqldump --single-transaction --routines --triggers <span class="hljs-string">"<span class="hljs-variable">$DB_NAME</span>"</span> &gt; <span class="hljs-string">"<span class="hljs-variable">$TEMP_DIR</span>/database.sql"</span>
cp -a /etc/passbolt <span class="hljs-string">"<span class="hljs-variable">$TEMP_DIR</span>/etc_passbolt"</span>
<span class="hljs-comment"># ... collect all the pieces</span>

<span class="hljs-comment"># Create timestamped archive</span>
tar -czf <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>/<span class="hljs-variable">${BACKUP_NAME}</span>.tar.gz"</span> <span class="hljs-string">"<span class="hljs-variable">$BACKUP_NAME</span>"</span>

<span class="hljs-comment"># Auto-cleanup old backups</span>
find <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>"</span> -name <span class="hljs-string">"passbolt_backup_*.tar.gz"</span> -<span class="hljs-built_in">type</span> f -mtime +<span class="hljs-variable">$RETENTION_DAYS</span> -delete
</code></pre>
<h2 id="heading-why-it-matters">Why It Matters</h2>
<p>This setup gives you:</p>
<ul>
<li><p><strong>Automated backups</strong> running every 6 hours (configurable)</p>
</li>
<li><p><strong>Point-in-time recovery</strong> with timestamped archives</p>
</li>
<li><p><strong>Automatic cleanup</strong> keeping only the last 30 days</p>
</li>
<li><p><strong>One-command restore</strong> that even works during coffee-deprived emergencies</p>
</li>
<li><p><strong>Systemd integration</strong> with proper logging and error handling</p>
</li>
</ul>
<p>The restore script even backs up your current data before restoring, because we've all been that person who "fixed" something into oblivion.</p>
<h2 id="heading-pro-tips-i-learned-the-hard-way">Pro Tips I Learned the Hard Way</h2>
<ol>
<li><p><strong>Always test your restore process</strong> - A backup you can't restore is just wasted disk space</p>
</li>
<li><p><strong>Use systemd's</strong> <code>ProtectSystem=strict</code> - It saved me from accidentally backing up to the wrong directory</p>
</li>
<li><p><strong>Include version info in backups</strong> - Future you will thank present you when dealing with migrations</p>
</li>
<li><p><strong>Set up monitoring</strong> - Add <code>OnFailure=</code> to your systemd service to get notified when backups fail</p>
</li>
</ol>
<h2 id="heading-tldr">TL;DR</h2>
<p>Three scripts + systemd timer = automated Passbolt backups with configurable retention. Install with one command, sleep better at night. (I know it’s in French but… <code>TODO: Translate logging to english</code>)</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="f072eca0e29f63199454b42312d6dcf7"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/itishermann/f072eca0e29f63199454b42312d6dcf7" class="embed-card">https://gist.github.com/itishermann/f072eca0e29f63199454b42312d6dcf7</a></div><p> </p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Run as root BUT READ IT BEFORE</div>
</div>

<h2 id="heading-dont-trust-strangers-on-internet"><mark>DON’T TRUST STRANGERS ON INTERNET</mark></h2>
<pre><code class="lang-bash"><span class="hljs-comment"># The whole setup in one line</span>
./setup-passbolt-backup.sh
</code></pre>
<p>Your passwords are now safer than a JavaScript developer's node_modules folder (and significantly smaller).</p>
<p>What's your backup horror story? Drop a comment below - misery loves company, and we all learn from each other's "learning experiences"! 🎢</p>
]]></content:encoded></item><item><title><![CDATA[Reactotron transport for react-native-logs]]></title><description><![CDATA[TL;DR
The custom Reactotron transport for react-native-logs improves logging by handling various message types, adding metadata, and allowing custom prefixes, making your logs more informative and easier to manage.
The Backstory
Logging is a crucial ...]]></description><link>https://itishermann.me/reactotron-transport-for-react-native-logs</link><guid isPermaLink="true">https://itishermann.me/reactotron-transport-for-react-native-logs</guid><category><![CDATA[Reactotron]]></category><category><![CDATA[React Native]]></category><category><![CDATA[logging]]></category><category><![CDATA[debugging]]></category><category><![CDATA[codesnippet]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Tue, 22 Apr 2025 11:11:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/f5pTwLHCsAg/upload/0373220528b3810f9dd1e86d6b708490.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<p>The custom Reactotron transport for <code>react-native-logs</code> improves logging by handling various message types, adding metadata, and allowing custom prefixes, making your logs more informative and easier to manage.</p>
<h2 id="heading-the-backstory">The Backstory</h2>
<p>Logging is a crucial part of development, especially when debugging or monitoring application behavior. Reactotron is a popular tool for inspecting React and React Native apps, and integrating it with <code>react-native-logs</code> can provide a more seamless logging experience. This custom transport enhances the default logging functionality by adding features like timestamps and custom prefixes.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>While <code>react-native-logs</code> is powerful, it might not cover all use cases out of the box. For instance, you might want to:</p>
<ul>
<li><p>Add timestamps to your logs for better traceability.</p>
</li>
<li><p>Prefix logs with custom identifiers to differentiate between different sources or modules.</p>
</li>
<li><p>Handle various types of log messages (strings, objects, arrays) gracefully.</p>
</li>
</ul>
<h2 id="heading-the-insight">The Insight</h2>
<p>The custom transport addresses these needs by:</p>
<ol>
<li><p><strong>Handling Different Message Types</strong>: It can process simple text messages, arrays, and objects, ensuring that each type is displayed appropriately in Reactotron.</p>
</li>
<li><p><strong>Adding Metadata</strong>: It includes the log level and an optional timestamp in the metadata.</p>
</li>
<li><p><strong>Custom Prefixes</strong>: It allows you to add a custom prefix to each log message, making it easier to identify the source of the log.</p>
</li>
<li><p><strong>Importance Flag</strong>: It marks important logs (based on severity) to make them stand out in Reactotron.</p>
</li>
</ol>
<p>Here's a breakdown of the code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">/**
 * Custom transport for sending logs to Reactotron with enhanced features
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> reactotronTransport: transportFunctionType&lt;{
  showTimestamp?: <span class="hljs-built_in">boolean</span>;
  customPrefix?: <span class="hljs-built_in">string</span>;
}&gt; = <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { level, msg, rawMsg, options = {} } = props;
  <span class="hljs-keyword">const</span> isImportant = level.severity &gt;= <span class="hljs-number">3</span>;
  <span class="hljs-keyword">const</span> prefix = options.customPrefix ? <span class="hljs-string">`<span class="hljs-subst">${options.customPrefix}</span>: `</span> : <span class="hljs-string">''</span>;

  <span class="hljs-comment">// Enhanced message handling</span>
  <span class="hljs-keyword">let</span> displayMessage: <span class="hljs-built_in">any</span>;
  <span class="hljs-keyword">let</span> previewText = <span class="hljs-string">''</span>;

  <span class="hljs-comment">// Case 1: Array of arguments (like console.log('text', object))</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Array</span>.isArray(rawMsg)) {
    <span class="hljs-keyword">const</span> firstItem = rawMsg[<span class="hljs-number">0</span>];
    <span class="hljs-keyword">const</span> restItems = rawMsg.slice(<span class="hljs-number">1</span>);

    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> firstItem === <span class="hljs-string">'string'</span> &amp;&amp; restItems.length &gt; <span class="hljs-number">0</span>) {
      previewText = firstItem;
      displayMessage = {
        message: firstItem,
        data: restItems.length === <span class="hljs-number">1</span> ? restItems[<span class="hljs-number">0</span>] : restItems,
      };
    } <span class="hljs-keyword">else</span> {
      previewText = <span class="hljs-string">`Array[<span class="hljs-subst">${rawMsg.length}</span>]`</span>;
      displayMessage = rawMsg;
    }
  }
  <span class="hljs-comment">// Case 2: Single object (including array)</span>
  <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> rawMsg === <span class="hljs-string">'object'</span> &amp;&amp; rawMsg !== <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Array</span>.isArray(rawMsg)) {
      previewText = <span class="hljs-string">`Array[<span class="hljs-subst">${rawMsg.length}</span>]`</span>;
    } <span class="hljs-keyword">else</span> {
      previewText = <span class="hljs-built_in">Object</span>.prototype.toString.call(rawMsg).slice(<span class="hljs-number">8</span>, <span class="hljs-number">-1</span>);
    }
    displayMessage = rawMsg;
  }
  <span class="hljs-comment">// Case 3: Simple text message</span>
  <span class="hljs-keyword">else</span> {
    previewText = msg;
    displayMessage = rawMsg !== <span class="hljs-literal">undefined</span> ? rawMsg : msg;
  }

  <span class="hljs-comment">// Metadata</span>
  <span class="hljs-keyword">const</span> metadata = {
    level: level.text,
    timestamp: options.showTimestamp ? <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString() : <span class="hljs-literal">undefined</span>,
  };

  <span class="hljs-comment">// Preview with prefix</span>
  <span class="hljs-keyword">const</span> preview = <span class="hljs-string">`<span class="hljs-subst">${prefix}</span><span class="hljs-subst">${previewText}</span>`</span>;

  <span class="hljs-comment">// Send to Reactotron</span>
  Reactotron.display({
    name: level.text.toUpperCase(),
    value: {
      ...(<span class="hljs-keyword">typeof</span> displayMessage === <span class="hljs-string">'object'</span>
        ? displayMessage
        : { message: displayMessage }),
      ...metadata,
    },
    preview,
    important: isImportant,
  });
};
</code></pre>
<h2 id="heading-why-it-matters">Why It Matters</h2>
<p>This custom transport makes your logging more flexible and informative. By adding timestamps and custom prefixes, you can better track and differentiate logs. The enhanced message handling ensures that all types of log data are displayed correctly in Reactotron, making debugging and monitoring more efficient.</p>
]]></content:encoded></item><item><title><![CDATA[How to run Playwright Tests in Parallel on GitLab]]></title><description><![CDATA[TL;DR
Add parallel: N to your GitLab CI job and pass --shard=${CI_NODE_INDEX}/${CI_NODE_TOTAL} to Playwright to slash your test execution time and keep your team shipping fast.
The Backstory
Last week, our test suite execution time crossed the dreade...]]></description><link>https://itishermann.me/how-to-run-playwright-tests-in-parallel-on-gitlab</link><guid isPermaLink="true">https://itishermann.me/how-to-run-playwright-tests-in-parallel-on-gitlab</guid><category><![CDATA[GitLab]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Testing]]></category><category><![CDATA[playwright]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Mon, 24 Mar 2025 13:42:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742823716169/df20ade4-67f5-43c6-8e1a-6a6beee1721e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<p>Add <code>parallel: N</code> to your GitLab CI job and pass <code>--shard=${CI_NODE_INDEX}/${CI_NODE_TOTAL}</code> to Playwright to slash your test execution time and keep your team shipping fast.</p>
<h2 id="heading-the-backstory">The Backstory</h2>
<p>Last week, our test suite execution time crossed the dreaded 30-minute mark. Team members were context-switching during test runs, and our deploy frequency started to suffer. That's when I discovered we weren't using GitLab CI's parallelization capabilities with our Playwright tests.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>Running Playwright tests sequentially in GitLab CI can take forever, especially as your test suite grows. Without parallelization, even simple PR validations become coffee-break-length waits:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># The slow, sequential way 😴</span>
<span class="hljs-attr">test:</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">mcr.microsoft.com/playwright:v1.51.1-noble</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">npx</span> <span class="hljs-string">playwright</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">artifacts:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">playwright-report/</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">junit.xml</span>
    <span class="hljs-attr">reports:</span>
      <span class="hljs-attr">junit:</span> <span class="hljs-string">junit.xml</span>
</code></pre>
<h2 id="heading-the-insight">The Insight</h2>
<p>GitLab CI supports running jobs in parallel using the <code>parallel</code> keyword, and Playwright can shard tests with the <code>--shard</code> flag. Combining these powers is like discovering you can breathe underwater:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># The speed-demon parallel way 🚀</span>
<span class="hljs-attr">test:</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">mcr.microsoft.com/playwright:v1.51.1-noble</span>
  <span class="hljs-attr">parallel:</span> <span class="hljs-number">5</span>  <span class="hljs-comment"># Run 5 parallel jobs</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">npm</span> <span class="hljs-string">ci</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">npx</span> <span class="hljs-string">playwright</span> <span class="hljs-string">test</span> <span class="hljs-string">--shard=${CI_NODE_INDEX}/${CI_NODE_TOTAL}</span>
  <span class="hljs-attr">artifacts:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">playwright-report/</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">junit.xml</span>
    <span class="hljs-attr">reports:</span>
      <span class="hljs-attr">junit:</span> <span class="hljs-string">junit.xml</span>
</code></pre>
<p>Playwright's <a target="_blank" href="https://playwright.dev/docs/test-sharding"><code>--shard</code> flag</a> takes two parameters: current shard index and total shards. The magic here is that GitLab CI automatically provides environment variables <a target="_blank" href="https://docs.gitlab.com/ci/variables/predefined_variables/"><code>CI_NODE_INDEX</code> and <code>CI_NODE_TOTAL</code></a> that perfectly match what Playwright expects!</p>
<h2 id="heading-why-it-matters">Why It Matters</h2>
<p>After implementing this change:</p>
<ul>
<li><p>Our test suite execution time dropped from 30+ minutes to under 10 minutes</p>
</li>
<li><p>Developers stopped context-switching during test runs</p>
</li>
<li><p>We caught integration issues faster and deployed more frequently</p>
</li>
</ul>
<p>For larger projects, you can refine this further by using GitLab's matrix syntax to run different browser tests in parallel:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">test:</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">mcr.microsoft.com/playwright:v1.51.1-noble</span>
  <span class="hljs-attr">parallel:</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">BROWSER:</span> [<span class="hljs-string">chromium</span>, <span class="hljs-string">firefox</span>, <span class="hljs-string">webkit</span>]
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">npx</span> <span class="hljs-string">playwright</span> <span class="hljs-string">test</span> <span class="hljs-string">--project=$BROWSER</span> <span class="hljs-string">--shard=${CI_NODE_INDEX}/${CI_NODE_TOTAL}</span>
  <span class="hljs-attr">artifacts:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">playwright-report/</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">junit.xml</span>
    <span class="hljs-attr">reports:</span>
      <span class="hljs-attr">junit:</span> <span class="hljs-string">junit.xml</span>
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Introducing Appwrite Exceptions Translator: Error Messages That Make Sense]]></title><description><![CDATA[Error messages are often the first interaction users have with your support system. When they're cryptic, technical, or in the wrong language, they create confusion rather than clarity. That's why I built the Appwrite Exceptions Translator - a lightw...]]></description><link>https://itishermann.me/introducing-appwrite-exceptions-translator-error-messages-that-make-sense</link><guid isPermaLink="true">https://itishermann.me/introducing-appwrite-exceptions-translator-error-messages-that-make-sense</guid><category><![CDATA[Appwrite]]></category><category><![CDATA[exceptionhandling]]></category><category><![CDATA[crowdin]]></category><category><![CDATA[translation]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Sun, 23 Mar 2025 18:36:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742754905914/6c493d12-6261-4d8c-8ce9-04e8dc1f04b4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Error messages are often the first interaction users have with your support system. When they're cryptic, technical, or in the wrong language, they create confusion rather than clarity. That's why I built the <a target="_blank" href="https://gitlab.com/itishermann/appwrite-exceptions-translator">Appwrite Exceptions Translator</a> - a lightweight library that transforms Appwrite's technical error messages into user-friendly, localized explanations.</p>
<h2 id="heading-the-problem-with-raw-error-messages">The Problem with Raw Error Messages</h2>
<p>If you've worked with Appwrite (or any backend service), you're familiar with errors like:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Invalid id: Parameter must be a valid number"</span>,
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"general_argument_invalid"</span>,
    <span class="hljs-attr">"code"</span>: <span class="hljs-number">400</span>
}
</code></pre>
<p>These messages make perfect sense to developers but can be bewildering to end users. They lack context, often contain technical jargon, and rarely offer guidance on how to resolve the issue.</p>
<h2 id="heading-a-better-way-to-handle-errors">A Better Way to Handle Errors</h2>
<p>The Appwrite Exceptions Translator addresses these issues by:</p>
<ol>
<li><p><strong>Translating error codes and types</strong> into human-readable explanations</p>
</li>
<li><p><strong>Supporting multiple languages</strong> (currently English, French, Spanish, and Arabic)</p>
</li>
<li><p><strong>Providing a consistent error handling pattern</strong> across your application</p>
</li>
<li><p><strong>Falling back gracefully</strong> when no translation is available</p>
</li>
</ol>
<h2 id="heading-how-it-works">How It Works</h2>
<p>Using the translator is straightforward:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { AppwriteExceptionTranslator } <span class="hljs-keyword">from</span> <span class="hljs-string">"@itishermann/appwrite-exceptions-translator"</span>;
<span class="hljs-keyword">import</span> { LocalTranslationProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"@itishermann/appwrite-exceptions-translator/providers"</span>;

<span class="hljs-comment">// Initialize the translator</span>
<span class="hljs-keyword">const</span> translator = <span class="hljs-keyword">new</span> AppwriteExceptionTranslator(
  <span class="hljs-keyword">new</span> LocalTranslationProvider()
);

<span class="hljs-comment">// Somewhere in your error handling...</span>
<span class="hljs-keyword">try</span> {
  <span class="hljs-comment">// Appwrite operations</span>
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-comment">// Transform the error into a user-friendly message</span>
  <span class="hljs-keyword">const</span> userMessage = translator.translate(error);

  <span class="hljs-comment">// Display to user (instead of the raw error)</span>
  showNotification(userMessage);
}
</code></pre>
<p>Instead of your users seeing "general_argument_invalid", they'll see something like "The request contains one or more invalid arguments" - or its equivalent in their preferred language.</p>
<h2 id="heading-flexible-and-extensible">Flexible and Extensible</h2>
<p>The library is designed with flexibility in mind:</p>
<ul>
<li><p><strong>Language switching on-the-fly</strong> - Change languages without restarting your app</p>
</li>
<li><p><strong>Prioritized translation strategy</strong> - Tries error type first, falls back to error code</p>
</li>
<li><p><strong>Custom translation providers</strong> - Implement your own provider for different translation sources</p>
</li>
<li><p><strong>Lightweight footprint</strong> - No heavy dependencies, just the translations you need</p>
</li>
</ul>
<h2 id="heading-built-for-modern-javascript-environments">Built for Modern JavaScript Environments</h2>
<p>The package is built with TypeScript, includes full type definitions, and supports both ESM and CommonJS. It works seamlessly with:</p>
<ul>
<li><p>Node.js applications</p>
</li>
<li><p>React, Vue, Angular, and other frontend frameworks</p>
</li>
<li><p>React Native and other mobile JavaScript frameworks</p>
</li>
<li><p>Bun projects (it's actually built with Bun!)</p>
</li>
</ul>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Installation is simple:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Using npm</span>
npm install @itishermann/appwrite-exceptions-translator

<span class="hljs-comment"># Using yarn</span>
yarn add @itishermann/appwrite-exceptions-translator

<span class="hljs-comment"># Using bun</span>
bun add @itishermann/appwrite-exceptions-translator
</code></pre>
<h2 id="heading-why-i-built-this">Why I Built This</h2>
<p>As a developer working with Appwrite, I found myself repeatedly writing code to transform error messages into something more user-friendly. After copy-pasting the same logic across multiple projects, I decided to extract it into a reusable package.</p>
<p>This translator is part of a larger effort to improve the developer and end-user experience when working with Appwrite - an excellent open-source backend that deserves equally excellent tooling.</p>
<h2 id="heading-contributing">Contributing</h2>
<p>This is an open-source project licensed under AGPL-3.0, and contributions are welcome! Whether you want to:</p>
<ul>
<li><p>Add translations for additional languages</p>
</li>
<li><p>Improve existing translations</p>
</li>
<li><p>Extend the functionality</p>
</li>
<li><p>Fix bugs</p>
</li>
</ul>
<p>Check out the <a target="_blank" href="https://gitlab.com/itishermann/appwrite-exceptions-translator/-/blob/main/CONTRIBUTING.md">contribution guidelines</a> to get started.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Error handling doesn't have to be an afterthought. With Appwrite Exceptions Translator, you can provide clear, helpful error messages to your users in their preferred language, improving the overall user experience of your application.</p>
<p>Give it a try in your next Appwrite project, and turn frustrating errors into helpful guidance.</p>
<p><a target="_blank" href="https://gitlab.com/itishermann/appwrite-exceptions-translator">Check out the repository on GitLab</a> or <a target="_blank" href="https://www.npmjs.com/package/@itishermann/appwrite-exceptions-translator">install it directly from npm</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Deploying Next.js Apps on Ubuntu 24.04 with Systemd and Caddy: A Minimalist Approach]]></title><description><![CDATA[When my friend approached me about deploying his Next.js applications on a tiny VPS (2vCPU, 2GB RAM) without Docker, I initially raised an eyebrow. But sometimes constraints breed creativity, and this deployment strategy turned out to be quite elegan...]]></description><link>https://itishermann.me/deploying-nextjs-apps-on-ubuntu-2404-with-systemd-and-caddy-a-minimalist-approach</link><guid isPermaLink="true">https://itishermann.me/deploying-nextjs-apps-on-ubuntu-2404-with-systemd-and-caddy-a-minimalist-approach</guid><category><![CDATA[Node.js]]></category><category><![CDATA[systemd]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[Caddy]]></category><category><![CDATA[architecture]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Mon, 17 Mar 2025 10:42:12 GMT</pubDate><content:encoded><![CDATA[<p>When my friend approached me about deploying his Next.js applications on a tiny VPS (2vCPU, 2GB RAM) without Docker, I initially raised an eyebrow. But sometimes constraints breed creativity, and this deployment strategy turned out to be quite elegant because it uses systemd which is included in most Debian distros and not some random process manager like PM2 or something like that</p>
<p>Below is a guide to setting up Next.js deployment using systemd and Caddy on Ubuntu 24.04 - perfect for resource-constrained environments or situations where Docker isn't an option.</p>
<h2 id="heading-setting-up-nodejs-20-lts">Setting Up Node.js 20 LTS</h2>
<p>First, we need to install Node.js 20 LTS.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Since this tutorial will be outdated, please refer to the <a target="_self" href="https://github.com/nodesource/distributions?tab=readme-ov-file#using-ubuntu-nodejs-20">official documentation link</a></div>
</div>

<p>Ubuntu's default repositories often contain older Node versions, so we'll use the NodeSource repository:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add NodeSource repository</span>
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -

<span class="hljs-comment"># Install Node.js and npm</span>
sudo apt-get install -y nodejs

<span class="hljs-comment"># Verify installation</span>
node -v  <span class="hljs-comment"># Should output v20.x.x</span>
npm -v   <span class="hljs-comment"># Should output 10.x.x</span>
</code></pre>
<h2 id="heading-building-the-nextjs-application">Building the Next.js Application</h2>
<p>Assuming your Next.js application is ready for production:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Navigate to your application directory</span>
<span class="hljs-built_in">cd</span> /path/to/nextjs-app

<span class="hljs-comment"># Install dependencies</span>
npm install

<span class="hljs-comment"># Create .env file if needed</span>

<span class="hljs-comment"># Build the application</span>
npm run build
</code></pre>
<h2 id="heading-creating-a-systemd-service-with-memory-limits">Creating a Systemd Service with Memory Limits</h2>
<p>Now let's create a systemd service to run our Next.js application with appropriate resource constraints:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># nano because noone could escape vi 💀</span>
sudo nano /etc/systemd/system/nextjs-app.service
</code></pre>
<p>Add the following configuration:</p>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=Next.js Application
<span class="hljs-attr">After</span>=network.target

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">Type</span>=simple
<span class="hljs-attr">User</span>=ubuntu  <span class="hljs-comment"># Replace with your server user</span>
<span class="hljs-attr">WorkingDirectory</span>=/path/to/nextjs-app
<span class="hljs-attr">ExecStart</span>=/usr/bin/npm start
<span class="hljs-attr">Restart</span>=<span class="hljs-literal">on</span>-failure
<span class="hljs-attr">RestartSec</span>=<span class="hljs-number">15</span>
<span class="hljs-comment"># Memory limits (1GB as requested by my boi)</span>
<span class="hljs-attr">MemoryMax</span>=<span class="hljs-number">1</span>G
<span class="hljs-attr">MemoryHigh</span>=<span class="hljs-number">768</span>M
<span class="hljs-attr">MemoryAccounting</span>=<span class="hljs-literal">true</span>
<span class="hljs-comment"># Environment variables if needed</span>
<span class="hljs-attr">Environment</span>=NODE_ENV=production
<span class="hljs-attr">Environment</span>=PORT=<span class="hljs-number">3000</span>
<span class="hljs-attr">Environment</span>=HOST=<span class="hljs-number">127.0</span>.<span class="hljs-number">0.1</span>

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target
</code></pre>
<p>Enable and start the service:</p>
<pre><code class="lang-bash">sudo systemctl <span class="hljs-built_in">enable</span> nextjs-app.service
sudo systemctl start nextjs-app.service
sudo systemctl status nextjs-app.service  <span class="hljs-comment"># Check if it's running correctly</span>
</code></pre>
<h2 id="heading-installing-caddy-web-server">Installing Caddy Web Server</h2>
<p>Caddy is a modern, security-first web server that automatically handles HTTPS certificates.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Since this article may become outdated, here is the link to the <a target="_self" href="https://caddyserver.com/docs/install">official installation documentation</a> from Caddy’s website</div>
</div>

<p>Let's install it:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install Caddy</span>
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf <span class="hljs-string">'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'</span> | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf <span class="hljs-string">'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt'</span> | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
</code></pre>
<h2 id="heading-configuring-caddy-as-a-reverse-proxy">Configuring Caddy as a Reverse Proxy</h2>
<p>Now we'll configure Caddy to serve our Next.js application:</p>
<pre><code class="lang-bash">sudo nano /etc/caddy/Caddyfile
</code></pre>
<p>Replace the contents with:</p>
<pre><code class="lang-json">yourwebsite.com {
    # Enable HTTPS automatically
    tls your@email.com

    # Reverse proxy to your Next.js app
    reverse_proxy localhost:<span class="hljs-number">3000</span>

    # For better logging
    log {
        output file /var/log/caddy/yourwebsite.com.log
    }

    # Optional: Compress responses for better performance
    encode gzip zstd
}
</code></pre>
<p>Apply the configuration:</p>
<pre><code class="lang-bash">sudo systemctl reload caddy
</code></pre>
<h2 id="heading-monitoring-your-deployment">Monitoring Your Deployment</h2>
<p>To keep an eye on your application:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check systemd service logs</span>
sudo journalctl -u nextjs-app.service -f

<span class="hljs-comment"># Check Caddy logs </span>
sudo tail -f /var/<span class="hljs-built_in">log</span>/caddy/yourwebsite.com.log
</code></pre>
<h2 id="heading-performance-optimizations-for-limited-resources">Performance Optimizations for Limited Resources</h2>
<p>Since you're working with only 2GB RAM and a 1GB limit for the Next.js app, consider these additional optimizations:</p>
<ol>
<li><p><strong>Enable Node's --max-old-space-size flag</strong> in your systemd service:</p>
<pre><code class="lang-ini"> <span class="hljs-attr">ExecStart</span>=/usr/bin/node --max-old-space-size=<span class="hljs-number">800</span> node_modules/.bin/next start
</code></pre>
</li>
<li><p><strong>Monitor swap usage</strong> and add swap if necessary:</p>
<pre><code class="lang-bash"> sudo fallocate -l 1G /swapfile
 sudo chmod 600 /swapfile
 sudo mkswap /swapfile
 sudo swapon /swapfile
 <span class="hljs-built_in">echo</span> <span class="hljs-string">'/swapfile none swap sw 0 0'</span> | sudo tee -a /etc/fstab

 <span class="hljs-comment"># check if the swap is active</span>
 sudo swapon --show
 free -h
</code></pre>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This setup provides a lightweight yet robust deployment solution for Next.js applications (or nodejs apps) without Docker. The combination of systemd (for process management and resource constraints) with Caddy (for HTTPS and reverse proxying) creates a surprisingly powerful stack that works well on limited hardware.</p>
<p>While containers offer more isolation, this approach has its own advantages: simplicity, lower resource overhead, and straightforward troubleshooting - perfect for that tiny VPS with just enough resources to get the job done.</p>
<hr />
<p>Need any specific adjustments or have questions about any part of this deployment strategy? Let me know in the comments!</p>
]]></content:encoded></item><item><title><![CDATA[Why .bind(this) put me through hell]]></title><description><![CDATA[The Backstory
I was peacefully implementing authentication for an Angular app, sipping my coffee, feeling productive. I added a simple tap operator to my login method to store the JWT token in localStorage. Easy-peasy, right? Wrong. The injected Stor...]]></description><link>https://itishermann.me/why-bindthis-put-me-through-hell</link><guid isPermaLink="true">https://itishermann.me/why-bindthis-put-me-through-hell</guid><category><![CDATA[Angular]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[dependency injection]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Sat, 15 Mar 2025 09:07:48 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-the-backstory">The Backstory</h2>
<p>I was peacefully implementing authentication for an Angular app, sipping my coffee, feeling productive. I added a simple <code>tap</code> operator to my login method to store the JWT token in localStorage. Easy-peasy, right? Wrong. The injected <code>StorageService</code> kept showing up as <code>undefined</code> despite being properly imported and injected. Cue the hair-pulling.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>The issue occurred in an authentication service that looked something like this:</p>
<pre><code class="lang-typescript"><span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-keyword">private</span> http: HttpClient,
    <span class="hljs-keyword">private</span> storageService: StorageService <span class="hljs-comment">// Properly injected!</span>
  </span>) {}

 <span class="hljs-keyword">private</span> handleLoginSuccess(response: AuthResponse): <span class="hljs-built_in">void</span> {
  <span class="hljs-comment">// ERROR: this.storageService is undefined here!</span>
  <span class="hljs-built_in">this</span>.storageService.setItem(<span class="hljs-string">'token'</span>, response.token);
}

  login(credentials: {username: <span class="hljs-built_in">string</span>, password: <span class="hljs-built_in">string</span>}): Observable&lt;AuthResponse&gt; {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.http.post&lt;AuthResponse&gt;(<span class="hljs-string">'/api/login'</span>, credentials).pipe(
      tap(<span class="hljs-built_in">this</span>.handleLoginSuccess)
    );
  }
}
</code></pre>
<p>When the <code>tap</code> operator executed, <code>this.storageService</code> was mysteriously <code>undefined</code>, despite being available in other methods. It's like my service got amnesia, but only for this one subscription.</p>
<h2 id="heading-the-insight">The Insight</h2>
<p>The culprit? Context binding! Inside the <code>tap</code> callback, <code>this</code> wasn't referring to my service instance anymore. The solution was elegantly simple:</p>
<pre><code class="lang-typescript">login(credentials: {username: <span class="hljs-built_in">string</span>, password: <span class="hljs-built_in">string</span>}): Observable&lt;AuthResponse&gt; {
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.http.post&lt;AuthResponse&gt;(<span class="hljs-string">'/api/login'</span>, credentials).pipe(
    <span class="hljs-comment">// The magic: .bind(this) preserves the correct context</span>
    tap(<span class="hljs-built_in">this</span>.handleLoginSuccess.bind(<span class="hljs-built_in">this</span>))
  );
}

<span class="hljs-keyword">private</span> handleLoginSuccess(response: AuthResponse): <span class="hljs-built_in">void</span> {
  <span class="hljs-comment">// Now this.storageService exists!</span>
  <span class="hljs-built_in">this</span>.storageService.setItem(<span class="hljs-string">'token'</span>, response.token);
}
</code></pre>
<p>By using <code>.bind(this)</code>, I explicitly told JavaScript: "Hey, when you execute this function later, remember that <code>this</code> should refer to the current context, not whatever context exists when the function runs."</p>
<h2 id="heading-why-it-matters">Why It Matters</h2>
<p>This isn't just an Angular issue—it's a fundamental JavaScript concept that applies to any framework. Arrow functions automatically bind <code>this</code>, which is why you'll often see:</p>
<pre><code class="lang-typescript">tap(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> <span class="hljs-built_in">this</span>.handleLoginSuccess(response))
</code></pre>
<p>But using <code>.bind(this)</code> with a method reference can make your code cleaner while properly maintaining context. It's especially important in:</p>
<ul>
<li><p>Observable operators like <code>tap</code>, <code>map</code>, and <code>filter</code></p>
</li>
<li><p>Event handlers</p>
</li>
<li><p>Callback functions</p>
</li>
<li><p>setTimeout/setInterval calls</p>
</li>
</ul>
<h2 id="heading-tldr">TL;DR</h2>
<p>When your injected services go missing inside callbacks, remember: <code>.bind(this)</code> is your context-preserving superhero.</p>
]]></content:encoded></item><item><title><![CDATA[Fixing the Mysterious 404 When Deploying Angular 19 to Vercel]]></title><description><![CDATA[TL;DR
When deploying Angular 19 apps to Vercel, override the output directory setting to dist/[your-app-name]/browserto avoid the dreaded 404 page if you use the @angular-devkit/build-angular:application builder
The Backstory
There I was, feeling acc...]]></description><link>https://itishermann.me/fixing-the-mysterious-404-when-deploying-angular-19-to-vercel</link><guid isPermaLink="true">https://itishermann.me/fixing-the-mysterious-404-when-deploying-angular-19-to-vercel</guid><category><![CDATA[Angular]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Vercel]]></category><category><![CDATA[troubleshooting]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Fri, 14 Mar 2025 23:00:30 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<p>When deploying Angular 19 apps to Vercel, override the output directory setting to <code>dist/[your-app-name]/browser</code>to avoid the dreaded 404 page if you use the <code>@angular-devkit/build-angular:application</code> builder</p>
<h2 id="heading-the-backstory">The Backstory</h2>
<p>There I was, feeling accomplished after building my shiny new Angular 19 app. "Time to deploy!" I thought gleefully. I connected my GitHub repo to Vercel, watched the build succeed with flying colors, and then... 404. The digital equivalent of showing up to a party at the wrong address. What gives, Vercel?</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>Angular 19 uses the <code>@angular-devkit/build-angular:application</code> builder as the default builder. However, Vercel stubbornly expects you to be using the <code>@angular-devkit/build-angular:browser</code> builder, and its deployment settings are configured accordingly.</p>
<pre><code class="lang-json"><span class="hljs-comment">// What Angular 19 has in angular.json</span>
<span class="hljs-string">"architect"</span>: {
  <span class="hljs-attr">"build"</span>: {
    <span class="hljs-attr">"builder"</span>: <span class="hljs-string">"@angular-devkit/build-angular:application"</span>,
    <span class="hljs-comment">// ...which outputs files to a different location than Vercel expects</span>
  }
}
</code></pre>
<p>When Vercel tries to serve your app, it's looking in the wrong place – like a delivery driver dropping your pizza at your neighbor's house, it sucks.</p>
<h2 id="heading-the-insight">The Insight</h2>
<p>The fix is surprisingly simple: you need to explicitly tell Vercel where your application's output is located. In the Vercel deployment settings, you must override the default output directory path.</p>
<p>Set your Vercel build output directory to:</p>
<pre><code class="lang-bash">dist/[your-app-name]/browser
</code></pre>
<p>For example, in my case:</p>
<pre><code class="lang-bash">dist/play-pulse/browser
</code></pre>
<p>This tells Vercel: "Hey, don't look in the usual place. The party's over here!"</p>
<h2 id="heading-why-it-matters">Why It Matters</h2>
<p>This small configuration hiccup can cause a lot of head-scratching and time wasted debugging deployments. Angular's evolution to the new application builder is great for development, but deployment platforms need time to catch up. Understanding these nuances between your build tools and deployment platforms is crucial for smooth DevOps.</p>
]]></content:encoded></item><item><title><![CDATA[SOLID Principles Saved My Angular App from Database Drama]]></title><description><![CDATA[Ever tried swapping LocalStorage for Dexie DB in your Angular app? That was me yesterday—surrounded by coffee cups, error messages, and regret.
My Angular service was tightly coupled to LocalStorage like peanut butter to jelly. Changing to Dexie mean...]]></description><link>https://itishermann.me/solid-principles-saved-my-angular-app-from-database-drama</link><guid isPermaLink="true">https://itishermann.me/solid-principles-saved-my-angular-app-from-database-drama</guid><category><![CDATA[SOLID principles]]></category><category><![CDATA[TIL]]></category><category><![CDATA[craft]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Fri, 14 Mar 2025 15:08:06 GMT</pubDate><content:encoded><![CDATA[<p>Ever tried swapping LocalStorage for Dexie DB in your Angular app? That was me yesterday—surrounded by coffee cups, error messages, and regret.</p>
<p>My Angular service was tightly coupled to LocalStorage like peanut butter to jelly. Changing to Dexie meant rewriting EVERYTHING. Then I remembered the SOLID principles:</p>
<p><strong>S</strong>ingle Responsibility: Each class should do one job (not my service juggling data AND storage)</p>
<p><strong>O</strong>pen/Closed: Classes open for extension, closed for modification (my code was welded shut)</p>
<p><strong>L</strong>iskov Substitution: Subtypes should be substitutable (my storage wasn't)</p>
<p><strong>I</strong>nterface Segregation: Don't force clients to depend on methods they don't use</p>
<p>But the real hero? <strong>D</strong>ependency Inversion! By depending on abstractions instead of concrete implementations, I created a <code>StorageService</code> interface that both LocalStorage and Dexie could implement.</p>
<p>Suddenly, switching databases was just swapping implementations, not rebuilding Rome.</p>
<p>The "D" in SOLID (Dependency Inversion) became my hero when I created a storage interface that both implementations could satisfy. Now my components don't care where data lives; they just ask nicely for it.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Instead of this nightmare 😱</span>
<span class="hljs-keyword">class</span> UserService {
  <span class="hljs-keyword">private</span> <span class="hljs-built_in">localStorage</span> = <span class="hljs-built_in">window</span>.localStorage;

  saveUser(user: User) {
    <span class="hljs-built_in">this</span>.localStorage.setItem(<span class="hljs-string">'user'</span>, <span class="hljs-built_in">JSON</span>.stringify(user));
  }
}

<span class="hljs-comment">// I now have this beauty 💅</span>
<span class="hljs-keyword">interface</span> StorageProvider {
  save(key: <span class="hljs-built_in">string</span>, data: <span class="hljs-built_in">any</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
  get(key: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">any</span>&gt;;
}

<span class="hljs-keyword">class</span> UserService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> storage: StorageProvider</span>) {}

  saveUser(user: User): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.storage.save(<span class="hljs-string">'user'</span>, user);
  }
}

<span class="hljs-comment">// we will be talking about abstractions later on, don't throw a rock at me right now</span>
</code></pre>
<p>Swapping implementations became as simple as changing underwear (but less embarrassing when done in public).</p>
<p><img src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdGU4bHR5eXl5bGpiZ2N0anU0bndqNmt5bTlvdDRwdmJuZXJvanZoNSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/UtcBRO8cxulRzkrVLc/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Moral of the story: When you SOLID-ify your code, future you will send present you a thank-you cake. Or at least fewer angry Slack messages.</p>
]]></content:encoded></item><item><title><![CDATA[I've been advised to make at least 10k steps a day, but how to make it an habit ? 🤨]]></title><description><![CDATA[We all know the benefits of walking – it’s great for your physical and mental well-being. But sometimes, fitting in those recommended 10,000 steps a day can feel like a chore. You might find yourself walking aimlessly around the block, or stuck on a ...]]></description><link>https://itishermann.me/ive-been-advised-to-make-at-least-10k-steps-a-day-but-how-to-make-it-an-habit</link><guid isPermaLink="true">https://itishermann.me/ive-been-advised-to-make-at-least-10k-steps-a-day-but-how-to-make-it-an-habit</guid><category><![CDATA[healthcare]]></category><category><![CDATA[sports]]></category><category><![CDATA[routing]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Sat, 25 Jan 2025 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ljoCgjs63SM/upload/45169988404ca30764fa817c9f64fc7b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We all know the benefits of walking – it’s great for your physical and mental well-being. But sometimes, fitting in those recommended 10,000 steps a day can feel like a chore. You might find yourself walking aimlessly around the block, or stuck on a treadmill, bored out of your mind. I've had the same problem, and that's why I decided to build a solution.</p>
<p><strong>The Challenge: Finding a Motivating Walking Routine</strong></p>
<p>Like many, I was advised to aim for at least 10,000 steps daily. The biggest hurdle wasn’t the walking itself, but the lack of a clear, engaging routine. I wanted a way to combine my daily steps with exploring the city, but none of the existing apps seemed to offer what I needed: a path that starts and ends at the same point, creating a looped walking route that would naturally help me hit my goal.</p>
<p><strong>The Solution: 10K Step Path Generator</strong></p>
<p>Frustrated by the lack of options, I decided to create my own. The result is the <strong>10K Step Path Generator</strong>, an open-source app designed to help you achieve your daily step goals by generating personalized looped walking routes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741873243125/3d3f2880-2871-4806-875f-0963272ccb0e.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>How It Works:</strong></p>
<p>The app is designed to make reaching your 10,000 step goal simple and fun:</p>
<ol>
<li><p><strong>Personalized Step Length:</strong> You start by entering your height and gender. The app uses this information to calculate your step length, which ensures that the generated routes are tailored to <em>you</em>.</p>
</li>
<li><p><strong>Looped Path Generation:</strong> Using the <strong>OpenRouteService API</strong>, the app calculates a looped path that starts and ends at your current location. The path length is automatically adjusted based on your desired step count and step length, guaranteeing that you walk the distance you need to reach your goal.</p>
</li>
<li><p><strong>Interactive Map Preview:</strong> The route is displayed on an interactive <strong>Leaflet map</strong>, allowing you to preview the path and terrain. (screenshot)</p>
</li>
<li><p><strong>GPX Download:</strong> For offline navigation, you can download the route as a GPX file, perfect for using with fitness trackers and apps.</p>
</li>
<li><p><strong>Persistent Storage:</strong> Your saved routes are stored locally using IndexedDB, so you can easily revisit your favorite paths.</p>
</li>
</ol>
<p><strong>Key Features:</strong></p>
<ul>
<li><p><strong>Personalized Routes:</strong> Tailored to your step length, for accurate step goal achievement.</p>
</li>
<li><p><strong>Interactive Map:</strong> Preview routes visually before you go.</p>
</li>
<li><p><strong>GPX Export:</strong> Download routes for offline use.</p>
</li>
<li><p><strong>Offline Storage:</strong> Save your favorite routes directly in your browser.</p>
</li>
<li><p><strong>Open Source:</strong> Available on GitHub, feel free to contribute!</p>
</li>
<li><p><strong>Free to use:</strong> The app is available at 10k-steps.itishermann.me</p>
</li>
</ul>
<p><strong>Getting Started:</strong></p>
<ol>
<li><p><strong>Visit the website</strong>: Go to 10k-steps.itishermann.me in your browser.</p>
</li>
<li><p><strong>Enter your height and gender:</strong> The app will calculate your step length.</p>
</li>
<li><p><strong>Set your step goal:</strong> The default is 10,000 steps but you can adjust it as you see fit.</p>
</li>
<li><p><strong>Generate your route:</strong> The app creates a looped walking path tailored to your step goal, starting and ending at your current location.</p>
</li>
<li><p><strong>Preview the route:</strong> Check the map to see where your walk will take you.</p>
</li>
<li><p><strong>Download:</strong> Download the GPX file.</p>
</li>
<li><p><strong>Start walking!</strong> Open the GPX track in your preferred app (I'd personally recommend <a target="_blank" href="https://www.komoot.com/fr-fr">Komoot</a>, they even have a tutorial on <a target="_blank" href="https://support.komoot.com/hc/fr/articles/360022834132-Exporter-et-importer-des-fichiers-GPX">importing a GPX file</a>) and hit the path and start working toward your daily step target.</p>
</li>
</ol>
<p><strong>Tech Stack</strong><br />The app is built using:</p>
<ul>
<li><p><strong>Bun</strong>: a fast JavaScript runtime</p>
</li>
<li><p><strong>Next.js</strong>: for the React framework</p>
</li>
<li><p><strong>Tailwind CSS</strong>: for styling</p>
</li>
<li><p><strong>OpenRouteService API</strong>: for pathfinding</p>
</li>
<li><p><strong>Leaflet.js</strong>: for map visualization</p>
</li>
<li><p><strong>Dexie.js</strong>: for local storage</p>
</li>
</ul>
<p><strong>Why I Built It</strong></p>
<p>I built this app because I believe that fitness should be integrated into daily life, not something you need to force yourself into. By creating a tool that generates enjoyable, looped walking routes, I hope to encourage others to make walking a regular part of their routine.</p>
<p><strong>Open Source and Contributions</strong></p>
<p>The 10K Step Path Generator is open-source, meaning the code is available for anyone to view, use, and contribute to. If you’re interested in seeing how it works under the hood or want to suggest improvements, you can find the project on <a target="_blank" href="https://gitlab.com/itishermann/10k-steps">GitLab</a>.</p>
<p><strong>Join the Movement</strong></p>
<p>Ready to make your 10,000 steps more enjoyable and engaging? Give the 10K Step Path Generator a try and start exploring your city, one loop at a time!</p>
]]></content:encoded></item><item><title><![CDATA[Got Sick of Writing Commit Messages, I Made an LLM Do It for Me]]></title><description><![CDATA[Writing commit messages is one of those tedious tasks every developer must do, but let's face it—most of us would rather be doing almost anything else. After years of struggling to craft meaningful, succinct commit messages, I finally had enough. Tha...]]></description><link>https://itishermann.me/got-sick-of-writing-commit-messages-i-made-an-llm-do-it-for-me</link><guid isPermaLink="true">https://itishermann.me/got-sick-of-writing-commit-messages-i-made-an-llm-do-it-for-me</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[intellij]]></category><category><![CDATA[ollama]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Wed, 26 Jun 2024 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/KPAQpJYzH0Y/upload/255a904a402ed857dcb4063b02ea8ff8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Writing commit messages is one of those tedious tasks every developer must do, but let's face it—most of us would rather be doing almost anything else. After years of struggling to craft meaningful, succinct commit messages, I finally had enough. That's when I decided to create something to do the job for me: the <a target="_blank" href="https://plugins.jetbrains.com/plugin/24734">Ollama Commit Summarizer</a>, an extension that leverages a language model to write commit messages.</p>
<h3 id="heading-the-problem">The Problem</h3>
<p>As any developer will tell you, commit messages are crucial for maintaining a clean and understandable project history. But they can also be a major time sink, especially when you're in "THE zone", and writing that perfect message just feels like an unnecessary interruption. My commit logs were beginning to look like a series of random thoughts hastily jotted down, and it was starting to impact my workflow.</p>
<p><img src="https://media.tenor.com/kjN84Zfd_vIAAAAC/monkey-computer.gif" alt class="image--center mx-auto" /></p>
<h3 id="heading-the-idea">The Idea</h3>
<p>The idea hit me during one of those late-night coding sessions when you're just desperate for a breakthrough. I had been experimenting with language models (LLMs) and their capabilities, and it suddenly struck me—why not use one to generate my commit messages? If an LLM could write essays, articles, and even poetry, surely it could handle something as straightforward as a commit message?</p>
<h3 id="heading-the-creation-of-ollama-commit-summarizer">The Creation of Ollama Commit Summarizer</h3>
<p>With a new sense of purpose, I set out to build the Ollama Commit Summarizer. The extension would analyze the changes made in the code and then generate a concise, relevant commit message. It had to be simple to use, efficient, and most importantly, it had to produce commit messages that made sense.</p>
<p>Built with Kotlin for IntelliJ-based IDEs, this plugin seamlessly integrates into your workflow, providing an automated solution for generating commit messages. You can find the extension on the <a target="_blank" href="https://plugins.jetbrains.com/plugin/24734">JetBrains Plugin Marketplace</a> and the source code on <a target="_blank" href="https://github.com/itishermann/ollama-commit-summarizer">GitHub</a>. The core functionality is built around the language models available through the <a target="_blank" href="https://ollama.com/">Ollama</a> API. The process is straightforward: once you’ve made your changes, you run the summarizer, and it spits out a commit message based on the modifications in your code.</p>
<h3 id="heading-how-it-works">How It Works</h3>
<p>Here's a quick rundown of how Ollama Commit Summarizer works:</p>
<ol>
<li><p><strong>Analyze the Code Changes:</strong> The extension get the diffs in your project to understand what changes were made and format it.</p>
</li>
<li><p><strong>Generate the Commit Message:</strong> It sends this information to the language model, which then crafts a commit message that succinctly describes the changes.</p>
</li>
<li><p><strong>Review and Commit:</strong> You get a suggested commit message, which you can review, tweak if necessary, and then commit.</p>
</li>
</ol>
<p><img src="https://media.tenor.com/IOEsG9ldvhAAAAAC/mr-bean.gif" alt class="image--center mx-auto" /></p>
<h3 id="heading-benefits">Benefits</h3>
<p>Since I started using the Ollama Commit Summarizer, my workflow has improved dramatically. Here are some of the benefits I've noticed:</p>
<ul>
<li><p><strong>Consistency:</strong> The commit messages are consistently clear and informative.</p>
</li>
<li><p><strong>Efficiency:</strong> It saves me time, allowing me to focus more on coding and less on writing.</p>
</li>
<li><p><strong>Ease of Use:</strong> It's incredibly easy to integrate and use within my existing workflow.</p>
</li>
</ul>
<h3 id="heading-conclusion">Conclusion</h3>
<p>Creating the Ollama Commit Summarizer has been a game-changer. It not only solved a persistent annoyance but also leveraged the power of language models in a practical, everyday task. If you're a developer who dreads writing commit messages as much as I did, I highly recommend giving it a try. You might just find that your commit logs become the most well-written part of your codebase!</p>
<p>You can check out and contribute to the project on <a target="_blank" href="https://github.com/itishermann/ollama-commit-summarizer">GitHub</a> and get the extension down bellow. Let's make tedious commit messages a thing of the past!</p>
]]></content:encoded></item><item><title><![CDATA[Boost Your Email Credibility with BIMI DNS Records: A Friendly Guide to Deployment and Verification]]></title><description><![CDATA[In the world of email marketing and communication, standing out and being trustworthy is crucial. But how do you ensure your emails don't get lost in the abyss of spam folders? Enter BIMI DNS records! Buckle up, because we're about to dive into why B...]]></description><link>https://itishermann.me/boost-your-email-credibility-with-bimi-dns-records-a-friendly-guide-to-deployment-and-verification</link><guid isPermaLink="true">https://itishermann.me/boost-your-email-credibility-with-bimi-dns-records-a-friendly-guide-to-deployment-and-verification</guid><category><![CDATA[marketing]]></category><category><![CDATA[mailing]]></category><category><![CDATA[dns]]></category><category><![CDATA[#BIMI]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Fri, 17 May 2024 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Xh6BpT-1tXo/upload/1338eff659c381007c1ed4ea96bf8eb8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the world of email marketing and communication, standing out and being trustworthy is crucial. But how do you ensure your emails don't get lost in the abyss of spam folders? Enter BIMI DNS records! Buckle up, because we're about to dive into why BIMI DNS records are your new best friends and how to deploy and verify them.</p>
<h4 id="heading-what-is-a-bimi-dns-record">What is a BIMI DNS Record?</h4>
<p>BIMI (Brand Indicators for Message Identification) is like giving your email a stylish business card. It allows your brand's logo to appear alongside your emails in recipients' inboxes, adding a layer of authenticity and recognition. Think of it as a digital signature with a personal touch, telling your recipients, "Hey, this email is legit!"</p>
<h4 id="heading-why-are-bimi-dns-records-useful">Why are BIMI DNS Records Useful?</h4>
<ol>
<li><p><strong>Enhanced Brand Recognition</strong>: Your logo in the inbox? Yes, please! BIMI helps your emails stand out and reinforces your brand identity every time someone opens their email.</p>
</li>
<li><p><strong>Increased Trust</strong>: With phishing and spam on the rise, a recognizable logo assures recipients that the email is genuinely from you. It's like having a friendly face greeting them.</p>
</li>
<li><p><strong>Improved Email Deliverability</strong>: Emails with BIMI are less likely to end up in the spam folder. This means more of your emails reach the intended audience, boosting engagement and conversions.</p>
</li>
<li><p><strong>Competitive Edge</strong>: Not everyone is using BIMI yet. By adopting it early, you position your brand as a trustworthy and forward-thinking entity.</p>
</li>
</ol>
<h4 id="heading-how-to-deploy-bimi-dns-records">How to Deploy BIMI DNS Records</h4>
<p>Deploying BIMI might sound technical, but it's simpler than you think. Follow these steps:</p>
<ol>
<li><p><strong>Create a Verified Mark Certificate (VMC)</strong>: A VMC verifies your logo and is a crucial part of the BIMI setup. You can obtain it from authorized providers like DigiCert or Entrust.</p>
</li>
<li><p><strong>Design Your SVG Logo</strong>: Ensure your logo is in SVG format. It needs to be square, less than 32KB in size, and meet BIMI specifications. Check <a target="_blank" href="https://bimigroup.org/svg-conversion-tools-released/">this page</a> for some software that can help you.</p>
</li>
<li><p><strong>Authenticate Your Emails</strong>: Ensure your emails are authenticated using SPF, DKIM, and DMARC. BIMI requires these protocols to be in place.</p>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💰</div>
<div data-node-type="callout-text">The certificates cost around 1 416,00 € / year BUT you can skip getting them BUT many mail providers won't take your BIMI in consideration without a VMC (eg. Gmail)</div>
</div>

<p><img src="https://media.tenor.com/RCBLM9AmJzkAAAAC/dollar-bills-cash.gif" alt class="image--center mx-auto" /></p>
<p><strong>Update Your DNS Record</strong>: Add a new TXT DNS record for BIMI. The format typically looks like this:</p>
<pre><code class="lang-plaintext">default._bimi.yourdomain.com. IN TXT "v=BIMI1; l=https://yourdomain.com/logo.svg; a=https://yourdomain.com/vmc.pem"
</code></pre>
<h4 id="heading-how-to-verify-bimi-dns-records">How to Verify BIMI DNS Records</h4>
<p>After setting up your BIMI DNS records, it's time to verify them:</p>
<ol>
<li><p><strong>Use BIMI Inspector Tools</strong>: Several online tools, like BIMI Inspector, can check if your BIMI records are correctly configured. <a target="_blank" href="https://bimigroup.org/bimi-generator/">This one for example</a>.</p>
</li>
<li><p><strong>Monitor Email Deliverability</strong>: Keep an eye on your email deliverability rates. If you've set up BIMI correctly, you should see an improvement.</p>
</li>
<li><p><strong>Check for Your Logo</strong>: Send test emails to major email providers like Gmail and Yahoo. Your logo should start appearing in the inbox once everything is verified.</p>
</li>
</ol>
<h4 id="heading-final-thoughts">Final Thoughts</h4>
<p>BIMI DNS records are a game-changer for email marketing and communication. By enhancing brand recognition, increasing trust, and improving email deliverability, they ensure your emails get the attention they deserve. Plus, setting them up isn't as daunting as it seems. So, give your emails the VIP treatment and watch your inbox presence shine!</p>
<p>Remember, in the world of email, a little effort goes a long way. So, why not give BIMI a try? Your brand will thank you for it!</p>
]]></content:encoded></item><item><title><![CDATA[Mastering Performance: How to Deploy a Self-Hosted Turbo Repo Remote Cache for Enhanced Project Builds]]></title><description><![CDATA[Turbocharging Your Development Workflow
In the bustling world of software development, efficiency isn't just a luxury—it's as essential as a morning croissant in Paris. Enter Turbo Repo, a nimble build system tailored for JavaScript and TypeScript mo...]]></description><link>https://itishermann.me/mastering-performance-how-to-deploy-a-self-hosted-turbo-repo-remote-cache-for-enhanced-project-builds</link><guid isPermaLink="true">https://itishermann.me/mastering-performance-how-to-deploy-a-self-hosted-turbo-repo-remote-cache-for-enhanced-project-builds</guid><category><![CDATA[turborepo]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Thu, 16 May 2024 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/HvvC-2egUrg/upload/cf607248c08f706ea61fe06573e1cfcb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Turbocharging Your Development Workflow</strong></p>
<p>In the bustling world of software development, efficiency isn't just a luxury—it's as essential as a morning croissant in Paris. Enter Turbo Repo, a nimble build system tailored for JavaScript and TypeScript monorepos that's as swift as a scooter weaving through the Marais district. While Turbo Repo natively supports various remote caching solutions, opting for a self-hosted remote cache is like having your own private café—complete control, no queue, and the coffee is always brewed to your liking. Let's embark on a journey to deploy your very own self-hosted Turbo Repo remote cache, using the resourceful open-source solution from <a target="_blank" href="https://github.com/ThibautMarechal/turborepo-remote-cache">ThibautMarechal's GitHub repository</a>. So, tighten your beret and let's dive in, because just like the Paris Metro at rush hour, development waits for no one! (nah i'm kidding the beret is overhyped, who wears a beret in paris ? Only tourists...please don't do that 💀)</p>
<p><strong>Why Self-Host a Turbo Repo Remote Cache?</strong></p>
<p>A remote cache serves as a central hub where build artifacts are stored. This means subsequent builds can fetch these artifacts instead of rebuilding them, significantly cutting down build times and improving developer productivity. By hosting your own remote cache, you gain:</p>
<ul>
<li><p><strong>Full control over your data:</strong> Keep your sensitive information within your infrastructure.</p>
</li>
<li><p><strong>Customized scaling options:</strong> Scale your caching needs according to your project size and team requirements.</p>
</li>
<li><p><strong>Reduced external dependencies:</strong> Minimize downtime and latency issues associated with third-party services.</p>
</li>
</ul>
<p>Last but not least, you get the <code>Full Turbo</code>. Isn't this beautiful ? 😎🤌🏾</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741874482793/ab21dd9d-d3bb-487a-98b3-9725ce420b81.png" alt="Example of cached turbo build task" class="image--center mx-auto" /></p>
<p><strong>Step 1: Preparing Your Environment</strong></p>
<p>Before you get started with your self-hosted Turbo Repo remote cache, ensure Docker is installed on your server. Docker provides a consistent environment for your cache, making it a bit more reliable. If Docker is not yet installed, you can find installation instructions for various platforms on the official <a target="_blank" href="https://docs.docker.com/engine/install/">Docker website</a>.</p>
<p><strong>Step 2: Deploying the compose file</strong></p>
<p><strong>Create a Project Directory</strong>: Start by creating a dedicated directory on your server for your project. This directory will house all necessary files, including your Docker Compose configuration. You can create it with a command like:</p>
<pre><code class="lang-bash">mkdir turborepo-cache
<span class="hljs-built_in">cd</span> turborepo-cache
</code></pre>
<p><strong>Prepare the Docker Compose File</strong>: Inside your new project directory, create a Docker Compose YAML file named <code>docker-compose.yml</code>. Paste the Docker Compose content provided earlier into this file. This content defines your PostgreSQL database and Turbo Repo remote cache service. Here's a simplified reminder of what to include:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span>
<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">trrc-cache:</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">db:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:15.5</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">turborepo</span>
      <span class="hljs-attr">PGDATA:</span> <span class="hljs-string">/var/lib/postgresql/data/pgdata</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./data/postgres/:/var/lib/postgresql/data</span>
  <span class="hljs-attr">turborepo-remote-cache:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">thibmarechal/turborepo-remote-cache:1.13.0</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">db</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8080:8080"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">PORT:</span> <span class="hljs-number">8080</span>
      <span class="hljs-attr">STORAGE_TYPE:</span> <span class="hljs-string">fs</span>
      <span class="hljs-attr">STORAGE_FS_PATH:</span> <span class="hljs-string">/data</span>
      <span class="hljs-attr">COOKIE_NOT_SECURE:</span> <span class="hljs-string">'true'</span>
      <span class="hljs-attr">DATABASE_URL:</span> <span class="hljs-string">postgres://postgres:postgres@db:5432/turborepo</span>
      <span class="hljs-attr">ADMIN_USERNAME:</span> <span class="hljs-string">admin</span>
      <span class="hljs-attr">ADMIN_NAME:</span> <span class="hljs-string">admin</span>
      <span class="hljs-attr">ADMIN_PASSWORD:</span> <span class="hljs-string">change-me-with-a-long-ahh-password</span>
      <span class="hljs-attr">ADMIN_EMAIL:</span> <span class="hljs-string">you@you-mail-provider.com</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./data/fs:/data</span>
</code></pre>
<p><strong>Configuration</strong>: All the configurations can be applied through the environment of the <code>turborepo-remote-cache</code> service.</p>
<ul>
<li><p><strong>PORT</strong>: The port on which the application will run.</p>
</li>
<li><p><strong>STORAGE_TYPE</strong>: The type of storage to use (<code>fs</code>, <code>s3</code>, <code>azure</code>).</p>
</li>
<li><p><strong>STORAGE_FS_PATH</strong>: The path where to store the cache when using file storage.</p>
</li>
<li><p><strong>STORAGE_S3_ACCESS_KEY_ID</strong>: Access key ID for Amazon S3 storage.</p>
</li>
<li><p><strong>STORAGE_S3_SECRET_ACCESS_KEY</strong>: Secret access key for Amazon S3 storage.</p>
</li>
<li><p><strong>STORAGE_S3_FORCE_PATH_STYLE</strong>: Boolean to force path style for S3 requests.</p>
</li>
<li><p><strong>STORAGE_S3_ENDPOINT</strong>: Endpoint URL for S3 storage.</p>
</li>
<li><p><strong>STORAGE_S3_REGION</strong>: Region for S3 storage.</p>
</li>
<li><p><strong>STORAGE_S3_SSL_ENABLED</strong>: Boolean to enable SSL for S3 storage.</p>
</li>
<li><p><strong>STORAGE_S3_BUCKET</strong>: Bucket name for S3 storage.</p>
</li>
<li><p><strong>STORAGE_AZURE_STORAGE_ACCOUNT</strong>: Account name for Azure Blob storage.</p>
</li>
<li><p><strong>STORAGE_AZURE_STORAGE_ACCESS_KEY</strong>: Access key for Azure Blob storage.</p>
</li>
<li><p><strong>STORAGE_AZURE_STORAGE_CONTAINER</strong>: Container name for Azure Blob storage.</p>
</li>
<li><p><strong>DATABASE_URL</strong>: Connection URL for the Postgres database.</p>
</li>
<li><p><strong>ADMIN_USERNAME</strong>: Username for the admin account.</p>
</li>
<li><p><strong>ADMIN_NAME</strong>: Name for the admin account.</p>
</li>
<li><p><strong>ADMIN_PASSWORD</strong>: Password for the admin account.</p>
</li>
<li><p><strong>ADMIN_EMAIL</strong>: Email for the admin account.</p>
</li>
<li><p><strong>OIDC</strong>: Boolean to enable OpenID Connect (OIDC) authentication.</p>
</li>
<li><p><strong>OIDC_NAME</strong>: Name for the OIDC provider.</p>
</li>
<li><p><strong>OIDC_AUTHORIZATION_URL</strong>: Authorization URL for the OIDC provider.</p>
</li>
<li><p><strong>OIDC_TOKEN_URL</strong>: Token URL for the OIDC provider.</p>
</li>
<li><p><strong>OIDC_CLIENT_ID</strong>: Client ID for the OIDC provider.</p>
</li>
<li><p><strong>OIDC_CLIENT_SECRET</strong>: Client secret for the OIDC provider.</p>
</li>
<li><p><strong>OIDC_PROFILE_URL</strong>: Profile URL for the OIDC provider.</p>
</li>
<li><p><strong>AZURE_AD</strong>: Boolean to enable Azure Active Directory authentication.</p>
</li>
<li><p><strong>AZURE_AD_CLIENT_ID</strong>: Client ID for Azure AD authentication.</p>
</li>
<li><p><strong>AZURE_AD_CLIENT_SECRET</strong>: Client secret for Azure AD authentication.</p>
</li>
<li><p><strong>AZURE_AD_TENANT_ID</strong>: Tenant ID for Azure AD authentication.</p>
</li>
<li><p><strong>COOKIE_NOT_SECURE</strong>: <code>false</code> if serving over <code>https</code> otherwise <code>true</code>.</p>
</li>
</ul>
<p><strong>Launch the Services</strong>: With your <code>docker-compose.yml</code> ready, use Docker Compose to build and start your services. Execute the following command from within your project directory:</p>
<pre><code class="lang-bash">docker compose up -d
</code></pre>
<p>This command runs your containers in detached mode, meaning they'll continue running in the background.</p>
<p><strong>Step 3: Integrating with Your Turbo Repo</strong></p>
<p>Let's make your Turbo Repo projects use your new self-hosted cache. It's easy, just run this command at the root of your Turbo Repo</p>
<pre><code class="lang-bash">turbo login --login <span class="hljs-string">"https://[your-turbo-repo-link]/turbo/login"</span> --api <span class="hljs-string">"https://[your-turbo-repo-link]/turbo/api"</span>
</code></pre>
<p>This command log you in but the repo is not linked yet, to do so you have to run this command. You will be prompted to choose a team for the project.</p>
<pre><code class="lang-bash">turbo link
</code></pre>
<p>These commands also create a config file at <code>.turbo/config.json</code>. It is not advised to commit this file.</p>
<p>By doing so, your builds will know exactly where to drop off and pick up their cached artifacts, similar to how Parisians know their way around the métro - but way quicker AND never late compared to the métro.</p>
<p><img src="https://media.tenor.com/tm4JV1Qs15AAAAAC/the-interview-james-franco.gif" alt /></p>
<p>Parisian metro and Turbo repo are the same but different</p>
<p><strong>Your Build Process, Streamlined</strong></p>
<p>Congratulations! Your Dockerized self-hosted Turbo Repo remote cache is now set up and running, akin to a well-oiled TGV on its way to the Marseille (but still faster). This setup not only optimizes your build times but does so with the elegance and efficiency that would make any developer puff out their chest like a proud Parisian pigeon. Enjoy the accelerated development cycle, enhanced control, and the delightful fact that your builds are now more reliable. Bonne continuation, and may your code compile smoothly.</p>
]]></content:encoded></item><item><title><![CDATA[Implementing OAuth with Expo and Authentik in TypeScript]]></title><description><![CDATA[Bonjour and welcome to the world of app security! Are you ready to secure your app as if it were the Louvre museum? Today, we're diving into setting up OAuth authentication using Expo, Authentik, and a sprinkle of TypeScript, all served on a silver p...]]></description><link>https://itishermann.me/implementing-oauth-with-expo-and-authentik-in-typescript</link><guid isPermaLink="true">https://itishermann.me/implementing-oauth-with-expo-and-authentik-in-typescript</guid><category><![CDATA[Expo]]></category><category><![CDATA[React Native]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[authentication]]></category><category><![CDATA[oauth]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Tue, 23 Apr 2024 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/lmFJOx7hPc4/upload/c7f7e807b151f4c82a535bbf4952c249.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Bonjour and welcome to the world of app security! Are you ready to secure your app as if it were the Louvre museum? Today, we're diving into setting up OAuth authentication using Expo, Authentik, and a sprinkle of TypeScript, all served on a silver platter of modern development techniques. Let’s get started, and as the French say, "Allons-y!"</p>
<h4 id="heading-project-initialization-with-typescript-template-and-pnpm">Project Initialization with TypeScript Template and PNPM</h4>
<p>First things first, let's set up our project. We'll use PNPM for its efficiency in handling node modules like a Parisian handling a croissant—carefully and effectively.</p>
<p>Create a new Expo project with the TypeScript template:</p>
<pre><code class="lang-bash">pnpx create-expo-app -t expo-template-blank-typescript
</code></pre>
<p>Install PNPM if you haven't:</p>
<pre><code class="lang-bash">npm install -g pnpm
</code></pre>
<p>Voilà! You have laid the foundation of your project.</p>
<h4 id="heading-installing-dependencies">Installing Dependencies</h4>
<p>To handle authentication, we'll need a few packages. Let’s install them like we’re stocking up before a French strike—better safe than sorry!</p>
<pre><code class="lang-bash">pnpm expo install expo-auth-session expo-secure-store
</code></pre>
<p>These will help us manage authentication sessions and securely store our tokens.</p>
<h4 id="heading-autodiscovery-with-authentik">AutoDiscovery with Authentik</h4>
<p>Authentik supports OpenID Connect (OIDC) auto-discovery, making it easier than a Monday morning in Parisian metro to configure. Here’s how to set it up:</p>
<ol>
<li><p>Create an app on your Authentik server</p>
</li>
<li><p>Ensure that your Authentik server URL is correct and the app slug is correct.</p>
</li>
</ol>
<p>In your project, create a configuration for Authentik:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// useAuth.ts</span>
<span class="hljs-keyword">import</span> { useAutoDiscovery } <span class="hljs-keyword">from</span> <span class="hljs-string">'expo-auth-session'</span>;

<span class="hljs-keyword">const</span> AuthentikClientId = <span class="hljs-string">'your-client-id-here'</span>;
<span class="hljs-keyword">const</span> AuthentikAppUrl = <span class="hljs-string">'https:/[Your authentik domain]/application/o/[Your authentik app slug]'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useAuth</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> discovery = useAutoDiscovery(AuthentikAppUrl);

  <span class="hljs-comment">/* ... */</span>
}
</code></pre>
<p>💡</p>
<p>Do not include the whole well-known part in the url, the library appends it</p>
<h4 id="heading-making-the-request">Making the Request</h4>
<p>When everything is set up, making the request is as simple as ordering a café au lait:</p>
<pre><code class="lang-tsx">// App.tsx
import { makeRedirectUri, useAutoDiscovery, useAuthRequest } from 'expo-auth-session';

const redirectUri = makeRedirectUri();
const AuthentikClientId = 'your-client-id-here';
const AuthentikAppUrl = 'https:/[Your authentik domain]/application/o/[Your authentik app slug]';

export default function App() {
  const discovery = useAutoDiscovery(AuthentikAppUrl);

    // To retrieve the redirect URL, add this to the callback URL list
  // of your Authentik application.
  // console.log(`Redirect URL: ${redirectUri}`);

    const [request, result, promptAsync] = useAuthRequest(
    {
      redirectUri,
      clientId: AuthentikClientId,
      // id_token will return a JWT token
      responseType: "id_token",
      // retrieve the user's profile
      scopes: ["openid", "profile"],
      usePKCE: true,
      extraParams: {
        // ideally, this will be a random value
        nonce: "nonce",
      },
    },
    discovery
  );

  /* ... */
}
</code></pre>
<h4 id="heading-saving-the-token">Saving the Token</h4>
<p>Once you receive the token, store it securely like a secret recipe for the perfect baguette:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// token.ts</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SecureStore <span class="hljs-keyword">from</span> <span class="hljs-string">'expo-secure-store'</span>;
<span class="hljs-keyword">import</span> { TokenResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-auth-session"</span>;

<span class="hljs-keyword">const</span> AUTH_TOKEN_KEY = <span class="hljs-string">'AUTH_TOKEN_KEY'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveToken</span>(<span class="hljs-params">token: TokenResponse</span>) </span>{
  <span class="hljs-keyword">await</span> SecureStore.setItemAsync(AUTH_TOKEN_KEY,<span class="hljs-built_in">JSON</span>.stringify(token));
}
</code></pre>
<pre><code class="lang-ts"><span class="hljs-comment">// useAuth.ts</span>
<span class="hljs-keyword">import</span> { useAutoDiscovery, makeRedirectUri, useAuthRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-auth-session"</span>;
<span class="hljs-keyword">import</span> { Alert } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { useCallback, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { saveToken } <span class="hljs-keyword">from</span> <span class="hljs-string">"./token"</span>;

<span class="hljs-keyword">const</span> redirectUri = makeRedirectUri();
<span class="hljs-keyword">const</span> AuthentikClientId = <span class="hljs-string">'your-client-id-here'</span>;
<span class="hljs-keyword">const</span> AuthentikAppUrl = <span class="hljs-string">'https:/[Your authentik domain]/application/o/[Your authentik app slug]'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useAuth</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> discovery = useAutoDiscovery(AuthentikAppUrl);

    <span class="hljs-comment">// To retrieve the redirect URL, add this to the callback URL list</span>
  <span class="hljs-comment">// of your Authentik application.</span>
  <span class="hljs-comment">// console.log(`Redirect URL: ${redirectUri}`);</span>

    <span class="hljs-keyword">const</span> [request, result, promptAsync] = useAuthRequest(
    {
      redirectUri,
      clientId: AuthentikClientId,
      <span class="hljs-comment">// id_token will return a JWT token</span>
      responseType: <span class="hljs-string">"id_token"</span>,
      <span class="hljs-comment">// retrieve the user's profile</span>
      scopes: [<span class="hljs-string">"openid"</span>, <span class="hljs-string">"profile"</span>],
      usePKCE: <span class="hljs-literal">true</span>,
      extraParams: {
        <span class="hljs-comment">// ideally, this will be a random value</span>
        nonce: <span class="hljs-string">"nonce"</span>,
      },
    },
    discovery
  );

    <span class="hljs-keyword">const</span> handleResult = useCallback(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span>(!result) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">if</span> (result.type === <span class="hljs-string">'error'</span>) {
      Alert.alert(
        <span class="hljs-string">"Authentication error"</span>,
        result.params.error_description || <span class="hljs-string">"something went wrong"</span>
      );
      <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">if</span> (result.type === <span class="hljs-string">"success"</span>) {
      <span class="hljs-keyword">if</span>(!result.authentication || !result.authentication) <span class="hljs-keyword">return</span>;
      saveToken(result.authentication);
    }
  }, [result]);

  useEffect(<span class="hljs-function">()=&gt;</span> {
        handleResult();
  }, [handleResult]);
  <span class="hljs-comment">/* ... */</span>
}
</code></pre>
<h4 id="heading-decoding-the-token-to-get-claims">Decoding the Token to Get Claims</h4>
<p>Decoding the token to retrieve claims is like decoding the French menu in a fancy restaurant—necessary to understand what you’re getting:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// token.ts</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SecureStore <span class="hljs-keyword">from</span> <span class="hljs-string">'expo-secure-store'</span>;
<span class="hljs-keyword">import</span> { jwtDecode, <span class="hljs-keyword">type</span> JwtPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'jwt-decode'</span>;

<span class="hljs-keyword">const</span> AUTH_TOKEN_KEY = <span class="hljs-string">'AUTH_TOKEN_KEY'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveToken</span>(<span class="hljs-params">token: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">await</span> SecureStore.setItemAsync(AUTH_TOKEN_KEY, token);
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Claims <span class="hljs-keyword">extends</span> JwtPayload {
  name: <span class="hljs-built_in">string</span>;
  preferred_username: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  nickname: <span class="hljs-built_in">string</span>;
  groups: <span class="hljs-built_in">string</span>[];
  given_name: <span class="hljs-built_in">string</span>;
}


<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> decodeToken = <span class="hljs-function">(<span class="hljs-params">token: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> decoded = jwtDecode&lt;Claims&gt;(token);
  <span class="hljs-keyword">return</span> decoded;
};
</code></pre>
<p>But wait jwt-decode relies on <code>atob</code> but react-native doesn't have it so lets fix this. Let's install <code>base-64</code> and polyfill ourselves.</p>
<pre><code class="lang-bash">pnpm add base-64 &amp;&amp; pnpm add --save-dev @types/base-64
</code></pre>
<p>Let's update our files</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// token.ts</span>
<span class="hljs-keyword">import</span> { decode } <span class="hljs-keyword">from</span> <span class="hljs-string">"base-64"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SecureStore <span class="hljs-keyword">from</span> <span class="hljs-string">'expo-secure-store'</span>;
<span class="hljs-keyword">import</span> { jwtDecode, <span class="hljs-keyword">type</span> JwtPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'jwt-decode'</span>;

<span class="hljs-keyword">const</span> AUTH_TOKEN_KEY = <span class="hljs-string">'AUTH_TOKEN_KEY'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveToken</span>(<span class="hljs-params">token: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">await</span> SecureStore.setItemAsync(AUTH_TOKEN_KEY, token);
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Claims <span class="hljs-keyword">extends</span> JwtPayload {
  name: <span class="hljs-built_in">string</span>;
  preferred_username: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  nickname: <span class="hljs-built_in">string</span>;
  groups: <span class="hljs-built_in">string</span>[];
  given_name: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">decodeToken</span>(<span class="hljs-params">token: <span class="hljs-built_in">string</span></span>) </span>{
    <span class="hljs-keyword">const</span> originalAtoB = <span class="hljs-built_in">global</span>.atob;
    <span class="hljs-built_in">global</span>.atob = decode; <span class="hljs-comment">// Dangerous overload</span>
    <span class="hljs-keyword">const</span> decoded = jwtDecode&lt;Claims&gt;(token);
    <span class="hljs-built_in">global</span>.atob = originalAtoB;
    <span class="hljs-keyword">return</span> decoded;
}
</code></pre>
<p>☠️</p>
<p>Always remember that overloading globals yourself is never a good idea. If some other libs rely on the said global, you're cooked. We do this only for science and you should never do this on production. Never ever....<br />You should maybe call your api and handle the token decode server side</p>
<pre><code class="lang-tsx">// useAuth.ts
import { useAutoDiscovery, makeRedirectUri, useAuthRequest } from "expo-auth-session";
import { Alert } from "react-native";
import { useCallback, useEffect, useState } from "react";
import { decodeToken, getToken, type Claims, saveToken } from "./token";

const redirectUri = makeRedirectUri();
const AuthentikClientId = 'your-client-id-here';
const AuthentikAppUrl = 'https:/[Your authentik domain]/application/o/[Your authentik app slug]';

export default function useAuth() {
    /* ... */
    const handleResult = useCallback(() =&gt; {
    if(!result) return;
    if (result.type === 'error') {
      Alert.alert(
        "Authentication error",
        result.params.error_description || "something went wrong"
      );
      return;
    }
    if (result.type === "success") {
            if(!result.authentication || !result.authentication.idToken) return;
      // Retrieve the JWT token and decode it
      const jwtToken = result.authentication.idToken;
      const decoded = decodeToken(jwtToken);
      setClaims(decoded);
            saveToken(result.authentication);
    }
  }, [result]);

  useEffect(()=&gt; {
        handleResult();
  }, [handleResult]);

  /* ... */

}
</code></pre>
<h4 id="heading-retrieving-and-checking-the-token">Retrieving and Checking the Token</h4>
<p>Fetching the token and checking its validity is like checking if the cheese has matured to perfection:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// token.ts</span>
<span class="hljs-comment">/* ... */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getToken</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">TokenResponse</span> | <span class="hljs-title">null</span>&gt; </span>{
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> SecureStore.getItemAsync(AUTH_TOKEN_KEY);
    <span class="hljs-keyword">if</span> (!result) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> TokenResponse(<span class="hljs-built_in">JSON</span>.parse(result));
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Claims <span class="hljs-keyword">extends</span> JwtPayload {
  name: <span class="hljs-built_in">string</span>;
  preferred_username: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  nickname: <span class="hljs-built_in">string</span>;
  groups: <span class="hljs-built_in">string</span>[];
  given_name: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">decodeToken</span>(<span class="hljs-params">token: <span class="hljs-built_in">string</span></span>) </span>{
    <span class="hljs-keyword">const</span> originalAtoB = <span class="hljs-built_in">global</span>.atob;
    <span class="hljs-built_in">global</span>.atob = decode; <span class="hljs-comment">// Dangerous overload</span>
    <span class="hljs-keyword">const</span> decoded = jwtDecode&lt;Claims&gt;(token);
  <span class="hljs-built_in">global</span>.atob = originalAtoB;
  <span class="hljs-keyword">return</span> decoded;
}
</code></pre>
<pre><code class="lang-tsx">// useAuth.ts
import { useAutoDiscovery, makeRedirectUri, useAuthRequest } from "expo-auth-session";
import { Alert } from "react-native";
import { useCallback, useEffect, useState } from "react";
import { decodeToken, getToken, type Claims, saveToken } from "./token";

const redirectUri = makeRedirectUri();
const AuthentikClientId = 'your-client-id-here';
const AuthentikAppUrl = 'https:/[Your authentik domain]/application/o/[Your authentik app slug]';

export default function useAuth() {
    /* .... */
    const retrieveTokenOnMount = useCallback(async () =&gt; {
        const token = await getToken();
        if (!token || !token.idToken) return;
        if(token.shouldRefresh()){
          // refresh if required
            await token.refreshAsync({
                ...token.getRequestConfig(),
                clientId: AuthentikClientId,
            },
                {
                    tokenEndpoint: discovery?.tokenEndpoint
                }
            );
        }
        const decoded = decodeToken(token.idToken);
        setClaims(decoded);
    }, [discovery]);

    useEffect(() =&gt; {
        retrieveTokenOnMount();
    }, [retrieveTokenOnMount]);

    /* .... */
}
</code></pre>
<h4 id="heading-logging-out-and-revoking-the-token">Logging Out and Revoking the Token</h4>
<p>Finally, logging out and revoking the token should be as dramatic as a French exit—silent but effective:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// useAuth.ts</span>
<span class="hljs-keyword">import</span> { useAutoDiscovery, makeRedirectUri, useAuthRequest, revokeAsync } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-auth-session"</span>;
<span class="hljs-keyword">import</span> { Alert } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { useCallback, useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { decodeToken, getToken, <span class="hljs-keyword">type</span> Claims, saveToken, removeToken } <span class="hljs-keyword">from</span> <span class="hljs-string">"./token"</span>;
<span class="hljs-keyword">import</span> { openBrowserAsync } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-web-browser"</span>;

<span class="hljs-keyword">const</span> redirectUri = makeRedirectUri();
<span class="hljs-keyword">const</span> AuthentikClientId = <span class="hljs-string">'your-client-id-here'</span>;
<span class="hljs-keyword">const</span> AuthentikAppUrl = <span class="hljs-string">'https:/[Your authentik domain]/application/o/[Your authentik app slug]'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useAuth</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">/* ... */</span>
    <span class="hljs-keyword">const</span> revoke = <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">const</span> token = <span class="hljs-keyword">await</span> getToken();
        <span class="hljs-keyword">if</span>(!token) <span class="hljs-keyword">return</span>;
        <span class="hljs-keyword">await</span> revokeAsync(
            {
                ...token.getRequestConfig(),
                token: token.accessToken,
                clientId: AuthentikClientId,
            },
            {
                revocationEndpoint: discovery?.revocationEndpoint,
            }
        );
        setClaims(<span class="hljs-literal">null</span>);
        <span class="hljs-keyword">await</span> removeToken();
        <span class="hljs-keyword">if</span>(discovery?.endSessionEndpoint) {
            <span class="hljs-keyword">await</span> openBrowserAsync(
                discovery?.endSessionEndpoint
            );
        }
    };

}
</code></pre>
<h3 id="heading-wrap-it-up">Wrap it up</h3>
<p>Now write a little app file with ui</p>
<pre><code class="lang-tsx">// App.tsx
import { Button, StyleSheet, Text, View } from "react-native";
import useAuth from "./useAuth";
import { Claims } from "./token";

export default function App() {
  const { claims,request, promptAsync,revoke } = useAuth();
  return (
    &lt;View style={styles.container}&gt;
      {claims ? (
        &lt;&gt;
          &lt;Text style={styles.title}&gt;You are logged in!&lt;/Text&gt;
          &lt;Text style={styles.title}&gt;JWT claims:&lt;/Text&gt;
          {Object.keys(claims).map((key) =&gt; (
            &lt;Text key={key}&gt;
              {key}: {claims[key as keyof Claims]}
            &lt;/Text&gt;
          ))}
          &lt;Button title="Log out" onPress={revoke} /&gt;
        &lt;/&gt;
      ) : (
        &lt;Button
          disabled={!request}
          title="Log in"
          onPress={() =&gt; promptAsync()}
        /&gt;
      )}
    &lt;/View&gt;
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  title: {
    fontSize: 20,
    textAlign: "center",
    marginTop: 40,
  },
});
</code></pre>
<h4 id="heading-conclusion">Conclusion</h4>
<p>And there you have it! You've just secured your app with OAuth using Expo, Authentik, and TypeScript, all while maintaining that effortless French chic. Now your app's security is as robust as the French spirit—unyielding and sophisticated. Enjoy coding, and remember, as they say in France, "Plus ça change, plus c'est la même chose" — the more things change, the more they stay the same, especially in coding standards!<br />The whole source code is available on <a target="_blank" href="https://gitlab.com/blog-itishermann-me/expo-authentik-oidc">Gitlab</a></p>
]]></content:encoded></item><item><title><![CDATA[Advanced TypeScript Error Handling with neverthrow: An E-commerce Example]]></title><description><![CDATA[In this tutorial, we'll explore how to handle errors in a TypeScript application using the neverthrow library, focusing on a practical e-commerce scenario. Specifically, we'll write a service to check product availability and place an order, handling...]]></description><link>https://itishermann.me/advanced-typescript-error-handling-with-neverthrow-an-e-commerce-example</link><guid isPermaLink="true">https://itishermann.me/advanced-typescript-error-handling-with-neverthrow-an-e-commerce-example</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[error handling]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Mon, 22 Apr 2024 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/XWar9MbNGUY/upload/b3eab4c70c6f47c56e9dfff7c706b012.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this tutorial, we'll explore how to handle errors in a TypeScript application using the <code>neverthrow</code> library, focusing on a practical e-commerce scenario. Specifically, we'll write a service to check product availability and place an order, handling potential errors in a type-safe manner similar to Rust's error handling.</p>
<h2 id="heading-initial-setup">Initial Setup</h2>
<p>Let's set up our environment with TypeScript and the necessary domain-driven design elements. First, we'll define the models and interfaces that represent the domain model, and import the necessary packages like <code>neverthrow</code> and <code>luxon</code> for date handling.</p>
<h3 id="heading-importing-required-libraries">Importing Required Libraries</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'@domain/product/Product'</span>;
<span class="hljs-keyword">import</span> { InventoryRepositoryInterface } <span class="hljs-keyword">from</span> <span class="hljs-string">'@domain/inventory/InventoryRepositoryInterface'</span>;
<span class="hljs-keyword">import</span> { Order, OrderDetails } <span class="hljs-keyword">from</span> <span class="hljs-string">'@domain/order/Order'</span>;
<span class="hljs-keyword">import</span> { OrderRepositoryInterface } <span class="hljs-keyword">from</span> <span class="hljs-string">'@domain/order/OrderRepositoryInterface'</span>;
<span class="hljs-keyword">import</span> { DateTime } <span class="hljs-keyword">from</span> <span class="hljs-string">'luxon'</span>;
<span class="hljs-keyword">import</span> { err, ok, Result } <span class="hljs-keyword">from</span> <span class="hljs-string">'neverthrow'</span>;
<span class="hljs-keyword">import</span> { makeTaggedUnion, none, MemberType } <span class="hljs-keyword">from</span> <span class="hljs-string">'safety-match'</span>;
</code></pre>
<h3 id="heading-defining-input-and-error-types">Defining Input and Error Types</h3>
<p>Next, we define the input for our service and the types of errors that could occur:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> PlaceOrderInput {
  productId: <span class="hljs-built_in">string</span>;
  quantity: <span class="hljs-built_in">number</span>;
  orderDate: DateTime;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> PlaceOrderError = makeTaggedUnion({
  ProductNotFound: none,
  InsufficientStock: none,
  OrderSaveFailed: none,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> PlaceOrderError = MemberType&lt;<span class="hljs-keyword">typeof</span> PlaceOrderError&gt;;
</code></pre>
<p>In this setup, <code>PlaceOrderInput</code> specifies the information required to place an order, and <code>PlaceOrderError</code> defines possible error scenarios.</p>
<h3 id="heading-implementing-the-service">Implementing the Service</h3>
<p>Now, let's implement the service. We'll use the <code>neverthrow</code> library to manage different outcomes of operations explicitly, ensuring that every function call that might fail is clear in its intent and handling.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> OrderService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> inventoryRepository: InventoryRepositoryInterface,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> orderRepository: OrderRepositoryInterface
  </span>) {}

  <span class="hljs-keyword">async</span> placeOrder(input: PlaceOrderInput): <span class="hljs-built_in">Promise</span>&lt;Result&lt;Order, PlaceOrderError&gt;&gt; {
    <span class="hljs-comment">// Check product availability</span>
    <span class="hljs-keyword">const</span> stockLevel = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.inventoryRepository.getStockLevel(input.productId);
    <span class="hljs-keyword">if</span> (stockLevel === <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> err(PlaceOrderError.ProductNotFound);
    <span class="hljs-keyword">if</span> (stockLevel &lt; input.quantity) <span class="hljs-keyword">return</span> err(PlaceOrderError.InsufficientStock);

    <span class="hljs-comment">// Create and save the order</span>
    <span class="hljs-keyword">const</span> orderDetails: OrderDetails = {
      productId: input.productId,
      quantity: input.quantity,
      orderDate: input.orderDate
    };
    <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">new</span> Order(orderDetails);
    <span class="hljs-keyword">const</span> saveResult = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.orderRepository.saveOrder(order);

    <span class="hljs-keyword">if</span> (!saveResult) <span class="hljs-keyword">return</span> err(PlaceOrderError.OrderSaveFailed);
    <span class="hljs-keyword">return</span> ok(order);
  }
}
</code></pre>
<h4 id="heading-explanation-of-service-logic">Explanation of Service Logic:</h4>
<ol>
<li><p><strong>Check Product Availability</strong>: The service first checks if the product is available and in sufficient quantity. It returns early with an appropriate error if not.</p>
</li>
<li><p><strong>Place the Order</strong>: If the product is available, the service creates an order. It attempts to save this order using the order repository.</p>
</li>
<li><p><strong>Error Handling</strong>: If saving the order fails, an error is returned. Otherwise, the successfully placed order is returned.</p>
</li>
</ol>
<h2 id="heading-benefits-of-this-approach">Benefits of This Approach</h2>
<p>Using <code>neverthrow</code> provides several advantages:</p>
<ul>
<li><p><strong>Explicit Error Handling</strong>: Each function's outcome, whether success or failure, is clearly defined through its return type.</p>
</li>
<li><p><strong>Type Safety</strong>: Errors are part of the type system, forcing developers to handle them explicitly, reducing the chance of unhandled exceptions.</p>
</li>
<li><p><strong>Improved Maintainability</strong>: Having a clear and predictable error handling strategy makes the code easier to maintain and debug.</p>
</li>
</ul>
<p>This method aligns with modern best practices in TypeScript application development, where enhancing clarity, type safety, and robustness are key priorities. By structuring your error handling in this detailed manner, your codebase becomes more predictable and less prone to runtime errors.<br />To conclude, while this tutorial showcases an approach to structured and type-safe error handling in TypeScript using the <code>neverthrow</code> library, I must credit the initial inspiration to my colleague, <a target="_blank" href="https://www.linkedin.com/in/%F0%9F%96%A5%EF%B8%8F-killian-guibout-chatelain-1623421a2">Killian</a>. His insights and innovative ideas significantly influenced the development of these error handling techniques, demonstrating the power of collaboration in software development.</p>
]]></content:encoded></item><item><title><![CDATA[What's the weather? Building an Weather app on iOS with SwiftUI]]></title><description><![CDATA[In this article, we will present you the process behind the design of this application that we named What's the weather? First, we will see the features of the application, why we chose SwiftUI and not storyboard, then we will see the architecture of...]]></description><link>https://itishermann.me/whats-the-weather-building-an-weather-app-on-ios-with-swiftui</link><guid isPermaLink="true">https://itishermann.me/whats-the-weather-building-an-weather-app-on-ios-with-swiftui</guid><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[OpenWeather API]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Mon, 22 Apr 2024 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741877195930/ca48c208-a56b-4eae-8269-67c77f0f334f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, we will present you the process behind the design of this application that we named What's the weather? First, we will see the features of the application, why we chose SwiftUI and not storyboard, then we will see the architecture of the application and how it works, and the different services we had to use. At the end of this article, there is a demo of the application.</p>
<h2 id="heading-features"><strong>Features</strong></h2>
<p>When we launched the module, we received a clear and concise specification. From it, we were able to extract the basic functionalities which are the following:</p>
<ul>
<li><p>Display the weather of a city with the weather according to the time, the 5 days forecast, the wind speed and direction, the temperature in celsius and Fahrenheit, the minimum and maximum temperatures as well as the pressure and humidity</p>
</li>
<li><p>Search for a city</p>
</li>
<li><p>Save and delete a city</p>
</li>
<li><p>List cities Beyond these features, we have chosen to implement geolocation and autocompletion when searching for a city.</p>
</li>
</ul>
<h2 id="heading-why-swiftui"><strong>Why SwiftUI?</strong></h2>
<p>Storyboard is very attractive when you start in iOS development because of the interface builder that allows you to design entire interfaces in drag and drop. By searching a little, we could notice very quickly that what often came back is the complexity induced by the use of Storyboard when the project becomes bigger. Having tried the Interface Builder, our first impression was that understanding it is like understanding Photoshop, there are so many tabs and buttons that you get lost. The interaction between the code and the storyboard is complicated. A string match will be used many times to link the code to the storyboard. In case there is a spelling error in the string, the application crashes during execution and not during compilation. Since we are using git, storyboard changes are complicated to track. Since the storyboards are not written in human-readable code, resolving merge conflicts is extremely difficult. After these misadventures with Storyboard, we tried SwiftUI, the first difference is that the interface is declarative so no more need for string matching to link the interface to the code. This implies that we will not be able to try to make a call to a deleted function that was linked by a string. Animations are easier to implement and above all, the application can be cross-platform (on all Apple platforms) because SwiftUI adapts the interface to the platform. Moreover, problems are detected at compile time and not during execution. Nevertheless, we had to make some concessions. SwiftUI is only usable from iOS 13.0. The community around SwiftUI is quite young and it is currently difficult to find help.</p>
<h2 id="heading-architecture"><strong>Architecture</strong></h2>
<p>For the architecture of our project, we chose to start with MVVM (Model View ViewModel) because we all had some basic knowledge of MVC architecture but we wanted to avoid code with interdependencies. We split our code into 4 main parts:</p>
<ul>
<li><p>Models (structs)</p>
</li>
<li><p>The abstractions of the API calls (TeleportApi and OpenWeatherApi)</p>
</li>
<li><p>The ViewModels (classes that contain all the logic of the application)</p>
</li>
<li><p>The Views which are the components defining the interface of the application</p>
</li>
</ul>
<p>The Views retrieve the data from the ViewModels which contain all the logic. The ViewModels execute the abstractions of the API calls to retrieve the data. The models are used to store the data retrieved from the API calls. Moreover, we followed an <a target="_blank" href="https://developer.apple.com/tutorials/app-dev-training">Apple tutorial</a> where it was recommended to have only one data source to avoid unpacking between different data sources. So we grouped all the functions that allow creating, modification, or deleting a city in a single class that we named CityStore and grouped the functions that allow us to search a city in another class named CitySearchViewModel. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741876846447/d3551ffc-2eeb-4503-8399-7e87373407cc.png" alt class="image--center mx-auto" /></p>
<p>MVVM Pattern illustration - <a target="_blank" href="https://github.com/ahmedeltaher/MVVM-Kotlin-Android-Architecture">source</a> We store the list of cities in an array except for the city where the user is geolocated. We write this list in a file which is then loaded at the start of the application.</p>
<h2 id="heading-how-it-works"><strong>How it works</strong></h2>
<p>Since we have a data persistence layer in the application, when we launch it, we check if the data file exists, if it doesn't exist or if there was an error reading it, we continue running the application. At the same time, we ask for authorization to use the geolocation. The home screen of the application is the screen displaying the city at the user's location. From there we slide from one screen to another to display the cities. We have a navigation bar at the bottom of the screen with a button to access the city management screen and another to access a weather map embedded in a WebView. On the city management screen, there is a list of cities, we can delete them and reorganize them. In addition to that, you can change the unit of measurement of the application. We can switch from imperial to metric units. There is a search bar that allows you to search for a city, visualize the weather at this position and add it to the list of saved cities.</p>
<h2 id="heading-services-and-libraries-used"><strong>Services and libraries used</strong></h2>
<p>To retrieve the weather, we used the <a target="_blank" href="https://openweathermap.org/api/one-call-3">OpenWeather</a> API which provides data on several days and also hour by hour. On the side of the auto-completion and the search of the city based on latitude and longitude, we used the <a target="_blank" href="https://developers.teleport.org/api/">Teleport API</a>. For animations based on the current weather, we used the <a target="_blank" href="https://github.com/airbnb/lottie-ios">Lottie</a> library. Finally, for HTTP requests, we used the <a target="_blank" href="https://github.com/Alamofire/Alamofire">Alamofire</a> library.</p>
<h2 id="heading-demo"><strong>Demo</strong></h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://gitlab.com/etna-projects/whats-the-weather/-/raw/11be2b6bd923a647ff5538358c5f779711040163/demo.mp4">https://gitlab.com/etna-projects/whats-the-weather/-/raw/11be2b6bd923a647ff5538358c5f779711040163/demo.mp4</a></div>
<p> </p>
<h2 id="heading-source-code"><strong>Source code</strong></h2>
<p>The source code is available on <a target="_blank" href="https://gitlab.com/etna-projects/whats-the-weather">GitLab</a>. To launch the project you have to get an API key from <a target="_blank" href="https://openweathermap.org/api">OpenWeather</a> and add it to <a target="_blank" href="https://gitlab.com/etna-projects/whats-the-weather/-/blob/11be2b6bd923a647ff5538358c5f779711040163/what's%20the%20weather/Networkings/OpenWeatherApi.swift#L13"><code>/Networkings/OpenWeatherApi.swift</code></a></p>
<p>I hope this helped you understand how Gryzle is born. If you have any questions, <a target="_blank" href="http://localhost:63342/markdownPreview/1644057310/markdown-preview-index-401463811.html?_ijt=26e17baoptiesi5j1a232pqpjr#">please send me a little message</a>. I will be happy to help you. If you liked this article, please show some love and share it with your friends. Thank you for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Snowtrust Closure Announcement: End of Service Details]]></title><description><![CDATA[Hello everyone, Long story short, i can no longer develop or maintain this service. It has become impossible due to my personal time constraints and studies. Thank you all who gave Snowtrust a shot and provided me with priceless feedback and educatio...]]></description><link>https://itishermann.me/snowtrust-closure-announcement-end-of-service-details</link><guid isPermaLink="true">https://itishermann.me/snowtrust-closure-announcement-end-of-service-details</guid><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Mon, 22 Apr 2024 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741877293948/f8358e17-4afb-42d2-bc84-ced09811e3d7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello everyone, Long story short, i can no longer develop or maintain this service. It has become impossible due to my personal time constraints and studies. Thank you all who gave Snowtrust a shot and provided me with priceless feedback and education.</p>
<h2 id="heading-what-will-happen"><strong>What will happen?</strong></h2>
<p>All servers, volumes, IPs and DNS records will be deleted on <strong>2023 April 1st</strong>. To prevent data loss I sent you an email with:</p>
<ul>
<li><p>All your invoices</p>
</li>
<li><p>All your SEO reports</p>
</li>
<li><p>All your case studies (if you have some)</p>
</li>
<li><p>All your source code and backups of your website or web app</p>
</li>
<li><p>All your database backups</p>
</li>
</ul>
<h2 id="heading-whats-next"><strong>What's next?</strong></h2>
<p>While Snowtrust will be soon gone, there are plenty of web agencies you can reach out to today. To name a few:</p>
<ul>
<li><p><a target="_blank" href="https://studio-lumia.fr/">Studio Lumia</a> providing you unbelievable photo-shoots</p>
</li>
<li><p>Wordpress hosting provided by <a target="_blank" href="https://cloud.studio-lumia.fr/">Cloud Lumia</a></p>
</li>
<li><p>Marketing strategy provided by <a target="_blank" href="https://etre-commercant.fr/">Etre commerçant</a></p>
</li>
<li><p>All in one marketing agency <a target="_blank" href="https://make-by-web.fr/">Make By Web</a></p>
</li>
</ul>
<p>In case of any questions feel free to reach out to me</p>
<p>Best, Hermann</p>
]]></content:encoded></item><item><title><![CDATA[Mastering SwiftUI Gauge: Create Custom Gauges in iOS 16]]></title><description><![CDATA[SwiftUI includes a new view named Gauge for displaying progress in iOS 16. It is used to display values inside a range. Let's look at how to use the Gauge view and deal with different gauge styles in this tutorial. But first let's take a look at Appl...]]></description><link>https://itishermann.me/mastering-swiftui-gauge-create-custom-gauges-in-ios-16</link><guid isPermaLink="true">https://itishermann.me/mastering-swiftui-gauge-create-custom-gauges-in-ios-16</guid><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Mon, 22 Apr 2024 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741877593861/828b9caf-1bdd-4b19-ace3-686bd20a12cd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>SwiftUI includes a new view named <code>Gauge</code> for displaying progress in iOS 16. It is used to display values inside a range. Let's look at how to use the Gauge view and deal with different gauge styles in this tutorial. But first let's take a look at Apple's official definition for the Gauge view.</p>
<blockquote>
<p>A gauge is a view that shows a current level of a value in relation to a specified finite capacity, very much like a fuel gauge in an automobile. Gauge displays are configurable; they can show any combination of the gauge’s current value, the range the gauge can display, and a label describing the purpose of the gauge itself. <a target="_blank" href="https://developer.apple.com/documentation/swiftui/gauge">Apple Developper documentation</a></p>
</blockquote>
<h2 id="heading-simplest-gauge"><strong>Simplest Gauge</strong></h2>
<p>The simplest way to use the Gauge view is to pass a value to the <code>init</code> method. In it's most basic form, the value of the gauge is in <code>0.0</code> to <code>1.0</code> range.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Most basic usage of the Gauge view</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Gauge</span>(value: <span class="hljs-number">0.5</span>) {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"Gauge"</span>)
        }
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741877699215/481a3d23-843b-4ce2-bb4d-7c78f4f6fbe6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-gauge-with-a-range"><strong>Gauge with a range</strong></h2>
<p>You can also make the Gauge view display a value as a percentage of a range. The value must be between the range's minimum and maximum values. The Gauge view will display the value as a percentage of the range.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Gauge view displaying a value as a percentage of a range</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Gauge</span>(value: <span class="hljs-number">25</span>, <span class="hljs-keyword">in</span>: <span class="hljs-number">0</span>...<span class="hljs-number">100</span>) {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"25% Gauge"</span>)
        }
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741877817235/5ef77393-c391-4287-b154-f101a999cfc5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-gauge-styles"><strong>Gauge styles</strong></h2>
<p>You can customize the Gauge view by passing a <code>GaugeStyle</code> to the <code>gaugeStyle</code> modifier. The <code>GaugeStyle</code> protocol defines the appearance of the Gauge view. There are two default styles provided by SwiftUI: <code>LinearGaugeStyle</code> and <code>RadialGaugeStyle</code>. The available gaugeStyles are:</p>
<ul>
<li><p><code>.accessoryCircularCapacity</code></p>
</li>
<li><p><code>.accessoryCircular</code></p>
</li>
<li><p><code>.accessoryLinearCapacity</code></p>
</li>
<li><p><code>.accessoryLinear</code></p>
</li>
<li><p><code>.linearCapacity</code></p>
</li>
<li><p><code>.automatic</code></p>
</li>
</ul>
<pre><code class="lang-swift"><span class="hljs-comment">// Gauge view displaying a value as a percentage of a range</span>
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
        <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
            <span class="hljs-type">Gauge</span>(value: <span class="hljs-number">25</span>, <span class="hljs-keyword">in</span>: <span class="hljs-number">0</span>...<span class="hljs-number">100</span>) {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"25% Gauge"</span>)
            }
        }
    }
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741877882912/36adf6a0-0696-4baa-9cef-cac0273a2e60.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-custom-gauge-style"><strong>Custom Gauge style</strong></h2>
<p>You can also create your own custom gauge style by conforming to the <code>GaugeStyle</code> protocol. The <code>GaugeStyle</code> protocol defines the appearance of the Gauge view.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Custom GaugeStyle</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">CustomGaugeStyle</span>: <span class="hljs-title">GaugeStyle</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">makeBody</span><span class="hljs-params">(configuration: GaugeStyleConfiguration)</span></span> -&gt; some <span class="hljs-type">View</span> {
        <span class="hljs-type">ZStack</span> {
            <span class="hljs-type">Circle</span>()
                .stroke(<span class="hljs-type">Color</span>.gray, lineWidth: <span class="hljs-number">10</span>)
                .frame(width: <span class="hljs-number">100</span>, height: <span class="hljs-number">100</span>)
            <span class="hljs-type">Circle</span>()
                .trim(from: <span class="hljs-number">0</span>, to: <span class="hljs-type">CGFloat</span>(configuration.value))
                .stroke(<span class="hljs-type">Color</span>.blue, lineWidth: <span class="hljs-number">10</span>)
                .frame(width: <span class="hljs-number">100</span>, height: <span class="hljs-number">100</span>)
                .rotationEffect(<span class="hljs-type">Angle</span>(degrees: -<span class="hljs-number">90</span>))
            <span class="hljs-type">Text</span>(<span class="hljs-string">"\(Int(configuration.value * 100))%"</span>)
                .font(.title)
        }
    }
}

<span class="hljs-comment">// Custom GaugeStyle usage</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Gauge</span>(value: <span class="hljs-number">0.5</span>) {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"Gauge"</span>)
        }
        .gaugeStyle(<span class="hljs-type">CustomGaugeStyle</span>())
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741877911705/dae76aa3-9194-41af-958a-2f8edfdb8b7a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-our-temperature-gauge"><strong>Our Temperature Gauge</strong></h2>
<p>And finally let's make a nice looking temperature gauge. We will use the <code>accessoryLinear</code> gauge style and a nice gradient <code>tint</code> modifier.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TemperatureGaugeView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> <span class="hljs-built_in">min</span>: <span class="hljs-type">Double</span> = <span class="hljs-number">7.0</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> <span class="hljs-built_in">max</span>: <span class="hljs-type">Double</span> = <span class="hljs-number">22.6</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> current: <span class="hljs-type">Double</span> = <span class="hljs-number">12.6</span>
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Gauge</span>(value: current, <span class="hljs-keyword">in</span>: <span class="hljs-built_in">min</span>...<span class="hljs-built_in">max</span>) {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"Tinted Gauge"</span>)
        }
        currentValueLabel: {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"\(Int(current))°"</span>)
        }
        minimumValueLabel: {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"\(Int(min))°"</span>)
        } maximumValueLabel: {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"\(Int(max))°"</span>)
        }
            .tint(<span class="hljs-type">Gradient</span>(colors: [.green, .yellow, .orange, .red]))
            .gaugeStyle(.accessoryLinear)
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741878000290/dc7c355d-c633-4dd1-8f89-11113024dff9.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>I hope this tutorial helped you understand how to use the Gauge view in SwiftUI. If you have any questions, please send me a little message, I will be happy to help you. If you liked this tutorial, please share it with your friends. Thank you for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Secure Your SSH Keys: Correct Permissions for Enhanced Security]]></title><description><![CDATA[I recently got an error on my work machine while restoring a backup of my ssh keys. The error stated that the permissions were too open for my private key.
Permissions 0777 for '/home/<user>/.ssh/id_rsa' are too open.

Following an article from Stack...]]></description><link>https://itishermann.me/secure-your-ssh-keys-correct-permissions-for-enhanced-security</link><guid isPermaLink="true">https://itishermann.me/secure-your-ssh-keys-correct-permissions-for-enhanced-security</guid><category><![CDATA[ssh-keys]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Mon, 22 Apr 2024 10:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I recently got an error on my work machine while restoring a backup of my ssh keys. The error stated that the permissions were too open for my private key.</p>
<pre><code class="lang-bash">Permissions 0777 <span class="hljs-keyword">for</span> <span class="hljs-string">'/home/&lt;user&gt;/.ssh/id_rsa'</span> are too open.
</code></pre>
<p>Following an article from Stackoverflow i've set the access rights of all the files in the .ssh folder to 600. This solved the issue but is actually not better, because each file has it’s own permission for a reason. We set read only permission on these files for security reasons. Some are even changed by your ssh client and need a different permission due to that.</p>
<h2 id="heading-fixing-these-permissions"><strong>Fixing these permissions</strong></h2>
<p>These commands can help you get your permissions right</p>
<pre><code class="lang-bash">chown -R <span class="hljs-variable">$USER</span>:<span class="hljs-variable">$USER</span> ~/.ssh
chmod 700 ~/.ssh
chmod 644 ~/.ssh/authorized_keys
chmod 644 ~/.ssh/known_hosts
chmod 644 ~/.ssh/config
chmod 600 ~/.ssh/id*
chmod 644 ~/.ssh/id*.pub
</code></pre>
<p>You may need to use sudo to avoid errors.</p>
<p>I hope this helped you 😉. If you have any questions, please send me a little message. I will be happy to help you. If you liked this tutorial, please share it with your friends. Thank you for reading!</p>
]]></content:encoded></item><item><title><![CDATA[Enhancing React Frontend Architecture with Domain-Driven Design and TypeScript Using Neverthrow]]></title><description><![CDATA[Domain-Driven Design (DDD) offers a strategic approach to developing complex software systems, emphasizing a deep focus on the core domain and its logic. When applied to frontend development, particularly with React and TypeScript, DDD facilitates a ...]]></description><link>https://itishermann.me/enhancing-react-frontend-architecture-with-domain-driven-design-and-typescript-using-neverthrow</link><guid isPermaLink="true">https://itishermann.me/enhancing-react-frontend-architecture-with-domain-driven-design-and-typescript-using-neverthrow</guid><category><![CDATA[React]]></category><category><![CDATA[DDD]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Hermann Kao]]></dc:creator><pubDate>Wed, 21 Feb 2024 11:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Domain-Driven Design (DDD) offers a strategic approach to developing complex software systems, emphasizing a deep focus on the core domain and its logic. When applied to frontend development, particularly with React and TypeScript, DDD facilitates a well-organized codebase that is easy to manage and scales gracefully. By using TypeScript along with Neverthrow, a library for handling errors in a functional way, developers can further enhance the robustness and maintainability of their applications.</p>
<h3 id="heading-use-case-online-e-commerce-store"><strong>Use Case: Online E-commerce Store</strong></h3>
<p>In this article, we'll implement DDD in a React project for an online e-commerce store. This store includes features like product listings, a shopping cart, user profiles, and order management.</p>
<h4 id="heading-folder-structure"><strong>Folder Structure</strong></h4>
<p>A clear folder structure helps separate the domain logic from application logic, adhering to DDD principles:</p>
<pre><code class="lang-plaintext">/src
  /domain
    /product
      product.ts
      productService.ts
    /cart
      cart.ts
      cartService.ts
    /user
      user.ts
      userService.ts
  /infrastructure
    /api
      productApi.ts
      userApi.ts
      cartApi.ts
  /ui
    /components
      ProductList.tsx
      Cart.tsx
      UserProfile.tsx
    /pages
      HomePage.tsx
      ProductPage.tsx
      CartPage.tsx
</code></pre>
<h4 id="heading-domain-model-example"><strong>Domain Model Example</strong></h4>
<p>Below is a TypeScript example of a domain model for a product:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/domain/product/product.ts</span>
<span class="hljs-keyword">import</span> { err, ok, Result } <span class="hljs-keyword">from</span> <span class="hljs-string">'neverthrow'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Product {
    id: <span class="hljs-built_in">string</span>;
    name: <span class="hljs-built_in">string</span>;
    price: <span class="hljs-built_in">number</span>;
    description: <span class="hljs-built_in">string</span>;
    imageUrl: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Product {
    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
        <span class="hljs-keyword">public</span> id: <span class="hljs-built_in">string</span>,
        <span class="hljs-keyword">public</span> name: <span class="hljs-built_in">string</span>,
        <span class="hljs-keyword">public</span> price: <span class="hljs-built_in">number</span>,
        <span class="hljs-keyword">public</span> description: <span class="hljs-built_in">string</span>,
        <span class="hljs-keyword">public</span> imageUrl: <span class="hljs-built_in">string</span>
    </span>) {}

    applyDiscount(discountPercentage: <span class="hljs-built_in">number</span>): Result&lt;<span class="hljs-built_in">void</span>, <span class="hljs-built_in">Error</span>&gt; {
        <span class="hljs-keyword">if</span> (discountPercentage &lt;= <span class="hljs-number">0</span> || discountPercentage &gt; <span class="hljs-number">100</span>) {
            <span class="hljs-keyword">return</span> err(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid discount percentage'</span>));
        }
        <span class="hljs-built_in">this</span>.price = <span class="hljs-built_in">this</span>.price * (<span class="hljs-number">1</span> - discountPercentage / <span class="hljs-number">100</span>);
        <span class="hljs-keyword">return</span> ok();
    }
}
</code></pre>
<h4 id="heading-services"><strong>Services</strong></h4>
<p>Services manage the business logic related to domain entities. Here’s how a product service might look using Neverthrow for better error handling:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/domain/product/productService.ts</span>
<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'./product'</span>;
<span class="hljs-keyword">import</span> { productApi } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../infrastructure/api/productApi'</span>;
<span class="hljs-keyword">import</span> { Result } <span class="hljs-keyword">from</span> <span class="hljs-string">'neverthrow'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ProductService {
    <span class="hljs-keyword">async</span> getAllProducts(): <span class="hljs-built_in">Promise</span>&lt;Result&lt;Product[], <span class="hljs-built_in">Error</span>&gt;&gt; {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> productApi.fetchProducts();
    }

    <span class="hljs-keyword">async</span> getProductById(id: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;Result&lt;Product, <span class="hljs-built_in">Error</span>&gt;&gt; {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> productApi.fetchProductById(id);
    }
}
</code></pre>
<h3 id="heading-product-api-with-neverthrow"><strong>Product API with Neverthrow</strong></h3>
<p>Now, let's implement the <code>productApi</code> with error handling using Neverthrow:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/infrastructure/api/productApi.ts</span>
<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../domain/product/product'</span>;
<span class="hljs-keyword">import</span> { err, ok, Result } <span class="hljs-keyword">from</span> <span class="hljs-string">'neverthrow'</span>;

<span class="hljs-keyword">const</span> API_URL = <span class="hljs-string">'https://api.yourdomain.com/products'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> productApi = {
    <span class="hljs-keyword">async</span> fetchProducts(): <span class="hljs-built_in">Promise</span>&lt;Result&lt;Product[], <span class="hljs-built_in">Error</span>&gt;&gt; {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`<span class="hljs-subst">${API_URL}</span>/`</span>, {
                method: <span class="hljs-string">'GET'</span>,
                headers: {
                    <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>
                }
            });
            <span class="hljs-keyword">if</span> (!response.ok) {
                <span class="hljs-keyword">return</span> err(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to fetch products'</span>));
            }
            <span class="hljs-keyword">const</span> products: Product[] = <span class="hljs-keyword">await</span> response.json();
            <span class="hljs-keyword">return</span> ok(products);
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-keyword">return</span> err(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Network error occurred while fetching products'</span>));
        }
    },

    <span class="hljs-keyword">async</span> fetchProductById(id: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;Result&lt;Product, <span class="hljs-built_in">Error</span>&gt;&gt; {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`<span class="hljs-subst">${API_URL}</span>/<span class="hljs-subst">${id}</span>`</span>, {
                method: <span class="hljs-string">'GET'</span>,
                headers: {
                    <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>
                }
            });
            <span class="hljs-keyword">if</span> (!response.ok) {
                <span class="hljs-keyword">return</span> err(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Failed to fetch product with id <span class="hljs-subst">${id}</span>`</span>));
            }
            <span class="hljs-keyword">const</span> product: Product = <span class="hljs-keyword">await</span> response.json();
            <span class="hljs-keyword">return</span> ok(product);
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-keyword">return</span> err(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Network error occurred while fetching product with id <span class="hljs-subst">${id}</span>`</span>));
        }
    }
};
</code></pre>
<h3 id="heading-conclusion"><strong>Conclusion</strong></h3>
<p>Incorporating Domain-Driven Design into your React applications using TypeScript and Neverthrow can significantly improve the structure, scalability, and error handling of your codebase. By aligning your software development with business needs,</p>
<p>DDD helps deliver solutions that are not only technically sound but also strategically focused. Start using these principles and tools to build robust and business-oriented frontend applications.</p>
<p>Happy coding!</p>
]]></content:encoded></item></channel></rss>