<?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[Will Braun's Blog]]></title><description><![CDATA[I explore the vast web development landscape and share my thoughts here.]]></description><link>https://blog.willbraun.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/640947ad4b4818e3636f79dc/64d9692d-c635-416f-8261-2b4d374d2913.jpg</url><title>Will Braun&apos;s Blog</title><link>https://blog.willbraun.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Apr 2026 01:35:31 GMT</lastBuildDate><atom:link href="https://blog.willbraun.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[7 Practical Tips for Building Your MVP]]></title><description><![CDATA[Last summer, I built an MVP called Unison for a friend of mine. The app is an online community for women to share health-related experiences and connect with others. I was honored to be asked to build]]></description><link>https://blog.willbraun.dev/7-practical-tips-for-building-your-mvp</link><guid isPermaLink="true">https://blog.willbraun.dev/7-practical-tips-for-building-your-mvp</guid><category><![CDATA[mvp]]></category><category><![CDATA[supabase]]></category><category><![CDATA[solopreneur ]]></category><category><![CDATA[freelance]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Will Braun]]></dc:creator><pubDate>Tue, 07 Apr 2026 12:18:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/640947ad4b4818e3636f79dc/dce13377-840f-4129-ad7d-64d130ba3f53.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last summer, I built an MVP called <a href="https://unisoncommunity.com">Unison</a> for a friend of mine. The app is an online community for women to share health-related experiences and connect with others. I was honored to be asked to build it, and a bit daunted by the task. A blank slate with unknown unknowns waiting to derail me around every corner. My experience in product management helped me stay on track and I learned a lot along the way, so I figured I'd share some highlights. If you're a solo developer or founder, this post is for you.</p>
<h1>1. Feedback is gold</h1>
<p>It's a catch-22. You need user feedback to build your MVP, but it's tough for someone to conceptualize how your non-existent app could help them, so you need to create an MVP to show them. User feedback is the best roadmap for your app, so it's best to seek it out early and often. This fact will drive many of the other decisions we make when developing an MVP. It's a cycle to clarify an idea from a hunch, to a list of pain points, to a list of requirements, mockups, and finally the MVP features in code. The more accurate of a starting point you have the better, so reach out to prospective users early.</p>
<h1>2. Don't skip mockups</h1>
<p>Before writing a line of code, you should have a clear visual representation of the app. The fidelity of the mockups isn't super important in my opinion. The real power is that the mockups are relatively quick to make and adjust and can be shared easily to facilitate communication between stakeholders. Everyone has assumptions, and it's so much easier to address conflicts early when you can easily drag boxes around.</p>
<p>Molding fuzzy ideas into boxes and buttons will bring up critical, unforeseen questions. It's worth spending time on mockups to discover these, rather than when you're deep in the code. These issues can be fixed by moving a box around or changing a layout in a drawing, rather than refactoring a large section of the app which wastes time and could lower quality since this critical item is being shoehorned in. Requirement changes get more expensive at every stage of development. The more that has been built, the more that may need to change.</p>
<p>If you're building MVPs often or need a high-fidelity mockup, I'd recommend learning <a href="https://www.figma.com/">Figma</a>. Being able to import components from your chosen UI library and modify layouts and spacing easily is pretty handy. <a href="https://app.diagrams.net/">Draw.io</a> is also a great tool if you want something simpler to drag and drop boxes around.</p>
<h1>3. Choose a framework that lets you ship quickly</h1>
<p>When you're building a first version, speed matters. Getting to a point where you can show off the app to stakeholders and receive feedback is the primary goal. You don't want to spend months building something and realize it doesn't meet users' needs. Better to find that out as soon as possible if that's the case.</p>
<p>AI-assisted development has become the norm, so pick a framework that AI models write good code with. At the time I was building Unison, React was clearly the strongest choice. I use both React and Svelte, and while I prefer Svelte, I noticed AI models occasionally producing outdated or made-up Svelte syntax. Since leveraging AI is mandatory for fast development speed, I went with React, specifically Next.js hosted on Vercel. Test how models perform with your chosen framework, since they're improving and changing rapidly.</p>
<h1>4. Use a backend-as-a-service</h1>
<p>Building and maintaining a custom backend from scratch adds a lot of complexity and risk to your MVP timeline. Backend-as-a-service (BAAS) platforms bundle hosting, database, APIs, auth, and other services in one place, letting you focus on the custom business logic that actually delivers value to users.</p>
<p>I used <a href="https://supabase.com/">Supabase</a> for Unison. It provides a hosted PostgreSQL database with a user-friendly JavaScript client, and CRUD APIs are included out of the box. That means no time spent developing, testing, and debugging common read/write operations and no server setup. And if the app takes off, the underlying infrastructure scales without a full rebuild.</p>
<h1>5. Skip frontend unit tests — for now</h1>
<p>This is a deliberate tradeoff, not an oversight. In an early-stage product, the frontend changes too frequently to justify the cost of maintaining tests. Wait until it stabilizes.</p>
<p>Bugs that frontend unit tests would catch, like a component not rendering or props handled incorrectly, are easy to spot quickly through manual testing. Your time is better spent shipping.</p>
<p>That said, I'd still recommend unit tests for backend functionality, since that's where your core logic lives. Our initial users are doing us a favor by testing an early-stage app, and we don't want to waste their time with bugs in the most important features. BAAS platforms like Supabase help here too. Their APIs are already tested, so you only need to cover your custom backend behavior, like Supabase Edge Functions.</p>
<h1>6. Don't rush estimating</h1>
<p>My toxic development trait is that often I hear a feature request and think it'll be straightforward, even if I haven't done it before. It's easy to get caught in the excitement of building something new and then forget that it needs to play nice with all of the other, slightly less shiny features. A lot of the pitfalls don't come from the feature itself, but how it fits into the existing system. Scoping a feature in a vacuum is a recipe for regret.</p>
<p>I recently added Google authentication to Unison and underestimated the complexity. Connecting to Google was simple, but refactoring the existing login and signup logic blew up the scope. The signup page became too cramped on mobile and needed to be split into two steps, which then introduced the problem of half-completed profiles. This spurred some discussion about the step order and overall messaging for the best UX.</p>
<p>Before giving an estimate, think through edge cases and which other parts of the app will need to change. And as always, add a buffer for unforeseen issues bound to arise.</p>
<h1>7. Beware of scope creep</h1>
<p>More features sound good on paper, but it comes at a cost. It obviously takes more work to build new features, and there is a higher risk if features are added to the scope mid-project. There may not be sufficient time to properly plan the work before the deadline, leading to lower quality work or pushing the deadline out.</p>
<p>Creating a plan that locks down the major requirements early, and treating new major features as a separate decision with its own cost and timeline is the best way to handle this. Not all feedback is scope creep. The goal during an MVP build is to receive feedback and iterate on the app. That feedback could come from the users, the business owner, or other stakeholders. This is what we want.</p>
<p>Feedback about the major features already in development can typically be included, but additional features outside of the plan should prompt a discussion about how to handle the expanded scope. Maybe you push out the timeline, add it to the next phase of the project, or swap out a lower priority feature. Scope creep didn't become an issue in my project, mainly because we had a plan in place. It gave us a framework to communicate the constraints of the project when new ideas started to enter the picture.</p>
<h1>Conclusion</h1>
<p>There are many ways to build a software MVP, and hopefully some of these ideas resonated. The main idea across all of them to get feedback as fast as possible. If you disagree with any of my tips, leave a comment and I've love to discuss it.</p>
]]></content:encoded></item><item><title><![CDATA[My Experience Upgrading to Svelte 5]]></title><description><![CDATA[Svelte 5 is out! It's packed with new features and I've heard good reviews so far, but I hadn’t had a chance to dive in until now. I've spent the last year building a SvelteKit web app using Svelte 4 (racquetrivals.com, come play). For comparison, th...]]></description><link>https://blog.willbraun.dev/upgrading-to-svelte-5</link><guid isPermaLink="true">https://blog.willbraun.dev/upgrading-to-svelte-5</guid><category><![CDATA[Svelte]]></category><category><![CDATA[migration]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Runes]]></category><dc:creator><![CDATA[Will Braun]]></dc:creator><pubDate>Mon, 25 Nov 2024 14:01:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732540649881/d6637277-a3f4-418d-bf37-1af00b0582ad.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Svelte 5 is out! It's packed with new features and I've heard good reviews so far, but I hadn’t had a chance to dive in until now. I've spent the last year building a SvelteKit web app using Svelte 4 (<a target="_blank" href="http://racquetrivals.com">racquetrivals.com</a>, come play). For comparison, the app currently has 9 pages and ~15 other components.</p>
<p>In this post, I'll share the steps I took to upgrade to Svelte 5 and what I learned along the way. Here is the official <a target="_blank" href="https://svelte.dev/docs/svelte/v5-migration-guide">migration guide</a> for all the details on the upgrade.</p>
<h1 id="heading-steps">Steps</h1>
<h2 id="heading-1-create-a-new-git-branch">1. Create a New Git Branch</h2>
<pre><code class="lang-sh">git switch -c svelte-5-migration
</code></pre>
<h2 id="heading-2-use-the-svelte-5-migration-scripthttpssveltedevdocssveltev5-migration-guidemigration-script">2. Use the <a target="_blank" href="https://svelte.dev/docs/svelte/v5-migration-guide#Migration-script">Svelte 5 Migration Script</a></h2>
<p>This script prompts you to choose which directories to migrate, automatically converts Svelte components in those directories to the Svelte 5 syntax, and updates the core dependencies in your <code>package.json</code>.</p>
<pre><code class="lang-sh">npx sv migrate svelte-5
</code></pre>
<p>At first, the script didn't execute correctly and warned me that I had invalid HTML in my project. Apparently, wrapping an <code>&lt;a&gt;</code> tag around a <code>&lt;tr&gt;</code> tag isn't a valid way to add a hyperlink to a table row. 🤔 I thought this was interesting as it always worked and I hadn't been warned before, but I went ahead, refactored, and committed the changes to clear my working directory. After that, the script ran successfully.</p>
<p>Note - You can also try migrating one component at a time in VS Code from the command palette <code>Cmd+Shift+P</code> with the command <code>Migrate Component to Svelte 5 Syntax</code> to get more control. I couldn't get this to work for me and went with the script, but it's worth looking into.</p>
<h2 id="heading-3-update-dependencies">3. Update Dependencies</h2>
<pre><code class="lang-sh">npm install
</code></pre>
<p>It's necessary to run <code>npm install</code> afterwards to update dependencies based on your new <code>package.json</code> to ensure everything still plays nice together. I got some errors saying I needed to update other packages so I did, but that didn't resolve the errors right away. I deleted my <code>node_modules</code> directory and my <code>package-lock.json</code> , then ran <code>npm install</code> again so they could be built from scratch, and that fixed my issues.</p>
<h2 id="heading-4-review-updated-components">4. Review Updated Components</h2>
<p>After my components had updated, VS Code still thought the new Svelte 5 syntax was invalid for some reason. I restarted VS Code and the errors went away, so you may need to do the same.</p>
<p>Here is part of my <code>ResetPassword.svelte</code> component script tag before the migration.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> email = <span class="hljs-string">''</span>
<span class="hljs-keyword">let</span> showEmailValidation = <span class="hljs-literal">false</span>
<span class="hljs-keyword">let</span> error = <span class="hljs-string">''</span>
<span class="hljs-keyword">let</span> loading = <span class="hljs-literal">false</span>
<span class="hljs-keyword">let</span> success = <span class="hljs-literal">false</span>
<span class="hljs-keyword">let</span> buttonRef: HTMLButtonElement

<span class="hljs-attr">$</span>: disabled = loading || showEmailValidation
<span class="hljs-attr">$</span>: <span class="hljs-keyword">if</span> (buttonRef &amp;&amp; !showEmailValidation) {
    buttonRef.disabled = <span class="hljs-literal">false</span>
    buttonRef.focus()
}
</code></pre>
<p>And here it is after</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> email = $state(<span class="hljs-string">''</span>)
<span class="hljs-keyword">let</span> showEmailValidation = $state(<span class="hljs-literal">false</span>)
<span class="hljs-keyword">let</span> error = $state(<span class="hljs-string">''</span>)
<span class="hljs-keyword">let</span> loading = $state(<span class="hljs-literal">false</span>)
<span class="hljs-keyword">let</span> success = $state(<span class="hljs-literal">false</span>)
<span class="hljs-keyword">let</span> buttonRef: HTMLButtonElement = $state()

<span class="hljs-keyword">let</span> disabled = $derived(loading || showEmailValidation)
run(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (buttonRef &amp;&amp; !showEmailValidation) {
        buttonRef.disabled = <span class="hljs-literal">false</span>
        buttonRef.focus()
    }
})
</code></pre>
<p>The new Svelte 5 rune syntax has been added so the code more explicitly shows our state with the <code>$state()</code> rune, derived state with <code>$derived()</code>, and effects with <code>run</code>. The <code>$:</code> syntax previously allowed for both derived state and side effects to run reactively when their dependencies changed, but if the migration script couldn't reliably migrate the statement to <code>$derived()</code>, it replaced it with <code>run</code> instead.</p>
<p>This is a stopgap solution that mimics the behavior of <code>$:</code> as it will run once on the server. <code>run</code> may look new, but it's technically deprecated as it is meant to be manually refactored during the migration. <code>$:</code> usages can usually replaced with one of the following</p>
<ul>
<li><p><code>$derived()</code> - for defining a new reactive state from other state</p>
</li>
<li><p><code>$effect()</code> - for running side effects based on the effect's dependencies on the client only</p>
</li>
<li><p><code>onMount()</code> - for running code once when your component has been mounted on the client</p>
</li>
</ul>
<p>Any state that depends on other state should use <code>$derived</code> so it can be rendered on the server. You should try to avoid using <code>$effect()</code> to update component state as it will look different on the client and server. Using effects to update state is a common pitfall in React as well, as it is often possible to define derived state instead. For code that just needs to run one time once it's on the client, prefer to use <code>onMount</code> over <code>$effect()</code>.</p>
<p>Rich Harris, the creator of Svelte, even said the <code>$effect()</code> rune was named that way to discourage people from using it, as many developers are aware of the pains of <code>useEffect</code> from React (see this <a target="_blank" href="https://x.com/Rich_Harris/status/1835427306967331190">post</a>). Effects in component frameworks are necessary but can be notoriously tricky to debug, so it's important not to overdo it.</p>
<h2 id="heading-5-refactor-run-statements">5. Refactor <code>run</code> Statements</h2>
<p>I started going through each file and refactoring the <code>run</code> statements. Thinking in terms of the new runes made it easier to reason about what should use <code>$derived()</code> and what I really need <code>$effect()</code> for. Here is an example.</p>
<p>I am loading whether a tournament’s leaderboard is toggled on from the server first, then I am transitioning control over to the client-side Svelte stores <code>$isAuth</code> and <code>$isLeaderboard</code> once the client is loaded.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// original</span>
<span class="hljs-keyword">let</span> combinedIsLeaderboard = serverIsLeaderboard
<span class="hljs-attr">$</span>: combinedIsLeaderboard = $isAuth &amp;&amp; $isLeaderboard
</code></pre>
<p>After the migration script, this is a little more clear that we are instantiating a stateful value with server data and then updating it client-side.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// migrated</span>
<span class="hljs-keyword">let</span> combinedIsLeaderboard = $state(serverIsLeaderboard)
run(<span class="hljs-function">() =&gt;</span> {
    combinedIsLeaderboard = $isAuth &amp;&amp; $isLeaderboard
})
</code></pre>
<p>I could easily replace <code>run</code> with <code>$effect()</code> and call it a day, but this code starts to smell. After the migration, it's pretty clear that I'd be using <code>$effect()</code> to update component state, which I just mentioned is an anti-pattern. It's possible to <em>derive</em> the state directly from server data and client state, so <code>$derived()</code> is best choice.</p>
<p>We can use the Svelte <code>browser</code> variable to determine whether to use the client or server variables during the page load. It would also be possible to use <code>onMount</code> to pass control to the client, this is just my preference.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// refactored</span>
<span class="hljs-keyword">let</span> combinedIsLeaderboard = 
    $derived(browser ? $isAuth &amp;&amp; $isLeaderboard : serverIsLeaderboard)
</code></pre>
<p>The result with <code>$derived()</code> is much more clear than the original code.</p>
<h2 id="heading-6-resolve-miscellaneous-errors">6. Resolve Miscellaneous Errors</h2>
<p>This section is a bit of a tangent, but the Svelte VS Code extension and TypeScript seem to have gotten smarter with Svelte 5. Runes provide more clarity in the code, and they also provide more clarity in error messages.</p>
<h3 id="heading-state"><code>$state()</code></h3>
<p>The migration did a perfect job converting the original state assignments to the new <code>$state()</code> syntax. However, if I never initialized the state in the first place, it's now pretty obvious that those errors are my fault.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// original</span>
<span class="hljs-keyword">let</span> buttonRef: HTMLButtonElement
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// migrated</span>
<span class="hljs-keyword">let</span> buttonRef: HTMLButtonElement = $state()
</code></pre>
<p>Now that we're required to use <code>$state()</code>, TypeScript read my initial state as <code>undefined</code> and complained since <code>buttonRef</code> is expected to be a <code>HTMLButtonElement</code>. This is a good thing because this issue existed before the migration, it just wasn't obvious to me (or TypeScript, for that matter).</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// refactored</span>
<span class="hljs-keyword">let</span> buttonRef: HTMLButtonElement | <span class="hljs-literal">null</span> = $state(<span class="hljs-literal">null</span>)
</code></pre>
<p>Now TypeScript is happy, and we just need to explicitly check if the <code>buttonRef</code> exists later in the code.</p>
<h3 id="heading-bind"><code>bind</code></h3>
<p>Here’s another hidden issue that already existed in my code. The <code>bind</code> keyword is used to sync data in both directions between a child component and state on a parent. VS Code now pointed out that I was incorrectly using it when passing data one way from parent to child, where it isn't necessary.</p>
<pre><code class="lang-svelte"><span class="xml">// incorrect
<span class="hljs-tag">&lt;<span class="hljs-name">FormError</span> <span class="hljs-attr">bind:error</span> /&gt;</span>     // shorthand for bind:error=</span><span class="javascript">{error}</span>
</code></pre>
<pre><code class="lang-svelte"><span class="xml">// correct
<span class="hljs-tag">&lt;<span class="hljs-name">FormError</span> </span></span><span class="javascript">{error}</span><span class="xml"><span class="hljs-tag"> /&gt;</span>        // shorthand for error=</span><span class="javascript">{error}</span>
</code></pre>
<p>Be sure to check your console for warnings as well. I got a warning on one component that I was "Binding to a non-reactive property". Once again, it wasn't lying.</p>
<pre><code class="lang-svelte"><span class="xml">// incorrect
<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"hidden"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"slotId"</span> <span class="hljs-attr">bind:value</span>=</span></span><span class="javascript">{slot.id}</span><span class="xml"><span class="hljs-tag"> /&gt;</span></span>
</code></pre>
<p><a target="_blank" href="http://slot.id"><code>slot.id</code></a> is a property on a prop being passed into my component, not a stateful value that needs to be bound to the child input's value. Just passing it one way to the input is sufficient.</p>
<pre><code class="lang-svelte"><span class="xml">// correct
<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"hidden"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"slotId"</span> <span class="hljs-attr">value</span>=</span></span><span class="javascript">{slot.id}</span><span class="xml"><span class="hljs-tag"> /&gt;</span></span>
</code></pre>
<p>These new errors were confusing at first but helped me understand Svelte better. I digress.</p>
<h2 id="heading-7-verify-migration-changes">7. Verify Migration Changes</h2>
<h3 id="heading-slots-snippets">Slots → Snippets</h3>
<p>Slots have now been replaced with a new concept called snippets. I didn't need to make any manual changes for this update but it's worth checking out on your end.</p>
<p>Snippets are reusable pieces of code inside your component. They can be passed to other components as props. Any content inside the component tags that is not a snippet declaration implicitly becomes part of that component's <code>children</code> prop. If you're familiar with React, it's like when a React component written as the child of another component becomes part of the parent component's <code>children</code> prop.</p>
<p>Here's an example from the Svelte <a target="_blank" href="https://svelte.dev/docs/svelte/snippet#Passing-snippets-to-components">docs</a>.</p>
<pre><code class="lang-svelte"><span class="xml">// App.svelte
<span class="hljs-tag">&lt;<span class="hljs-name">Button</span>&gt;</span>click me<span class="hljs-tag">&lt;/<span class="hljs-name">Button</span>&gt;</span></span>
</code></pre>
<p>The string <code>click me</code> becomes a snippet that is passed to the <code>Button</code> component as the <code>children</code> prop. The new <code>@render</code> tag can then render snippets in the markup.</p>
<pre><code class="lang-svelte"><span class="xml">// Button.svelte
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span></span><span class="javascript">
    <span class="hljs-keyword">let</span> { children } = $props()
</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> 

</span><span class="hljs-comment">&lt;!-- result will be &lt;button&gt;click me&lt;/button&gt; --&gt;</span><span class="xml"> 
<span class="hljs-tag">&lt;<span class="hljs-name">button</span>&gt;</span></span><span class="javascript">{@render children()}</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
</code></pre>
<p>So instead of content being passed into a slot, it's defined as a snippet, passed as the <code>children</code> prop, and rendered with <code>@render</code>.</p>
<p>Also note the new <code>$props()</code> rune for defining component props more explicitly. I never liked the old <code>export</code> syntax, because we're importing these props into the component right??</p>
<p>Anyways...</p>
<p>This is a simplified version of what happened in my root <code>+layout.svelte</code> component.</p>
<pre><code class="lang-svelte"><span class="xml">// original
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span></span><span class="javascript">
    <span class="hljs-keyword">import</span> type { RootLayoutData } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/types'</span>
    <span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> data: RootLayoutData
<span class="hljs-comment">// ...</span>
</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">slot</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">footer</span> /&gt;</span></span>
</code></pre>
<pre><code class="lang-svelte"><span class="xml">// migrated
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span></span><span class="javascript">
<span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">import</span> type { RootLayoutData } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/types'</span>
    <span class="hljs-keyword">import</span> type { Snippet } <span class="hljs-keyword">from</span> <span class="hljs-string">'svelte'</span>

    interface Props {
        <span class="hljs-attr">data</span>: RootLayoutData
        children?: Snippet
    }

    <span class="hljs-keyword">let</span> { data, children }: Props = $props()
<span class="hljs-comment">// ...</span>
</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

</span><span class="javascript">{@render children?.()}</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">footer</span> /&gt;</span></span>
</code></pre>
<p>More complicated? Maybe. But snippets seem pretty useful so I like them so far. Be sure to check your components for other changes made by the migration.</p>
<h3 id="heading-unit-testing">Unit Testing</h3>
<p>Writing unit tests for this project was a slog since I neglected them at first, but this was a moment I was thankful to have them. After the migration refactored almost every component automatically, being able to run <code>npm test</code> and find issues quickly was very handy. My unit tests pointed me to the problematic components and gave me confidence that everything worked once all the tests passed.</p>
<p>I also ran the app locally to see it with my own eyes. Nothing changed, and it was beautiful.</p>
<h2 id="heading-8-merge-branch-back-into-main">8. Merge Branch Back Into Main</h2>
<pre><code class="lang-sh">git switch main
git merge svelte-5-migration
</code></pre>
<p>...or create a pull request if that's what your team does. You're done!</p>
<h1 id="heading-takeaways">Takeaways</h1>
<p>I didn't know what to think about runes at first but now I'm a big fan. The <code>$:</code> was cool but maybe a little too cool. 😎 Replacing <code>$:</code> with <code>$state()</code>, <code>$derived()</code>, and <code>$effect()</code> makes it easier to use the right tool for the job. I would highly recommend upgrading when you have the chance.</p>
<p>Component frameworks are meant to hide complexity so developers can focus on the custom app logic. Determining how much to abstract away is both an art and a science that framework authors think about. More abstraction usually means less complexity for the developer but also less control.</p>
<p>Frameworks exist on a spectrum of abstraction and in my opinion Svelte 4 was pretty high on the list, at least compared to React or Angular. Features like <code>$:</code> and the <code>export</code> props syntax now feel too abstracted and "magical" as they're doing a lot under the hood.</p>
<p>Svelte 5 turns the abstraction dial down and adds more clarity to the syntax, which feels a lot more natural to me. This update is a big improvement on my favorite framework and I can't wait to dig into it more.</p>
]]></content:encoded></item><item><title><![CDATA[Learning to Code on a Black Market]]></title><description><![CDATA[Hello JavaScript
Back in 2016, I started my career as a software consultant for a healthcare tech company. I worked with customers to learn about their needs and then configured their business logic into our product. We used a browser-based interface...]]></description><link>https://blog.willbraun.dev/learning-to-code-on-a-black-market</link><guid isPermaLink="true">https://blog.willbraun.dev/learning-to-code-on-a-black-market</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[bookmarklet]]></category><category><![CDATA[dunning-kruger-effect]]></category><dc:creator><![CDATA[Will Braun]]></dc:creator><pubDate>Mon, 07 Oct 2024 12:59:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/BStW5kYXw4E/upload/beb82285426d8986ba150eb82028dcd7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-hello-javascript">Hello JavaScript</h1>
<p>Back in 2016, I started my career as a software consultant for a healthcare tech company. I worked with customers to learn about their needs and then configured their business logic into our product. We used a browser-based interface to enter these settings, and it often involved a LOT of mundane, repetitive work. Think checking hundreds of checkboxes or filling out a form hundreds of times. My muscle memory got pretty good, but my poor fingers could only move so fast. It was a grind to get through it once I had planned out what needed to be done.</p>
<p>During our initial training, we were learning how to use the app and there was a page that required us to enter a default number into many fields. Our instructor told us to create a new browser shortcut and shared a long line of code that started with <code>javascript:</code> as the URL. Hmm 🤔, that doesn't look like a website... When I clicked the shortcut, it immediately filled out every field with a default value, and I was amazed. I was already excited to start my job, but now I was even more enticed by this sorcery I just witnessed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728242049207/be57a4a9-48bf-4fff-aac1-ea9270ca7e2e.webp" alt class="image--center mx-auto" /></p>
<p>I had taken a Matlab coding class in college, but dry engineering homework didn't really hit the same way as seeing manual work disappear in front of my eyes. I had to put my curiosity on the back burner for the first few months so I could get settled learning the product and the industry, but I knew I would be revisiting this magic string of text.</p>
<p>The company was growing rapidly, and lots of new work was coming in. It was an exciting place to be but also very busy. My teammates and I started working longer hours to stay afloat, and we needed a way to implement projects more quickly. There were a few other consultants who knew JavaScript and tinkered with creating new scripts, so I asked them to teach me.</p>
<p>I learned that they were making <em>bookmarklets</em>, or snippets of client-side JavaScript packaged as a browser bookmark. These bookmarklets had access to everything in the browser, and had the power to manipulate page elements however we wanted. My teammates showed me the basics, then on the weekends I studied <a target="_blank" href="https://eloquentjavascript.net/">Eloquent JavaScript</a> with the goal of making my own.</p>
<h1 id="heading-all-about-bookmarklets">All About Bookmarklets</h1>
<p>Web apps store data in a database on a server, but they often need to store some data in the user's browser after the page is loaded so the page has quick access to it. In a modern framework, the client data can be tricky to access from outside the app, but this app was old enough that it used the browser's global scope to hold all the app's client-side variables. This meant all the client-side variables were available directly from the browser console and by extension, bookmarklets. Bingo.</p>
<p>Because bookmarklets only have access to client-side data, we used them to augment our own client-side human actions like filling out input fields, submitting forms, and navigating the app. We weren’t able to edit the backend, APIs, database, etc.</p>
<p>Our bookmarklets all had a similar style, though some were more complex than others. Some common themes were -</p>
<ul>
<li><p>We wrapped everything in an IIFE (Immediately Invoked Function Expression), allowing us to define and execute our logic as soon as the bookmarklet was clicked.</p>
</li>
<li><p>We used <code>prompt()</code> to collect user input, such as the number to populate in multiple text fields.</p>
</li>
<li><p>We implemented basic input validation was to ensure correct data entry, and used <code>alert()</code> to notify users of any errors.</p>
</li>
<li><p>For more complex DOM element selection, we occasionally relied on JQuery.</p>
</li>
</ul>
<p>Here is an example of a JavaScript snippet that prompts the user for a number, then fills that value into every number input field on the page. It also has some validation to ensure a number is entered.</p>
<pre><code class="lang-javascript">(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> number = prompt(<span class="hljs-string">'Enter a number to fill into all number fields:'</span>);
  <span class="hljs-keyword">if</span> (number !== <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">isNaN</span>(number)) {
      alert(<span class="hljs-string">'Please enter a valid number.'</span>);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">const</span> inputs = <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'input[type="number"]'</span>);
      inputs.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">input</span>) </span>{
        input.value = number;
      });
    }
  }
})();
</code></pre>
<p>During development we would copy and paste our code from a code editor into the browser console to test it out. Once the script was finished, we would minify it to reduce it's size for sharing. Then we’d add <code>javascript:</code> in front to tell the browser to execute the code rather than redirect to a website, and finally we’d create the browser bookmarklet with the minified script as the URL. The final code would look something like this.</p>
<pre><code class="lang-javascript">javascript:!<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>)</span>{
<span class="hljs-keyword">const</span> e=prompt(<span class="hljs-string">"Enter a number to fill into all number fields:"</span>);
<span class="hljs-literal">null</span>!==e&amp;&amp;(<span class="hljs-built_in">isNaN</span>(e)?alert(<span class="hljs-string">"Please enter a valid number."</span>)
:<span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'input[type="number"]'</span>)
.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">l</span>)</span>{l.value=e}))}();
</code></pre>
<h1 id="heading-i-was-hooked">I was hooked!</h1>
<p>This was the perfect playground to learn how to code. The most important part was that I was naturally incentivized to build useful tools so that my teammates and I could finish our work faster and go home on time. We built scripts to -</p>
<ul>
<li><p>Fill in many input fields</p>
</li>
<li><p>Check many boxes/radio buttons</p>
</li>
<li><p>Submit forms</p>
</li>
<li><p>Import/export data</p>
</li>
<li><p>Filter items on the page</p>
</li>
<li><p>Navigate to other pages</p>
</li>
<li><p>and much more</p>
</li>
</ul>
<p>Eventually we created custom keyboard shortcuts with a special Chrome extension to increase our efficiency even more. All of this made learning how to code absolutely addictive because I could quickly whip something together and save my team of many hours of menial labor.</p>
<p>I don't think I've ever felt more powerful than the moment my first script executed properly. Now I could make our product do (almost) whatever I needed it to do, without anyone else's approval. I've been chasing that dragon ever since. 🐉</p>
<h1 id="heading-turmoil-brews">Turmoil Brews</h1>
<p>More and more consultants became proficient in JavaScript and created scripts for their own use cases. We started to share these with each other and sometimes jokingly bartered them on a "black market" in return for favors around the office like covering the support phone or the kitchen cleanup shift. The black market provided yet another source of motivation to continue developing new scripts.</p>
<p>The company was aware of a few approved scripts like the ones we received in our training, but they were not onboard with all of these new ones popping up every week. They were concerned for a few reasons -</p>
<ul>
<li><p>Having the ability to solve our own problems was great for the short term, but it concealed necessary product enhancements from our development team so they were not able to implement these features into the product.</p>
</li>
<li><p>Critical project timelines were relying on hobbyist code.</p>
</li>
<li><p>There was always a risk of a consultant sharing this code with a tech-savvy customer who could later demand support for these unsanctioned features. This would have been the cardinal sin, and never happened as far as I'm aware.</p>
</li>
</ul>
<p>We knew their concerns, but the scripts made us so much more efficient and our project timelines were now relying on them. It would have been impossible to hit our deadlines without them.</p>
<h1 id="heading-where-are-they-now">Where Are They Now?</h1>
<p>Eventually the company documented all of these scripts and prioritized building them into the product as first-class features. The app has since been fully rebuilt with modern tech and no longer puts everything into global variables in the browser, so the scripts aren't effective anymore. These scripts faded into the distance, relics of the past. Old-timers now talk of the good old days when you could imagine a feature, build it, and start using it that day.</p>
<p>I ended up with a decent understanding of JavaScript, but absolutely no other coding skills. As an activity during COVID, I tried freelancing with Google Apps Script, which is essentially just JavaScript with Google functionality added for managing G-Suite apps. It started off fine but quickly escalated in complexity, and I realized how in-over-my-head I was and had to quit. I had fallen off of the proverbial "Mount Stupid" of the Dunning-Kruger effect curve, headfirst into the "Valley of Despair".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728239467263/482c6b9b-81ff-41bc-b6a9-6cf9d4ca682c.png" alt class="image--center mx-auto" /></p>
<p>It was a terrible feeling at first, and I felt disgusted when looking at code - like it was mocking me, and that I'd never be able to understand it. I took about a year off from coding before remembering how much I loved it in the first place, got started on a new project, and never stopped again.</p>
<p>My company required a Bachelors degree in computer science or equivalent experience to join their development team. Another degree would have taken me three years to complete while working, not to mention the cost. By this point I knew for sure I wanted to become a programmer and couldn't imagine waiting that long to make it a reality. At the time of writing this article, I’d still be working on that degree.</p>
<p>I tried learning on my own in the evenings but I wasn't sure where to focus. I needed outside help to cross the Valley of Despair, and found <a target="_blank" href="https://carolinacodeschool.org/">Carolina Code School</a>, a full-time in-person bootcamp near me. After mulling it over for months, I took a leap of faith and quit my job to pursue a new career in software development. I completed the bootcamp and was fortunate to find a job before the post-COVID tech layoffs. This was the biggest risk I've taken in my life, but it felt like a bigger risk to do nothing.</p>
]]></content:encoded></item><item><title><![CDATA[Critical Industry Context for Junior Developers]]></title><description><![CDATA[I had a big advantage during and after the coding bootcamp I attended - I already had over 5 years of experience in tech. I was not able to pursue software development at my previous company, so I quit and enrolled in a full-time coding bootcamp. Aft...]]></description><link>https://blog.willbraun.dev/critical-industry-context-for-junior-developers</link><guid isPermaLink="true">https://blog.willbraun.dev/critical-industry-context-for-junior-developers</guid><category><![CDATA[portfolio]]></category><category><![CDATA[Junior developer ]]></category><category><![CDATA[industry]]></category><dc:creator><![CDATA[Will Braun]]></dc:creator><pubDate>Mon, 22 Jan 2024 13:29:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Oalh2MojUuk/upload/1e7be72f06689843ce75fa4c2f453411.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I had a big advantage during and after the coding bootcamp I attended - I already had over 5 years of experience in tech. I was not able to pursue software development at my previous company, so I quit and enrolled in a full-time coding bootcamp. Afterward, I knew exactly how my target development role fit into the broader scope of the software development lifecycle. I pulled from my previous jobs collaborating with developers, product management, QA, UX, sales, and customers to build a portfolio that I was confident in.</p>
<p>As a budding developer building a portfolio of work, you won't have the luxury of having a product analyst to determine requirements or a sales team to sell your app. On top of learning new tech and learning how to code, right now you are also in charge of every other business role at your one-person software shop. The more familiar you are with each role, the more professional and competitive your portfolio will be.</p>
<p>In this article, I'm going to share some context about typical development-adjacent departments, and practical tips to make you stand out.</p>
<h1 id="heading-departments">Departments</h1>
<p>I realize it is incredibly arrogant to reduce all of the following fields to fit into the rest of this blog post, and that is what I'm about to attempt to do. Each one is deep enough to dedicate your entire career to, and I encourage you to explore each of them in more detail. Here are some starting points for you to incorporate them into your projects.</p>
<h2 id="heading-product-management">Product Management</h2>
<p>The product management team is responsible for defining what features the product should have. I previously worked as a product analyst, so I have a bit more to say about this section than others. It is closely linked with development, and there is a lot to be gained by having an understanding of the product team. Day to day, it boils down to a few things.</p>
<h3 id="heading-meeting-with-customers-to-determine-pain-points">Meeting with customers to determine pain points</h3>
<p>Code is just a tool to solve problems, and the first step in the lifecycle of a software project is the problem itself. It can be difficult for a company to know exactly what the right problem to solve is, so it is common to have discussions with customers experiencing the problem regularly. Steve Jobs famously stated that you must start with the customer experience, and then work backwards.</p>
<h3 id="heading-forming-requirements-for-new-features">Forming requirements for new features</h3>
<p>Large features are organized into <em>epics</em> like "Create a user authentication workflow", which are comprised of smaller <em>stories</em> such as "Build a login page", "Build a registration page", and "Build a Forgot Password workflow". These stories are broken down into <em>acceptance criteria</em> like "Ensure a login with correct credentials is successful" and "Do not allow passwords of less than 10 characters during registration." Together, all of these stores and AC make up the requirements for the feature, which development will later use as a blueprint to build the product.</p>
<h3 id="heading-prioritizing-what-features-to-build">Prioritizing what features to build</h3>
<p>...and maybe more importantly, what features to forget. Saying "No" is one of the most powerful tools in the product team's toolbox, because there are simply too many possible features to build with the the team's finite resources (people and time). Product teams use a backlog, which is just a list of all the stories and bugs they are responsible for, ordered by importance. They constantly reprioritize the backlog based on the current demand for each item to make sure the most important ones are being handled.</p>
<h3 id="heading-tips">Tips</h3>
<ul>
<li><p>When building a portfolio project, think about your hypothetical customer/user before you start writing any code. What is the main problem they would need to solve? What is the simplest way you can solve it?</p>
</li>
<li><p>During your planning phase, write out some user stories for the most important features. They are from the perspective of a typical user and commonly take the form "As ___, I want to ___ so that I can ___." It's a good exercise to get you thinking about your project from the perspective of someone using it, who in real life could be someone who hires you!</p>
</li>
<li><p>Create a backlog and any time you have an idea for a new feature or find a bug that needs to be fixed, add it to the list. If it is the most important, put it on top. If it isn't critical, put it lower. Determine what is absolutely necessary for your MVP (minimum viable product) and deprioritize the rest. Once your MVP is finished, feel free to polish it up with the remaining items from the top of your backlog.</p>
</li>
</ul>
<h2 id="heading-user-experience-ux">User Experience (UX)</h2>
<p>The UX team focuses on enhancing user satisfaction using the product. They design interfaces that focus on solving the users' problems in a way that would be most natural. They research market trends to find what would make their product more engaging. Usability, accessibility, visual design, and many other factors go into building high-fidelity mocks of user interfaces. They share these mocks with the product and development teams, then iterate upon them before development brings them to life.</p>
<h3 id="heading-tips-1">Tips</h3>
<ul>
<li><p>Make mocks of your screens before coding. They can be hand-drawn wireframes, basic shapes in a tool like draw.io, or something higher fidelity like Figma. It saves a lot of time in the long run to figure out the layout and flow of your app before starting to code.</p>
</li>
<li><p>Make your app mobile responsive, and actually test it out on a real device. People scrolling on LinkedIn may pop it open on their phone, so it should look good! Most UI libraries provide tools to make responsiveness easier than raw media queries.</p>
</li>
<li><p>Be intentional with the visual design. Spending a little time learning about color theory, typography, negative space, and other UI concepts can go a long way. Pick a color palette and a font, and find a UI library to handle basic components.</p>
</li>
<li><p>Make the app as easy to use as possible. Reduce clicks, offer some functionality without a login if possible, and group related components near each other.</p>
</li>
<li><p>Develop an eye for "jank", and remove it. If your app has components moving around that shouldn't, requiring more clicks than expected, or generally doing things that might raise an eyebrow, find out why and resolve it. If your app doesn't have any noticeable flaws, the important parts will shine.</p>
</li>
</ul>
<h2 id="heading-quality-assurance-qa">Quality Assurance (QA)</h2>
<p>The QA team is responsible for ensuring that the product meets the requirements set forth by the product team, and is free of bugs. They are the final human check before the product is released into the wild. QA conducts both manual and automated testing to simulate typical user behavior as well as more unlikely edge cases to give the company confidence in all code changes. In a larger codebase, a code change can affect something in a seemingly unrelated area, so the more automated testing you have, the more likely these sneaky bugs will be caught.</p>
<p>Unit tests are technically a developer's responsibility since they are part of the code, but I've included it in this section because it's another tool that professional teams use to test code. Unit tests allow you to test the behavior of a single "unit" of code against predefined test cases by simply running a test command.</p>
<p>Instead of preparing a fake scenario and manually calling an API endpoint to test something, you could write a unit test loaded with mock data, see its response to that data, and assert that its output equals the value you expect. If anything in the codebase changes that affects the API endpoint, the unit test will fail when you run it and you've now located a bug that you may not have known to check for. It's important to test the behavior of your code as a user might interact with it, rather than internal implementation details.</p>
<p>There is a lot of debate about how much unit test coverage is sufficient, as it may not be practical to write a unit test for every single thing. A common number I've seen to strive for is 80% coverage. Having a high coverage allows you to be more confident in your code changes and adapt to changing requirements. Test-driven development (TDD) takes this to the next level and recommends that you write unit tests before you write any code, watch them fail, and then write the code to make them pass. TDD is beyond the scope of this article, but it's definitely something to look into.</p>
<h3 id="heading-tips-2">Tips</h3>
<ul>
<li><p>Be your own QA team. After you add a new feature, check other related features to ensure they behave as expected. Confirm that they fulfill the requirements you created previously when you had your "Product" hat on.</p>
</li>
<li><p>Think of edge cases and try to break your app through user interactions. What happens when you add letters to a field expecting a number? What happens if you click a button very quickly many times?</p>
<ul>
<li>Prevent your users from getting into a bad situation. Use form field validation, API endpoint permission rules, disabling a button when its action is being awaited, etc.</li>
</ul>
</li>
<li><p>Write some unit tests. While they aren't quite as critical as in a professional app, having a few unit tests on API endpoints and key frontend behavior will give you confidence in your code and in your ability to speak about testing in interviews.</p>
</li>
</ul>
<h2 id="heading-sales-and-marketing">Sales and Marketing</h2>
<p>Sales and marketing are typically two closely related departments. The sales team is responsible for selling the product to customers and generating new revenue for the business. The marketing team drives product awareness, interest, and brand reputation. Their goal is to get prospective customers thinking about the product and connecting them with the sales team.</p>
<p>As you are developing your portfolio apps, always think about who you are serving and how you would reach them. An excellent portfolio project would have real users you can market to and maybe turn into a business, but that is not necessary to demonstrate your skills. You will, however, need to market your projects online at least via LinkedIn and Github so that someone looking at your profile can see what you have to offer.</p>
<h3 id="heading-tips-3">Tips</h3>
<ul>
<li><p>HOST. HOST. HOST. If at all possible for your app, get it hosted somewhere online so that anyone who wants to check it out can. Nobody is going to clone your repo locally and run it on their machine.</p>
<ul>
<li>For static sites and frontend repos, look at Netlify, Vercel, or GitHub Pages. For backend and full stack repos, check out a PaaS like Render or Railway. At the time of writing these have a free tier.</li>
</ul>
</li>
<li><p>Write good README's for your GitHub repos. You've done all the hard work, now let the world in on what you've been up to. Without a <a target="_blank" href="https://www.makeareadme.com/">README.md</a> file, your repo is just a folder with some code. In your README, add an overview of the project, tell people how to use it, list what technologies you used, and add any other relevant details.</p>
</li>
<li><p>Post about your project on LinkedIn. You can post updates as you go, or just when it is completed and hosted.</p>
<ul>
<li><p>Bonus points - make a video walkthrough of the project and share it. It's an easy way to demonstrate two of the most important things interviewers are looking for.</p>
<ol>
<li><p>You have the skills</p>
</li>
<li><p>You can communicate with other humans</p>
</li>
</ol>
</li>
<li><p>You can use Loom to record your screen and face easily, <a target="_blank" href="https://www.loom.com/share/ec1cf09e45434ab29c6b323d3856634a">here is mine as an example</a>.</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-selling-yourself">Selling Yourself</h2>
<p>No, I'm not talking about selling your soul to the devil or selling out to The Man. The bigger picture of marketing your portfolio projects is to make a strong showing for yourself on the job market. When you are job searching, you are selling your skills and experience to buyers - companies, startups, freelance clients, etc.</p>
<h3 id="heading-tips-4">Tips</h3>
<ul>
<li><p>Build a network so that people can find you. Connect with people on LinkedIn including other developers, people in related fields, or anyone who seems interesting. Go to in-person meetups to make local connections with other developers and techies in these adjacent fields.</p>
</li>
<li><p>Post regularly on LinkedIn. It lets your audience know that you are on the market and that you're continuing to sharpen your skills. Share updates about what you're learning, or when your project is ready to check out.</p>
<ul>
<li>Add posts you are proud of to the Featured section on your LinkedIn profile so that your network can easily find your best work.</li>
</ul>
</li>
<li><p>Before an interview, think about what salary you could realistically ask for based on your skills, the position, and the market. Also know what number you'd be happy with if they negotiate, and know what your minimum is.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>There are several other departments at tech companies like Site Reliability Engineering (SRE), Data Engineering/Analysis, and Customer Success, but I find that the ones listed are the most applicable to your journey. I know this is a lot on top of your normal studies, but the job market is full of junior developers who want the same thing that you do. Leveraging these tips is something within your control to give you an edge. As a side note, many people assume that they must be a developer to get into tech, but you can target one of these other fields instead if they seem like a better fit. Good luck!</p>
]]></content:encoded></item><item><title><![CDATA[Scrape Anything with ScraperAPI]]></title><description><![CDATA[Disclosure: I get a small commission from sign-ups via links on this page, at no additional cost to you.
Web scraping is tough. From getting blocked by websites to parsing incomplete data, many roadblocks can slow you down, reroute you, or stop you e...]]></description><link>https://blog.willbraun.dev/scrape-anything-with-scraperapi</link><guid isPermaLink="true">https://blog.willbraun.dev/scrape-anything-with-scraperapi</guid><category><![CDATA[web scraping]]></category><category><![CDATA[APIs]]></category><category><![CDATA[Scraping]]></category><category><![CDATA[scraperapi]]></category><dc:creator><![CDATA[Will Braun]]></dc:creator><pubDate>Fri, 20 Oct 2023 16:08:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696686003126/5fe75d76-6744-4d0c-9622-f7fb0474abd2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Disclosure: I get a small commission from sign-ups via links on this page, at no additional cost to you.</em></p>
<p>Web scraping is tough. From getting blocked by websites to parsing incomplete data, many roadblocks can slow you down, reroute you, or stop you entirely. <a target="_blank" href="https://www.scraperapi.com?fpr=williambraun">ScraperAPI</a> removes the headaches so you can focus on your project's big picture instead of the intricacies of web scraping.</p>
<h1 id="heading-what-is-scraperapi">What is ScraperAPI?</h1>
<p>You guessed it - it's an API service that makes web scraping a breeze. Rather than requesting data from websites directly, you can route all of your requests through ScraperAPI's endpoints to take advantage of their rich feature set. It acts as a middleman to make web scraping requests for you so that you won't be blocked and then returns the correct data in various formats.</p>
<p>Here's a quick example using Python's <code>requests</code> package. Let's scrape a page I built with React. Traditional websites render HTML on the server, so requests to scrape them will return the full HTML you're looking for. Many modern websites use popular frameworks like React to render it in the browser <em>after</em> your request is made, meaning the HTML from the server will essentially be empty at first. ScraperAPI has a feature to render the HTML for you before returning it, making it super simple to access the data you need.</p>
<p>Without ScraperAPI -</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests

