<?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: Zul Ikram Musaddik Rayat</title>
    <description>The latest articles on DEV Community by Zul Ikram Musaddik Rayat (@devrayat000).</description>
    <link>https://dev.to/devrayat000</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%2F867084%2F50a0e027-0439-4c8a-8733-1cad7dff769b.jpg</url>
      <title>DEV Community: Zul Ikram Musaddik Rayat</title>
      <link>https://dev.to/devrayat000</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devrayat000"/>
    <language>en</language>
    <item>
      <title>Why I Walked Back from Next.js and RSC to a Plain SPA and a Separate Backend</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Thu, 21 May 2026 14:04:09 +0000</pubDate>
      <link>https://dev.to/devrayat000/why-i-walked-back-from-nextjs-and-rsc-to-a-plain-spa-and-a-separate-backend-3ibo</link>
      <guid>https://dev.to/devrayat000/why-i-walked-back-from-nextjs-and-rsc-to-a-plain-spa-and-a-separate-backend-3ibo</guid>
      <description>&lt;p&gt;I've been writing React for a long time, and I want to tell you a story about a round trip. It starts with me being a true believer in Next.js, runs through the era when the App Router and Server Components moved into my house and rearranged the furniture, passes through a couple of genuinely scary security incidents, and ends with me happily back on a boring, well-understood architecture: a single-page app and a separate backend that talk over a plain HTTP boundary.&lt;/p&gt;

&lt;p&gt;This isn't a "framework X is dead" hot take. It's a description of how my own thinking changed, and why the thing I reach for in 2026 looks a lot like the thing I would have reached for in 2018... except smarter about the parts that actually matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Next.js was genuinely great
&lt;/h2&gt;

&lt;p&gt;I want to be fair to Next.js, because for a long stretch it earned its reputation.&lt;/p&gt;

&lt;p&gt;The Pages Router era was a sweet spot. You had file-based routing that you could explain to a new hire in five minutes. You had &lt;code&gt;getStaticProps&lt;/code&gt; and &lt;code&gt;getServerSideProps&lt;/code&gt; - two functions, clearly named, with an obvious mental model: one runs at build time, the other runs per request, and everything else is just React. You got code splitting, image optimization, and a dev server that mostly worked. Deploying was a non-event.&lt;/p&gt;

&lt;p&gt;The thing I appreciated most was that the boundary between server and client was &lt;em&gt;legible&lt;/em&gt;. Data fetching happened in named lifecycle-ish functions at the top of a route. The component tree below them was ordinary client React. I could point at any line of code and tell you which machine it ran on. That legibility is worth more than people realized at the time, and we mostly only noticed it once it was gone.&lt;/p&gt;

&lt;p&gt;So when I say I left, understand that I left something I used to love.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the App Router and Server Components moved in
&lt;/h2&gt;

&lt;p&gt;The App Router and React Server Components were pitched as the next evolution, and on paper the pitch is seductive: render components on the server, ship less JavaScript, colocate data fetching with the component that needs it, stream HTML to the browser as it becomes ready. Who doesn't want less JS and faster paint?&lt;/p&gt;

&lt;p&gt;In practice, here is what I actually experienced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mental model fractured.&lt;/strong&gt; Suddenly every file was either a Server Component or a Client Component, the distinction was load-bearing, and the boundary was declared with a &lt;code&gt;"use client"&lt;/code&gt; string at the top of a file. That directive is viral in one direction and silent about it. A component that was fine yesterday breaks today because something three levels up crossed the boundary, and the error you get is rarely "you crossed a boundary"; it's a serialization complaint, or a hydration mismatch, or a hook being called somewhere it can't be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It works in dev" stopped meaning anything.&lt;/strong&gt; Caching behavior between &lt;code&gt;next dev&lt;/code&gt;, &lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;, and the production edge deployment diverged enough that I stopped trusting local results. I had bugs that only existed in the production cache. I had stale data that no &lt;code&gt;revalidate&lt;/code&gt; value seemed to fix. I learned more about the Next.js caching layers than I ever wanted to, and the reward for that knowledge was a permanent low-grade anxiety about whether any given page was actually fresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server/client boundary stopped being legible.&lt;/strong&gt; This is the one that really got me. The thing I loved most about the Pages era, being able to look at code and know where it ran, was exactly the thing RSC took away. Data fetching is now scattered through the tree. &lt;code&gt;async&lt;/code&gt; components look like normal components but aren't. The boundary is real and consequential but invisible at the call site.&lt;/p&gt;

&lt;p&gt;None of these are unfixable in isolation. Together they meant I was spending a large fraction of my time fighting the framework's model instead of building the product. That's the signal I should have read sooner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the security incidents made it concrete
&lt;/h2&gt;

&lt;p&gt;Architectural friction is a judgment call. Security incidents are not, and a run of them turned my vague unease into a decision.&lt;/p&gt;

&lt;p&gt;It started with &lt;strong&gt;CVE-2025-29927&lt;/strong&gt;, the middleware authorization bypass disclosed in March 2025 at CVSS 9.1. The mechanism was almost embarrassingly simple: Next.js used the internal header &lt;code&gt;x-middleware-subrequest&lt;/code&gt; to mark subrequests and prevent infinite middleware loops, but the header was never meant to be client-controlled, and by spoofing it, attackers could skip middleware entirely, auth and authorization included, and reach protected routes with one crafted &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If that had been a one-off, I'd have shrugged it off; every framework has bugs. But it wasn't a one-off. The back half of 2025 and the start of 2026 brought a steady drumbeat, and the pattern is what mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2025-55182&lt;/strong&gt; (December 2025) - a React Server Components flaw rated &lt;strong&gt;CVSS 10.0&lt;/strong&gt;, the maximum. It sat in the &lt;code&gt;react-server-dom-*&lt;/code&gt; packages and therefore affected every framework built on RSC &amp;amp; Next.js, React Router's RSC APIs, Waku, Parcel's RSC plugin, and the Vite RSC plugin. A perfect score is rare, and this one lived in the RSC machinery itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-23864&lt;/strong&gt; (January 2026, CVSS 7.5) - a denial-of-service flaw in React Server Components: a malicious payload sent to a Server Function endpoint triggers memory exhaustion or runaway CPU. Patched alongside two more medium-severity Next.js CVEs in the same release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-23869&lt;/strong&gt; and &lt;strong&gt;CVE-2026-23870&lt;/strong&gt; (early 2026) - more DoS issues in Server Components, triggered by specially crafted HTTP requests to App Router Server Function endpoints that blow up CPU on deserialization. Affected Next.js 13.x through 16.x on the App Router.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-44578&lt;/strong&gt; (May 2026, CVSS 7.8) - a server-side request forgery in the self-hosted Next.js Node server: crafted WebSocket upgrade requests let an attacker proxy requests to arbitrary internal destinations, including cloud metadata endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look at where these clusters are. A middleware bypass, then a string of Server Component and Server Function flaws, several of them triggered specifically &lt;em&gt;on deserialization of a crafted request&lt;/em&gt;. This isn't bad luck distributed randomly across a codebase. The vulnerable surface is the server-rendering and server-function machinery, the exact part of the stack that the App Router and RSC made central. The more your framework does on the server on every request, the more attack surface you have signed up for, and a 10.0 in the shared RSC packages means a single bug ripples across every framework that adopted the model.&lt;/p&gt;

