<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Patricio Salazar</title>
    <description>The latest articles on DEV Community by Patricio Salazar (@patriciosalazar).</description>
    <link>https://dev.to/patriciosalazar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F583216%2Fea905c0b-1af2-4313-8a71-ae6856c9ff39.jpg</url>
      <title>DEV Community: Patricio Salazar</title>
      <link>https://dev.to/patriciosalazar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/patriciosalazar"/>
    <language>en</language>
    <item>
      <title>I Turned My Old Broken 2011 MacBook Pro Into My First Home Server</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Fri, 29 May 2026 15:17:09 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/i-turned-my-old-broken-2011-macbook-pro-into-my-first-home-server-4ji7</link>
      <guid>https://dev.to/patriciosalazar/i-turned-my-old-broken-2011-macbook-pro-into-my-first-home-server-4ji7</guid>
      <description>&lt;p&gt;I’ve been thinking about three things a lot recently.&lt;/p&gt;

&lt;p&gt;With a growing family, I've been thinking a lot about data and privacy. About how aggressive ads have gotten on platforms we already pay for. About what happens to a lifetime of photos.&lt;/p&gt;

&lt;p&gt;Those three things were in the back of my mind when YouTube served me a video about turning old MacBook Pros into home servers... and I happened to have one. Of course Google/YouTube know that somehow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The machine
&lt;/h2&gt;

&lt;p&gt;It's an old MacBook Pro from 2011. I thought it was completely dead. One day the screen suddenly failed and never worked again. A Genius Bar tech told me the graphics card was toast. Classic 2011 MacBook Pro problem, apparently. Google "RadeonGate" if you want to fall down that rabbit hole.&lt;/p&gt;

&lt;p&gt;Anyway, I had so much stuff on that hard drive that I held onto it for all these years with a plan to recover all the data.&lt;/p&gt;

&lt;p&gt;After watching some of those YouTube videos, it made me wonder: did my dusty MacBook Pro have a second life in it?&lt;/p&gt;

&lt;p&gt;I ordered a new charging cable and a mini DisplayPort adapter off Amazon and figured I'd find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it back on
&lt;/h2&gt;

&lt;p&gt;The first little win was seeing that the MacBook still charged. Good sign. I connected the external monitor, hit the power button, and voila - it turned on.&lt;/p&gt;

&lt;p&gt;But still, no display.&lt;/p&gt;

&lt;p&gt;The mini DisplayPort cable wasn't sending a signal to the monitor whether the lid was open or closed (and of course, the MacBook's screen was still failing).&lt;/p&gt;

&lt;p&gt;But I could hear it. The fans were spinning up. The startup chime was firing. Something was happening in there.&lt;/p&gt;

&lt;p&gt;What happened most commonly on boot was this: it would reach the Apple loading screen and freeze. The display looked completely destroyed with rainbow pixels and distortion so bad you couldn't see the progress of the loading bar. It was just stuck there.&lt;/p&gt;

&lt;p&gt;However, in true tinkering spirit, I was determined to get this thing to work and I was expecting a battle, so a few bumps in the road were not going to deter me.&lt;/p&gt;

&lt;p&gt;I searched for alternative boot modes and the mode that actually gave me something to work with was verbose boot. On a Mac, you hold &lt;strong&gt;Command + V&lt;/strong&gt; right at startup. Instead of the Apple loading screen, you get a wall of scrolling text showing exactly what the system is doing as it loads. It gives you pure information.&lt;/p&gt;

&lt;p&gt;The problem was the text was just as distorted as everything else. All the letters and words looked like corrupted data. It wasn't legible at all.&lt;/p&gt;

&lt;p&gt;So I started taking photos of the screen with my phone and sending them to Claude.&lt;/p&gt;

&lt;p&gt;This is where things got interesting. Claude could pick out patterns in the distortion. It picked up on partial words, recognizable kernel extension names, and fragments of log lines. And it identified exactly where the boot process was hanging, which was the AMD GPU driver loading and causing a cascade failure. Turns out the machine wasn't completely toast, but it was fighting with itself. The GPU firmware indeed seemed corrupt or degraded, and every normal boot attempt ended with the system trying to initialize it and failing to do so.&lt;/p&gt;

&lt;p&gt;With that information, I continued to stumble forward with high hopes.&lt;/p&gt;

