<?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[Arpit Dalal's blog]]></title><description><![CDATA[I'm a web developer enthusiastic about all things React and TS!]]></description><link>https://blog.arpitdalal.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1738379607389/7c9bde97-71b8-4bed-bb7d-b95e19ac7c55.png</url><title>Arpit Dalal&apos;s blog</title><link>https://blog.arpitdalal.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 10:04:35 GMT</lastBuildDate><atom:link href="https://blog.arpitdalal.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Is MCP just an API? Let's find out!]]></title><description><![CDATA[MCP Is Not Just an API — It’s a Protocol, and That Matters
When people hear MCP, they often think of “just another API.” That shorthand might be convenient, but it misses the point.
MCP stands for Model Context Protocol. And the crucial word is Proto...]]></description><link>https://blog.arpitdalal.dev/is-mcp-just-an-api-lets-find-out</link><guid isPermaLink="true">https://blog.arpitdalal.dev/is-mcp-just-an-api-lets-find-out</guid><category><![CDATA[mcp]]></category><category><![CDATA[AI]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[mcp server]]></category><dc:creator><![CDATA[Arpit Dalal]]></dc:creator><pubDate>Tue, 30 Sep 2025 00:02:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759190403942/494d0f4f-7623-436f-ae96-440452bce978.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-mcp-is-not-just-an-api-its-a-protocol-and-that-matters">MCP Is Not <em>Just an API</em> — It’s a Protocol, and That Matters</h2>
<p>When people hear <em>MCP</em>, they often think of “just another API.” That shorthand might be convenient, but it misses the point.</p>
<p><strong>MCP stands for <em>Model Context Protocol</em></strong>. And the crucial word is <strong>Protocol</strong>.</p>
<p>Protocols define <strong>rules for how systems exchange data</strong>. They shape expectations, behavior, and guarantees. Treating MCP as “just an API” risks overlooking the major benefits it brings when integrating models with tools and services.</p>
<hr />
<h2 id="heading-protocols-vs-apis-a-quick-analogy">Protocols vs. APIs: A Quick Analogy</h2>
<p>Think of two well-known protocols:</p>
<ul>
<li><p><strong>FTP</strong> exists to move files. Narrow, explicit, and predictable.</p>
</li>
<li><p><strong>HTTP</strong> (Hypertext Transfer Protocol) is more flexible. It moves HTML, JSON, files, and more. It enables servers to expose arbitrary APIs, written in any language, with the server deciding what payloads it accepts and returns.</p>
</li>
</ul>
<p>When we build <strong>HTTP servers</strong>, we define:</p>
<ul>
<li><p>What payloads are allowed</p>
</li>
<li><p>How responses look</p>
</li>
<li><p>What errors mean</p>
</li>
</ul>
<p><strong>MCP follows the same pattern</strong> — but with different goals and stricter constraints.</p>
<hr />
<h2 id="heading-what-mcp-servers-expose">What MCP Servers Expose</h2>
<p>Instead of arbitrary endpoints, <strong>MCP servers</strong> expose:</p>
<ul>
<li><p><strong>Tools</strong> — well-defined capabilities a model can invoke</p>
</li>
<li><p><strong>Resources</strong> — structured data or state that the model can use</p>
</li>
<li><p><strong>Prompts</strong> — reusable instructions and contextual snippets</p>
</li>
</ul>
<p><strong>MCP clients</strong> (including model-driven agents) call these tools <strong>over a standardized protocol</strong>.</p>
<p>Because message types, action lifecycles, and behaviors are explicit, the <strong>runtime — not the model — handles coordination and guardrails</strong>.</p>
<hr />
<h2 id="heading-why-mcp-is-stricter-than-http-and-why-thats-good">Why MCP Is Stricter Than HTTP (and Why That’s Good)</h2>
<p>MCP’s strictness is not a limitation. It’s the point.</p>
<p>It delivers real advantages:</p>
<ul>
<li><p>✅ <strong>Safer interactions</strong>: formalized behaviors reduce hallucinations and unintended actions.</p>
</li>
<li><p>✅ <strong>Predictability</strong>: agents follow a clear contract instead of improvising ad hoc requests.</p>
</li>
<li><p>✅ <strong>Easier integration</strong>: validation, retries, and authorization can be enforced by tooling.</p>
</li>
<li><p>✅ <strong>Clear separation of concerns</strong>: models focus on intent; the protocol governs execution and context exchange.</p>
</li>
</ul>
<p><strong>In short:</strong></p>
<ul>
<li><p>HTTP → HTTP servers → APIs for applications</p>
</li>
<li><p>MCP → MCP servers → Tools for AI agents</p>
</li>
</ul>
<p>Both are <em>protocol + servers → exposed resources</em>, but <strong>MCP is purpose-built for governing model behavior with tighter, safer rules.</strong></p>
<hr />
<h2 id="heading-when-to-think-in-terms-of-protocols">When to Think in Terms of Protocols</h2>
<p>If you’re building any of the following:</p>
<ul>
<li><p>🛠️ <strong>Agent tooling</strong></p>
</li>
<li><p>🔗 <strong>Orchestrations where LLMs call external services</strong></p>
</li>
<li><p>📊 <strong>Integrations where models act on data or systems</strong></p>
</li>
</ul>
<p>...then designing around a protocol like <strong>MCP</strong> will:</p>
<ul>
<li><p>reduce complexity,</p>
</li>
<li><p>increase reliability, and</p>
</li>
<li><p>give you consistent behavior across models, clients, and runtimes.</p>
</li>
</ul>
<hr />
<h2 id="heading-final-note">Final Note</h2>
<p>Protocols let you <strong>codify expectations once</strong> and reap benefits everywhere.</p>
<p>If you’ve built something interesting with MCP, share it! The ecosystem grows stronger with every experiment.</p>
]]></content:encoded></item><item><title><![CDATA[Don't Fight the Current]]></title><description><![CDATA[When I was about to join a new team at another company, I learned that all the developers were using MacBooks. Meanwhile, I was firmly rooted in the Android and Windows world. I had zero experience with macOS, and zero interest in switching.
So I did...]]></description><link>https://blog.arpitdalal.dev/dont-fight-the-current</link><guid isPermaLink="true">https://blog.arpitdalal.dev/dont-fight-the-current</guid><category><![CDATA[Career]]></category><category><![CDATA[Developer]]></category><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[lessons learned]]></category><dc:creator><![CDATA[Arpit Dalal]]></dc:creator><pubDate>Tue, 27 May 2025 17:45:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748367721804/d3764161-3cab-4dad-9d36-62d4c626c560.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I was about to join a new team at another company, I learned that all the developers were using MacBooks. Meanwhile, I was firmly rooted in the Android and Windows world. I had zero experience with macOS, and zero interest in switching.</p>
<p>So I didn’t. I chose a Windows laptop, confident I could make it work. How hard could it be to set up a dev environment and find macOS alternatives?</p>
<p>Turns out, <em>very</em> hard.</p>
<p>For the next 1.5 years, I ran WSL 2, hunted down app alternatives, and built my own workflows from scratch. My setup worked, technically—but it came at a cost. I was constantly troubleshooting, Googling obscure errors, and navigating issues my teammates couldn’t help with because our systems were too different.</p>
<p>That extra friction showed up in my work. I had to regularly overestimate timelines just to give myself buffer for setup and workaround time. On paper, it made me look like a sluggish developer, consistently taking longer than average. My direct manager understood, but I knew it wasn't a good look with the higher-ups, as there was no easy way to communicate this up the chain.</p>
<p>You might wonder: once it’s all set up, doesn’t it get easier?</p>
<p>Partially, yes—but nothing stays still in tech. New, groundbreaking tools are introduced all the time, and our team’s processes were often updated to include them—always designed for macOS first. For me, that meant finding alternatives, getting them approved by IT and Security, and <em>then</em> starting my actual work. It was a never-ending treadmill of catch-up.</p>
<p>Eventually, I realized that my stubbornness was hurting my career more than it was helping my comfort. So, I switched to a MacBook.</p>
<p>I took a couple of weeks of official "training" to make it clear that I was learning a new platform. Immediately, the experience changed. When I ran into issues, teammates could actually help. I could dive into work instead of battling my setup.</p>
<p>My biggest lesson wasn’t about technology, but about trade-offs. Letting go of personal preferences isn’t a defeat; it’s about setting yourself up for success—and your team, too. Exploration and challenging the norm are important, but when you fight the current for too long, you risk drifting further from where you’re trying to go.</p>
]]></content:encoded></item><item><title><![CDATA[Building Great Rapport]]></title><description><![CDATA[In my first couple of weeks as a software engineer transitioning to a new team, I was handed a big ticket that required touching nearly every service we had. It was overwhelming — tons of new codebases, processes, and tools to learn all at once.
As I...]]></description><link>https://blog.arpitdalal.dev/building-great-rapport</link><guid isPermaLink="true">https://blog.arpitdalal.dev/building-great-rapport</guid><category><![CDATA[Career]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[software development]]></category><category><![CDATA[internships]]></category><dc:creator><![CDATA[Arpit Dalal]]></dc:creator><pubDate>Sun, 13 Apr 2025 17:41:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/_kd5cxwZOK4/upload/4d2c21302e8fb2b842e48e35805569a0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my first couple of weeks as a software engineer transitioning to a new team, I was handed a big ticket that required touching nearly every service we had. It was overwhelming — tons of new codebases, processes, and tools to learn all at once.</p>
<p>As I navigated through it, I asked a lot of questions in different Slack channels. One support person really stood out. He not only answered my questions in the channel but also DMed me to help debug issues. He was consistently helpful, patient, and made understanding their service much easier.</p>
<p>After finishing the task, I wanted to thank him publicly and looked him up — turns out, he’s an intern. That really stuck with me. Despite being early in his career, he showed strong communication, initiative, and technical skills.</p>
<p>I gave him a shoutout and messaged his manager, because honestly, he earned it. I learned how to make an early impression from him. If you’re an intern or early in your career, this is how you make a strong impression.</p>
]]></content:encoded></item><item><title><![CDATA[My Cloudflare Workers Migration: The Good, The Bad, and The Confusing]]></title><description><![CDATA[Introduction
I built this little URL shortener for myself. Think of it as a personal version of something like dub.co, which lets me share short, memorable links instead of full URLs. For example, when I want to share my Linkedin profile, I can share...]]></description><link>https://blog.arpitdalal.dev/my-cloudflare-workers-migration-the-good-the-bad-and-the-confusing</link><guid isPermaLink="true">https://blog.arpitdalal.dev/my-cloudflare-workers-migration-the-good-the-bad-and-the-confusing</guid><category><![CDATA[cloudflare-worker]]></category><category><![CDATA[Bun]]></category><category><![CDATA[hono]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[migration]]></category><category><![CDATA[cloudflare]]></category><dc:creator><![CDATA[Arpit Dalal]]></dc:creator><pubDate>Sun, 02 Mar 2025 19:41:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/4yqWYpNfDCU/upload/b2d6753963a10057cb27913d1f2519a7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>I built <a target="_blank" href="https://arpit.im/gh/arpit.im">this little URL</a> shortener for myself. Think of it as a personal version of something like <a target="_blank" href="http://dub.co">dub.co</a>, which lets me share short, memorable links instead of full URLs. For example, when I want to share my Linkedin profile, I can share "<a target="_blank" href="http://arpit.im/in">arpit.im/in</a>" instead of remembering my Linkedin username, and the fact that Linkedin likes to add <code>/in</code> before the username (linkedin.com/in/arpitdalal) in their URL. Super convenient when sharing links or adding them to my resume! Plus, there's a hidden superpower: when I change usernames on various platforms (which happens more than I'd like to admit), I only need to update one redirect in my service rather than hunting down every instance of that link across the internet.</p>
<p>This humble URL router started its life as a traditional Node.js and Express app running happily on <a target="_blank" href="http://fly.io">fly.io</a>. It was comfortable, familiar territory. Until I decided to shake things up and migrate the whole thing to Cloudflare Workers using Bun and Hono.</p>
<p>Why would I abandon a perfectly functional setup for the wild west of edge computing? Well, that's where my adventure begins, and let me tell you – it's been quite the roller coaster ride!</p>
<h2 id="heading-why-i-jumped-ship">Why I Jumped Ship</h2>
<p>"Why mess with something that works?" Fair question! My Node.js and Express setup was solid and reliable, quick to build and easy to modify, despite the occasional TypeErrors that JavaScript loves to surprise you with.</p>
<p>Since I was planning to migrate to TypeScript anyway, I figured this was the perfect opportunity to expand my horizons. Why just add types when I could learn something completely new in the process?</p>
<p>I initially chose the familiar stack to ship quickly (a decision we've all made). But once my project was live and stable, I couldn't ignore that developer urge to evolve it into something more, to actually learn from this little side project instead of letting it stagnate.</p>
<p>That’s when Hono caught my eye because it has a similar API to Express (which is pretty robust), but it's supposedly "<a target="_blank" href="https://hono.dev/docs/concepts/benchmarks#on-node-js">blazingly fast</a>" and comes with native TypeScript support. Win-win!</p>
<p>Life got busy (as it does), and a couple of months flew by. When I finally circled back to this idea, I thought, "Why stop at just changing the framework? Let's go all in and try a new runtime too!"</p>
<p>I was torn between Deno, Bun, and Cloudflare's workerd. This was my first deep dive into CF Workers, and what really blew me away was learning they run on V8 Isolates with practically <a target="_blank" href="https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/">zero cold-start time</a>.</p>
<p>That last point was a game-changer! AWS Lambdas and its different flavours from Netlify or Vercel have pretty noticeable cold-starts. That’s why I chose a long-running server on fly.io to avoid those painfully slow serverless cold-starts. Nobody wants to wait ages for a simple redirection, right? Plus, Cloudflare's edge network meant my app would run in multiple regions worldwide, not just from my lonely <a target="_blank" href="http://fly.io">fly.io</a> server in Toronto.</p>
<p>I'd been using npm as my package manager for ages, but kept hearing about faster alternatives. The benchmarks for pnpm and bun looked impressive, and I was curious. I ultimately went with bun, partly for the speed, but mostly because I wanted to experiment with their cutting-edge approach to JavaScript tooling.</p>
<p>So that settled it - Hono as the framework, Bun for the tooling, and Cloudflare Workers as the infrastructure. Time to jump in!</p>
<h2 id="heading-the-good-stuff-aka-why-i-dont-completely-regret-my-decision">The Good Stuff (aka Why I Don't Completely Regret My Decision)</h2>
<p><strong>Amazing Community Support</strong> - The Hono community absolutely loves Cloudflare Workers, which meant there were tons of starter guides and examples. This saved me hours of head-scratching!</p>
<p><strong>Documentation That Doesn't Make You Cry</strong> - Refreshingly, Hono's docs for Cloudflare Workers integration are actually current and comprehensive. It's rare to find such well-maintained documentation when one tool integrates with another. I didn't need to waste hours digging through GitHub issues or outdated guides to get things working, the answers were right there in the docs.</p>
<p><strong>Surprisingly Good Library Support</strong> - The Cloudflare Workers ecosystem surprised me with its maturity, far more libraries offer native support than I expected. From testing to rate limiting, I found packages for most common needs without having to build workarounds.</p>
<p><strong>Super Simple Local Development</strong> - Setting up my local development environment was remarkably smooth. The community starter templates combined with wrangler's local dev server had me coding productively within minutes, not hours, which is a welcome change from my previous serverless experiences.</p>
<p><strong>Testing That Doesn't Suck</strong> - Testing with Hono turned out to be surprisingly pleasant. Cloudflare's vitest plugin effectively simulated the production environment in my test suite, eliminating those frustrating "works locally, fails in production" moments that plague serverless development.</p>
<h2 id="heading-the-not-so-good-stuff-aka-the-parts-that-made-me-question-my-life-choices">The Not-So-Good Stuff (aka The Parts That Made Me Question My Life Choices)</h2>
<p><strong>Cloudflare Dashboard: The Labyrinth</strong> - Is it just me, or is the Cloudflare dashboard intentionally designed to confuse people? I spent more time than I'd like to admit just trying to find the right settings.</p>
<p><strong>Domain Setup From Hell</strong> - To serve my worker on a custom domain, I had to change my nameservers to Cloudflare. This seemingly simple change took my domain down for over HALF A DAY. The more baffling thing is that I have no idea why it took that long and there’s no way for me to expedite it or fix it. Not exactly what I'd call a smooth transition!</p>
<p><strong>Wrangler: Powerful but Mysterious</strong> - Wrangler (Cloudflare's CLI tool) is incredibly powerful, but it's also incredibly confusing. The learning curve is steeper than it needs to be. <em>Maybe it’s a skill issue?</em></p>
<p><strong>Wrangler Config Nightmare</strong> - Cloudflare forces you to stuff everything into wrangler.jsonc/.toml files: environment variables, domain zone IDs, KV store configs… you name it! This creates an immediate security dilemma: how do I version control the wrangler config file without exposing my secrets on GitHub? I wasted countless hours putting together a secure CI/CD pipeline. My "solution"? Storing secrets in GitHub Actions and using scripts to dynamically replace placeholders in the wrangler config during deployment. It works, but feels like a clunky workaround for what should be a solved problem. While Netlify and Vercel elegantly separate configuration from secrets through an intuitive dashboard, Cloudflare's approach feels like it was designed by people who've never had to manage a production deployment pipeline for an open source project.</p>
<p><strong>Mysterious Versioning System</strong> - Instead of using sensible semantic versioning like every other tool in the JavaScript ecosystem, Cloudflare decided to implement "compatibility dates" in wrangler configs that magically change available types and behaviors. Change one date and suddenly your TypeScript throws errors because APIs have different signatures? Fun times! I still don't fully understand the reasoning behind this approach - there's a reason npm package versions exist and are universally adopted. This unconventional system makes it unnecessarily difficult to predict how your code will behave across environments or when upgrading.</p>
<p><strong>Too Much Magic, Not Enough Explanation</strong> - Some types are just magically available in the workerd environment, which becomes a real headache when your tests are running in a non-workerd environment.</p>
<p><strong>Debugging in the Dark</strong> - Since it's not a long-running server, debugging when something goes wrong can feel like trying to find a specific snowflake in a blizzard.</p>
<h2 id="heading-what-i-learned-from-all-this">What I Learned From All This</h2>
<p>Would I recommend Cloudflare Workers? Yes, but know what you're getting into.</p>
<p>Despite all my griping, I don't regret making the switch. My links now load noticeably faster thanks to Cloudflare's global edge network - when someone clicks my URL in Australia, they're not waiting for a server in Toronto to respond. Yes, I traded my zero-cold-start setup for occasional millisecond delays, but they're so minimal that neither I nor my users can detect them. The unexpected bonus? Cloudflare's built-in bot protection system. My old setup would occasionally get hammered by random bots, causing unexpected spikes and headaches. Now I sleep better knowing there's an enterprise-grade security system standing guard over my humble little URL shortener. The migration headaches were real, but for my specific use case, the benefits have outweighed the costs.</p>
<p>That said, if I could do it again, I would spend more time upfront understanding Cloudflare's ecosystem before diving in. Their approach is fundamentally different from traditional servers, and that requires a mental shift.</p>
<p>For anyone considering a similar move, my advice would be: spend some time to understand how CF works, start with a small project, expect some frustration with the config and deployment process, but stick with it because the performance and security benefits from Node.js are worth it in the end.</p>
<p><a target="_blank" href="https://github.com/arpitdalal/arpit.im/compare/32228c53fc2c4c73cb24c9dcb78f91a13074ee5f...d3e6ece5c9efacd419bbcbd04b30afc458e00d25">Here’s the before and after diff for my project.</a></p>
<p>Have you tried Cloudflare Workers or similar edge computing platforms? Was any of this a skill issue? I'd love to hear about your experiences and how you overcame similar challenges!</p>
]]></content:encoded></item><item><title><![CDATA[Setup PostHog reverse proxy with Remix/React Router]]></title><description><![CDATA[💡
This is not a sponsored or feature post. I genuinely like PostHog and its products.


Introduction
Analytics are crucial for understanding user behavior resulting in better products and higher user satisfaction. But, sometimes these analytics tool...]]></description><link>https://blog.arpitdalal.dev/setup-posthog-reverse-proxy-with-remix-react-router</link><guid isPermaLink="true">https://blog.arpitdalal.dev/setup-posthog-reverse-proxy-with-remix-react-router</guid><category><![CDATA[posthog]]></category><category><![CDATA[analytics]]></category><category><![CDATA[Remix]]></category><category><![CDATA[react router]]></category><category><![CDATA[Reverse Proxy]]></category><dc:creator><![CDATA[Arpit Dalal]]></dc:creator><pubDate>Tue, 25 Feb 2025 22:30:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/JKUTrJ4vK00/upload/04b5ea0d6baccf9c816ef22a5dc9f223.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is not a sponsored or feature post. I genuinely like PostHog and its products.</div>
</div>

<h2 id="heading-introduction">Introduction</h2>
<p>Analytics are crucial for understanding user behavior resulting in better products and higher user satisfaction. But, sometimes these analytics tools are blocked by adblockers. This article explains how to ensure that analytics data is collected reliably while respecting user privacy.</p>
<p>PostHog is an excellent tool for web/product analytics, session replays, feature flags, A/B testing, and much more. It's also quite cheap compared to alternatives in this space.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>The main issue with analytics tools like PostHog is that they get blocked by adblockers, which results in the loss of valuable data.</p>
<h2 id="heading-the-solution-a-backend-proxy">The Solution: A Backend Proxy</h2>
<p>This can be solved by setting up a reverse proxy in the backend that forwards analytics events to PostHog, bypassing adblockers completely. While this guide shows implementation with Remix/react-router, the same concepts apply to any modern JS framework.</p>
<blockquote>
<p>Note: PostHog has <a target="_blank" href="https://posthog.com/docs/advanced/proxy/remix">official Remix integration docs</a> but in my experience it doesn’t work as is, I had to tweak some things to make it work.</p>
</blockquote>
<h2 id="heading-how-it-works">How it works</h2>
<p>The proxy works by redirecting analytics traffic through the server:</p>
<ol>
<li><p>Instead of the browser sending analytics directly to PostHog servers (where they can be blocked): Browser → PostHog (❌ blocked by adblockers)</p>
</li>
<li><p>The traffic can be routed through the server first: Browser → Server → PostHog (✅ not blocked)</p>
</li>
</ol>
<p>This simple redirection makes adblockers ineffective against the analytics collection.</p>
<h2 id="heading-implementation-steps">Implementation Steps</h2>
<h3 id="heading-1-create-the-endpoint">1. Create the endpoint</h3>
<p>First, create an endpoint that won't trigger adblockers. It's recommended to avoid names like "analytics" or "posthog”. Instead, use something generic like <code>/resources/ingest</code>.</p>
<p>Create a file named <code>ingest.$.tsx</code> under <code>app/routes/resources</code>. The <code>$</code> creates a catch-all route that will handle all PostHog endpoints like <code>/resources/ingest/e</code>, <code>/resources/ingest/decide</code>, etc. Official <a target="_blank" href="https://reactrouter.com/how-to/file-route-conventions#splat-routes">react-router docs</a> call these Splat routes.</p>
<p>The splat route is essential here because PostHog's SDK sends requests to multiple endpoints. Using a catch-all route allows us to handle all these different PostHog endpoints with a single file rather than creating separate handlers for each one.</p>
<h3 id="heading-2-set-up-constants">2. Set up constants</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> API_HOST = <span class="hljs-string">"us.i.posthog.com"</span>; <span class="hljs-comment">// use eu.i.posthog.com for EU</span>
<span class="hljs-keyword">const</span> ASSET_HOST = <span class="hljs-string">"us-assets.i.posthog.com"</span>; <span class="hljs-comment">// use eu-assets.i.posthog.com for EU</span>

<span class="hljs-keyword">type</span> RequestInitWithDuplex = RequestInit &amp; {
  duplex?: <span class="hljs-string">"half"</span>; <span class="hljs-comment">// https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1483</span>
};
</code></pre>
<p>These constants define which PostHog servers to connect to. The custom type <code>RequestInitWithDuplex</code> adds the <code>duplex</code> property needed for handling larger POST requests properly.</p>
<p>The <code>duplex: 'half'</code> property is necessary when working with request bodies in a streaming context. It tells the fetch API that the server will only be reading from the stream (not writing to it), which is important for properly handling PostHog's data payloads, especially larger ones.</p>
<h3 id="heading-3-create-the-proxy-function">3. Create the proxy function</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">posthogProxy</span>(<span class="hljs-params">request: Request</span>) </span>{
  <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> URL(request.url);

  <span class="hljs-comment">// Choose the right host based on the request path</span>
  <span class="hljs-keyword">const</span> hostname = url.pathname.startsWith(<span class="hljs-string">"/resources/ingest/static"</span>)
    ? ASSET_HOST
    : API_HOST;

  <span class="hljs-comment">// Build the new URL to forward to PostHog</span>
  <span class="hljs-keyword">const</span> newUrl = <span class="hljs-keyword">new</span> URL(url);
  newUrl.protocol = <span class="hljs-string">"https"</span>;
  newUrl.hostname = hostname;
  newUrl.port = <span class="hljs-string">"443"</span>;
  newUrl.pathname = newUrl.pathname.replace(<span class="hljs-regexp">/^\/resources\/ingest/</span>, <span class="hljs-string">""</span>);
  <span class="hljs-comment">//                                        ^ depends on the endpoint</span>

  <span class="hljs-comment">// Prepare headers</span>
  <span class="hljs-keyword">const</span> headers = <span class="hljs-keyword">new</span> Headers(request.headers);
  headers.set(<span class="hljs-string">"host"</span>, hostname);
  headers.delete(<span class="hljs-string">"accept-encoding"</span>); <span class="hljs-comment">// to let the fetch handle compression</span>

  <span class="hljs-comment">// Setup fetch options</span>
  <span class="hljs-keyword">const</span> fetchOptions: RequestInitWithDuplex = {
    method: request.method,
    headers,
    redirect: <span class="hljs-string">"follow"</span>, <span class="hljs-comment">// enable fetch to follow the redirect in case PostHog throws a redirect</span>
  };

  <span class="hljs-comment">// Add body for non-GET/HEAD requests</span>
  <span class="hljs-keyword">if</span> (![<span class="hljs-string">"GET"</span>, <span class="hljs-string">"HEAD"</span>].includes(request.method)) {
    fetchOptions.body = request.body;
    fetchOptions.duplex = <span class="hljs-string">"half"</span>; <span class="hljs-comment">// needed for larger POST bodies</span>
  }

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Forward the request to PostHog</span>
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(newUrl, fetchOptions);

    <span class="hljs-comment">// Clean up response headers</span>
    <span class="hljs-keyword">const</span> responseHeaders = <span class="hljs-keyword">new</span> Headers(response.headers);
    responseHeaders.delete(<span class="hljs-string">"content-encoding"</span>);
    responseHeaders.delete(<span class="hljs-string">"content-length"</span>);
    responseHeaders.delete(<span class="hljs-string">"transfer-encoding"</span>);
    responseHeaders.delete(<span class="hljs-string">"Content-Length"</span>);

    <span class="hljs-comment">// Get response data if needed</span>
    <span class="hljs-keyword">const</span> data =
      response.status === <span class="hljs-number">304</span> || response.status === <span class="hljs-number">204</span>
        ? <span class="hljs-literal">null</span>
        : <span class="hljs-keyword">await</span> response.arrayBuffer();

    <span class="hljs-comment">// Return the response</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(data, {
      status: response.status,
      statusText: response.statusText,
      headers: responseHeaders,
    });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-comment">// Track and log the error</span>
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Proxy error:"</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">"Proxy Error"</span>, { status: <span class="hljs-number">500</span> });
  }
}
</code></pre>
<p>This function:</p>
<ul>
<li><p>Determines which PostHog host to use based on the request path</p>
</li>
<li><p>Constructs a new URL with the correct host and modified path</p>
</li>
<li><p>Sets up the proper headers for the proxied request</p>
</li>
<li><p>Handles the request body for non-GET/HEAD methods</p>
</li>
<li><p>Makes the fetch request to PostHog</p>
</li>
<li><p>Cleans up the response headers to avoid encoding issues</p>
</li>
<li><p>Returns the proxied response or handles any errors</p>
</li>
</ul>
<h3 id="heading-4-set-up-the-loader-and-action">4. Set up the loader and action</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> LoaderFunctionArgs, <span class="hljs-keyword">type</span> ActionFunctionArgs } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request }: LoaderFunctionArgs</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> posthogProxy(request);
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">action</span>(<span class="hljs-params">{ request }: ActionFunctionArgs</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> posthogProxy(request);
}
</code></pre>
<p>In Remix/react-router, the <code>loader</code> function handles GET/HEAD requests, while the <code>action</code> function handles all other HTTP methods (POST, PUT, DELETE, etc.). Both functions simply call the <code>posthogProxy</code> function created earlier.</p>
<h3 id="heading-5-configuring-the-posthog-client">5. Configuring the PostHog Client</h3>
<p>Once the proxy is set up, the PostHog client needs to be configured to use it:</p>
<pre><code class="lang-typescript">posthog.init(<span class="hljs-string">'&lt;YOUR_API_KEY&gt;'</span>, {
  api_host: <span class="hljs-string">'/resources/ingest'</span> <span class="hljs-comment">// point to the proxy endpoint instead of PostHog's servers</span>
  ui_host: <span class="hljs-string">'https://us.posthog.com'</span>, <span class="hljs-comment">// use https://eu.posthog.com for EU</span>
  <span class="hljs-comment">// ^ Necessary when using reverse proxy - https://posthog.com/docs/libraries/js#config</span>
})
</code></pre>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p>Here are some common issues one might encounter:</p>
<ol>
<li><p><strong>CORS errors</strong>: If there are CORS-related errors, ensure the server is properly handling OPTIONS requests and forwarding the appropriate headers.</p>
</li>
<li><p><strong>Missing data</strong>: Check the browser's Network tab to see if requests to the proxy endpoint are succeeding with 200 status codes.</p>
</li>
<li><p><strong>Encoding issues</strong>: If there are any corrupted data or encoding errors, make sure the response headers are properly handled as shown in the proxy function.</p>
</li>
<li><p><strong>Feature flags not working</strong>: Feature flags require the <code>/decide</code> endpoint to work properly. Verify that requests to this endpoint are being correctly proxied.</p>
</li>
</ol>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>With this reverse proxy in place, analytics data will flow to PostHog even when users have adblockers enabled. This gives more accurate insights into how people are using products and helps make better data-driven decisions.</p>
<p>For the complete code, check out <a target="_blank" href="https://gist.github.com/arpitdalal/ccc807fa6a15638b86a128d9b7ac51a1">this gist.</a></p>
<p><em>For other frameworks or providers, see</em> <a target="_blank" href="https://posthog.com/docs/advanced/proxy"><em>PostHog's official proxy documentation.</em></a></p>
]]></content:encoded></item><item><title><![CDATA[The AI Developer Divide: Autonomous Agents vs Coding Companions]]></title><description><![CDATA[Introduction
At this point, you've likely heard of AI products like GitHub Copilot, Cursor, and Devin. If not, it's worth understanding what these different products offer, especially as Devin has just launched publicly for USD 500 per month for a se...]]></description><link>https://blog.arpitdalal.dev/the-ai-developer-divide-autonomous-agents-vs-coding-companions</link><guid isPermaLink="true">https://blog.arpitdalal.dev/the-ai-developer-divide-autonomous-agents-vs-coding-companions</guid><category><![CDATA[AI]]></category><category><![CDATA[copilot]]></category><category><![CDATA[cursor]]></category><category><![CDATA[devinai]]></category><category><![CDATA[software development]]></category><category><![CDATA[#agent]]></category><dc:creator><![CDATA[Arpit Dalal]]></dc:creator><pubDate>Fri, 10 Jan 2025 16:15:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734280882459/bc90147b-c0f7-4d3c-a54d-e8d188f0bf4a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction"><strong>Introduction</strong></h2>
<p>At this point, you've likely heard of AI products like GitHub Copilot, Cursor, and Devin. If not, it's worth understanding what these different products offer, especially as Devin has just launched publicly for USD 500 per month for a seat, generating both excitement and concern across the tech industry.</p>
<p>As companies invest heavily in AI development tools, understanding these different approaches becomes crucial for developers and businesses alike. Let's explore how these approaches are shaping the future of coding.</p>
<h2 id="heading-the-fundamental-difference-teammate-vs-companion"><strong>The Fundamental Difference: Teammate vs Companion</strong></h2>
<p>To understand the impact of these tools, we first need to examine their fundamental differences in approach and implementation.</p>
<h3 id="heading-positioning-and-marketing"><strong>Positioning and Marketing</strong></h3>
<ul>
<li><p>Devin markets itself as a "teammate" and "junior developer," suggesting an autonomous role</p>
</li>
<li><p>GitHub Copilot and Cursor position themselves as "companions", integrating directly into your development workflow</p>
</li>
</ul>
<h3 id="heading-usage-patterns"><strong>Usage Patterns</strong></h3>
<p>These different positioning strategies are reflected in how these tools are actually used in practice, with stark contrasts in their operation, target users, and pricing models.</p>
<h4 id="heading-devin-the-autonomous-agent"><strong>Devin: The Autonomous Agent</strong></h4>
<ol>
<li><p><strong>Operation</strong></p>
<ul>
<li><p>Operates independently through Slack or GitHub</p>
</li>
<li><p>Makes its own coding decisions and creates pull requests</p>
</li>
<li><p>Cannot assist with real-time coding; works as a standalone developer</p>
</li>
</ul>
</li>
<li><p><strong>Cost &amp; Target Audience</strong></p>
<ul>
<li><p>Target audience appears to be non-technical stakeholders like product owners and project managers</p>
</li>
<li><p>Enables communication through familiar platforms (Slack, GitHub) for non-developers</p>
</li>
<li><p>Costs USD 500 per month for a seat</p>
</li>
</ul>
</li>
</ol>
<h4 id="heading-copilot-amp-cursor-the-developers-assistant"><strong>Copilot &amp; Cursor: The Developer's Assistant</strong></h4>
<ol>
<li><p><strong>Operation</strong></p>
<ul>
<li><p>Integrated directly into IDEs/editors</p>
</li>
<li><p>Developer maintains control over architectural decisions</p>
</li>
<li><p>Assists with autocomplete, refactoring, and feature creation</p>
</li>
</ul>
</li>
<li><p><strong>Cost &amp; Target Audience</strong></p>
<ul>
<li><p>Focuses on enhancing developer productivity rather than replacement</p>
</li>
<li><p>Usually costs around USD 10 to 20 per month for a seat</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-a-personal-experience-working-with-ai-companions"><strong>A Personal Experience: Working with AI Companions</strong></h2>
<p>To illustrate these differences in practice, let me share a recent experience…</p>
<p>I recently worked on a React Native app where I needed to refactor several similar screen components. Using Claude 3.5 Sonnet via Cursor, I experienced firsthand how these companion tools enhance developer workflow:</p>
<ol>
<li><p>Initial refactoring attempt produced a good but rigid abstraction</p>
</li>
<li><p>Requested customization led to an overcomplicated API</p>
</li>
<li><p>Finally guided it to create a composable component architecture, resulting in an elegant solution. I documented the before and after code transformation on X</p>
</li>
</ol>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://x.com/arpitdalal_dev/status/1867715409173979429?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1867715409173979429%7Ctwgr%5Eb75f26a6be758833798507a32008ad4699a569e9%7Ctwcon%5Es1_c10&amp;ref_url=https%3A%2F%2Fpublish.twitter.com%2F%3Furl%3Dhttps%3A%2F%2Ftwitter.com%2Farpitdalal_dev%2Fstatus%2F1867715409173979429">https://x.com/arpitdalal_dev/status/1867715409173979429?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1867715409173979429%7Ctwgr%5Eb75f26a6be758833798507a32008ad4699a569e9%7Ctwcon%5Es1_c10&amp;ref_url=https%3A%2F%2Fpublish.twitter.com%2F%3Furl%3Dhttps%3A%2F%2Ftwitter.com%2Farpitdalal_dev%2Fstatus%2F1867715409173979429</a></div>
<p> </p>
<p>The key insight here is: While the AI wrote the code, I made the architectural decisions, demonstrating the ideal companion relationship.</p>
<h2 id="heading-review-of-devin">Review of Devin</h2>
<p>While my experience focuses on AI companions, let's examine how Devin performs as an autonomous agent.</p>
<p>I haven't personally tested Devin, but Steve from <a target="_blank" href="https://builder.io">builder.io</a> recently shared a comprehensive hands-on review that provides valuable insights into its capabilities. His demonstration highlights both Devin's strengths and limitations in real-world development scenarios.</p>
<p>Steve's review (linked below) showcases:</p>
<ul>
<li><p>How Devin interacts with development tasks</p>
</li>
<li><p>Where it excels compared to traditional coding companions</p>
</li>
<li><p>Current limitations and areas for improvement</p>
</li>
</ul>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/oU3H581uCsA?si=UdTaAcZ0A0y6Ezty">https://youtu.be/oU3H581uCsA?si=UdTaAcZ0A0y6Ezty</a></div>
<p> </p>
<p>The main takeaway from Steve's review is clear: while Devin shows promise as an autonomous agent, it currently falls short both as a developer replacement and as a practical tool for existing development teams.</p>
<p>While Devin's current limitations are apparent, its emergence and the market's response to it reveal broader implications for the future of software development.</p>
<h2 id="heading-market-impact-and-future-implications"><strong>Market Impact and Future Implications</strong></h2>
<h3 id="heading-current-state"><strong>Current State</strong></h3>
<p>The emergence of Devin signals the birth of a new category in software development: autonomous development agents that aim to replace human developers. Cognition AI, the company behind Devin, has achieved a USD 2 billion valuation based on its ambitious goal: creating the first AI tool capable of replacing human developers. This valuation suggests strong confidence in this direction, but the current reality presents significant challenges:</p>
<ul>
<li><p>Limited ability to follow complex instructions accurately</p>
</li>
<li><p>The relatively high cost of USD 500 per month restricts widespread adoption considering it cannot replace a human developer yet</p>
</li>
<li><p>Inconsistent output quality compared to human developers</p>
</li>
<li><p>Architectural decisions often require human intervention</p>
</li>
</ul>
<p>While these challenges are significant, looking ahead reveals how these tools might reshape the development landscape.</p>
<h3 id="heading-future-trajectory-and-industry-impact">Future Trajectory and Industry Impact</h3>
<p>The implications of tools like Devin extend beyond their current capabilities:</p>
<ol>
<li><p><strong>Developer Stratification</strong></p>
<ul>
<li><p>Lower-productivity developers may face increasing pressure from autonomous AI tools</p>
</li>
<li><p>The role of developers may evolve to focus more on system architecture and AI oversight</p>
</li>
<li><p>Teams might restructure around AI capabilities, with humans focusing on high-level decision-making</p>
</li>
</ul>
</li>
<li><p><strong>Adaptation and Evolution</strong></p>
<ul>
<li><p>Developers who effectively integrate AI tools into their workflow will likely thrive</p>
</li>
<li><p>The focus may shift from coding proficiency to AI collaboration skills</p>
</li>
<li><p>New roles might emerge at the intersection of development and AI operations</p>
</li>
</ul>
</li>
<li><p><strong>Market Transformation</strong></p>
<ul>
<li><p>The success of early autonomous agents could accelerate investment in this space</p>
</li>
<li><p>Traditional development tools may evolve to include more autonomous features</p>
</li>
<li><p>The definition of "developer productivity" may need to be reconsidered</p>
</li>
</ul>
</li>
</ol>
<p>This transition suggests not a wholesale replacement of developers, but rather a redistribution of skills and responsibilities in the development ecosystem. Success in this new landscape will likely depend on adapting to and leveraging these emerging technologies rather than competing against them.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>While GitHub Copilot and Cursor excel at enhancing developer productivity through collaboration and can produce high-quality code, they ultimately rely on human developers for architectural decisions and system design. Devin represents an ambitious attempt to cross this frontier, aiming to create truly autonomous development capabilities. However, despite its bold vision and Cognition AI's significant market valuation, Devin's current capabilities fall short of this goal. This fundamental difference in approach - companion vs teammate - reflects a broader industry shift toward AI integration in software development, with each type of tool serving distinct audiences and use cases. As these technologies evolve, the question remains: will AI tools continue to complement human developers, or will they eventually achieve the level of autonomy that Devin aspires to?</p>
]]></content:encoded></item><item><title><![CDATA[Enhancing User Experience with Notion-Style URL Architecture]]></title><description><![CDATA[URL is the web's user interface. While most users today rely on search engines to find websites, URLs remain the core mechanism for accessing any resource on the internet. Understanding URLs—especially their structure and design—is crucial for creati...]]></description><link>https://blog.arpitdalal.dev/enhancing-user-experience-with-notion-style-url-architecture</link><guid isPermaLink="true">https://blog.arpitdalal.dev/enhancing-user-experience-with-notion-style-url-architecture</guid><category><![CDATA[url-architecture]]></category><category><![CDATA[user experience]]></category><category><![CDATA[#Web Architecture]]></category><category><![CDATA[UX]]></category><category><![CDATA[software architecture]]></category><dc:creator><![CDATA[Arpit Dalal]]></dc:creator><pubDate>Tue, 08 Oct 2024 17:40:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/YPMFARhHxxw/upload/b67f069566026ef017d486a6452e13b3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>URL is <em>the</em> web's user interface.</strong> While most users today rely on search engines to find websites, URLs remain the core mechanism for accessing any resource on the internet. Understanding URLs—especially their structure and design—is crucial for creating user friendly web applications.</p>
<h2 id="heading-introduction-understanding-the-power-of-urls">Introduction: Understanding the Power of URLs</h2>
<h3 id="heading-what-is-a-url">What is a URL?</h3>
<p>Think of a website like a house: you need an address to reach there. Whether you arrive via GPS (search engines), someone's directions (links), or knowing the address by heart (direct URL), you still need that unique location identifier. The browser's <strong>address bar</strong> serves exactly this purpose, providing direct access to any resource on the web.</p>
<p>To understand URL structure better, visit <a target="_blank" href="http://howurls.work">howurls.work</a>. In this article, we'll focus specifically on the <code>path</code> component and how thoughtful URL design can enhance user experience.</p>
<h3 id="heading-why-does-the-path-matter">Why does the <code>path</code> matter?</h3>
<p>It doesn’t… to most people using the internet. But to the power users, it’s the way they might access a resource on the server. For example, if you visit <a target="_blank" href="https://arpitdalal.dev/me">arpitdalal.dev/me</a>, you’ll land on my about page. Here, you accessed the <code>me</code> resource on the server where my website is hosted.</p>
<p>That was easy, but let’s take this example to access different repositories and different parts of those repositories on GitHub. On GitHub, every repository has to have an owner account or an owner organization. In the case of <a target="_blank" href="https://github.com/arpitdalal/arpitdalal.dev">github.com/arpitdalal/arpitdalal.dev</a>, <code>arpitdalal</code> is the owner and <code>arpitdalal.dev</code> is the repository. For <a target="_blank" href="https://github.com/remix-run/remix">github.com/remix-run/remix</a>, <code>remix-run</code> is the organization and <code>remix</code> is the repository. You can also directly access the commits on that repository by adding <code>/commits</code> to the URL, <a target="_blank" href="https://github.com/remix-run/remix/commits">github.com/remix-run/remix/commits</a>. Also, individual commits can be accessed like so <a target="_blank" href="https://github.com/remix-run/remix/commit/66bb870c17a4d778a3cff66973fee5314c694f82">github.com/remix-run/remix/commit/66bb870c17a4d778a3cff66973fee5314c694f82</a>.</p>
<p>Every page/resource can be accessed via <code>path</code> in a URL.</p>
<h3 id="heading-what-does-it-have-to-do-with-notion">What does it have to do with Notion?</h3>
<p>Notion is built with <em>power users</em> in mind, so they architected their URLs in a really clever way.</p>
<p>In the earlier example of Remix’s individual commit URL on GitHub, could you have guessed what that commit was about? It’s unlikely. The gibberish after <code>/commit/</code> is an <code>id</code> that GitHub uses to know which commit to show, but it doesn’t help us to understand what the commit is about.</p>
<p>Now if I show you this URL</p>
<p><a target="_blank" href="https://arpitdalal.notion.site/Arpit-Dalal-115f0f16d2cd80ea8cf0d37ffb8ccfdf">arpitdalal.notion.site/Arpit-Dalal-115f0f16d2cd80ea8cf0d37ffb8ccfdf</a></p>
<p>You’ll instantly know that you’re going to a custom Notion site, but read the full <code>path</code> of the URL <code>Arpit-Dalal-115f0f16d2cd80ea8cf0d37ffb8ccfdf</code>. It also has some gibberish but it has <code>Arpit-Dalal</code> in front of that gibberish.</p>
<p>How about this URL?</p>
<p><a target="_blank" href="https://arpitdalal.notion.site/About-Arpit-Dalal-115f0f16d2cd8029b92ae679687607dd">arpitdalal.notion.site/About-Arpit-Dalal-115f0f16d2cd8029b92ae679687607dd</a></p>
<p>The <code>path</code> here has <code>About-Arpit-Dalal</code> before the gibberish. Just by reading that you understand that the page has to be about… well… Arpit Dalal.</p>
<p>What if you type this URL in your browser? You get autocomplete on <code>path</code> of the URL that you’re trying to access. By only typing <code>Abo</code>, I already see a suggestion for <code>About-Arpit-Dalal</code> page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728063132732/0816d1c9-0c6a-44b6-887a-a1f43de91fc3.gif" alt="a GIF showing autocomplete on notion.so web app when accessing resources through the address bar of the Arc browser" /></p>
<p>Now imagine trying to access a commit from your project on GitHub, would that be possible? I don’t think so. But, Notion architected their URL paths in a way that the <code>id</code> used internally to identify pages doesn’t really matter when accessing those pages directly through a browser’s address bar.</p>
<h2 id="heading-implementation-building-notion-style-urls">Implementation: Building Notion-Style URLs</h2>
<h3 id="heading-how-do-these-urls-work">How do these URLs work?</h3>
<p>That is a good question. While the exact implementation details aren't public, we can implement it in our own way.</p>
<p>This section will demonstrate a simple app that shows all the posts on the <code>/</code> path and each post details page on <code>/{slug-id}</code> path.</p>
<p>This example uses <a target="_blank" href="https://remix.run/">remix.run</a> to create this app but this URL architecture can be built using any language or framework of your choice.</p>
<h3 id="heading-the-intrinsics">The Intrinsics</h3>
<p>Rather than showing the full application setup, we’ll implement the necessary code to build this architecture.</p>
<p>We’ll first need a function that takes <code>title</code> and <code>id</code> of the post. Then, replace anything other than characters and numbers from the <code>title</code> with a <code>-</code> and then append the <code>id</code> after a <code>-</code> too. There we have the title’s <code>slug</code> prepended to the <code>id</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSlugWithId</span>(<span class="hljs-params">{ title, id }: { title: <span class="hljs-built_in">string</span>; id: <span class="hljs-built_in">string</span> }</span>) </span>{
  <span class="hljs-comment">// Validate inputs</span>
  <span class="hljs-keyword">if</span> (!title || !id) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Title and ID are required"</span>);
  }


  <span class="hljs-keyword">const</span> slugifiedTitle = title
    .trim() <span class="hljs-comment">// Trim the title for any leading/trailing spaces</span>
    .replace(<span class="hljs-regexp">/[^a-zA-Z0-9]+/g</span>, <span class="hljs-string">"-"</span>) <span class="hljs-comment">// Replace special characters and spaces with hyphens</span>
    .replace(<span class="hljs-regexp">/^-+|-+$/g</span>, <span class="hljs-string">""</span>); <span class="hljs-comment">// Trim any leading/trailing hyphens</span>
    <span class="hljs-comment">// You can lowercase the title too</span>
    <span class="hljs-comment">// I chose not to, to stay in parity with Notion's approach</span>

  <span class="hljs-comment">// Ensure we have valid content before creating slug</span>
  <span class="hljs-keyword">if</span> (!slugifiedTitle) {
    <span class="hljs-keyword">return</span> id; <span class="hljs-comment">// Fallback to just ID if title produces empty slug</span>
  }

  <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${slugifiedTitle}</span>-<span class="hljs-subst">${id}</span>`</span>;
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">We are NOT going to store this <code>slug-id</code> in the database because if the title of the post changes, then the <code>id</code> also needs to be updated in the database. That might cause issues with caching, database indexes, etc.</div>
</div>

<p>How to use it?</p>
<p>First, we need to show a link with <code>slug-id</code> for each post to the users. Let’s assume we’re getting the posts from a database as <code>postsData</code>. Then, we’ll need to replace the id with <code>slug-id</code> using our function <code>getSlugWithId</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> posts = postsData.map(<span class="hljs-function">(<span class="hljs-params">post</span>) =&gt;</span> ({
  ...post,
  id: getSlugWithId(post),
}));
</code></pre>
<p>We can show this data using React but you can choose to show it however you want. <code>post</code> here is a single post object containing <code>title</code> and <code>id</code>.</p>
<pre><code class="lang-typescript">&lt;Link to={<span class="hljs-string">`/<span class="hljs-subst">${post.id}</span>`</span>}&gt;
  &lt;h2&gt;{post.title}&lt;/h2&gt;
&lt;/Link&gt;
</code></pre>
<p>Now that users can go to the post details page <code>/First-Post-aabbccddeeff</code>, we need to retrieve the post details using this <code>id</code>.</p>
<p>Most frameworks will allow you to define dynamic routes, for Remix, the syntax is <code>$postId.tsx</code>. This will ensure that the dynamic value is named <code>postId</code> and can be accessed using <code>params</code> object passed in the <code>loader</code>. You can read more about it on <a target="_blank" href="https://remix.run/docs/en/main/file-conventions/routes#dynamic-segments">Remix docs</a>.</p>
<p>We have the post id <code>First-Post-aabbccddeeff</code> but we cannot search a post using it as the database doesn’t have <code>slug-id</code>. To remove the <code>slug</code> part from it, we can split the string with <code>-</code> and access the last part of it to retrieve the actual <code>id</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">extractPostId</span>(<span class="hljs-params">slugOrId: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">string</span> </span>{
  <span class="hljs-comment">// Validate input</span>
  <span class="hljs-keyword">if</span> (!slugOrId) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Invalid post identifier"</span>);
  }

  <span class="hljs-comment">// Split the slug-id combination</span>
  <span class="hljs-keyword">const</span> parts = slugOrId.split(<span class="hljs-string">"-"</span>);
  <span class="hljs-keyword">const</span> id = parts.at(<span class="hljs-number">-1</span>);

  <span class="hljs-comment">// Validate that we got a valid ID</span>
  <span class="hljs-keyword">if</span> (!id || id.length &lt; <span class="hljs-number">1</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Invalid post ID format"</span>);
  }

  <span class="hljs-comment">// Optional: Add validation for expected ID format</span>
  <span class="hljs-comment">// For example, if IDs should be 12 characters</span>
  <span class="hljs-keyword">if</span> (!<span class="hljs-regexp">/^[a-f0-9]{12}$/</span>.test(id)) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Invalid post ID format"</span>);
  }

  <span class="hljs-keyword">return</span> id;
}

<span class="hljs-comment">// Usage</span>
<span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">const</span> postIdRaw = params.postId;
  <span class="hljs-keyword">const</span> postId = extractPostId(postIdRaw);
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-comment">// Handle error appropriately</span>
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">"Invalid Post ID"</span>, { status: <span class="hljs-number">400</span> });
}
</code></pre>
<p>The <code>split("-").at(-1)</code> method handles both new <code>slug-id</code> URLs and legacy URLs that contain only the <code>id</code>, maintaining backward compatibility. But the <code>split("-")</code> will return our <code>id</code> as the only item in an array if it doesn’t find the separator <code>-</code>, and since there’s only 1 item in the array, <code>at(-1)</code> will give us our <code>id</code> which means we are good on that front too.</p>
<p>Now it’s pretty easy to do a query against your database to find a post with this <code>id</code> and show it to your users.</p>
<p>Using these building blocks, we are able to prepend <code>slug</code> to the <code>id</code>, show it to the users in a link, retrieve it, and separate the <code>slug</code> from the <code>id</code> to retrieve more details. This way, the database stays clean of the <code>slug</code> and only has to care about <code>id</code> and we achieve an amazing UX for the power users who like to access their posts directly from the address bar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728319949861/2c78af68-4b43-485d-a5aa-3704bc9fa18b.gif" alt="A webpage showing posts titled &quot;First Post&quot;, &quot;Second Post&quot;, etc. Below there's Arc browser's address bar showing autocomplete on the URL using the implemented URL architecture." /></p>
<h3 id="heading-thats-it-is-it">That’s it… is it?</h3>
<p>The core functionality is done, but we can improve the UX further by forcing a refresh when a user lands on the post details page without a <code>slug</code>. This will tell their browser that the URL without the <code>slug</code> has been moved to the URL with the <code>slug</code>.</p>
<p>We can easily achieve this by checking if the received <code>postId</code> has a <code>-</code> in it or not. We can rely on this simple check because even if the <code>slug</code> was only 1 word, we’d have a <code>-</code> to separate <code>slug</code> from <code>id</code> like so <code>first-aabbccddeeff</code>.<br />We already have the <code>id</code> that our database understands so we can directly search it and retrieve its title. Then, we can simply redirect the user to what we receive from our <code>getSlugWithId</code> function.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Check if we need to redirect</span>
<span class="hljs-keyword">if</span> (!postIdRaw.includes(<span class="hljs-string">"-"</span>)) {
  <span class="hljs-comment">// Attempt to fetch post details</span>
  <span class="hljs-keyword">const</span> post = getPostById(postIdRaw);

  <span class="hljs-keyword">if</span> (!post) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">"Post not found"</span>, {
      status: <span class="hljs-number">404</span>,
      statusText: <span class="hljs-string">"Not Found"</span>,
    });
  }

  <span class="hljs-comment">// Generate new slug and redirect</span>
  <span class="hljs-keyword">const</span> newSlug = getSlugWithId(post);

  <span class="hljs-comment">// Check if the new slug is different</span>
  <span class="hljs-keyword">if</span> (newSlug !== postIdRaw) {
    <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">`/<span class="hljs-subst">${newSlug}</span>`</span>);
  }
}
</code></pre>
<h2 id="heading-conclusion-elevating-user-experience-with-smart-url-design">Conclusion: Elevating User Experience with Smart URL Design</h2>
<p>In conclusion, we explored the impact of URL architecture on user experience. By examining Notion’s approach to URL design, we determined incorporating meaningful <code>slug</code>s alongside <code>id</code>s can improve accessibility and usability for power users. We implemented the Notion-like URL architecture that supports both <code>slug-id</code> and just <code>id</code> in the <code>path</code> part of the URL which increases the user experience. This approach not only maintains a clean database structure but also provides a seamless and intuitive navigation experience for users. The demonstration of this architecture highlights its potential to enhance how users interact with web applications, making it a valuable consideration for developers aiming to provide a remarkable user experience.</p>
<p>You can find the complete code for this demo app on my <a target="_blank" href="https://github.com/Beyond-the-Basics-Dev/notion-urls">GitHub</a> or play with the code directly on <a target="_blank" href="https://stackblitz.com/~/github.com/Beyond-the-Basics-Dev/notion-urls">StackBlitz</a>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I’ve added test posts with titles that will test the <code>getSlugWithId</code>, <code>extractPostId</code>, and the <code>redirect</code> functionalities.</div>
</div>]]></content:encoded></item></channel></rss>