&lt;p&gt;What unsettled me wasn't any single CVE... it was what the cluster &lt;em&gt;implied about the architecture&lt;/em&gt;. Teams like mine had been quietly nudged to treat middleware as the authorization layer, because in the App Router model, auth-in-middleware is the path of least resistance. The level-headed takeaway in every writeup was the one that stung: middleware should supplement, not replace, security enforced closer to the data. So, I sat with a question: how much of my security posture do I want coupled to a rendering framework's internal request plumbing? My answer was "as little as possible."&lt;/p&gt;

&lt;h2&gt;
  
  
  The TanStack route - also not a free lunch
&lt;/h2&gt;

&lt;p&gt;When developers get fed up with Next.js, a common next stop is the TanStack ecosystem - TanStack Router and TanStack Start. And I'll be honest: TanStack Router is a genuinely nice piece of engineering. The type-safe routing is best-in-class, the search-param handling is thoughtful, and the data loading is well-considered. If you'd asked me purely about developer experience, I'd have said good things.&lt;/p&gt;

&lt;p&gt;But "switch to TanStack" is not the escape hatch some people think it is, and recent history makes that point sharply.&lt;/p&gt;

&lt;p&gt;In May 2026, the TanStack npm packages were hit by a supply chain attack. On 11 May 2026, a threat group ran a coordinated supply chain attack against the npm and PyPI ecosystems, compromising packages across multiple namespaces, including the &lt;code&gt;@tanstack&lt;/code&gt; namespace, which contains &lt;code&gt;@tanstack/react-router&lt;/code&gt;, one of the most widely-used routing libraries in the React ecosystem, with roughly 12 million weekly downloads. Between 19:20 and 19:26 UTC on a single Monday, the attacker published 84 malicious versions across 42 TanStack packages.&lt;/p&gt;

&lt;p&gt;The payload was not subtle. It targeted CI/CD tokens, cloud credentials across AWS, GCP, and Azure, Kubernetes service accounts, Vault, and registry tokens, and it used stolen npm and GitHub Actions tokens to publish poisoned versions of more packages, functioning as a worm spreading through the npm ecosystem. It also installed a persistent daemon that polled GitHub every 60 seconds, and on detecting token revocation would attempt to run &lt;code&gt;rm -rf&lt;/code&gt; on the user's home directory. One detail makes this especially grim: the compromised packages carried valid SLSA Build Level 3 provenance attestations, making it the first documented npm worm to produce validly attested malicious packages because the malicious versions were published through the project's own GitHub Actions release pipeline using hijacked OIDC tokens.&lt;/p&gt;

&lt;p&gt;I want to be careful and fair here. This was &lt;strong&gt;not&lt;/strong&gt; a flaw in TanStack Router's code. The attackers got in by forking a TanStack repository on GitHub and submitting a malicious commit under a fabricated identity. The TanStack maintainers responded quickly and communicated well. This could have happened, and has happened, to almost anyone - 2025 saw significant growth in supply chain attacks, and the npm ecosystem, because of its popularity, absorbed a large share of them. The September 2025 Shai-Hulud worm was the first self-propagating worm in the npm ecosystem and affected over 500 packages.&lt;/p&gt;

&lt;p&gt;So the lesson I drew from the TanStack incident isn't "TanStack is unsafe." It's the opposite of tribal. The lesson is: &lt;strong&gt;no framework choice immunizes you from supply chain risk, and the more of your stack you concentrate into one heavyweight meta-framework, the larger and more attractive a single compromise becomes.&lt;/strong&gt; Switching framework brand doesn't fix that. Reducing surface area and dependency count does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper reason full-stack RSC was always going to be hard: serialization
&lt;/h2&gt;

&lt;p&gt;Set the CVEs aside for a moment, because I think there's a more fundamental issue, and it's the one that finally settled my thinking.&lt;/p&gt;

&lt;p&gt;The whole promise of Server Components, server actions, and streaming is that the server/client boundary should feel seamless... You just write components and call functions, and the framework figures out what runs where. But a function call across a network is not a function call. The boundary is real, and it is made of &lt;strong&gt;serialization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Everything that crosses from server to client has to be serialized into the RSC payload, streamed, and deserialized. That constraint quietly shapes everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't pass a function to a Client Component from a Server Component unless it's a server action, because functions don't serialize. So "just pass a callback", the most ordinary thing in React, becomes a boundary decision.&lt;/li&gt;
&lt;li&gt;Class instances, &lt;code&gt;Date&lt;/code&gt; semantics, &lt;code&gt;Map&lt;/code&gt;/&lt;code&gt;Set&lt;/code&gt;, anything with methods or identity, all of it has to be flattened to plain data, or it doesn't make the trip cleanly.&lt;/li&gt;
&lt;li&gt;Errors that originate on the server are serialized before you see them on the client, so the stack trace you get is often a translation of the real problem rather than the problem itself.&lt;/li&gt;
&lt;li&gt;Streaming adds &lt;em&gt;time&lt;/em&gt; as a dimension. Components resolve out of order, Suspense boundaries flush independently, and now you're reasoning about the partial states of a tree mid-stream, which is a genuinely harder mental model than "fetch, then render."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a bug anyone can patch. It's intrinsic. The instant you let a UI tree straddle a network boundary, you have signed up for a distributed-systems problem dressed in component clothing, and serialization is where that problem leaks through. RSC's seamlessness is, to a real degree, a leaky abstraction over an inherently unseamless thing. You can build impressive demos on it. Holding it stable across a large app, a team, and a year of changes is a different sport.&lt;/p&gt;