url = <span class="hljs-string">'https://state-management.willbraun.dev'</span>

r = requests.get(url)
print(r.text)
</code></pre>
<p>Response</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"image/svg+xml"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/vite.svg"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>State Management Patterns<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">crossorigin</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/assets/index-8d0aa6b9.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/assets/index-cda38e90.css"</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"root"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        // NOTE - No content is loaded yet!
    <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Notice the body of the response has one empty element - not much to work with! Let's try using ScraperAPI, with JavaScript rendering enabled. I'll be using the <code>payload</code> parameter of Python <code>requests</code> to build the full URL to send to ScraperAPI, and I've stored my API key in an environment variable.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> decouple <span class="hljs-keyword">import</span> config

url = <span class="hljs-string">'https://state-management.willbraun.dev'</span>
payload = {
  <span class="hljs-string">'api_key'</span>: config(<span class="hljs-string">'API_KEY'</span>), 
  <span class="hljs-string">'url'</span>: url, 
  <span class="hljs-string">'render'</span>: <span class="hljs-string">'true'</span>, 
}

r = requests.get(<span class="hljs-string">'https://api.scraperapi.com'</span>, params=payload)
print(r.text)
</code></pre>
<p>Full URL</p>
<p><code>https://api.scraperapi.com/?api_key=&lt;your_api_key&gt;&amp;url=https://state-management.willbraun.dev&amp;render=true</code></p>
<p>Response</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"image/svg+xml"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/vite.svg"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>State Management Patterns<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">""</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/assets/index-8d0aa6b9.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/assets/index-cda38e90.css"</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"root"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">header</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://blog.willbraun.dev/demystifying-state-management"</span> <span class="hljs-attr">target</span>=<span class="hljs-string">"_blank"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"noopener noreferrer"</span>&gt;</span>Blog Post<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://github.com/willbraun/state-mgmt-patterns"</span> <span class="hljs-attr">target</span>=<span class="hljs-string">"_blank"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"noopener noreferrer"</span>&gt;</span>GitHub<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">header</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>OFF<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>&gt;</span>Toggle with Prop Drilling<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>OFF<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>&gt;</span>Toggle with Context<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>OFF<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>&gt;</span>Toggle with Zustand<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>OFF<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>&gt;</span>Toggle with Redux<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Look at all that HTML! This problem is a thing of the past, and ScraperAPI's other features are just as easy to set up.</p>
<h2 id="heading-features">Features</h2>
<h3 id="heading-rotating-ip-addresses">Rotating IP Addresses</h3>
<p>ScraperAPI automatically rotates through millions of IP addresses as it scrapes data, which provides you with a variety of benefits.</p>
<p>Many websites have measures in place to prevent scraping. ScraperAPI distributes scraping requests across many IPs and can automatically retry on failed requests, greatly reducing the likelihood of triggering IP blocks.</p>
<p>Some websites may impose rate limits on the number of requests that can be made from a single IP address within a specific time frame. Rotating IP addresses allows you to bypass these limits and scrape more data without being throttled.</p>
<h3 id="heading-javascript-rendering">JavaScript Rendering</h3>
<p>This is the feature from our example. JavaScript running in the browser may be required to render the part of the HTML. It is more complicated to scrape as the initial response from the server is incomplete. You have to wait for the JavaScript to run <em>after</em> you send the request to the server.</p>
<p>By adding <code>&amp;render=true</code> to your API call, ScraperAPI will handle this step for you. It will render all of the JavaScript on your desired page, and return the full HTML to you.</p>
<h3 id="heading-auto-parse-to-json">Auto Parse to JSON</h3>
<p>HTML is typically returned from a scrape request as a long string of text. This may be fine for your use case since it is standard, but the next step is usually to transform the response into a more workable format so that you can find what you need.</p>
<p>By adding <code>&amp;autoparse=true</code> to your API call, ScraperAPI will format the response as JSON if possible. One less step for you to worry about!</p>
<h3 id="heading-geolocation">Geolocation</h3>
<p>Sometimes websites show different data depending on what part of the world you are requesting data from. To show results for a particular location, you can specify the geolocation of the IP addresses used by adding the <code>&amp;country_code</code> parameter to your API call. The available countries are listed in the ScraperAPI <a target="_blank" href="https://www.scraperapi.com/documentation/?fpr=williambraun">documentation</a>.</p>
<h3 id="heading-structured-data-endpoints">Structured Data Endpoints</h3>
<p>Trying to scrape Amazon or Google Search? ScraperAPI has you covered with prebuilt endpoints that return relevant data as JSON. This is a powerful feature that can really accelerate you towards your project goals. Here are the services currently available.</p>
<ul>
<li><p>Amazon Search</p>
</li>
<li><p>Amazon Product Page</p>
</li>
<li><p>Google Search Engine Result Page (SERP)</p>
</li>
</ul>
<p>Let's check out the Amazon Search structured data endpoint, which returns Amazon search results from any search query. I was recently researching computer monitors, so let's see what we find with this method.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> decouple <span class="hljs-keyword">import</span> config
<span class="hljs-keyword">import</span> json