&lt;p&gt;I tried a few different approaches across several sessions and boot modes including single-user mode (&lt;strong&gt;Command + S&lt;/strong&gt;), safe mode (hold &lt;strong&gt;Shift&lt;/strong&gt;), different NVRAM and SMC resets. Over the course of a couple days I realized I could actually get the MacBook screen to turn on randomly without the distorted graphics after many power up attempts (which was very revealing that again, it wasn't totally toast). So I just kept doing that. I eventually got the machine booted far enough a handful of times, to do what I needed. I backed up everything on the hard drive. Said goodbye to the old system; and wiped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Debian
&lt;/h2&gt;

&lt;p&gt;Following Claude's instructions (always cross-check these), I flashed a USB drive with the server-only distro of Debian 13 and got ready to install it on the MacBook.&lt;/p&gt;

&lt;p&gt;The server-only distro of Debian is text-based, so it doesn't rely on the GPU for display. But I still needed to address the AMD GPU hardware issue. For this, I learned I could set &lt;code&gt;radeon.modeset&lt;/code&gt; to zero, which would prevent the Radeon kernel driver from loading. This driver tries to initialize the broken GPU and would freeze the system before the installer even appears. This variable gets set at the GRUB boot menu, so I edited the boot parameters and added &lt;code&gt;radeon.modeset=0&lt;/code&gt; to the kernel line before starting.&lt;/p&gt;

&lt;p&gt;Next, I began to get familiar with the system and started learning specific commands to see what was going on inside. When I checked the temperature sensors, I saw the GPU was sitting around 47°C and the fans were spinning at over 5,000 RPM trying to compensate. Even with &lt;code&gt;radeon.modeset=0&lt;/code&gt; preventing the driver from loading, the GPU hardware itself was still active and generating heat. And it turns out Linux does not natively control Apple fans, so it seems that this was the Apple hardware reacting to the heat on its own. I needed to disable the GPU and fix the communication between Linux and the Apple hardware.&lt;/p&gt;

&lt;p&gt;I immediately did these two things in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Made sure the GPU was permanently disabled&lt;/strong&gt;. I had Claude create a custom GRUB script that sends low-level commands to the graphics multiplexer chip, telling it to route everything through the Intel integrated graphics and power down the AMD GPU entirely. After that, the GPU temperature dropped from ~47°C to basically 3°C (reporting as off), and fan speeds dropped from 5,000+ RPM to around 2,800 RPM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Installed fan control.&lt;/strong&gt; I installed &lt;code&gt;macfanctld&lt;/code&gt;, which gives Linux the ability to actively manage fan response. I also wrote a custom temperature monitoring script that runs every minute and sends a push notification to my phone via &lt;a href="https://ntfy.sh/" rel="noopener noreferrer"&gt;ntfy.sh&lt;/a&gt; if the server gets too hot, with a follow-up notification when it cools back down to avoid false positives.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My unexpected networking problem
&lt;/h2&gt;

&lt;p&gt;I assumed getting the server onto the internet would be simple. It was not.&lt;/p&gt;

&lt;p&gt;I had the server hard-wired to my Ruckus router via ethernet. It had an IP address, but it couldn't resolve any domain names. This turned out to be a problem caused by not having the &lt;code&gt;resolvconf&lt;/code&gt; package installed. Without &lt;code&gt;resolvconf&lt;/code&gt;, DHCP could not update the DNS configuration. Running &lt;code&gt;apt install resolvconf&lt;/code&gt; fixed this issue.&lt;/p&gt;

&lt;p&gt;The bigger problem was that my other devices couldn't reach the server at all. The server on ethernet was sitting on one subnet, while all my other devices on WIFI were on a totally different subnet. The Ruckus router isolates them from each other. From my devices on WIFI, it was as if the server didn't exist on the network.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt;. Tailscale solves this problem immediately. It creates a WireGuard-based mesh VPN, so all my devices are able to join the same private network (regardless of what subnet they're on). Plus I can join from anywhere else in the world. Once I had Tailscale running, I could SSH into the server no problem. I realize that I haven't actually fixed the root subnet problem, and that Tailscale can be seen like a band-aid for my situation, but I'm okay with this setup for now. The Ruckus router has been difficult to deal with, so as long Tailscale keeps working for me, I'm good.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's running on it now
&lt;/h2&gt;

&lt;p&gt;After networking was stable, everything else moved fast. I set up Docker, &lt;a href="https://www.portainer.io/" rel="noopener noreferrer"&gt;Portainer&lt;/a&gt; (a web UI for managing containers), and &lt;a href="https://cockpit-project.org/" rel="noopener noreferrer"&gt;Cockpit&lt;/a&gt; (a system dashboard for monitoring the machine itself). The fun part came next, deploying services.&lt;/p&gt;

&lt;p&gt;Services currently running on the server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://immich.app/" rel="noopener noreferrer"&gt;Immich&lt;/a&gt;: Photo management for our family photos, finally.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.firefly-iii.org/" rel="noopener noreferrer"&gt;Firefly III&lt;/a&gt;: Fully featured personal finance tracking system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.home-assistant.io/" rel="noopener noreferrer"&gt;Home Assistant&lt;/a&gt;: Primarily for a custom built family dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt;: Reverse proxy that handles HTTPS automatically for each service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gethomepage.dev/" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;: A single dashboard view of everything running on the server.&lt;/p&gt;

&lt;p&gt;All of this is accessible over Tailscale, meaning nothing is exposed to the public internet. My family keeps convenience, while I get to keep the control. Once I make some hardware upgrades, I'll work on adding either Plex or Jellyfin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The documentation
&lt;/h2&gt;

&lt;p&gt;One thing I did early on that I'd recommend doing from day one is documentation. In my case, I created a private Git repo documenting everything. For me, that meant a setup log file tracking every change in chronological order, specific hardware notes, architecture diagrams, and a troubleshooting guide for issues I've already encountered and solved. A separate active log file gets updated every time I make a meaningful change.&lt;/p&gt;

&lt;p&gt;The goal is that if something breaks six months from now, I'm not starting from scratch trying to remember what I learned or what I did. And, the repo becomes the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's been a couple months, What do I think?
&lt;/h2&gt;

&lt;p&gt;My server came online a couple months ago.&lt;/p&gt;

&lt;p&gt;I'm making real progress addressing my privacy concerns. I feel some level of ownership and control, and that feels so good. I don't control everything I want to yet, but I'm on the path.&lt;/p&gt;

&lt;p&gt;There is a lot of maintenance involved so far, which is one thing about managed services - it's so nice to not have to maintain any of it. So most of this comes down to what you're willing to trade. Have everything managed for you, but give up all your privacy and data, or gain control of them at the expense of some time and effort.&lt;/p&gt;

&lt;p&gt;With that being said, I've learned so much and still am. This whole process has exposed me to new skills. It's helped build on top of other ones. And in general, I feel a little more well rounded as a dev. But the best part is I get to keep tinkering with this system and take back control of my family's privacy and data.&lt;/p&gt;

&lt;p&gt;If you have an old piece of metal laying around, I encourage you to find a use for it. It's so worth it and incredibly satisfying to give an old piece of gear a new life.&lt;/p&gt;

&lt;p&gt;Maybe the first step is to just turn the thing back on.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>debian</category>
      <category>macbook</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>You Don't Need A Framework - Build A Dynamic Blog with HTML &amp; Vanilla JavaScript</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Wed, 28 May 2025 18:37:57 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/you-dont-need-a-framework-build-a-dynamic-blog-with-html-vanilla-javascript-15e3</link>
      <guid>https://dev.to/patriciosalazar/you-dont-need-a-framework-build-a-dynamic-blog-with-html-vanilla-javascript-15e3</guid>
      <description>&lt;p&gt;Months ago, when I was reworking parts of my website, I began questioning every part of the tech stack. During that process, I thought about how I would build a dynamic blog with vanilla JavaScript. Eventually, I realized I was still going to end up using a framework. So, I worked through the main concepts in my head to scratch the itch, then shelved the idea...until, I couldn't ignore the fact that the itch was still there.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article is simply about the JavaScript side of building a dynamic blog. It is not a claim on the best approach for such a system, nor an argument for or against using vanilla JS or a framework. We're just looking at how this could get done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Let's get into it
&lt;/h2&gt;

&lt;p&gt;First things first, we need a few HTML and JS files, so let's get a new project going in your favorite editor (feel free to code along).&lt;/p&gt;

&lt;p&gt;Create a new folder, name it whatever you want, and open it in your editor. Create the following files and folders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;index.html&lt;/li&gt;
&lt;li&gt;blog.html&lt;/li&gt;
&lt;li&gt;blog/post.html&lt;/li&gt;
&lt;li&gt;js/blogCards.js&lt;/li&gt;
&lt;li&gt;js/blogPost.js&lt;/li&gt;
&lt;li&gt;js/blogPostsData.js&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;No Webpack, no Vite, no package.json... and no Tailwind 🤯. Ahhh, how refreshing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Boilerplate code
&lt;/h2&gt;

&lt;p&gt;Here is the relevant HTML&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;blog.html&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Include the script tag for blogCards.js in the head&lt;br&gt;
&lt;code&gt;&amp;lt;script type="module" src="/js/blogCards.js" async&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frm8yydqsaj1bndjtlbdp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frm8yydqsaj1bndjtlbdp.png" alt="screenshot of code in blog.html" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;blog-post.html&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Include the script tag for blogPost.js in the head&lt;br&gt;
&lt;code&gt;&amp;lt;script type="module" src="/js/blogPost.js" async&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsr6j1ba9xgfa47talxs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsr6j1ba9xgfa47talxs.png" alt="screenshot of code in blog-post.html" width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, the blog post data&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;blogPosts.js&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Add as many blog post objects as you want. I had ChatGPT generate some dummy data here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqvfu6mqad9uo71mjja2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqvfu6mqad9uo71mjja2.png" alt="screenshot of code in blogPosts.js" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start and preview
&lt;/h2&gt;

&lt;p&gt;This is a good time to start the project to see what we have so far. If you're using something like VS Code, view the project using &lt;a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer" rel="noopener noreferrer"&gt;Live Server&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamically populate the blog listing page with blog-post cards
&lt;/h2&gt;

&lt;p&gt;Let's start by importing the data at the top of the &lt;code&gt;blogCards.js&lt;/code&gt; file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;import { blogPosts } from "./blogsPosts.js"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're going to have to place each blog card inside of a the container element we created in the blog listing page, so let's declare a &lt;code&gt;blogCardsContainer&lt;/code&gt; variable and store a reference to the container in it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const blogCardsContainer = document.querySelector("#blog-cards-container");&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, we can loop over each blog post in our data, create the cards, and then finally append them to &lt;code&gt;blogCardsContainer&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08zv5qjyj7yfjiwx287b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08zv5qjyj7yfjiwx287b.png" alt="screenshot of code in blogCards.js" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first three lines inside of the forEach loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a div element to be styled as the card&lt;/li&gt;
&lt;li&gt;Adds a "blog-card" class to the div&lt;/li&gt;
&lt;li&gt;Sets a "data-id" attribute on the div with the value of current post id to uniquely identify each blog post card (in case you want control for individual cards).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's one of the most important parts of the &lt;code&gt;blogCardHTML&lt;/code&gt; content we created above: We set the href attribute to the individual post.html page, but, and &lt;strong&gt;here it is&lt;/strong&gt;, we add in a &lt;code&gt;slug&lt;/code&gt; parameter which is the value of the blog post slug. This is how the individual post.html page knows what blog post it's supposed to be loading when navigated to.&lt;/p&gt;

&lt;p&gt;Now that we know which post is supposed to be showing, all we have to do is create the HTML structure for our blog post and load in the post data, similar to how we did for the blogCards - except we're not doing a loop this time. Let's apply the logic...&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamically building the individual blog post page
&lt;/h2&gt;

&lt;p&gt;When our post.html file loads, we already know that we're going to have a post slug as a parameter. &lt;strong&gt;This is how we know which post to work with, so let's set it up&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the top of blogPost.js, import &lt;code&gt;blogPosts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may know about the &lt;code&gt;window.location&lt;/code&gt; object. But, to isolate the query parameters from the URL, we can access the &lt;code&gt;search&lt;/code&gt; property on window.location, and we'll pass that into the &lt;code&gt;URLSearchParams&lt;/code&gt; interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;`const params = new URLSearchParams(window.location.search);&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;`The URLSearchParams interface exposes a 'get' utility method allowing us to retrieve the exact parameter we're looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const slug = params.get("slug");&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect. Now, let's find the exact post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const blogPost = blogPosts.find((post) =&amp;gt; post.slug === slug);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like with the blogCards, let's grab the blog post container on our post.html file so that we know where to inject the post after we build it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const blogPostContainer = document.querySelector("#blogPost");&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Okay, now let's build the blog post HTML structure, and inject into the blog post container, but only on the condition that the post was actually found. For now, we handle the situation where the post isn't found by displaying some simple text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16rlr6rsi8wfjxe4smfj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16rlr6rsi8wfjxe4smfj.png" alt="screenshot of code in blogPost.js" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;p&gt;Go back to the blog listing page. Navigate to different posts, and watch them be displayed onto the page.&lt;/p&gt;

&lt;p&gt;🎉 Congrats! You now know some essential concepts needed to build a dynamic blog system in vanilla JavaScript 🎉&lt;/p&gt;

&lt;p&gt;If all this has made you curious to find out how much more it would take to get this blog to a complete, production-ready level, I encourage you to scratch that itch - It'll be worth it.&lt;/p&gt;

&lt;p&gt;Here's a hint to some modifications and optimizations that would make this project better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPA experience

&lt;ul&gt;
&lt;li&gt;load all content and pages using a single div in the index.html file, just like frameworks do&lt;/li&gt;
&lt;li&gt;this will offer better URLs and faster load times since you won't be loading full pages&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Store blog posts in an external db&lt;/li&gt;

&lt;li&gt;Use a mouseover event listener on blog post cards to prefetch html content&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Although there are many changes you could make to this project, the point is you don't always need every framework feature. Often, all you need is simplicity.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If this article helped you learn something, I'm so glad. But there are three important takeaways I hope you’ll leave with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remember to question things from time to time.&lt;/li&gt;
&lt;li&gt;Go beyond the question, and let your curiosity scratch the itch (think about the solution)&lt;/li&gt;
&lt;li&gt;You don't always need a framework 😏️&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://patriciosalazar.hashnode.dev/you-dont-need-a-framework" rel="noopener noreferrer"&gt;https://patriciosalazar.hashnode.dev/you-dont-need-a-framework&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>html</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Use Laravel Sail with Docker for PHP 8.2 (No Global PHP Upgrade Needed)</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Tue, 06 May 2025 18:21:18 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/how-to-use-laravel-sail-with-docker-for-php-82-no-global-php-upgrade-needed-1c28</link>
      <guid>https://dev.to/patriciosalazar/how-to-use-laravel-sail-with-docker-for-php-82-no-global-php-upgrade-needed-1c28</guid>
      <description>&lt;p&gt;How to Use Laravel Sail with Docker for PHP 8.2 (No Global PHP Upgrade Needed)&lt;/p&gt;

&lt;p&gt;Working on a Laravel project that requires a newer PHP version can be tricky if your system’s PHP is older. My personal computer is a Mac, but my work computer is a Linux machine where I sometimes run small projects for exploring and learning new technologies. In that case, upgrading my global PHP installation is not an option since I’d risk messing with work projects.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of situation Laravel Sail is for. Laravel Sail lets you use Docker to run your Laravel app with the required dependencies, all while keeping your global system versions untouched.&lt;/p&gt;

&lt;p&gt;Here is how I set up new Laravel projects on a Linux computer (works on macOS, Linux, or Windows via WSL2) with Sail and the latest versions of PHP (without touching the global version).&lt;/p&gt;

&lt;h2&gt;
  
  
  First, get your project and terminal ready
&lt;/h2&gt;

&lt;p&gt;Once you have your Laravel project files ready it’s time to run some commands.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you’re using a Laravel starter kit with Inertia and React or Vue, the installation process might have already run &lt;code&gt;npm install&lt;/code&gt; for you. If not, go ahead and run that. Otherwise, skip this command.&lt;/li&gt;
&lt;li&gt;Now, try running &lt;code&gt;composer install&lt;/code&gt;. If you get an error because you have an older version of PHP than required…good! We’re on the same page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we need to run a temporary Docker container to get the latest versions of the required dependencies. Run this command:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker run --rm \ -u "$(id -u):$(id -g)" \ -v "$PWD":/var/www/html \ -w /var/www/html \ laravelsail/php82-composer:latest \ composer install&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the above command does&lt;/strong&gt;: It spins up a temporary Docker container using the Laravel Sail PHP 8.2 image, executes composer install inside the project folder, and then stops and removes the container.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker run --rm&lt;/code&gt;: Start a new container, and remove it (–rm) when it’s done (so it won’t hang around on your system).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-u "$(id -u):$(id -g)&lt;/code&gt;: Run the container with your user’s UID:GID. This ensures any files Composer creates (like the vendor folder) will have the correct permissions matching your user, not root.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v "$PWD":/var/www/html&lt;/code&gt;: This command mounts your current host directory into the container at &lt;code&gt;/var/www/html&lt;/code&gt;, allowing the container to access and modify files in your project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-w /var/www/html&lt;/code&gt;: This sets the working directory inside the container to &lt;code&gt;/var/www/html&lt;/code&gt;, ensuring that subsequent commands run in the correct context.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;laravelsail/php82-composer:latest&lt;/code&gt;: This specifies the Docker image to use. It’s an image with PHP 8.2 and Composer installed (provided by Laravel Sail).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;composer install&lt;/code&gt;: This is the command we want to run inside the container. It will install all the dependencies as if you ran it on a machine with PHP 8.2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configure Sail and Docker
&lt;/h2&gt;

&lt;p&gt;Run the Sail installation (Artisan) command inside Docker. This command publishes the &lt;code&gt;docker-compose.yml&lt;/code&gt; file and updates your &lt;code&gt;.env&lt;/code&gt; with environment variables for Docker.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: If you want to use a different database in your project rather than the default MySQL, add the &lt;code&gt;--with=&lt;/code&gt; flag to the command. To keep the default MySQL db, omit the additional flag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker run --rm \ -u "$(id -u):$(id -g)" \ -v "$(pwd)":/var/www/html \ -w /var/www/html \ laravelsail/php82-composer:latest \ php artisan sail:install --with=pgsql&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;This docker command is similar to the previous one, except now we’re executing the artisan sail:install setting up Docker/Sail along with the optional PostgreSQL db configuration.&lt;/p&gt;

&lt;p&gt;You should see a &lt;code&gt;docker-compose.yml&lt;/code&gt; file in the root of your project now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The values added to your &lt;code&gt;.env&lt;/code&gt; (and the ones in &lt;code&gt;docker-compose.yml&lt;/code&gt;) are mostly default/local settings. For example, Sail might set &lt;code&gt;DB_PASSWORD=password&lt;/code&gt; and &lt;code&gt;DB_USERNAME=sail&lt;/code&gt; (or &lt;code&gt;postgres&lt;/code&gt;) for your database, which are local credentials for the Docker containers. If you’re anything like me and have multiple Laravel projects running at the same time, you might need to adjust port numbers. For instance, you probably need to add &lt;code&gt;APP_PORT&lt;/code&gt; in the &lt;code&gt;.env&lt;/code&gt; file and customize the value with something like &lt;code&gt;APP_PORT=7070&lt;/code&gt; to have your app run on port &lt;code&gt;7070&lt;/code&gt; instead of the default port &lt;code&gt;80&lt;/code&gt;, where &lt;code&gt;80&lt;/code&gt; is already in use. Just make sure you update your &lt;code&gt;docker-compose.yml&lt;/code&gt; file to match this. The same is true for &lt;code&gt;SERVER_PORT&lt;/code&gt;, and &lt;code&gt;SERVER_HOST&lt;/code&gt; (and &lt;code&gt;VITE_PORT&lt;/code&gt;, if you have an inertia frontend).&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s time to launch Sail
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;code&gt;./vendor/bin/sail up -d&lt;/code&gt;. OR&lt;br&gt;
&lt;code&gt;sail up -d&lt;/code&gt;, if you already created the alias for sail. The &lt;code&gt;-d&lt;/code&gt; flag runs containers in the background and frees up your terminal shell.&lt;/p&gt;

&lt;p&gt;At this point, the above command should run successfully.&lt;/p&gt;

&lt;p&gt;If you cloned a repo your &lt;code&gt;.env&lt;/code&gt; file won’t have an app key value. If this is true for you, run this command while Sail is running: &lt;code&gt;sail artisan key:generate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, run migrations: &lt;code&gt;sail artisan migrate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’re using Inertia, this is where you want to start the frontend application: &lt;code&gt;sail composer run dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Congrats, now you can happily code to your programmer hearts content. Go build, but don’t forget to ship!&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Getting My First Users From Building Something Unexpected</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Tue, 17 Dec 2024 07:38:23 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/getting-my-first-users-from-building-something-unexpected-20kp</link>
      <guid>https://dev.to/patriciosalazar/getting-my-first-users-from-building-something-unexpected-20kp</guid>
      <description>&lt;p&gt;
This one has to start with the story. I’ll try to keep it short.
&lt;/p&gt;



&lt;h2&gt;A Random Memory&lt;/h2&gt;



&lt;p&gt;
When I was younger, say in my teens, I would do this thing in my head where I would double numbers. Sometimes random numbers, but most of the time from the number 2. So for example, 2, 4, 8, 16, etc. And I put a mental timer on it, so my goal was to see how fast I could do it without thinking much and see how far I could take it.
&lt;/p&gt;



&lt;p&gt;
Side-note: I don’t know why I did that. I was never big on math. It was like a mental fidget during random in-between moments.
&lt;/p&gt;



&lt;p&gt;
I had absolutely forgotten that I did that. Three months ago, I suddenly remembered while I was having lunch with my wife. I asked if she’d ever done anything like that, and yup, you guessed it—she hadn’t. Now, I’ve never been interested in building a web game, but one of the next thoughts I had was “I have to build this thing”.
&lt;/p&gt;



&lt;p&gt;
Okay stop. Story over. There you have it, that’s the unexpected thing. I built a web game. BUT, please don’t leave just yet. I also want to tell you about what happened after I built the game.
&lt;/p&gt;



&lt;h2&gt;Building and Sharing&lt;/h2&gt;



&lt;p&gt;
Building the game took me about a couple days, it was done in a weekend (this post isn’t about the code, so I won’t get into that). I didn’t focus on making it look fancy or anything like that. I made it for myself, as an ode to the memory of an old mental fidget. I did however, know I would take a chance on sharing it. After all, if people like it, they’ll like it for the experience, not a shiny UI. By the way, it’s called &lt;a href="https://playdoubles.org" rel="noopener noreferrer"&gt;Doubles&lt;/a&gt;.
&lt;/p&gt;



&lt;p&gt;
Enter Reddit. I went looking for the right subreddits and ended up posting in these:
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;r/math&lt;/li&gt;
&lt;li&gt;r/gamedev&lt;/li&gt;
&lt;li&gt;r/IndieDev&lt;/li&gt;
&lt;li&gt;r/learnmath&lt;/li&gt;
&lt;li&gt;r/WebGames&lt;/li&gt;
&lt;li&gt;r/IndieGaming&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;
Sharing on Reddit taught me that the right audience will amplify visibility.
&lt;/p&gt;



&lt;p&gt;
Right away, and to my complete surprise, strangers on the internet loved it. I got responses like this:
&lt;/p&gt;
&lt;br&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FDoubles_Reddit_1.png" alt="Reddit user comment challenging and exciting nature of the timer aspect of Doubles game" width="748" height="142"&gt;
&lt;br&gt;&lt;br&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FDoubles_Reddit_2.png" alt="Reddit user comment stating he shared the Doubles game in a group chat with friends" width="585" height="114"&gt;
&lt;br&gt;&lt;br&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FDoubles_Reddit_3.png" alt="Reddit user comment posted his share-able score and saying how funny it is for addicts to calculation" width="380" height="228"&gt;
&lt;br&gt;&lt;br&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FDoubles_Reddit_4.png" alt="Reddit user comment about Doubles stating how fun it is to see how high you can go" width="440" height="124"&gt;
&lt;br&gt;&lt;br&gt;&lt;br&gt;

&lt;p&gt;
🤯🤯🤯 As the title of this post suggests, these are my first-ever users. A.K.A. you’re watching me create my first public side project, learn how to get and interact with users, and iterate on a project based on those interactions.
&lt;/p&gt;



&lt;p&gt;
I have to say, getting users on something you built, and having positive feedback has to be one of the best feelings ever. Not in an ego way, in a purely shocking way. It’s gotta be the “indie dev’s” version of a musician’s first time hearing themselves on the radio. Alright, chill—I’m dating myself 🤣.
&lt;/p&gt;



&lt;p&gt;
The first few days my Cloudflare analytics showed somewhere around &lt;strong&gt;200—300 users&lt;/strong&gt; had visited the site. That was incredible already. Then, days after not having posted anything, I checked the analytics and there was a spike of &lt;strong&gt;1K+ visitors overnight!&lt;/strong&gt; Whaat?? Excuse mee?! I started investigating.
&lt;/p&gt;



&lt;p&gt;
It turns out they were all referred by &lt;a href="https://B3ta.com" rel="noopener noreferrer"&gt;B3ta.com&lt;/a&gt;. A UK based newsletter. I’m not sure how long they’ve been around, but they claim it “currently has nearly 80,000 subscribers”. Someone on their team must have seen my game, liked it enough, and shared it to their subscriber base. They also share a version of their newsletter on their website, and guess what? I FOUND IT. Here’s what they shared:
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FB3ta_write_up_on_Doubles.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnsdysbxlabtmtxscxqvw.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fpersonal-website%2FDoubles%2FB3ta_write_up_on_Doubles.png" alt="Comedic summary of Doubles game by B3ta newsletter" width="609" height="161"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;&lt;/p&gt;

&lt;p&gt;
Hahaha. Good by me.
&lt;/p&gt;



&lt;p&gt;
Fast forwarding a bit, I saw traffic from them trickle in for a long time. Nothing as big as the first couple days after they shared, but it was amazing enough. I’ll never forget that.
&lt;/p&gt;



&lt;p&gt;
Since then, I still haven’t shared anything new, but I saw Doubles get picked up by another couple sites. One was &lt;a href="https://www.mattrutherford.co.uk/" rel="noopener noreferrer"&gt;this guy’s website&lt;/a&gt;. Shoutout to him. I saw a small amount come from there, but he never had to do that in the first place. Another is a site called &lt;a href="https://cloudhiker.net/" rel="noopener noreferrer"&gt;CloudHiker&lt;/a&gt;. I see a couple visitors from that site every day. I’m not sure how it ended up there.
&lt;/p&gt;



&lt;p&gt;
Traction continued for a couple months every single day all on its own. I saw anywhere from 20-80 regular users a day. I know because they were navigating directly to the site, not from any referrer.
&lt;/p&gt;



&lt;h3&gt;Monetization&lt;/h3&gt;



&lt;p&gt;
Nothing to report here, lol!
&lt;/p&gt;

&lt;p&gt;
I tried to figure out a way, but this kind of site is not easy to monetize. And as far as ads go, no worthwhile ad service wants to serve ads on my little game website. Oh well. This is something I will consider more deeply ahead of future projects, but not everything needs to make money.
&lt;/p&gt;



&lt;h2&gt;
How’s It Doing Today?
&lt;/h2&gt;



&lt;p&gt;
Truthfully, it’s died down hahaha. I never talked about it again, and obviously I think that was a mistake, but you can consider this the start of me talking about it again. You can still play it here &lt;a href="https://playdoubles.org" rel="noopener noreferrer"&gt;Doubles&lt;/a&gt;.
&lt;/p&gt;



&lt;p&gt;
&lt;strong&gt;Here’s what I learned&lt;/strong&gt;:
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Picking the right project matters.
&lt;/li&gt;
&lt;li&gt;
As a solo creator, I can make decisions, build, iterate, fail, and learn fast.
&lt;/li&gt;
&lt;li&gt;
Caring about user feedback was highly valuable. Talking to the users and responding to them as soon as I implemented a feature request or bug fix is major and made them feel closer to the project. For example, after working with one of my users requests, he replied like this: &lt;span&gt;“Love to see a dev actively responding and excited for feedback :)”&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;
You spent time and effort building something, you should do the same for communicating and marketing it.
&lt;/li&gt;
&lt;li&gt;
Some users will give you the best ideas to make your project better. Build relationships with them.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;
There you have it. That’s my story of getting my very first users from a game.
&lt;/p&gt;

&lt;p&gt;
Thanks for sticking around until the end.
&lt;/p&gt;



&lt;p&gt;
If you enjoyed this content, stay tuned for more updates on the projects I build, along with more stories and lessons, by subscribing below (no spam). And if you haven't already, click on one of the emoji’s on this page!
&lt;/p&gt;



&lt;p&gt;Until next time!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>gamedev</category>
      <category>sideprojects</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>How I Added CSV Importing In My React-Node.js Project</title>
      <dc:creator>Patricio Salazar</dc:creator>
      <pubDate>Wed, 11 Dec 2024 21:20:36 +0000</pubDate>
      <link>https://dev.to/patriciosalazar/how-i-added-csv-importing-in-my-react-nodejs-project-2mij</link>
      <guid>https://dev.to/patriciosalazar/how-i-added-csv-importing-in-my-react-nodejs-project-2mij</guid>
      <description>&lt;p&gt;&lt;a href="https://touchbaseapp.co/" rel="noopener noreferrer"&gt;Touch Base&lt;/a&gt; was fine. It was a cool project. It worked. But, let's face it—was it usable? (Touch Base is a full stack React contact management app that I made).&lt;/p&gt;

&lt;p&gt;I was thinking about this and realized something obvious. When a user starts using Touch Base they have to add contacts manually. Which might be fine if you have 5 contacts. If you have 1,000 contacts you want to add, this sucks… and you probably won't want to use this system. So of course, I knew I had to add the ability to import contacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Researching Options&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first Google search was “csv importers”, or something like that. I looked through some of the options available and found FlatFile. Their main heading read “The fastest way to collect, onboard and migrate data.” Perfect… except, it wasn't all that for me. Now, this is probably my fault (they seem like an amazing service) but the process of implementing their importer was taking more effort than I was willing to put in for this. This is the perfect time for a little sidebar context:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lately, I've been really valuing scrappiness. I want to get things done, fast. This isn't about cutting corners, I just don't want to have any excuses or unnecessary delays. After all, I'm just one guy working on side projects. So my current attitude is fail, learn, and iterate fast. All while doing good work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Back to FlatFile. As much as I wanted to use their promising software I asked myself if I really needed all their bells and whistles and if fighting their docs was worth it. Definitely not. So I went back to my search and landed on Papa Parse. I recalled seeing it in my previous search. Their main heading read “The powerful, in-browser CSV parser for big boys and girls.” 😆 I was in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First things first, I added a POST route to my API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw64z8cd1tqj30thqbxpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw64z8cd1tqj30thqbxpj.png" alt="'import-contacts API POST route'" width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;verifyToken&lt;/code&gt; is a function I use in all my routes that does exactly that—verifies the users id token. I use multer in my app which is a node.js middleware for handling file uploads. &lt;code&gt;upload.single('file')&lt;/code&gt; is a multer function that helps me upload files to my s3 bucket.&lt;/p&gt;

&lt;p&gt;Inside the route, I grab the user id and file through destructuring.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4giv8u7dsgs66y5tybz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4giv8u7dsgs66y5tybz.png" alt="code displaying the extraction of a user id and file properties from the request through the use of destructuring" width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Like the npm package docs for Papa Parse state, “Papa Parse can parse a Readable Stream instead of a File when used in Node.js environments (in addition to plain strings).”&lt;/p&gt;

&lt;p&gt;So I prepared to stream the file directly to Papa Parse by creating said stream from my s3 bucket as well as an empty array to hold the results data. Can't forget about handling potential errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2byqjkj7v6o3ux2odpg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2byqjkj7v6o3ux2odpg.png" alt="create a read stream of a file from an s3 bucket" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I finally pass the stream to Papa Parse, set my config options and handle any errors coming from the results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04s93550zlkpq8kzsmuo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04s93550zlkpq8kzsmuo.png" alt="passing the stream directly to Papa Parse" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the code above, &lt;code&gt;complete&lt;/code&gt; is a Papa Parse property that takes a callback function. It executes once the parsing is complete. I then grab a hold of the data provided by &lt;code&gt;results&lt;/code&gt; as &lt;code&gt;parsedData&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After this, it's time to run some queries on the database and process the contacts. But, I need to store a connection the the db to run the queries on first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83x693rrh6rak3x8rz10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83x693rrh6rak3x8rz10.png" alt="code displaying the storing of a database connection" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This next part is a lot of code in a try catch statement, so I'll just give it to you straight with some comments on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fen78ud9wqgaimi3odqin.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fen78ud9wqgaimi3odqin.png" alt="processing and conditionally inserting contacts data into a PostgresQL database table" width="800" height="1256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, I&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch existing contacts&lt;/li&gt;
&lt;li&gt;filter out duplicate contacts using emails, since no two emails can be the same&lt;/li&gt;
&lt;li&gt;bulk insert the non-duplicate contacts into the table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The frontend will be largely specific to my approach of the app, but let's connect the dots here.&lt;/p&gt;

&lt;p&gt;The Import Contacts page does one thing so it's very simple. I use the native file upload button which is really an input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fknfdfe64dd0ljzikjerk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fknfdfe64dd0ljzikjerk.png" alt="native input element that accepts .csv files and triggers a handling function" width="800" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the input detects a change I trigger a &lt;code&gt;handleFileUpload&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Inside of the &lt;code&gt;handleFileUpload&lt;/code&gt; function, I first set the loading state to true so that I can display my little loading spinner to the user while this process takes place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwg4t9ckx7jo6torr0hqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwg4t9ckx7jo6torr0hqv.png" alt="code displaying a loading state being set to true at the beginning of a function" width="780" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a user successfully uploads their .csv file, I append it to a new &lt;code&gt;formData&lt;/code&gt; object and send it to my backend route above to process it. I directly use a fetch request here since it's the only place in my app that's going to hit the &lt;code&gt;/import-contacts&lt;/code&gt; endpoint. If another part of my app needed to hit the route, I would store the request using context and use that to avoid repeating code. Also, you can see the error handling I have set up…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0l0nvylwp1w0lixoc2n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0l0nvylwp1w0lixoc2n.png" alt="code displaying the sending of a file and handling of the response from an api endpoint" width="800" height="1042"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upon successful handling of the file or if it errors out, the loading state gets set back to false, and I trigger an appropriate toast alert to let the user know exactly what happened in a nice way.&lt;/p&gt;

&lt;p&gt;It feels so nice to log in, upload a .csv file of contacts, get a successful toast alert, and then see all of the new contacts populated in your account. And it's so quick. You might see the loading spinner for just a second. The bulk insert query also helps a lot there.&lt;/p&gt;

&lt;p&gt;From Maybe Usable to Usable&lt;/p&gt;

&lt;p&gt;Before adding this feature I wondered how usable the app truly was. Now, there's no question about that. Although it wasn't super complex, it's a feature you would expect to see in this type of application so I found it a requirement to implement. I think it makes it a little more serious of a project. Aside from that, I've never done anything with .csv files which made this super fun to work on. Papa Parse integrated so well with all the tools I was already using which made it super easy. I definitely recommend using it.&lt;/p&gt;

&lt;p&gt;If you made it this far, cheers to you for reading this 🥂...&lt;br&gt;
and cheers to software that doesn't suck 🥂&lt;/p&gt;

&lt;p&gt;p.s I'm still wondering if my project sucks 😂&lt;br&gt;
If you want to check it out &lt;a href="https://touchbaseapp.co/" rel="noopener noreferrer"&gt;here's the link again&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Til next time!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