&lt;p&gt;And notice the connection back to that CVE cluster. Several of those 2026 flaws, the Server Function DoS bugs, trigger &lt;em&gt;on deserialization of a crafted request&lt;/em&gt;. That is not a coincidence. Deserializing untrusted input into a live program is one of the oldest, most dangerous operations in computing, and the RSC model puts a deserialization boundary on the hot path of ordinary feature work. The architecture didn't just make my app harder to reason about; it made the framework itself a richer target. Serialization is both the ergonomic tax and the security surface, the same seam, charged twice.&lt;/p&gt;

&lt;p&gt;Once I framed it that way, my decision basically made itself. I don't want my UI framework and my data boundary fused. I want the network boundary to be &lt;strong&gt;explicit, visible, and boring&lt;/strong&gt;, a place I deliberately walk up to, not a seam hidden inside my component tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack I actually use now
&lt;/h2&gt;

&lt;p&gt;So here's where I landed. None of it is exotic. That's the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend: React Router in framework mode, on Vite, mostly SPA
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;React Router in its framework mode&lt;/strong&gt; with &lt;strong&gt;Vite&lt;/strong&gt;. Framework mode gives me the things I genuinely missed from Next.js file-based-ish routing conventions, loaders and actions, nested layouts, type-safe params without forcing a server-rendering model on me.&lt;/p&gt;

&lt;p&gt;The crucial part: &lt;strong&gt;I don't run SSR.&lt;/strong&gt; A handful of pages that are truly static marketing, docs, and the landing page get &lt;strong&gt;prerendered&lt;/strong&gt; at build time, so they ship as fast static HTML and are friendly to crawlers. Everything else is a &lt;strong&gt;plain client-rendered SPA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;People will say, "But SPAs are slow/bad for SEO / out of fashion." For an authenticated application, a dashboard, a tool, or a product behind a login, almost none of that critique applies. There's nothing for Google to index behind the login wall. The first paint is a thin shell, the bundle is code-split per route, and from then on, navigation is instant because it's all client-side. I get a build artifact that is just static files. I can put it on any CDN or object store. There is no server-rendering process to keep alive, patch, scale, or get a CVE in. The deployment story is "upload a folder," and the security surface of the frontend collapses to almost nothing.&lt;/p&gt;

&lt;p&gt;And critically: there is &lt;strong&gt;no serialization boundary inside my UI&lt;/strong&gt;. The component tree runs entirely in the browser. It talks to the backend through &lt;code&gt;fetch&lt;/code&gt;, against an API I designed on purpose. The boundary is visible in the code, at exactly the lines where it exists.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice. A route loads its data in a &lt;code&gt;clientLoader&lt;/code&gt; which, despite living in a framework-mode route file, runs entirely in the browser and just calls my API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/routes/projects.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./+types/projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/lib/api-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clientLoader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ClientLoaderArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Runs in the browser. No server. Just an HTTP call to my backend.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;loaderData&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ComponentProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loaderData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ul&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no &lt;code&gt;loader&lt;/code&gt; (server) here at all, only &lt;code&gt;clientLoader&lt;/code&gt;. The seam is the &lt;code&gt;api.projects.$get()&lt;/code&gt; call, and I can point at it. Auth is enforced the same way, with a &lt;code&gt;clientMiddleware&lt;/code&gt; that runs before the loaders on protected routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/routes/dashboard.tsx - a protected layout route&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./+types/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/lib/api-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ClientMiddlewareFunction&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But notice what this middleware &lt;em&gt;is not&lt;/em&gt;: it is not my security boundary. It's a UX convenience; it bounces an unauthenticated user to the login screen so they don't stare at a broken page. If someone skips it entirely, they gain nothing, because &lt;strong&gt;every API endpoint enforces auth itself, server-side, on every request.&lt;/strong&gt; This is the whole lesson of CVE-2025-29927 applied: client-side middleware is for experience, never for enforcement. The frontend can't bypass a check that the backend owns.&lt;/p&gt;

&lt;p&gt;The backend side of that contract, in Hono, is explicit and inspectable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono/cors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;csrf&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono/csrf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;csrf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Real enforcement: the API itself rejects unauthenticated requests.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// set by authMiddleware, trusted here&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projectsForUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authRoutes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;- exported for the frontend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the only reason I reach for Hono specifically. Exporting &lt;code&gt;AppType&lt;/code&gt; lets the frontend build a fully typed client with no codegen step and no shared runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/lib/api-client.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hc&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// `api` is end-to-end typed against the backend routes.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the backend changes a route's shape, the frontend stops compiling. That's the type safety people credit good Next.js setups with, and I keep it without fusing the two halves into one runtime. Swap Hono for Go or Python, and I lose only this typed-client convenience; the architecture is unchanged, because the contract is just HTTP.&lt;/p&gt;

&lt;p&gt;For data that loads &lt;em&gt;after&lt;/em&gt; the initial render, a slow widget on an otherwise-ready page, I reach for &lt;code&gt;Suspense&lt;/code&gt; and the &lt;code&gt;use&lt;/code&gt; hook. This is the one genuinely good idea from the streaming era, and the nice part is it works perfectly well in a plain SPA, with no server rendering involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/lib/api-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Kick off the request; do NOT await it. `use` will suspend on the promise.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ActivityFeed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;feedPromise&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;feedPromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedPromise&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// suspends until resolved&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Feed&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Promise created during render, in the browser. No RSC payload.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feedPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeedSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActivityFeed&lt;/span&gt; &lt;span class="na"&gt;feedPromise&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;feedPromise&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same &lt;code&gt;Suspense&lt;/code&gt;, same &lt;code&gt;use&lt;/code&gt;, same streaming-feel UX of content arriving progressively, but the promise is an ordinary &lt;code&gt;fetch&lt;/code&gt; resolving in the browser, not a chunk of a serialized server tree. There's no out-of-order flush to reason about, no hydration boundary, no payload format. I got the ergonomic win of the streaming era and left behind the serialized-tree machinery that came bundled with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend: a separate service - Hono if I want TypeScript
&lt;/h3&gt;