payload = {
  <span class="hljs-string">'api_key'</span>: config(<span class="hljs-string">'API_KEY'</span>), 
  <span class="hljs-string">'query'</span>: <span class="hljs-string">'computer monitors'</span>,  
}

r = requests.get(
    <span class="hljs-string">'https://api.scraperapi.com/structured/amazon/search'</span>, 
    params=payload
)
parsed = json.loads(r.text)
print(json.dumps(parsed, indent=<span class="hljs-number">2</span>))
</code></pre>
<p>Full URL</p>
<p><code>https://api.scraperapi.com/structured/amazon/search?api_key=&lt;your_api_key&gt;&amp;query=computer+monitors</code></p>
<p>Response (shortened, it's a lot of data)</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"ads"</span>: [
    {
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"SAMSUNG 32\" Odyssey G55A QHD 165Hz 1ms FreeSync Curved Gaming Monitor with HDR 10, Futuristic Design for Any Desktop (LS32AG550ENXZA)"</span>,
      <span class="hljs-attr">"asin"</span>: <span class="hljs-string">"B09TMJ9LGR"</span>,
      <span class="hljs-attr">"brand"</span>: <span class="hljs-string">"Samsung Gaming Monitors"</span>,
      <span class="hljs-attr">"image"</span>: <span class="hljs-string">"https://m.media-amazon.com/images/I/81a+yL6ii9L.jpg"</span>,
      <span class="hljs-attr">"has_prime"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_best_seller"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_amazon_choice"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_limited_deal"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"stars"</span>: <span class="hljs-number">4.4</span>,
      <span class="hljs-attr">"total_reviews"</span>: <span class="hljs-number">0</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://aax-us-iad.amazon.com/x/c/RPqPaOmO9TkKF7V6K9QDMy0AAAGLCkRA-gEAAAH2AQBvbm9fdHhuX2JpZDMgICBvbm9fdHhuX2ltcDEgICA-fF7_/https://www.amazon.com/gp/aw/d/B09TMJ9LGR/?_encoding=UTF8&amp;pd_rd_plhdr=t&amp;aaxitk=e43e49e9df54e5c84f02ddf50d96ae4a&amp;hsa_cr_id=0&amp;qid=1696684327&amp;sr=1-1-9e67e56a-6f64-441f-a281-df67fc737124&amp;ref_=sbx_be_s_sparkle_mcd_asin_0_bkgd&amp;pd_rd_w=9Nsbn&amp;content-id=amzn1.sym.cd95889f-432f-43a7-8ec8-833616493f4a%3Aamzn1.sym.cd95889f-432f-43a7-8ec8-833616493f4a&amp;pf_rd_p=cd95889f-432f-43a7-8ec8-833616493f4a&amp;pf_rd_r=9X10K8EY448WDY60KJJV&amp;pd_rd_wg=lpoc4&amp;pd_rd_r=93c1edaf-f358-4104-bbb9-6e608dc2b024"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"top_stripe_ads"</span>
    },
    &lt;more ads&gt;
  ],
  <span class="hljs-attr">"results"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"search_product"</span>,
      <span class="hljs-attr">"position"</span>: <span class="hljs-number">1</span>,
      <span class="hljs-attr">"asin"</span>: <span class="hljs-string">"B0773ZY26F"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Sceptre 24-inch Professional Thin 1080p LED Monitor 99% sRGB 2x HDMI VGA Build-in Speakers, Machine Black (E248W-19203R Series)"</span>,
      <span class="hljs-attr">"image"</span>: <span class="hljs-string">"https://m.media-amazon.com/images/I/81zM2vVM+wL.jpg"</span>,
      <span class="hljs-attr">"has_prime"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_best_seller"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_amazon_choice"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_limited_deal"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"stars"</span>: <span class="hljs-number">4.6</span>,
      <span class="hljs-attr">"total_reviews"</span>: <span class="hljs-number">30226</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://www.amazon.com/Sceptre-E248W-19203R-Monitor-Speakers-Metallic/dp/B0773ZY26F/ref=sr_1_1?keywords=computer+monitors&amp;qid=1696684327&amp;sr=8-1"</span>,
      <span class="hljs-attr">"availability_quantity"</span>: <span class="hljs-literal">null</span>,
      <span class="hljs-attr">"spec"</span>: {},
      <span class="hljs-attr">"price_string"</span>: <span class="hljs-string">"$89.56"</span>,
      <span class="hljs-attr">"price_symbol"</span>: <span class="hljs-string">"$"</span>,
      <span class="hljs-attr">"price"</span>: <span class="hljs-number">89.56</span>
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"search_product"</span>,
      <span class="hljs-attr">"position"</span>: <span class="hljs-number">2</span>,
      <span class="hljs-attr">"asin"</span>: <span class="hljs-string">"B0148NNKTC"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Acer 23.8\u201d Full HD 1920 x 1080 IPS Zero Frame Home Office Computer Monitor - 178\u00b0 Wide View Angle - 16.7M - NTSC 72% Color Gamut - Low Blue Light - Tilt Compatible - VGA HDMI DVI R240HY bidx"</span>,
      <span class="hljs-attr">"image"</span>: <span class="hljs-string">"https://m.media-amazon.com/images/I/91K9SyGiyzL.jpg"</span>,
      <span class="hljs-attr">"has_prime"</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">"is_best_seller"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_amazon_choice"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_limited_deal"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"stars"</span>: <span class="hljs-number">4.7</span>,
      <span class="hljs-attr">"total_reviews"</span>: <span class="hljs-number">15156</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://www.amazon.com/Acer-Frame-Office-Computer-Monitor/dp/B0148NNKTC/ref=sr_1_2?keywords=computer+monitors&amp;qid=1696684327&amp;sr=8-2"</span>,
      <span class="hljs-attr">"availability_quantity"</span>: <span class="hljs-literal">null</span>,
      <span class="hljs-attr">"spec"</span>: {},
      <span class="hljs-attr">"price_string"</span>: <span class="hljs-string">"$99.99"</span>,
      <span class="hljs-attr">"price_symbol"</span>: <span class="hljs-string">"$"</span>,
      <span class="hljs-attr">"price"</span>: <span class="hljs-number">99.99</span>
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"search_product"</span>,
      <span class="hljs-attr">"position"</span>: <span class="hljs-number">3</span>,
      <span class="hljs-attr">"asin"</span>: <span class="hljs-string">"B0BS9TDY31"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Acer KB272 Hbi 27\" Full HD (1920 x 1080) Zero-Frame Gaming Office Monitor | AMD FreeSync Technology | 100Hz | 1ms (VRB) | Low Blue Light | Tilt | HDMI &amp; VGA Ports,Black"</span>,
      <span class="hljs-attr">"image"</span>: <span class="hljs-string">"https://m.media-amazon.com/images/I/81FTa3aSdnL.jpg"</span>,
      <span class="hljs-attr">"has_prime"</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">"is_best_seller"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_amazon_choice"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"is_limited_deal"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"stars"</span>: <span class="hljs-number">4.6</span>,
      <span class="hljs-attr">"total_reviews"</span>: <span class="hljs-number">2158</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://www.amazon.com/KB272-Hbi-Zero-Frame-FreeSync-Technology/dp/B0BS9TDY31/ref=sr_1_3?keywords=computer+monitors&amp;qid=1696684327&amp;sr=8-3"</span>,
      <span class="hljs-attr">"availability_quantity"</span>: <span class="hljs-literal">null</span>,
      <span class="hljs-attr">"spec"</span>: {},
      <span class="hljs-attr">"price_string"</span>: <span class="hljs-string">"$149.99"</span>,
      <span class="hljs-attr">"price_symbol"</span>: <span class="hljs-string">"$"</span>,
      <span class="hljs-attr">"price"</span>: <span class="hljs-number">149.99</span>
    },
    &lt;many more search results&gt;
  ],
  <span class="hljs-attr">"explore_more_items"</span>: [],
  <span class="hljs-attr">"pagination"</span>: [
    <span class="hljs-string">"https://www.amazon.com/s?k=computer+monitors&amp;page=2&amp;qid=1696684327&amp;ref=sr_pg_2"</span>,
   &lt;more page links&gt;
  ]
}
</code></pre>
<h3 id="heading-more">More</h3>
<p><em>See all features, including the following, in the full documentation</em> <a target="_blank" href="https://www.scraperapi.com/documentation/?fpr=williambraun"><em>here</em></a><em>.</em></p>
<ul>
<li><p><em>Concurrent threads</em></p>
</li>
<li><p><em>Automated retries</em></p>
</li>
<li><p><em>Async requests</em></p>
</li>
<li><p><em>Proxy ports</em></p>
</li>
<li><p><em>SDKs</em></p>
</li>
</ul>
<h1 id="heading-lets-get-scraping">Let's get scraping!</h1>
<p>There's no substitute for having the right tool for the job. Scraping without assistance can cost you valuable time searching for a usable site, wading through errors, and formatting data. ScraperAPI is a dead-simple way to level up your scraping game and put those challenges behind you.</p>
<p><a target="_blank" href="https://www.scraperapi.com?fpr=williambraun">ScraperAPI</a> has a generous free tier that offers 5,000 API credits per month and paid plans that scale to your needs. They also offer professional support to assist you with your setup. The sign-up process is painless, and free plans do not require a credit card. I was able to start scraping with it in less than 5 minutes.</p>
<p>If only I had known about this when I built a scraping tool to simulate bets on tennis matches (link <a target="_blank" href="https://blog.willbraun.dev/how-i-tried-to-get-rich-using-web-scraping">here</a>). Since I was scraping websites directly, I was getting blocked and dealing with incomplete data. It would have been a much faster and smoother experience with ScraperAPI.</p>
]]></content:encoded></item><item><title><![CDATA[Four Frontend State Management Patterns]]></title><description><![CDATA[State management is a key concept in modern front-end frameworks, and often a headache for developers. It can be difficult to write quality code in a front-end framework without a good understanding of the existing state management solution, and why ...]]></description><link>https://blog.willbraun.dev/four-frontend-state-management-patterns</link><guid isPermaLink="true">https://blog.willbraun.dev/four-frontend-state-management-patterns</guid><category><![CDATA[React]]></category><category><![CDATA[Redux]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[State Management ]]></category><category><![CDATA[zustand]]></category><dc:creator><![CDATA[Will Braun]]></dc:creator><pubDate>Wed, 05 Jul 2023 12:41:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/sBEZs5swh0Y/upload/b1566c6f92cd5bf1b65f1612fe7c1b07.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>State management is a key concept in modern front-end frameworks, and often a headache for developers. It can be difficult to write quality code in a front-end framework without a good understanding of the existing state management solution, and why it may have been chosen over the alternatives. Choosing one for a new application is also a challenge, as it must be ready for the twists and turns your project takes.</p>
<p>Originally, I wanted to find the absolute best solution for any project so that I could just learn that and continue building, but like many decisions in software development, the answer is - "It depends". In this post, I'll share the overarching patterns I've noticed along with technologies that implement them. Examples are shown for React, but there are similar solutions available for other frameworks.</p>
<p>I've created a functional demo for the examples shown in the following sections where you can follow along if you like. Here is the <a target="_blank" href="http://state-management.willbraun.dev">demo</a> and <a target="_blank" href="https://github.com/willbraun/state-mgmt-patterns">Github</a>.</p>
<h2 id="heading-what-is-state-management">What is State Management?</h2>
<p>Many front-end frameworks use declaratively defined components that change over time. Components have an internal state that dictates their behavior at any given moment. A component's state does not live in a vacuum - it nearly always needs to communicate with other components' state data. Changing one component's state often requires updating many others in a domino effect, so managing and updating state is critical to controlling your app's behavior. As more components are added, it becomes more important to have a good state management solution in place.</p>
<h2 id="heading-prop-drilling">Prop Drilling</h2>
<p>As I mentioned, components have an internal state that can be controlled using built-in features. In React, a stateful value can be created inside a component using the <code>useState</code> hook. This value can be used to control the behavior of the component, such as conditionally rendering different outputs based on the state. That's great, but we usually need to share the state with other components so they can update in unison based on input from the user or database. Components can share state with child components (components rendered in their output) via the <code>props</code> object. The children can now use that value to control their own behavior and optionally pass it down to their children also.</p>
<p>This is a simple pattern that can work for small projects, but the main issue with this approach is <em>prop drilling</em>, which is not unique to React. If you have a complex app with many layers of components in the tree, and a low-level component needs the state of a high-level component, you will have to drill down from the top level and pass the state as a prop to every intermediate component along the way, even if those components will not use the value themselves.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688385651886/f40fa248-2918-4ee6-9adf-d30b40a60d0f.jpeg" alt class="image--center mx-auto" /></p>
<p>This is an example to show the state in the Parent component being used by a Grandchild component. We have a simple toggle button that is controlling the state in the parent. Yes, this specific example could be made in a single component, but let's imagine this is a small part of a larger state where there are many settings controlled by many nested components.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// PropDrilling.tsx</span>
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">interface</span> Props {
  isToggled: <span class="hljs-built_in">boolean</span>
  handleClick: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Parent: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [isToggled, setIsToggled] = useState(<span class="hljs-literal">false</span>)

  <span class="hljs-keyword">const</span> handleClick = <span class="hljs-function">() =&gt;</span> {
    setIsToggled(!isToggled)
  }

  <span class="hljs-keyword">return</span> &lt;Child isToggled={isToggled} handleClick={handleClick} /&gt;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Child: React.FC&lt;Props&gt; = <span class="hljs-function">(<span class="hljs-params">{ isToggled, handleClick }</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> &lt;Grandchild isToggled={isToggled} handleClick={handleClick} /&gt;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Grandchild: React.FC&lt;Props&gt; = <span class="hljs-function">(<span class="hljs-params">{ isToggled, handleClick }</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;p&gt;{isToggled ? <span class="hljs-string">'ON'</span> : <span class="hljs-string">'OFF'</span>}&lt;/p&gt;
      &lt;button onClick={handleClick}&gt;Toggle <span class="hljs-keyword">with</span> Prop Drilling&lt;/button&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>In order for Grandchild to have access to update the state, we pass <code>isToggled</code> and <code>handleClick</code> as props from Parent to Child, and again from Child to Grandchild. The props object argument in Child and Grandchild is destructured in this example to simplify access to the props themselves.</p>
<p>Complexity varies slightly depending on if you are using a framework with one-way data binding (React) or one with two-way data binding (an option in Svelte). Data flows one way in React - from parent to child, whereas in a two-way data binding framework, updates to the parent or child will update the other. With one-way binding, you also need to pass a function to update the parent state down to the child as a prop, as shown in the example. There are pros and cons to each method of data binding, but that is a separate discussion. The important part is that sharing data around the tree easily is commonly needed.</p>
<p>Each child that accepts props is logically linked to its parent, so prop drilling creates a chain of dependencies that quickly becomes tedious to maintain. The rest of the patterns show different ways of avoiding this problem.</p>
<h2 id="heading-context-api">Context API</h2>
<p>In programming, the term "context" refers to the specific environment or set of conditions that code is run in. React has adopted this term for its Context API, which allows you to create a state context for your component tree. A context can be created at any point in the tree, then any child components that are wrapped inside the context provider will have access to the context's state.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688385723250/ce4eb8c6-162d-4193-829f-c92b2ab71002.jpeg" alt class="image--center mx-auto" /></p>
<p>Context is usually created at the top level so that the entire tree can have access to it. This bypasses the need for prop drilling between every intermediate component. The state still lives in the tree, it is just easier to access from the components.</p>
<p>Here is our previous example, rewritten using the Context API.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Context.tsx</span>
<span class="hljs-keyword">import</span> { createContext, useState, useContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">interface</span> ToggleContextProps {
  isToggled: <span class="hljs-built_in">boolean</span>
  handleClick: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>
}

<span class="hljs-keyword">const</span> ToggleContext = createContext&lt;ToggleContextProps&gt;({
  isToggled: <span class="hljs-literal">false</span>,
  handleClick: <span class="hljs-function">() =&gt;</span> ({}),
})

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Parent: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [isToggled, setIsToggled] = useState(<span class="hljs-literal">false</span>)

  <span class="hljs-keyword">const</span> handleClick = <span class="hljs-function">() =&gt;</span> {
    setIsToggled(!isToggled)
  }

  <span class="hljs-keyword">return</span> (
    &lt;ToggleContext.Provider value={{ isToggled, handleClick }}&gt;
      &lt;Child /&gt;
    &lt;/ToggleContext.Provider&gt;
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Child: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> &lt;Grandchild /&gt;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Grandchild: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> { isToggled, handleClick } = useContext(ToggleContext)

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;p&gt;{isToggled ? <span class="hljs-string">'ON'</span> : <span class="hljs-string">'OFF'</span>}&lt;/p&gt;
      &lt;button onClick={handleClick}&gt;Toggle <span class="hljs-keyword">with</span> Context&lt;/button&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>The toggle state and click function are still stored in the state at the Parent level, but they did not have to be passed down to Child or Grandchild as props. Grandchild was able to access them with the <code>useContext</code> hook.</p>
<p>Common use cases for this include global settings like color theme and current user account permissions. Context is accessible in React without any additional libraries, and it can be combined with the <code>useReducer</code> hook to add even more control to state updates.</p>
<p>More information on the Context API is shown <a target="_blank" href="https://react.dev/learn/passing-data-deeply-with-context">here</a>.</p>
<h2 id="heading-external-store">External Store</h2>
<p>Another approach to sharing state without prop drilling is to separate the state from the tree entirely. A store is a home for your application's state outside of the component tree. Many libraries host state outside of the component tree and provide various ways of interacting with it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688385974113/d2eaab55-7aae-4a48-9257-85c5f86a800d.jpeg" alt class="image--center mx-auto" /></p>
<p>Global variables are typically considered a bad practice in programming because they create dependencies between components using them, so what makes external stores different? Rather than merely creating a global variable that can be updated in any capacity from anywhere in the code, these libraries add guardrails so that state interactions are well-defined and testable. In <a target="_blank" href="https://docs.pmnd.rs/zustand/getting-started/introduction">Zustand</a> and other libraries, you can create a store and define functions for updating it in a specific manner. There are still logical dependencies that would occur in any global variable scenario, but many find that the simplicity of this solution when storing complex data like your application state makes it worth it compared to the alternatives.</p>
<p>Here is our toggle, now written using Zustand.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Zustand.tsx</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> { create } <span class="hljs-keyword">from</span> <span class="hljs-string">'zustand'</span>

<span class="hljs-keyword">interface</span> ToggleState {
  isToggled: <span class="hljs-built_in">boolean</span>
  handleClick: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>
}

<span class="hljs-keyword">const</span> useToggleStore = create&lt;ToggleState&gt;(<span class="hljs-function"><span class="hljs-params">set</span> =&gt;</span> ({
  isToggled: <span class="hljs-literal">false</span>,
  handleClick: <span class="hljs-function">() =&gt;</span> set(<span class="hljs-function"><span class="hljs-params">state</span> =&gt;</span> ({ isToggled: !state.isToggled })),
}))

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Parent: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> &lt;Child /&gt;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Child: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> &lt;Grandchild /&gt;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Grandchild: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> { isToggled, handleClick } = useToggleStore()

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;p&gt;{isToggled ? <span class="hljs-string">'ON'</span> : <span class="hljs-string">'OFF'</span>}&lt;/p&gt;
      &lt;button onClick={handleClick}&gt;Toggle <span class="hljs-keyword">with</span> Zustand&lt;/button&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>Notice how the state now lives in the store and not in the Parent component at all. The <code>handleClick</code> function uses Zustand's <code>set</code> function, which accepts the current state of the store and returns the updated portion of the state. Zustand merges the updated portion with the overall store, and the Grandchild component subscribes to the latest version of the state. See more information on this <a target="_blank" href="https://docs.pmnd.rs/zustand/guides/updating-state">here</a>.</p>
<p>Many state management libraries with an external store are inspired by our next pattern but with an emphasis on simplicity.</p>
<h2 id="heading-flux-model">Flux Model</h2>
<p>No state management article would be complete without the Flux model, which is almost synonymous with the <a target="_blank" href="https://redux.js.org/">Redux</a> library for React. The Flux model also includes an external store to hold state outside of the component tree, but it uses a more structured, one-way data flow to manage state predictably. It consists of four key parts.</p>
<ol>
<li><p>Actions - The different events or user interactions that can occur in an application.</p>
</li>
<li><p>Dispatcher - The central hub that receives actions and applies (dispatches) them to the appropriate store.</p>
</li>
<li><p>Stores - A store holds the application state and defines logic for handling actions and updating the state. There can be multiple stores, and they listen for actions from the Dispatcher and update their state accordingly.</p>
<ul>
<li>Reducers are functions that accept the current state and incoming action and return the updated state. Reducers aren't explicitly defined in the Flux model, but the concept is still important here.</li>
</ul>
</li>
<li><p>Views - This represents the user interface, specifically the components in the framework. They subscribe to changes in the store so the current state is always shown. Views are responsible for re-rendering content on the page when the state in a store is updated.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688385911521/4c35acb3-5727-4448-a6f4-9ae3f6a4cafc.jpeg" alt class="image--center mx-auto" /></p>
<p>Here is our example using Redux, with the officially recommended <a target="_blank" href="https://redux-toolkit.js.org/">Redux-Toolkit</a> library as well. Redux-Toolkit is an abstraction over Redux with its own functions to simplify the configuration.</p>
<p>With Redux-Toolkit, a <em>store</em> is broken up into slices to divide the state into simpler pieces. First, we create a slice, set an initial state value, then add our <em>actions</em> and <em>reducers</em>. We have an action named <code>toggle</code> which corresponds to a reducer function that flips <code>state.isToggled</code> when the action is dispatched. Typically, the slice name would not have the word "slice" in it, but I wanted to make that more clear when it is used later. Reducer functions are automatically created by the <code>createSlice</code> function, and it is common to export those as the default export.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// toggleSlice.ts</span>
<span class="hljs-keyword">import</span> { createSlice } <span class="hljs-keyword">from</span> <span class="hljs-string">'@reduxjs/toolkit'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ToggleState {
  isToggled: <span class="hljs-built_in">boolean</span>
}

<span class="hljs-keyword">const</span> initialState: ToggleState = {
  isToggled: <span class="hljs-literal">false</span>,
}

<span class="hljs-keyword">const</span> toggleSlice = createSlice({
  name: <span class="hljs-string">'toggleSlice'</span>,
  initialState,
  reducers: {
    toggle: <span class="hljs-function"><span class="hljs-params">state</span> =&gt;</span> {
      state.isToggled = !state.isToggled
    },
  },
})

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> { toggle } = toggleSlice.actions
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> toggleSlice.reducer
</code></pre>
<p>Next, we configure the store itself. The store has a root reducer that aggregates the reducers from each slice.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// store.ts</span>
<span class="hljs-keyword">import</span> { configureStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@reduxjs/toolkit'</span>
<span class="hljs-keyword">import</span> toggleReducer <span class="hljs-keyword">from</span> <span class="hljs-string">'./toggleSlice'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> RootState = ReturnType&lt;<span class="hljs-keyword">typeof</span> store.getState&gt;

<span class="hljs-keyword">const</span> store = configureStore({
  reducer: {
    toggleSlice: toggleReducer,
  },
})

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> store
</code></pre>
<p>Finally, we have the components or <em>views</em> in the Flux model. The Redux Provider allows the store to be accessed by all nested components. Components can use the <code>useSelector</code> hook to specify what state to pull from the store. This subscribes components to the store so that as actions are dispatched and the state is updated, components will update automatically as well. We also import our <code>toggle</code> action from <code>toggleSlice.ts</code> so that we can dispatch it from our component.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Redux.tsx</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> { useSelector, useDispatch, Provider } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-redux'</span>
<span class="hljs-keyword">import</span> store, { RootState } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store'</span>
<span class="hljs-keyword">import</span> { toggle } <span class="hljs-keyword">from</span> <span class="hljs-string">'./toggleSlice'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Parent: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    &lt;Provider store={store}&gt;
      &lt;Child /&gt;
    &lt;/Provider&gt;
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Child: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> &lt;Grandchild /&gt;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Grandchild: React.FC = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> isToggled = useSelector(<span class="hljs-function">(<span class="hljs-params">state: RootState</span>) =&gt;</span> state.toggleSlice.isToggled)
  <span class="hljs-keyword">const</span> dispatch = useDispatch()

  <span class="hljs-keyword">const</span> handleClick = <span class="hljs-function">() =&gt;</span> {
    dispatch(toggle())
  }

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;p&gt;{isToggled ? <span class="hljs-string">'ON'</span> : <span class="hljs-string">'OFF'</span>}&lt;/p&gt;
      &lt;button onClick={handleClick}&gt;Toggle <span class="hljs-keyword">with</span> Redux&lt;/button&gt;
    &lt;/&gt;
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Parent
</code></pre>
<p>The Flux model is a powerful way to manage state, and Redux is the choice for many enterprise React applications. Redux is overkill for our example, but it shows how much control you have at each step of the process. Also, Redux has a ton of additional functionality that is beyond the scope of this article. The robust structure does add complexity, so it's worth doing some research to determine if it is right for your project.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>If you are searching for "what state management solution is the best", then you're going to get many different answers. It is common to use a combination of built-in component state where it makes sense and one of the other patterns I mentioned for more complex state. There are also technologies not mentioned that combine elements of these patterns in various ways. The best choice for you comes down to your application's business requirements and the other technologies in your stack.</p>
<p>Did I miss any patterns? Leave a comment and let me know!</p>
]]></content:encoded></item><item><title><![CDATA[How I Tried to Get Rich Using Web Scraping]]></title><description><![CDATA[Disclosure: I get a small commission from sign-ups via links on this page, at no additional cost to you.
I've been a tennis player and fan of the pro tour my whole life. Since sports betting in the US was legalized, I've seen some friends of mine mak...]]></description><link>https://blog.willbraun.dev/how-i-tried-to-get-rich-using-web-scraping</link><guid isPermaLink="true">https://blog.willbraun.dev/how-i-tried-to-get-rich-using-web-scraping</guid><category><![CDATA[web scraping]]></category><category><![CDATA[Python]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[BeautifulSoup]]></category><category><![CDATA[SQL]]></category><dc:creator><![CDATA[Will Braun]]></dc:creator><pubDate>Sun, 28 May 2023 19:49:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1684931592316/6d69b35b-8783-417d-947d-9985b5fd48da.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Disclosure: I get a small commission from sign-ups via links on this page, at no additional cost to you.</em></p>
<p>I've been a tennis player and fan of the pro tour my whole life. Since sports betting in the US was legalized, I've seen some friends of mine make ridiculous bets on tennis matches. Even with my knowledge of the game and pro players, I can't stomach betting on matches as it seems too unpredictable. What if there was some way I could predict who was going to win matches well enough to win money in the long run?</p>
<p>Sure, this isn't a new idea. People have built elaborate sports betting models to predict outcomes well enough to make them rich, but it's difficult and risky. Casino oddsmakers have machine learning algorithms and an ocean of data at their disposal when setting betting lines, making it tough for amateur gamblers to win money over time. I'm always up for a challenge, so I formulated a plan to make smarter bets on tennis matches and dreamed of making easy money.</p>
<p>In this post, I'll share my plan and how it evolved, what technologies worked for me, and what I learned about them. I'll be discussing Python, Beautiful Soup, APIs, PostgreSQL, SQL, PLpgSQL, and launchd. This is not intended to be a step-by-step tutorial, but rather a way to show my decision-making through the entire stack.</p>
<p>Here is the <a target="_blank" href="https://github.com/willbraun/tennis-predictions">GitHub Repository</a> for this project if you'd like to follow along. Relevant code samples are included in this post.</p>
<h1 id="heading-my-master-plan">My Master Plan</h1>
<h2 id="heading-step-1-make-a-good-bet">Step 1: Make a good bet</h2>
<p>My core idea was simple, but why start complicated? To decide who to bet on for a given match, I needed two pieces of data:</p>
<ol>
<li><p>The betting odds for an upcoming match</p>
</li>
<li><p>A prediction of who will win</p>
</li>
</ol>
<h3 id="heading-american-betting-odds">American Betting Odds</h3>
<p>American betting odds are based on a bet of $100, though you can bet any amount. Each player has odds associated with them to show how much you can win with your bet. There are odds for the money line, point spread, and many other bets. We are using the money line, which is only based on the win-loss outcome of the match.</p>
<ul>
<li><p>The favorite's odds are usually negative. Odds of -170 mean if you bet $170, you make $100 if that player wins (plus your original bet of $170).</p>
</li>
<li><p>The underdog's odds are usually positive. Odds of 200 mean that if you bet $100, you make $200 if that player wins (plus your original bet of $100).</p>
</li>
<li><p>Of course, if your player loses, you lose your bet!</p>
</li>
</ul>
<p>Heavily lopsided matchups may have odds of -900/+700, while more even matchups may have odds of -150/+110. Casinos set these odds in such a way that they will probably make money no matter what, given a large enough number of people placing bets.</p>
<p>This information is already out there on the internet, I would just need to figure out the best way to bring it into my program to do the calculations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684931154110/5071d06e-1ab0-4875-9290-e37f1ae5aed7.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-predicting-the-winner">Predicting The Winner</h3>
<p>This part was a mystery for a while. At first, it seemed like I would need to build a separate program involving AI to determine this, which would have been a very deep rabbit hole. I stumbled upon a website called <a target="_blank" href="https://www.ultimatetennisstatistics.com/">Ultimate Tennis Stats</a>, which has a treasure trove of pro tennis data and built-in analytics.</p>
<p>They have a feature that allows you to set up a hypothetical tennis match between any two players, and it will output a probability of who will win as a percentage. For example, as of the time of writing, it predicts that Novak Djokovic has a 74.2% chance of winning against Rafael Nadal. Novak just won the Australian Open and Rafa has had some injuries lately, so this seems reasonable. This number is just what I needed to continue!</p>
<p>Credit to the Ultimate Tennis Stats team for building an amazing tool.</p>
<h3 id="heading-putting-it-all-together">Putting It All Together</h3>
<p>Remember, I'm not just betting on who the prediction says will win the match. I want to make bets that will win money over time factoring in the odds for both players. To do this, I imagined that the players would play 1000 matches, with the match results being split based on the predicted win probability. Then I'd calculate my payout if I bet $1 on player 1 for all matches vs player 2 for all matches. Whichever bet had the higher payout would be my choice. If both bets lose money, I would skip betting on this match entirely.</p>
<p>Here is a table to show the calculation. The light green fields are the inputs for each bet, and the player with the higher positive value in row 7 is my choice.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681733215595/3185a17b-bac7-4f1e-bb00-595c2839aa3d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-2-simulate-bets">Step 2: Simulate bets</h2>
<p>Before wagering a single cent, I wanted to ensure that this method would actually make money. I needed to <em>simulate</em> bets on real tennis matches as a risk-free proof of concept for my idea, rather than bet real money and find out the hard way.</p>
<p>In addition to the odds and win prediction before the match, I would also need to pull in the results of the match afterward so I could simulate a bet result in terms of dollars. If I bet $1 per match, a win on a player whose odds are +320 would earn me $3.20, while a loss would lose my $1 bet. As more matches are played, I would track my simulated earnings over time and store the results in a database for analysis.</p>
<h2 id="heading-step-3-automate-bets-with-real-money">Step 3: Automate bets with real money</h2>
<p>If my hypothesis in step 2 is correct and the simulation shows a positive return, the next step would be to automate bets with real money, and get rich!</p>
<h1 id="heading-time-for-action">Time for Action</h1>
<h2 id="heading-designing-the-simulation">Designing the Simulation</h2>
<p>Now that I had a plan for individual bets, I needed to build out my program to simulate them over multiple matches. The structure of the program would be simple - a single script file that would gather the data from the web, crunch the numbers, and output the results to a database. There would be no need for a graphical user interface. For these reasons, Python was an obvious choice of language to use. It is a multi-purpose, open-source scripting language with many packages that can be used to extend its functionality. Not to mention, the syntax is easy to read and it's a joy to work with.</p>
<p>There are two main objectives for the script</p>
<ol>
<li><p>Create bet decisions for upcoming matches</p>
</li>
<li><p>Determine the simulated payout for completed matches</p>
</li>
</ol>
<p>In my script, the updates come first as it made more sense to me to update completed matches before predicting future matches, but these sections could be run in either order.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684759565445/25674d53-1a4c-46bd-94a6-95f8e67050c3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-gathering-the-inputs">Gathering the Inputs</h2>
<p>I needed to reach out to the web to pull the odds and a win prediction into my program. This is a classic use case for an API (Application Programming Interface), a typical way for applications to communicate over the internet. However, many customer-facing casino odds APIs aren't free, and I still wasn't sure if this project would make any money. I also needed a way to get the win probability from Ultimate Tennis Stats.</p>
<p>This information is available publicly on the web, and after some research, I found that <strong>web scraping</strong> was what I needed. It's a programmatic way to pull in data from a human-readable web page. Web pages are made up of HTML, CSS, and JavaScript, where HTML represents the raw content of the page. Its main purpose is to provide a user with information after it is styled by CSS and made functional by JavaScript, but it can also be read by search engines for SEO.</p>
<p>Web scraping in itself is completely legal as long as the data is publicly available. However, there are some ethical issues you should consider to make sure your program isn't breaking other laws regarding personal or copyrighted data. For example, I wouldn't recommend scraping personal information to sell without permission. Check out this <a target="_blank" href="https://blog.apify.com/is-web-scraping-legal/">article</a> for more information on ethical web scraping.</p>
<p>To recap, the three pieces of data that I needed for each match were</p>
<ol>
<li><p>Betting odds for the match</p>
</li>
<li><p>A prediction of who will win</p>
</li>
<li><p>The results of the match</p>
</li>
</ol>
<p>I had chosen my website for the prediction, so I went ahead to see if I could scrape it into my program. The most popular Python package for web scraping is called Beautiful Soup, so I installed it into my project directory. The magic is its ability to turn a long string of HTML as text into a BeautifulSoup object, which can be easily parsed with its built-in functions to extract the desired data.</p>
<p>Here is the web page with the probability I was trying to scrape.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681904520171/d41b874f-3e79-46d5-88ae-014a9cd3b1ad.png" alt class="image--center mx-auto" /></p>
<p>This is a hypothetical match between the 2 top-ranked male players at the time of writing, but this could show any two players by manipulating the player IDs in the URL. I only need player 1's win probability as the other is dependent on it. Let's try to scrape this information with a demo program, <code>demo.py</code>, first without Beautiful Soup to get the raw HTML as text.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests

session = requests.Session()

p1_id = <span class="hljs-number">4920</span> <span class="hljs-comment"># Novak Djokovic</span>
p2_id = <span class="hljs-number">52602</span> <span class="hljs-comment"># Carlos Alcaraz</span>
url = <span class="hljs-string">f'https://www.ultimatetennisstatistics.com/headToHead?tab=hypotheticalMatchup&amp;playerId1=<span class="hljs-subst">{p1_id}</span>&amp;playerId2=<span class="hljs-subst">{p2_id}</span>'</span>

response = session.get(url, headers={<span class="hljs-string">'User-Agent'</span>: <span class="hljs-string">'Mozilla/5.0'</span>})
print(response.text)
</code></pre>
<p>Here, we are using the built-in Python <code>requests</code> package to make a call to the URL of the web page. The session allows us to make the request and store additional parameters like the request header. Some websites block web scraping by checking the user agent, which has a default value similar to <code>python-requests/2.25.0</code> depending on your version of Python. We need to change the User-Agent header to simulate a request from a browser so we are less likely to be blocked. I'm using <code>Mozilla/5.0</code> but there are others that will work.</p>
<p>The IDs are Ultimate Tennis Stats' internal IDs for each player. I pulled them out of the URL as those will be determined dynamically later on depending on the upcoming matches. We can run this program by opening the terminal, cd-ing into our project directory, and running <code>python3 demo.py</code>. We get a long string of HTML as text, just as we were hoping. Great!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681905808466/9877ee3d-a2e2-47da-994f-bd2ca855e77f.png" alt class="image--center mx-auto" /></p>
<p>Now let's try parsing it with Beautiful Soup to find the probability. I used Beautiful Soup's <code>find</code> function to navigate the object and find where the text "Win Probability" is on the page, as I know the value isn't far away.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> bs4 <span class="hljs-keyword">import</span> BeautifulSoup

session = requests.Session()

p1_id = <span class="hljs-number">4920</span> <span class="hljs-comment"># Novak Djokovic</span>
p2_id = <span class="hljs-number">52602</span> <span class="hljs-comment"># Carlos Alcaraz</span>
url = <span class="hljs-string">f'https://www.ultimatetennisstatistics.com/headToHead?tab=hypotheticalMatchup&amp;playerId1=<span class="hljs-subst">{p1_id}</span>&amp;playerId2=<span class="hljs-subst">{p2_id}</span>'</span>

response = session.get(url, headers={<span class="hljs-string">'User-Agent'</span>: <span class="hljs-string">'Mozilla/5.0'</span>})
doc = BeautifulSoup(response.text, <span class="hljs-string">'html.parser'</span>)
win_prob_row = doc.find(string=<span class="hljs-string">'Win Probability'</span>)
print(win_prob_row)
</code></pre>
<p>But when I run it, I get a surprise 😱</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681905909671/07fb1af8-982c-4bde-b583-faca23a69712.png" alt class="image--center mx-auto" /></p>
<p>None!? I can see the element on the page, so what's going on? After searching the HTML provided by that URL, the text "Win Probability" was, in fact, not there at all. So how was it showing up on the page?</p>
<p>The issue is that not all of the data is loaded immediately. The rest is being loaded from separate API calls after the initial page request. I found that it is common for web scrapers to request data from these API endpoints directly, rather than requesting just from the page URL. The response can come back as HTML, JSON, XML, or other formats.</p>
<p>You may be wondering - didn't I say earlier that using an API wasn't an option? I was referring to customer-facing APIs, where access is officially supported for building applications. The APIs that web pages use internally are not supported for external use (like web scraping) and can change frequently. Web scraping programs often require maintenance because the data can change without warning.</p>
<p>By checking the Chrome dev tools Network tab, we can find the API call the page makes to get the probability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681907118903/2dec4caf-09a9-470d-b3db-bd7c831e3d2a.png" alt class="image--center mx-auto" /></p>
<p>Bingo! The probability is in our sights now. The response format is HTML, so we can continue to use Beautiful Soup to parse it. Let's replace the URL currently in our code with the URL for the API (full URL shown in the Network &gt; Headers tab).</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> bs4 <span class="hljs-keyword">import</span> BeautifulSoup

session = requests.Session()

p1_id = <span class="hljs-number">4920</span> <span class="hljs-comment"># Novak Djokovic</span>
p2_id = <span class="hljs-number">52602</span> <span class="hljs-comment"># Carlos Alcaraz</span>
url = <span class="hljs-string">f'https://www.ultimatetennisstatistics.com/h2hHypotheticalMatchup?playerId1=<span class="hljs-subst">{p1_id}</span>&amp;playerId2=<span class="hljs-subst">{p2_id}</span>'</span>

response = session.get(url, headers={<span class="hljs-string">'User-Agent'</span>: <span class="hljs-string">'Mozilla/5.0'</span>})
doc = BeautifulSoup(response.text, <span class="hljs-string">'html.parser'</span>)
win_prob_row = doc.find(string=<span class="hljs-string">'Win Probability'</span>)
print(win_prob_row)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681907436202/8ee0d2ce-d063-4ad6-afe9-9e0b146eb6a6.png" alt class="image--center mx-auto" /></p>
<p>Now we have the correct BeautifulSoup object, so we just need to do a little more manipulation to get the probability.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> bs4 <span class="hljs-keyword">import</span> BeautifulSoup

session = requests.Session()

p1_id = <span class="hljs-number">4920</span> <span class="hljs-comment"># Novak Djokovic</span>
p2_id = <span class="hljs-number">52602</span> <span class="hljs-comment"># Carlos Alcaraz</span>
url = <span class="hljs-string">f'https://www.ultimatetennisstatistics.com/h2hHypotheticalMatchup?playerId1=<span class="hljs-subst">{p1_id}</span>&amp;playerId2=<span class="hljs-subst">{p2_id}</span>'</span>

response = session.get(url, headers={<span class="hljs-string">'User-Agent'</span>: <span class="hljs-string">'Mozilla/5.0'</span>})
doc = BeautifulSoup(response.text, <span class="hljs-string">'html.parser'</span>)
win_prob_row = doc.find(string=<span class="hljs-string">'Win Probability'</span>)
p1_win_prob = float(win_prob_row.parent.parent.find(<span class="hljs-string">'h4'</span>).contents[<span class="hljs-number">0</span>].replace(<span class="hljs-string">'%'</span>, <span class="hljs-string">''</span>))
print(p1_win_prob)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681907769419/661b7ede-3199-44f3-b15f-0d7f21ea1682.png" alt class="image--center mx-auto" /></p>
<p>Success! 🎉 Calling the API endpoints directly is now my preferred web scraping method. I experimented with several other sites to find the remaining numbers I needed, then I used this method to bring them into the project. Some endpoints returned JSON data, which can be parsed with the built-in "json" Python package rather than with Beautiful Soup.</p>
<p>After finishing this project, I found a tool called <a target="_blank" href="https://www.scraperapi.com/?fpr=williambraun">ScraperAPI</a> that would have made life easier here. It allows scraping programs to bypass IP restrictions, retry automatically, execute JavaScript, and do many other things so that you don't need to scour the web to find a scrapable site like I did. They have a nice free plan to get started.</p>
<h2 id="heading-configuring-the-database">Configuring the Database</h2>
<p>There are several reasons I needed to store data. First, the bet decision needs to be made before the match is played. I had to store it so that the program can check it against the result once the match is played to calculate the simulated payout. Second, the results need to be stored so that the total simulated payout can be calculated.</p>
<p>I chose PostgreSQL (Postgres) as the database since it is a free, open-source, relational database and is popular at the enterprise level. I could have used something simpler like Google Sheets or even Excel, but this was a perfect opportunity for me to get my hands dirty with SQL.</p>
<p>Originally, I wanted to host the database and the program online so that this could run around the clock, but I decided to keep everything running locally on my computer for simplicity. Running the program a few times a day would be sufficient to get the match odds, win predictions, and results, and I could do that manually to start. I installed Postgres locally as well as pgAdmin, a program where I could easily write SQL statements and manage the data. I set up a table named <code>matches</code> with the following columns, which evolved over time as I realized what I needed.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Column Name</td><td>Description</td><td>Postgres Data Type</td></tr>
</thead>
<tbody>
<tr>
<td>Unique ID</td><td>Uniquely enforced identifier for each table row (Primary Key in table)</td><td>Integer</td></tr>
<tr>
<td>Player 1 Name</td><td>Player 1's full name</td><td>Character varying (255)</td></tr>
<tr>
<td>Player 2 Name</td><td>Player 2's full name</td><td>Character varying (255)</td></tr>
<tr>
<td>Player 1 Probability</td><td>The predicted percentage that Player 1 will win</td><td>Double precision</td></tr>
<tr>
<td>Player 1 Odds</td><td>Player 1's betting odds</td><td>Integer</td></tr>
<tr>
<td>Player 2 Odds</td><td>Player 2's betting odds</td><td>Integer</td></tr>
<tr>
<td>Player 1 Total</td><td>The simulated payout after 1000 matches</td><td>Double precision</td></tr>
<tr>
<td>Player 2 Total</td><td>The simulated payout after 1000 matches</td><td>Double precision</td></tr>
<tr>
<td>Decision</td><td>0 (no bet), 1 (bet on player 1), or 2 (bet on player 2)</td><td>Integer</td></tr>
<tr>
<td>Bet Result</td><td>Calculated payout based on a $1 bet, after match completion</td><td>Double precision</td></tr>
<tr>
<td>Start Epoch</td><td>Epoch time of the match start</td><td>Big integer</td></tr>
<tr>
<td>DateTimeUTC</td><td>UTC time of the match start</td><td>Timestamp without time zone</td></tr>
<tr>
<td>Match ID</td><td>The ID of the match from the odds site</td><td>Integer</td></tr>
</tbody>
</table>
</div><p>The next piece of the puzzle was connecting my Python script to the Postgres database. I needed to create SQL statements and send them to the database for execution. I found <a target="_blank" href="https://pypi.org/project/psycopg2/">psycopg2</a>, which is the most popular Postgres database adapter for Python, and installed it into my project. With this tool, I could now open a connection to my database, send it a string of SQL to execute, and close the connection when completed.</p>
<h2 id="heading-crunching-the-numbers">Crunching the Numbers</h2>
<h3 id="heading-the-bet-prediction">The Bet Prediction</h3>
<p><em>Previously, the prediction we've referred to is the external prediction of who will win the match, which we scraped from Ultimate Tennis Stats. Now, the prediction we're referring to is the program's prediction of who to bet on - the Decision column in the database. In hindsight, my variable naming could have been more clear.</em> 😅</p>
<p>Once the program scrapes what the upcoming matches are along with each player's betting odds, it can determine who to bet on. Here is the calculation of its prediction in Python. I've taken the betting odds scraped earlier and distilled them into <code>p1</code> and <code>p2</code> objects for each match, which have the properties <code>name</code> and <code>odds</code> for each player. <code>p1_win_prob</code> is the probability that we scraped earlier (61.4 in our example).</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_factor</span>(<span class="hljs-params">odds</span>):</span>
    <span class="hljs-keyword">if</span> odds &gt;= <span class="hljs-number">0</span>:
        <span class="hljs-keyword">return</span> abs(odds)/<span class="hljs-number">100</span>
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-number">100</span>/abs(odds)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">make_prediction</span>(<span class="hljs-params">p1_win_prob, p1, p2</span>):</span>
    sim_p1_wins = p1_win_prob * <span class="hljs-number">10</span>
    sim_p2_wins = <span class="hljs-number">1000</span> - sim_p1_wins

    p1_win = sim_p1_wins * get_factor(p1[<span class="hljs-string">'odds'</span>])
    p1_lose = sim_p2_wins * <span class="hljs-number">-1</span>
    p2_win = sim_p2_wins * get_factor(p2[<span class="hljs-string">'odds'</span>])
    p2_lose = sim_p1_wins * <span class="hljs-number">-1</span>

    p1_total = p1_win + p1_lose
    p2_total = p2_win + p2_lose

    <span class="hljs-keyword">global</span> prediction

    <span class="hljs-keyword">if</span> p1_total &lt; <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> p2_total &lt; <span class="hljs-number">0</span>:
        prediction = <span class="hljs-number">0</span>
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">if</span> p1_total &gt; p2_total:
            prediction = <span class="hljs-number">1</span>
        <span class="hljs-keyword">else</span>:
            prediction = <span class="hljs-number">2</span>

    <span class="hljs-keyword">return</span> [p1_total, p2_total, prediction]
</code></pre>
<p>The function <code>get_factor</code> determines how much money you win per dollar bet if that player wins. Then in <code>make_prediction</code>, it follows the same logic as shown earlier in the table.</p>
<ol>
<li><p>Find the predicted number of wins for each player out of 1000 matches. In our example, it would be 614 for player 1 and 386 for player 2.</p>
</li>
<li><p>Find the total (net) payout after all wins and losses for a bet on each player</p>
</li>
<li><p>Bet on the player with the higher positive total. If neither is positive, don't bet.</p>
</li>
</ol>
<p>It now has all the data it needs to upload its match prediction to the <code>matches</code> table as "Decision". First, it formats the relevant match data into a string, representing a single row insertion to the table.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_row_string</span>(<span class="hljs-params">event</span>):</span>
    [p1, p2, start_epoch, match_id] = unpack_event(event)
    p1_win_prob = get_win_prob(p1, p2)
    [p1_total, p2_total, prediction] = make_prediction(p1_win_prob, p1, p2)

    row_string = <span class="hljs-string">f"""(DEFAULT, '<span class="hljs-subst">{p1[<span class="hljs-string">'name'</span>]}</span>', '<span class="hljs-subst">{p2[<span class="hljs-string">'name'</span>]}</span>', <span class="hljs-subst">{p1_win_prob}</span>, <span class="hljs-subst">{p1[<span class="hljs-string">'odds'</span>]}</span>, <span class="hljs-subst">{p2[<span class="hljs-string">'odds'</span>]}</span>, <span class="hljs-subst">{p1_total}</span>, <span class="hljs-subst">{p2_total}</span>, <span class="hljs-subst">{prediction}</span>, <span class="hljs-subst">{start_epoch}</span>, <span class="hljs-subst">{match_id}</span>, '<span class="hljs-subst">{convert_time(start_epoch)}</span>')"""</span>
    <span class="hljs-keyword">return</span> row_string
</code></pre>
<p>These strings are used to build an INSERT SQL statement to upload all of them into <code>matches</code>. The <code>betresult</code> column is excluded since the matches haven't happened and do not have results yet.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">insert_new_matches</span>(<span class="hljs-params">match_data</span>):</span>
    start_string = <span class="hljs-string">f"""INSERT INTO <span class="hljs-subst">{db_table}</span> (Id, Player1Name, Player2Name, Player1Prob, Player1Odds, Player2Odds, Player1Total, Player2Total, Decision, StartEpoch, MatchId, DateTimeUTC) VALUES """</span>
    value_string = <span class="hljs-string">', '</span>.join(list(map(create_row_string, match_data)))
    where_string = <span class="hljs-string">' ON CONFLICT (MatchId) DO NOTHING'</span>

    insert_string = start_string + value_string + where_string + <span class="hljs-string">';'</span>
    util.sql_command(cur, conn, insert_string)
</code></pre>
<h3 id="heading-the-simulated-payout">The Simulated Payout</h3>
<p>After the matches have been completed, we can come back and calculate the payout (<code>betresult</code>) based on a bet of $1 on each match. All we need for that is</p>
<ol>
<li><p>The betting odds</p>
</li>
<li><p>Our bet decision from the previous step</p>
</li>
<li><p>The result of the match</p>
</li>
</ol>
<p>The odds and decisions are already in our database, and we can scrape the match results from the web, so what's the best way to do this calculation for each match? There were two approaches I could take.</p>
<ol>
<li><p>Pull the decision and odds out of the database, do the calculation in Python, and send the <code>betresult</code> back to the database.</p>
</li>
<li><p>Send the match result to the database first, do the calculation at the database level, and update the match rows with the <code>betresult</code>.</p>
</li>
</ol>
<p>With option 1, I already had most of the logic for determining a <code>betresult</code> in Python, but I would have to make two SQL commands for each match: SELECT and UPDATE. With option 2, I would have to rewrite some of the logic as a function in SQL, but I would only need one SQL command per match: UPDATE. While option 1 may have been easier, option 2 seemed more efficient since the number of database calls was reduced.</p>
<p>First, I defined the SQL function <code>get_bet_result</code>. It takes the winner and loser of the match as parameters, pulls in the decision and odds from the table, and calculates the payout. <a target="_blank" href="https://www.postgresql.org/docs/current/plpgsql-overview.html">PLpgSQL</a> is a language built into Postgres that allows you to define functions and other logic to execute at the database level. I wrote out the function as a string in the Python script, and when the script runs it sends a CREATE command to the database to define it. The function is long so check out my repo if interested. Then, for each match, the program sends an UPDATE command that sets the <code>betresult</code> column to the returned value from <code>get_bet_result(winner, loser)</code> for the row in the table that correlates to the same match as the results.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_match</span>(<span class="hljs-params">match_result</span>):</span>
    winner = util.sanitize(match_result[<span class="hljs-number">0</span>])
    loser = util.sanitize(match_result[<span class="hljs-number">1</span>])

    update_string = <span class="hljs-string">f"""
        UPDATE 
            <span class="hljs-subst">{db_table}</span> 
        SET 
            betresult = get_bet_result('<span class="hljs-subst">{winner}</span>', '<span class="hljs-subst">{loser}</span>')
        WHERE 
            (player1name LIKE '%' || '<span class="hljs-subst">{winner}</span>' || '%' OR player2name LIKE '%' || '<span class="hljs-subst">{winner}</span>' || '%')
            AND (player1name LIKE '%' || '<span class="hljs-subst">{loser}</span>' || '%' OR player2name LIKE '%' || '<span class="hljs-subst">{loser}</span>' || '%')
            AND CAST(EXTRACT(epoch FROM NOW()) AS BIGINT)*1000 - startepoch &lt; 345600000;
    """</span>

    util.sql_command(cur, conn, update_string)
</code></pre>
<p>Now that we can simulate payouts, we can get a sense of if this is a viable strategy! Let's open pgAdmin and check out our table.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682596942664/2f1b705e-514a-4005-83f6-288d408e21b4.png" alt class="image--center mx-auto" /></p>
<p>Here is a close-up view of the first row.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682596964204/f20ebfcf-fa3e-4a6a-8343-0d3efbb176cc.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682596970440/17bc1ca3-7c23-4ea7-a17e-dfa16652608f.png" alt class="image--center mx-auto" /></p>
<p>In this match, Baez is the favorite against Darderi based on the odds and the win probability. Our program simulated a gain of $68.17 after 1000 $1 bets on him and a loss of $274.30 after the same bets on Darderi. Therefore, the <code>decision</code> is 1 (Baez, player 1). The <code>betresult</code> column is positive, meaning Baez won the match and we won our bet! We gained $0.41 on a hypothetical $1 bet.</p>
<p>Any match with a decision of 0 means we didn't bet on it, so the <code>betresult</code> is automatically 0. If match results aren't found, then <code>betresult</code> stays null. The odds data and match result data are coming from two different sources, so sometimes we don't get results. Oh well.</p>
<h2 id="heading-running-the-program">Running the Program</h2>
<p>I can run the program manually now, but the ATP Tour doesn't wait for me. Matches are played all the time around the world, so I needed to automate running this script to gather the odds and results around the clock. I mentioned before that ideally this would run on a cloud server that is always alive, but for simplicity and cost, it's running locally on my computer.</p>
<p>I found a program called <a target="_blank" href="https://www.launchd.info/">launchd</a>, which allows Mac users to run programs automatically as long as the computer is awake. This is installed on Mac computers by default to run processes in the background. To add my own process, I needed to add a special <code>.plist</code> file to the <code>/Library/LaunchAgents</code> directory on my computer. Here's what it looks like.</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">plist</span> <span class="hljs-meta-keyword">PUBLIC</span> <span class="hljs-meta-string">"-//Apple//DTD PLIST 1.0//EN"</span> <span class="hljs-meta-string">"http://www.apple.com/DTDs/PropertyList-1.0.dtd"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">plist</span> <span class="hljs-attr">version</span>=<span class="hljs-string">"1.0"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">dict</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>Label<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>launchd_tennis_predictions<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>RunAtLoad<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">true</span>/&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>ProgramArguments<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">array</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/usr/bin/python3<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/absolute/path/to/index.py<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">array</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>WorkingDirectory<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/absolute/path/to/working/directory<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>StartInterval<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">integer</span>&gt;</span>3600<span class="hljs-tag">&lt;/<span class="hljs-name">integer</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>StandardOutPath<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/absolute/path/to/out.log<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>StandardErrorPath<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/absolute/path/to/error.log<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dict</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">plist</span>&gt;</span>
</code></pre>
<p>The format is an HTML-like list of key-value pairs with the settings. All file and directory paths need to be absolute paths.</p>
<ul>
<li><p>Label - a name for the program</p>
</li>
<li><p>RunAtLoad - whether or not to run when the computer boots up</p>
</li>
<li><p>Program Arguments - an array of string arguments. Think of it as the arguments you would type in the command line. Instead of <code>python3 index.py, it would be /usr/bin/python3 /absolute/path/to/index.py</code>.</p>
</li>
<li><p>Working directory - absolute path to the directory holding your program file</p>
</li>
<li><p>Start Interval - the number of seconds between each execution</p>
</li>
<li><p>Standard Out Path - absolute path to the file logging any output messages, such as print statements.</p>
</li>
<li><p>Standard Error Path - absolute path to the file logging errors.</p>
</li>
</ul>
<p>Now my file runs every hour as long as my computer is awake. I set up the scraping to pull odds for all available upcoming matches and match results for the last few days so that I have the best chance of gathering those while I use my computer normally for other things.</p>
<h2 id="heading-testing">Testing</h2>
<p>During development, I didn't want to run untested SQL commands on my table of real data, so I needed a test table. I created a <code>matches_test</code> database table and an <code>index_test.py</code> script file to act as my test environment. The script file checks its own file name and uses the corresponding database table. All development can be done in <code>index_test.py</code> and <code>matches_test</code>, then when I'm satisfied with the results I just copy index_test.py to index.py. It's not the most robust test environment but it works well enough for my needs.</p>
<h1 id="heading-the-results">The Results</h1>
<p>The measure of success for my hypothesis is simply the sum of the <code>betresult</code> column, across all matches. Here is the SQL statement I used to get the sum, along with the total dollars bet and rate of return on our simulated investment.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> 
    <span class="hljs-keyword">SUM</span>(betresult) <span class="hljs-keyword">AS</span> payout,
    <span class="hljs-keyword">COUNT</span>(betresult) <span class="hljs-keyword">AS</span> dollars_bet,
    <span class="hljs-keyword">SUM</span>(betresult)*<span class="hljs-number">100</span>/<span class="hljs-keyword">COUNT</span>(betresult) <span class="hljs-keyword">AS</span> rate_of_return
<span class="hljs-keyword">FROM</span> 
    matches
<span class="hljs-keyword">WHERE</span>
    betresult != <span class="hljs-number">0</span> <span class="hljs-keyword">AND</span> betresult <span class="hljs-keyword">IS</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>;
</code></pre>
<p>I wrapped this in another Python script called <code>get_result.py</code> so that I could quickly check from the terminal rather than booting up pgAdmin. Drumroll, please!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682686618655/f8a03945-bdee-4a47-a9fd-d68a2cf8286f.png" alt class="image--center mx-auto" /></p>
<p>After about 6 months, the program made predictions on 909 matches. Some match results were missed if my computer was off for a while, and some matches it chose not to bet on. After simulating $366 in bets, we have a loss of $48.60 for a rate of return of <strong>-13.28%</strong>. Darn!</p>
<p>The script showed that this method in particular would not make money, but I was successful in testing my idea for free! While this program did not make me rich, I gained a wealth of knowledge and had a blast while doing it.</p>
<h1 id="heading-the-end">The End</h1>
<p>I hope you enjoyed this post! Please leave a like and a comment as I would love to hear your feedback. Was there a flaw in my logic? Could I have improved this somehow? Let me know!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682708616715/b1916d30-48b4-4d4d-997f-2da03dd74b88.jpeg" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Hello World]]></title><description><![CDATA[Hi! I'm Will, and I'll be writing about topics in software development that I find exciting. When I'm not coding at work, I enjoy tinkering with new tech, whether it's cutting-edge or just new to me. I want to share what I learn with the community th...]]></description><link>https://blog.willbraun.dev/hello-world</link><guid isPermaLink="true">https://blog.willbraun.dev/hello-world</guid><category><![CDATA[introduction]]></category><category><![CDATA[coding]]></category><category><![CDATA[First Blog]]></category><category><![CDATA[aboutme]]></category><dc:creator><![CDATA[Will Braun]]></dc:creator><pubDate>Wed, 12 Apr 2023 12:26:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Wj1D-qiOseE/upload/ef5c813d2e12c4f391b9d8245d9c56cd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi! I'm Will, and I'll be writing about topics in software development that I find exciting. When I'm not coding at work, I enjoy tinkering with new tech, whether it's cutting-edge or just new to me. I want to share what I learn with the community that I have learned so much from, and I hope this blog helps someone along their journey.</p>
<h2 id="heading-how-i-got-here-in-a-nutshell">How I got here, in a nutshell</h2>
<p>Last year, I changed careers to software development. I previously worked in product management within the tech industry, designing new features for software products before handing them over to the development team to be built. I have always loved problem-solving, and I decided that I wanted to build the features myself. By this point in my career, I had dabbled with small coding projects and had definitely caught "the itch". However, with the pandemic and other life events going on, I couldn’t take the leap to pursue a career where I could code full-time.</p>
<p>After several years of pondering the decision and waiting for the stars to align, I quit my job so that I could gain the necessary skills at Carolina Code School, a full-time web development bootcamp. I am now a software engineer at SIOS Technology Corp., where I get to develop high availability and disaster recovery solutions for cloud systems. Sounds crazy when I think about where I was just last year!</p>
<h2 id="heading-what-to-expect">What to expect</h2>
<p>Since becoming employed, I have had more time to explore the wide world of web development (pun intended). People ask if I prefer frontend or backend development as if it's a binary decision, but I can't pick just between those as there are so many interesting topics out there. I am currently interested in frontend frameworks, full stack development, UX, AI, machine learning, and cloud computing to name a few.</p>
<p>The field is expanding faster than any one person can keep up with, which makes it exciting to be a part of. My goal with this blog is to share what I build, the problems I solve, and the lessons I learn in my software development career.</p>
<h2 id="heading-interested">Interested?</h2>
<ul>
<li><p>Subscribe to my <a target="_blank" href="https://blog.willbraun.dev/newsletter">newsletter</a></p>
</li>
<li><p>Check out my <a target="_blank" href="https://willbraun.dev">website</a></p>
</li>
<li><p>Connect with me on <a target="_blank" href="https://www.linkedin.com/in/williamhbraun/">LinkedIn</a></p>
</li>
<li><p>View my projects on <a target="_blank" href="https://github.com/willbraun">GitHub</a></p>
</li>
</ul>
<h2 id="heading-thank-you">Thank you!</h2>
]]></content:encoded></item></channel></rss>