&lt;p&gt;The backend is its own thing, deployed, scaled, and reasoned about independently. Right now, I mostly reach for &lt;strong&gt;Hono&lt;/strong&gt;, because it's small, fast, runs anywhere from Node to workers to Bun, and lets me keep TypeScript end to end. With Hono, I can share types between client and server and even get a typed client, so I lose none of the type safety that good Next.js setups gave me.&lt;/p&gt;

&lt;p&gt;But, and this matters &lt;strong&gt;the backend doesn't have to be JavaScript at all.&lt;/strong&gt; It could just as easily be Go or Python. The reason that's now a free choice is precisely the serialization point from earlier: when your frontend is an SPA talking to an API over plain HTTP and JSON, there is no RSC payload, no server-action boundary, no shared-component-tree contract that forces both sides to be the same language. The contract is just HTTP. Go is fantastic for a backend. Python is fantastic for a backend. I use Hono specifically and only when I want the TypeScript type-sharing convenience; it's a preference, not a requirement. The architecture itself is language-agnostic, and that flexibility is a feature I gave myself by &lt;em&gt;removing&lt;/em&gt; the fused boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security lives in the backend, on purpose
&lt;/h3&gt;

&lt;p&gt;This is the part I care about most, and it's the direct lesson of CVE-2025-29927. &lt;strong&gt;Authorization and security are the backend's job, enforced at the API layer, every request, no exceptions.&lt;/strong&gt; Not in framework middleware. Not coupled to a renderer's internal request plumbing. In the service that owns the data.&lt;/p&gt;

&lt;p&gt;Concretely, on the backend, I run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; as a real, deliberate layer of sessions or tokens, validated server-side on every protected request. The server, which owns the database, decides what you can see. The frontend is never trusted to enforce this; it only reflects it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS&lt;/strong&gt; configured tightly - an explicit allowlist of origins, not a wildcard, so the browser only lets my actual frontend talk to the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSRF protection&lt;/strong&gt; - since a SPA-plus-API setup often uses cookies, I use proper anti-CSRF tokens and/or &lt;code&gt;SameSite&lt;/code&gt; cookie attributes so a malicious site can't ride a logged-in user's session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAPTCHA&lt;/strong&gt; on the abuse-prone endpoints, signup, login, password reset, public form submissions to keep bots and credential-stuffing out.&lt;/li&gt;
&lt;li&gt;Plus the usual unglamorous hygiene: rate limiting, strict input validation at the API edge, security headers, secrets kept out of the client bundle, and least-privilege everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is something I configure and can point at. There's no invisible header that flips my auth off, because my auth was never a header; it's a checked condition in the request handler that sits between the attacker and the database. If someone bypasses a routing layer, they hit the API, and the API still says no.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually gained
&lt;/h2&gt;

&lt;p&gt;Stepping back, here's the trade I made and why I'm happy with it.&lt;/p&gt;

&lt;p&gt;I gave up: server-rendered dynamic pages out of the box, and the "it's all one project" convenience of a meta-framework.&lt;/p&gt;

&lt;p&gt;I got back, in exchange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A legible architecture.&lt;/strong&gt; I can point at any line and say which machine it runs on. The network boundary is one explicit place where the API, instead of being smeared invisibly through a component tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No serialization tax.&lt;/strong&gt; The UI tree lives entirely in the browser. No RSC payload, no server-action plumbing, no streaming-order puzzles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tiny frontend attack surface.&lt;/strong&gt; Static files on a CDN. Nothing to keep running, nothing to patch, no Server Functions to send crafted payloads at, nothing to catch the next 10.0 in the RSC packages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security I can see.&lt;/strong&gt; Auth, CORS, CSRF, CAPTCHA, and rate limiting all live in one service, enforced per request, decoupled from any renderer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend freedom.&lt;/strong&gt; Hono today for the TypeScript ergonomics; Go or Python tomorrow if a project wants them. The HTTP contract doesn't care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller blast radius.&lt;/strong&gt; Two modest, separately-updated pieces with fewer heavyweight dependencies, instead of one large meta-framework whose single compromise is a very attractive target.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So, is Next.js bad? Is TanStack bad?
&lt;/h2&gt;

&lt;p&gt;No. I want to end honestly, because tribal blog posts age badly.&lt;/p&gt;

&lt;p&gt;Next.js and TanStack are both serious, well-engineered projects built by talented people. The CVEs and the supply chain attack I described are real and worth knowing about, but the CVEs got patched, and the TanStack incident was an account-and-pipeline compromise that could have hit almost any popular package. If your product genuinely needs server rendering for SEO on dynamic content, or you have a content-heavy site where streaming pays for itself, a meta-framework can absolutely be the right call. Plenty of teams ship great things on exactly the stack I walked away from.&lt;/p&gt;

&lt;p&gt;My point is narrower and more personal. For the apps &lt;strong&gt;I&lt;/strong&gt; build, authenticated products where the UI is interactive and the data is the crown jewels, the full-stack RSC model added a serialization-shaped boundary I didn't want, hid the seam I most needed to see, and nudged my security toward the framework's plumbing instead of my own backend. A SPA plus a separate, well-secured backend gave me a smaller, more legible, more boring system. And in software, boring is usually a compliment.&lt;/p&gt;

&lt;p&gt;That's the round trip. I left, I looked around, and I came back to a plain SPA and a real backend, older ideas, held with sharper reasons.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
    <item>
      <title>Email Verification with Better-Auth (Basics Tutorial, Ep. 2)</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Sun, 21 Sep 2025 16:27:58 +0000</pubDate>
      <link>https://dev.to/devrayat000/email-verification-with-better-auth-basics-tutorial-ep-2-3mm3</link>
      <guid>https://dev.to/devrayat000/email-verification-with-better-auth-basics-tutorial-ep-2-3mm3</guid>
      <description>&lt;p&gt;Welcome back to the &lt;a href="https://www.youtube.com/playlist?list=PLKNCWppgn6elkj28NRedKURDMAp-cuIJm" rel="noopener noreferrer"&gt;&lt;strong&gt;Better-Auth Basics&lt;/strong&gt;&lt;/a&gt; series! 🚀  &lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/devrayat000/authentication-using-better-auth-basics-tutorial-1bc1"&gt;Episode 1&lt;/a&gt;, we set up &lt;strong&gt;Better-Auth&lt;/strong&gt; with Drizzle ORM and implemented sign-up and login functionality. It worked great… but there’s a big flaw:  &lt;/p&gt;

&lt;p&gt;👉 Anyone can sign up with &lt;strong&gt;any random email&lt;/strong&gt; — even one they don’t own.  &lt;/p&gt;

&lt;p&gt;That’s obviously not safe for production. So in this post, we’ll fix that by adding &lt;strong&gt;Email Verification&lt;/strong&gt; with Better-Auth.  &lt;/p&gt;




&lt;h2&gt;
  
  
  ❌ The Problem Without Verification
&lt;/h2&gt;

&lt;p&gt;Right now, a user can type &lt;strong&gt;any email address&lt;/strong&gt; on the registration page, and the server will happily accept it.  &lt;/p&gt;

&lt;p&gt;That means fake accounts, spam signups, and security risks. We need a way to make sure the person &lt;strong&gt;actually owns the email&lt;/strong&gt; they’re registering with.  &lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Email Verification with Better-Auth
&lt;/h2&gt;

&lt;p&gt;The good news? Better-Auth already has &lt;strong&gt;email verification built in&lt;/strong&gt; for email/password authentication. We just need to configure it. Let’s go step by step.  &lt;/p&gt;




&lt;h2&gt;
  
  
  🛠 Step 1: Configure an Email Provider
&lt;/h2&gt;

&lt;p&gt;For sending verification emails, I used &lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;Resend&lt;/a&gt;.  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Resend account
&lt;/li&gt;
&lt;li&gt;Add your custom domain
&lt;/li&gt;
&lt;li&gt;Generate an API key
&lt;/li&gt;
&lt;li&gt;Save it in your &lt;code&gt;.env&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-api-key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠 Step 2: Create Email Templates
&lt;/h2&gt;

&lt;p&gt;Inside Resend, configure simple templates for your verification emails.&lt;br&gt;
This is the message your users will see in their inbox with the verification link.&lt;/p&gt;


&lt;h2&gt;
  
  
  🛠 Step 3: Enable Email Verification in Better-Auth
&lt;/h2&gt;

&lt;p&gt;In your Better-Auth configuration, enable the verification features:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;betterAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../db/schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betterAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;emailAndPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;requireEmailVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;emailVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sendOnSignUp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;autoSignInAfterVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠 Step 4: Send Verification Email on Sign Up
&lt;/h2&gt;

&lt;p&gt;Better-Auth will now automatically send a verification email when a new user registers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After sign-up, redirect the user to a verify page in your app.&lt;/li&gt;
&lt;li&gt;Tell them to check their inbox for the verification link.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/verify/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;VerifyPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Verify Your Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        We’ve sent you a link. Please check your inbox and click it to activate your account.
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠 Step 5: Testing the Flow
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Register with a real email address&lt;/li&gt;
&lt;li&gt;Check your inbox → you’ll see the verification email&lt;/li&gt;
&lt;li&gt;Click the link → the server validates it&lt;/li&gt;
&lt;li&gt;You’re automatically signed in and redirected to the homepage 🎉&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s what the successful response looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Email verified and user signed in"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎉 And That’s It!
&lt;/h2&gt;

&lt;p&gt;With just a few lines of configuration, we added secure email verification to our Better-Auth setup.&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users must prove they own their email&lt;/li&gt;
&lt;li&gt;Fake signups are blocked&lt;/li&gt;
&lt;li&gt;Onboarding is safer and more production-ready&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📌 What’s Next?
&lt;/h2&gt;

&lt;p&gt;This was Episode 2 of Better-Auth Basics.&lt;br&gt;
In future episodes, we’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;👥 Role-based authentication&lt;/li&gt;
&lt;li&gt;⚡ Rate limiting&lt;/li&gt;
&lt;li&gt;🛡 Middleware for protecting routes
Stay tuned — we’re just getting started.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔗 Stay Connected
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📽️ Youtube: &lt;a href="https://www.youtube.com/@code_unhinged" rel="noopener noreferrer"&gt;code_unhinged&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 Twitter (X): &lt;a href="https://x.com/zul_rayat" rel="noopener noreferrer"&gt;zul_rayat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 GitHub: &lt;a href="https://x.com/zul_rayat" rel="noopener noreferrer"&gt;devrayat000&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 LinkedIn: &lt;a href="https://www.linkedin.com/in/zim-rayat" rel="noopener noreferrer"&gt;zim-rayat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📷 Instagram: &lt;a href="https://www.instagram.com/rayatttttttt/" rel="noopener noreferrer"&gt;rayatttttttt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 Facebook: &lt;a href="https://www.facebook.com/rayat.ass" rel="noopener noreferrer"&gt;rayat.ass&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💬 Got questions? Drop them in the comments — I reply to every one!&lt;br&gt;
👍 Don’t forget to like, share, and subscribe for more dev content.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Authentication Using Better-Auth (Basics Tutorial)</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Wed, 17 Sep 2025 00:04:40 +0000</pubDate>
      <link>https://dev.to/devrayat000/authentication-using-better-auth-basics-tutorial-1bc1</link>
      <guid>https://dev.to/devrayat000/authentication-using-better-auth-basics-tutorial-1bc1</guid>
      <description>&lt;p&gt;What’s up, devs! 👋&lt;br&gt;
In this post, I’ll walk you through &lt;strong&gt;setting up authentication in a Node.js/Next.js project using Better-Auth&lt;/strong&gt; — a powerful new authentication library. This is based on the first episode of my YouTube playlist &lt;strong&gt;&lt;a href="https://www.youtube.com/playlist?list=PLKNCWppgn6elkj28NRedKURDMAp-cuIJm" rel="noopener noreferrer"&gt;Better-Auth Basics&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Better-Auth makes it really easy to implement secure authentication with features like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Email + Password authentication&lt;/li&gt;
&lt;li&gt;✅ Social sign-ins&lt;/li&gt;
&lt;li&gt;✅ Built-in rate limiting&lt;/li&gt;
&lt;li&gt;✅ Automatic database management &amp;amp; adapters&lt;/li&gt;
&lt;li&gt;✅ Two-factor authentication support&lt;/li&gt;
&lt;li&gt;✅ Simple client API for frontend integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds exciting? Let’s dive in! 🚀&lt;/p&gt;
&lt;h2&gt;
  
  
  🛠 Project Setup
&lt;/h2&gt;

&lt;p&gt;For this tutorial, I’m working on my personal project — a ride-sharing app.&lt;br&gt;
We’ll integrate Better-Auth into it step by step.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Install Dependencies
&lt;/h3&gt;

&lt;p&gt;We’ll need Better-Auth, Drizzle ORM, and Postgres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add better-auth drizzle-orm postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Environment Variables
&lt;/h3&gt;

&lt;p&gt;Generate a secret key and add it to your .env file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;BETTER_AUTH_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-secret-key&lt;/span&gt;
&lt;span class="py"&gt;BETTER_AUTH_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000 # Base URL of your app&lt;/span&gt;
&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgresql://user:password@localhost:5432/dbname&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚙️ Database Setup with Drizzle
&lt;/h2&gt;

&lt;p&gt;Better-Auth ships with ready-to-use schemas.&lt;br&gt;
We’ll copy those into our project and run migrations.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Generate Schema
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @better-auth/cli generate &lt;span class="c"&gt;# create better-auth schema&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Run Migrations
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx drizzle-kit generate &lt;span class="c"&gt;# generate migration files&lt;/span&gt;
bunx drizzle-kit migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once done, your database will have all the required tables.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔑 Enable Email/Password Authentication
&lt;/h2&gt;

&lt;p&gt;Inside your &lt;strong&gt;Better-Auth server setup&lt;/strong&gt;, enable email/password auth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/auth.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;betterAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzleAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth/adapters/drizzle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// your drizzle instance&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/db/schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// your drizzle schema&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betterAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;drizzleAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or "mysql", "sqlite"&lt;/span&gt;
    &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;emailAndPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🖥 Setting Up the Client
&lt;/h2&gt;

&lt;p&gt;Better-Auth provides a client utility to make frontend integration simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/auth-client.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAuthClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAuthClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="cm"&gt;/** The base URL of the server (optional if you're using the same domain) */&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📡 Next.js API Routes
&lt;/h2&gt;

&lt;p&gt;Now, let’s connect it with Next.js routes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/auth/[...all]/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toNextJsHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth/next-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toNextJsHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This route will handle all sign-in, sign-up, and session management APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  👤 Implement Sign-Up
&lt;/h2&gt;

&lt;p&gt;On your &lt;strong&gt;register page&lt;/strong&gt;, use the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleRegister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signUp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callbackUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔐 Implement Sign-In
&lt;/h2&gt;

&lt;p&gt;Similarly, on your login page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callbackUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎉 Testing It Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tried signing up → User was created successfully ✅&lt;/li&gt;
&lt;li&gt;Tried logging in → Redirected to homepage with 200 response ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it! We now have a working authentication flow with Better-Auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  📌 What’s Next?
&lt;/h2&gt;

&lt;p&gt;This tutorial only covers the basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up Better-Auth&lt;/li&gt;
&lt;li&gt;Running database migrations with Drizzle&lt;/li&gt;
&lt;li&gt;Implementing Sign-In &amp;amp; Sign-Up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In upcoming posts/videos, I’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔑 Authorization &amp;amp; Role-Based Access&lt;/li&gt;
&lt;li&gt;⚡ Rate Limiting&lt;/li&gt;
&lt;li&gt;🔒 Two-Factor Authentication&lt;/li&gt;
&lt;li&gt;🔗 Social Logins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Connect With Me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🐦 &lt;a href="https://www.facebook.com/rayat.ass" rel="noopener noreferrer"&gt;Facebook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/devrayat000" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/zim-rayat" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📽️ &lt;a href="https://www.youtube.com/@code_unhinged" rel="noopener noreferrer"&gt;Youtube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 If you found this useful, drop a comment and let me know what you’re building with Better-Auth.&lt;br&gt;
And don’t forget to &lt;strong&gt;follow me here on DEV.to + subscribe on YouTube&lt;/strong&gt; for more tutorials.&lt;/p&gt;

&lt;p&gt;Happy coding! ✨&lt;/p&gt;

</description>
      <category>betterauth</category>
      <category>nextjs</category>
      <category>drizzle</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>🍳 Recipe Generator – What’s in Your Pantry?</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Sun, 14 Sep 2025 20:33:41 +0000</pubDate>
      <link>https://dev.to/devrayat000/recipe-generator-whats-in-your-pantry-4lnk</link>
      <guid>https://dev.to/devrayat000/recipe-generator-whats-in-your-pantry-4lnk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-ai-studio-2025-09-03"&gt;Google AI Studio Multimodal Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Have a few random ingredients lying around but no idea what to cook?&lt;br&gt;
&lt;strong&gt;Recipe Generator&lt;/strong&gt; helps you turn everyday pantry items into delicious meals.&lt;/p&gt;

&lt;p&gt;Simply type or select ingredients you already have, and the app instantly generates recipe ideas — complete with images, instructions, and serving tips.&lt;/p&gt;

&lt;p&gt;This makes meal planning fun, reduces food waste, and gives home cooks an easy way to experiment with new dishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://gemini-recipe-generator-134638118864.us-west1.run.app" rel="noopener noreferrer"&gt;Live Demo Link&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&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%2Fv31rpqhwql48uguuhs6v.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%2Fv31rpqhwql48uguuhs6v.png" alt="Intro"&gt;&lt;/a&gt;&lt;br&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%2Fwqtp1zuckyy5n4v7wqci.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%2Fwqtp1zuckyy5n4v7wqci.png" alt="Start Generating"&gt;&lt;/a&gt;&lt;br&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%2Fwey01oiqroyby5m1k7kn.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%2Fwey01oiqroyby5m1k7kn.png" alt="Recipe 1"&gt;&lt;/a&gt;&lt;br&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%2Fivgp7t51rh1awoirklvt.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%2Fivgp7t51rh1awoirklvt.png" alt="Recipe 2"&gt;&lt;/a&gt;&lt;br&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%2Fvf3aozsbq1tahp32vgxh.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%2Fvf3aozsbq1tahp32vgxh.png" alt="Recipe 3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Google AI Studio
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Built the app on &lt;strong&gt;Google AI Studio&lt;/strong&gt;, deployed via &lt;strong&gt;Cloud Run&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Used Gemini’s &lt;strong&gt;multimodal capabilities&lt;/strong&gt; for:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text understanding&lt;/strong&gt;: Parsing ingredient inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image generation&lt;/strong&gt;: Producing realistic dish images that match the recipe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recipe generation&lt;/strong&gt;: Turning ingredient lists into step-by-step cooking instructions.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Multimodal Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingredient → Recipe (Text)&lt;/strong&gt;: Natural language input is converted into structured recipes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recipe → Image (Visual)&lt;/strong&gt;: Each generated recipe is paired with a dish image for inspiration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instructional Output&lt;/strong&gt;: Clear step-by-step cooking instructions with serving notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Practical impact: Helps reduce food waste by suggesting meals from what you already own.&lt;/li&gt;
&lt;li&gt;User delight: A fun, interactive way to explore cooking ideas.&lt;/li&gt;
&lt;li&gt;Strong multimodal showcase: Combines text understanding, recipe creation, and visual generation in a single workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Team
&lt;/h2&gt;

&lt;p&gt;Solo submission by &lt;a class="mentioned-user" href="https://dev.to/devrayat000"&gt;@devrayat000&lt;/a&gt; &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Hairstyle AI Try-On ✂️🤖</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Sun, 14 Sep 2025 19:55:25 +0000</pubDate>
      <link>https://dev.to/devrayat000/hairstyle-ai-try-on-4p0e</link>
      <guid>https://dev.to/devrayat000/hairstyle-ai-try-on-4p0e</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-ai-studio-2025-09-03"&gt;Google AI Studio Multimodal Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;What if, before getting a haircut, you could actually see how you’d look?&lt;br&gt;
&lt;strong&gt;Hairstyle AI Try-On&lt;/strong&gt; lets you upload your photo and instantly preview multiple hairstyles, complete with AI-generated ratings to help you find your perfect match.&lt;/p&gt;

&lt;p&gt;This app solves a very real problem: most of us take a risk when we try a new hairstyle. By using multimodal AI, you can confidently test different styles virtually before visiting the barber.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://hairstyle-ai-try-on-441360358721.us-west1.run.app" rel="noopener noreferrer"&gt;Live Demo Link&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;Initial State (upload your photo):&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%2F6xj1q5kg94ytune9h0rt.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%2F6xj1q5kg94ytune9h0rt.png" alt="Initial"&gt;&lt;/a&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%2Fgda20z56mrsqdd0pmzro.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%2Fgda20z56mrsqdd0pmzro.png" alt="Add Image"&gt;&lt;/a&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%2Fecj9coee8k0488lgl9sh.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%2Fecj9coee8k0488lgl9sh.png" alt="Start Processing"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI-Generated Hairstyles:&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%2Ftomcm9iv75bal1muopy1.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%2Ftomcm9iv75bal1muopy1.png" alt="Hairstyle 1"&gt;&lt;/a&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%2F8u58tfbdh5s0grmwkcy0.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%2F8u58tfbdh5s0grmwkcy0.png" alt="Hairstyle 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Google AI Studio
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I built and deployed the entire experience on &lt;strong&gt;Google AI Studio&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The backend runs on &lt;strong&gt;Cloud Run&lt;/strong&gt;, making it scalable and easy to deploy.&lt;/li&gt;
&lt;li&gt;Gemini was used for &lt;strong&gt;image understanding&lt;/strong&gt; (detecting the face and aligning hairstyles) and &lt;strong&gt;content generation&lt;/strong&gt; (evaluating &amp;amp; scoring hairstyles).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Multimodal Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image Understanding&lt;/strong&gt;: The uploaded photo is analyzed to detect face features and properly overlay hairstyles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Generation/Transformation&lt;/strong&gt;: Hairstyles are applied virtually, with realistic blending to match lighting and head shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text + Scoring Output&lt;/strong&gt;: Gemini provides natural-language feedback and a numerical “style score” (e.g. 8.5/10 for Curly Fringe).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mix of visual + evaluative output makes the experience both fun and practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Delightful user experience&lt;/strong&gt;: Try on hairstyles virtually, no risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical application&lt;/strong&gt;: Helps people make confident styling decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shows multimodal power&lt;/strong&gt;: Combines &lt;strong&gt;vision, generation, and evaluation&lt;/strong&gt; in one workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Team
&lt;/h2&gt;

&lt;p&gt;Solo submission by &lt;a class="mentioned-user" href="https://dev.to/devrayat000"&gt;@devrayat000&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This project demonstrates how Gemini’s multimodal capabilities can power consumer-friendly, real-world experiences. It’s fun, engaging, and practical — all in one.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Highlighting Image Text</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Tue, 30 Apr 2024 22:10:00 +0000</pubDate>
      <link>https://dev.to/devrayat000/highlighting-image-text-4hhh</link>
      <guid>https://dev.to/devrayat000/highlighting-image-text-4hhh</guid>
      <description>&lt;p&gt;Image processing and data extraction has become one of the most powerful features of Machine Learning now. But doing it from scratch is a pain in the a**. The one thing programming taught me that no one else did is not to reinvent the wheel every time and to prioritize getting the job done. Keeping that in mind, I have come across an easy solution for the problem at hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem At Hand
&lt;/h2&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%2Fp6fvubs2ftao8gfhjile.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%2Fp6fvubs2ftao8gfhjile.png" alt="Raw image before highlighting" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For simplicity's sake, let us consider this to be a page from a book. We want to highlight the word &lt;em&gt;comment&lt;/em&gt; wherever it occurs. This could be an intuitive feature for image search engines to direct the users' attention to their desired content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;We are going to be using an OCR (Optical Character Recognition) engine called &lt;a href="https://tesseract-ocr.github.io" rel="noopener noreferrer"&gt;Tesseract&lt;/a&gt; for the image-to-text recognition part. It is free software, released under the Apache License. Install the engine for your desired OS from their official website. I'm using Windows for this. Add the installation path to your environment variables.&lt;/p&gt;

&lt;p&gt;Create a python project with a virtual environment set up on it. Install the necessary packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;opencv-python &lt;span class="c"&gt;# for image processing&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;pytesseract &lt;span class="c"&gt;# to use the ocr engine in your project&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;pandas &lt;span class="c"&gt;# to conduct search queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your main.py import the necessary libraries and define the necessary variables. Read the image from the source using the &lt;em&gt;imread&lt;/em&gt; method. Make a copy of the original image for the overlay. Extract text information from the image. It is important to set the &lt;em&gt;output_type&lt;/em&gt; to be a pandas Dataframe object which will ease the filtering process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytesseract&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytesseract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;

&lt;span class="n"&gt;ALPHA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;

&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devto.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# make a copy of the original image for the highlight overlay
&lt;/span&gt;&lt;span class="n"&gt;overlay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# extract text data from the image as a pandas Dataframe object
&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pytesseract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image_to_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ben+eng&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DATAFRAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dataframe object returned has the following structure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;page_num&lt;/th&gt;
&lt;th&gt;block_num&lt;/th&gt;
&lt;th&gt;par_num&lt;/th&gt;
&lt;th&gt;line_num&lt;/th&gt;
&lt;th&gt;word_num&lt;/th&gt;
&lt;th&gt;left&lt;/th&gt;
&lt;th&gt;top&lt;/th&gt;
&lt;th&gt;width&lt;/th&gt;
&lt;th&gt;height&lt;/th&gt;
&lt;th&gt;conf&lt;/th&gt;
&lt;th&gt;text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;td&gt;537&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;96.276794&lt;/td&gt;
&lt;td&gt;comments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We are only interested in the &lt;em&gt;text&lt;/em&gt;, &lt;em&gt;left&lt;/em&gt;, &lt;em&gt;top&lt;/em&gt;, &lt;em&gt;width&lt;/em&gt;, and &lt;em&gt;height&lt;/em&gt; columns. We need to prepare the dataframe for this specific job by applying various filters. Drop the rows that have &lt;em&gt;NaN&lt;/em&gt; or empty string in the &lt;em&gt;text&lt;/em&gt; column to make our data error-proof and the computations more efficient. The text column usually contains single words. We can iterate through each row to find out if any of them matches our query string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# drop rows that have NaN values in the text column
&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# remove empty text rows
&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# Search through the text column for matching words
&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can get started with the highlighting part. We will draw rectangular highlight boxes around the matched positions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# draw a yellow rectangle around the matched text
&lt;/span&gt;    &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rectangle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Add the overlay on the original image
&lt;/span&gt;&lt;span class="n"&gt;img_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addWeighted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ALPHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ALPHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Some more image processing to make the highlights more realistic
&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;img_new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;dim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img_new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;resized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img_new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interpolation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTER_AREA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Show the modified image using opencv's &lt;em&gt;imshow&lt;/em&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imshow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Highlighted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroyAllWindows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is this modified image with every occurring &lt;em&gt;comment&lt;/em&gt; highlighted in yellow.&lt;br&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%2Feaftpbrka1vur4y8l8rm.jpg" 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%2Feaftpbrka1vur4y8l8rm.jpg" alt="Highlighted image" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus Tip
&lt;/h2&gt;

&lt;p&gt;The search-through mechanism in this process can only detect and highlight a single word or full sentence with exact matches. If we want to highlight words that are not in a single sentence, we just need to filter the dataframe with a little bit of pandas magic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;concat&lt;/span&gt;

&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, the user can query "essential comments" and it will highlight &lt;em&gt;essential&lt;/em&gt; and &lt;em&gt;comments&lt;/em&gt; even though they are not together.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opencv</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Hash routing in Remix!</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Fri, 29 Jul 2022 15:09:58 +0000</pubDate>
      <link>https://dev.to/devrayat000/hash-routing-in-remix-e2p</link>
      <guid>https://dev.to/devrayat000/hash-routing-in-remix-e2p</guid>
      <description>&lt;p&gt;&lt;a href="https://remix.run/" rel="noopener noreferrer"&gt;Remix&lt;/a&gt; is the newest hottest full-stack React framework. Remix supports file-based routing which uses &lt;code&gt;react-router-dom&lt;/code&gt; under the hood which is a popular react routing library.&lt;/p&gt;

&lt;p&gt;Because Remix users react-router's &lt;em&gt;BrowserRouter&lt;/em&gt; internally, you can't actually do &lt;strong&gt;Hash Routing&lt;/strong&gt; (id based routing that triggers scrolling) with it. &lt;/p&gt;

&lt;p&gt;For example, if you create a link like this,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#footer"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Go to bottom
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it will surely change the browser URL, but won't scroll down to the element with an id of footer.&lt;/p&gt;

&lt;p&gt;Of course, there's an easier way to achieve this behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning off Client-Side Routing
&lt;/h2&gt;

&lt;p&gt;We can add a &lt;code&gt;reloadDocument&lt;/code&gt; prop to the specialized &lt;strong&gt;Link&lt;/strong&gt; component and it will start acting like a normal anchor tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#footer"&lt;/span&gt; &lt;span class="na"&gt;reloadDocument&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Go to bottom
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This in turns, turns off client-side routing and lets the browser handle the transition, as &lt;a href="https://reactrouter.com/docs/en/v6/components/link" rel="noopener noreferrer"&gt;mentioned here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is fine until you want both client-side routing and scroll behavior on hash routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual Scrolling via Side-Effect
&lt;/h2&gt;

&lt;p&gt;The workaround this is to catch the side-effect created by the route change inside a &lt;code&gt;useEffect&lt;/code&gt; and handle the scrolling manually.&lt;/p&gt;

&lt;p&gt;In the root (&lt;em&gt;root.jsx&lt;/em&gt; file) of our projects, we have to get the &lt;em&gt;location&lt;/em&gt; object from the &lt;code&gt;useLocation&lt;/code&gt; hook provided by remix. Then, we have to create a &lt;em&gt;useEffect&lt;/em&gt; which depends on that location object. The &lt;em&gt;location&lt;/em&gt; object has a property called &lt;em&gt;hash&lt;/em&gt; which provides us with the hash portion of the URL e.g. &lt;strong&gt;#footer&lt;/strong&gt; if the URL is &lt;strong&gt;&lt;a href="http://www.example.com/#footer" rel="noopener noreferrer"&gt;www.example.com/#footer&lt;/a&gt;&lt;/strong&gt;. Then we can look up the element containing that id and manually scroll down to it using the &lt;code&gt;scrollIntoView&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLocation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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