<?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: Juan Torchia</title>
    <description>The latest articles on DEV Community by Juan Torchia (@jtorchia).</description>
    <link>https://dev.to/jtorchia</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%2F885942%2F5b3b3860-d364-4de0-a335-cb7c251109d9.jpeg</url>
      <title>DEV Community: Juan Torchia</title>
      <link>https://dev.to/jtorchia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jtorchia"/>
    <language>en</language>
    <item>
      <title>Rate limiting in web apps: what to protect before picking a library</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 05 Jun 2026 12:02:54 +0000</pubDate>
      <link>https://dev.to/jtorchia/rate-limiting-in-web-apps-what-to-protect-before-picking-a-library-4fki</link>
      <guid>https://dev.to/jtorchia/rate-limiting-in-web-apps-what-to-protect-before-picking-a-library-4fki</guid>
      <description>&lt;h1&gt;
  
  
  Rate limiting in web apps: what to protect before picking a library
&lt;/h1&gt;

&lt;p&gt;I made the mistake of adding rate limiting like it was a convenience dependency — &lt;code&gt;npm install&lt;/code&gt;, copy middleware from a tutorial, paste the magic number of 100 requests per minute, and get back to the sprint. I did it because "security" was on the backlog and I wanted to tick the box. The result was predictable: the middleware existed, but it wasn't protecting anything in particular. And the first time I actually looked at the logs with fresh eyes, I realized I had no idea what would have happened if someone had abused the login endpoint.&lt;/p&gt;

&lt;p&gt;I'm telling you this because that exact pattern is what I keep seeing recycled in Next.js tutorials: install a library, wrap it as global middleware, call it "security." That's not security. That's security vibes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My take is concrete:&lt;/strong&gt; rate limiting isn't a dependency; it's an abuse policy. And a policy without a definition is a rule without a subject.&lt;/p&gt;




&lt;h2&gt;
  
  
  What rate limiting in Next.js actually is — and what it isn't
&lt;/h2&gt;

&lt;p&gt;Rate limiting is a mechanism to reject or delay requests that exceed a defined threshold within a time window. That's all it is, technically. The real value isn't in the threshold — it's in the decision that led to that threshold.&lt;/p&gt;

&lt;p&gt;OWASP, in its &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;Authentication Cheat Sheet&lt;/a&gt;, recommends specific defensive controls around authentication: temporary account lockout after failed attempts, progressive throttling, and uniform responses to avoid leaking whether a user exists. What OWASP does &lt;em&gt;not&lt;/em&gt; say is "install library X and set 100 req/min on all routes." That's an interpretation, not a prescription.&lt;/p&gt;

&lt;p&gt;The difference matters: the OWASP guide gives you the &lt;em&gt;what to protect&lt;/em&gt; (authentication, password recovery, endpoints that expose account state). The concrete implementation depends on your stack, your traffic, and the cost you're willing to absorb when you block a legitimate user.&lt;/p&gt;

&lt;p&gt;In Next.js App Router, the natural interception point is Middleware (&lt;code&gt;middleware.ts&lt;/code&gt;), which runs at the edge before the request reaches the route — I covered that in detail in &lt;a href="https://juanchi.dev/en/blog/nextjs-16-middleware-authorization-patterns-race-conditions" rel="noopener noreferrer"&gt;this post on authorization patterns in Next.js 16 Middleware&lt;/a&gt;. But the execution layer doesn't replace the policy; it just enforces it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The classic mistake: copying middleware before defining the asset
&lt;/h2&gt;

&lt;p&gt;The typical tutorial starts like this:&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;// middleware.ts — example of what NOT to do first&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;NextResponse&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;next/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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;next/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;Ratelimit&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;@upstash/ratelimit&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;Redis&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;@upstash/redis&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;ratelimit&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;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnv&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slidingWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1 m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// why 100? why 1 minute?&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;middleware&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="nx"&gt;NextRequest&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="p"&gt;}&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;ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&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;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too Many Requests&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;429&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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;/((?!_next|favicon.ico).*)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// applies to EVERYTHING — really?&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code works. The problem is the chain of unanswered questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why 100 requests per minute? Based on what measured legitimate behavior?&lt;/li&gt;
&lt;li&gt;Does applying it to every route even make sense? A static image and a login endpoint don't share the same abuse profile.&lt;/li&gt;
&lt;li&gt;What happens to a legitimate user behind a corporate proxy or a university network where dozens of people share the same IP?&lt;/li&gt;
&lt;li&gt;Is there a log of the 429 that lets you tell the difference between a real attack and a false positive?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those questions get answered by the library. You answer them before you touch the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision matrix: four questions before writing a single line
&lt;/h2&gt;

&lt;p&gt;Before choosing an algorithm, a library, or a threshold, these four questions determine whether the rate limiting you're about to implement actually makes sense:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. What asset are you protecting?
&lt;/h3&gt;

&lt;p&gt;Not "the app." Something concrete:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Why limiting it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login endpoint&lt;/td&gt;
&lt;td&gt;Credential stuffing, brute force&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password recovery endpoint&lt;/td&gt;
&lt;td&gt;Account enumeration, email spam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form submission API&lt;/td&gt;
&lt;td&gt;Spam, notification flooding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expensive search endpoint&lt;/td&gt;
&lt;td&gt;Compute resource abuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static routes / assets&lt;/td&gt;
&lt;td&gt;Probably not — leave it to the CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OWASP explicitly calls out the first two. The rest are product decisions that you have to make.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What abuse are you expecting?
&lt;/h3&gt;

&lt;p&gt;The abuse you want to limit determines the right algorithm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High-velocity credential stuffing&lt;/strong&gt;: sliding window with a low per-IP threshold on the auth endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow, distributed scraping&lt;/strong&gt;: IP alone isn't enough — you need fingerprinting or session tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated form spam&lt;/strong&gt;: CAPTCHA first, rate limiting as a second layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legitimate traffic spikes (launch day, going viral)&lt;/strong&gt;: aggressive rate limiting can hurt you here — consider queueing or backpressure first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't know what abuse you're expecting, whatever threshold you set is arbitrary. And an arbitrary threshold is just as likely to annoy real users as it is to stop someone with actual malicious intent.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. What does a false positive cost you?
&lt;/h3&gt;

&lt;p&gt;This is the cost tutorials always skip. A 429 hitting a legitimate user has real consequences that depend on context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a SaaS with paying customers: lost trust, churn, a support ticket.&lt;/li&gt;
&lt;li&gt;In a public app with anonymous users: frustration, abandonment.&lt;/li&gt;
&lt;li&gt;In an internal API: it can silently break a critical flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost of a false positive defines how tight you can make the threshold. If the cost is high, you need a more permissive threshold and better abuse signals (user-agent, behavioral patterns, tokens) instead of just IP.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. How do you observe that it's working?
&lt;/h3&gt;

&lt;p&gt;If you don't have an answer to this, what you implemented is decorative security. A rate limiter without observability won't tell you whether it's blocking real abuse or legitimate users, whether the threshold is too aggressive or too permissive, or whether someone is already bypassing the control with a different technique.&lt;/p&gt;

&lt;p&gt;The useful minimum is logging every 429 with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;li&gt;IP (or whatever identifier you use as the key)&lt;/li&gt;
&lt;li&gt;Affected route&lt;/li&gt;
&lt;li&gt;Authentication context if available (authenticated user vs. anonymous)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts — version with minimal observability&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;NextResponse&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;next/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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;next/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;// Reproducible example using in-memory storage (dev only or edge with local state)&lt;/span&gt;
&lt;span class="c1"&gt;// In real production you need Redis or another distributed store&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// only for the login endpoint&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WINDOW_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1 minute&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;middleware&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="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only applying to the login route — defined asset, not global&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&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/auth/login&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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;x-forwarded-for&lt;/span&gt;&lt;span class="dl"&gt;"&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="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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;attempts&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="nx"&gt;ip&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;record&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;attempts&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;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&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="na"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;WINDOW_MS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&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;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Observable log: in production this goes to your logging system&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate_limit_exceeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;route&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="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too many attempts. Wait a moment.&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;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&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;Content-Type&lt;/span&gt;&lt;span class="dl"&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;application/json&lt;/span&gt;&lt;span class="dl"&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;Retry-After&lt;/span&gt;&lt;span class="dl"&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;60&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="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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only intercept the route we defined as the asset&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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;/api/auth/login&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;blockquote&gt;
&lt;p&gt;⚠️ This example uses an in-memory &lt;code&gt;Map&lt;/code&gt;, which doesn't persist across edge runtime instances and doesn't survive a redeploy. For production on Railway or any other distributed environment, you need an external store like Redis (Upstash, Redis Cloud). This example is a decision pattern, not a production recipe.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Where people get it wrong: three patterns that look right but aren't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: Global rate limiting as a substitute for endpoint security&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A middleware that caps all routes at 100 req/min doesn't protect the login endpoint any better than no rate limiting at all — if the threshold is above the volume of a typical brute-force attack. The attacker just respects the limit and keeps going. What actually helps is a low, specific threshold on the right asset — closer to what OWASP recommends for authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 2: IP as the only key dimension&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user behind CGNAT (IPv4 shared across thousands of people) has the same IP as everyone else on that network. In corporate or university contexts, rate limiting them all together can block dozens of legitimate people because of one person's behavior. If the asset you're protecting is primarily accessed by authenticated users, the key should be the user or session identifier, not the IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 3: Forgetting the &lt;code&gt;Retry-After&lt;/code&gt; header&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RFC 6585 defines that a 429 response should include a &lt;code&gt;Retry-After&lt;/code&gt; header indicating how long the client should wait. Without that header, automated clients — SDKs, mobile apps, integrations — will retry immediately and make the problem you were trying to limit worse. Small detail, concrete consequences.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limits of this guide: what you can't conclude without your own data
&lt;/h2&gt;

&lt;p&gt;There are things I'm not going to claim here because they depend on context I don't have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What threshold to use&lt;/strong&gt;: there's no universally correct number. The 10 req/min in the login example is illustrative. The real number comes from measured legitimate behavior in production — or a conservative initial decision with room to adjust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether Upstash, Redis Cloud, or something else is better&lt;/strong&gt;: that depends on latency from where you run the edge, cost per operation, and the operational complexity you're willing to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether rate limiting solves slow, distributed scraping&lt;/strong&gt;: probably not, at least not alone. That scenario needs other signals. Claiming otherwise without data would be selling an incomplete solution to someone with a real problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before assuming your rate limiting is working, you need real logs. Without them, the control exists on paper but not in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: rate limiting in Next.js web applications
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where do I implement rate limiting in Next.js App Router — in Middleware or in the Route Handler?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depends on the asset. If you want to act before the request reaches the route logic (and before compute costs kick in), Middleware is the right place. If you need full authentication context or business logic to decide the limit, the Route Handler has more information. In practice, both layers can coexist: Middleware for coarse IP-level limits, Route Handler for fine-grained per-authenticated-user limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use an in-memory &lt;code&gt;Map&lt;/code&gt; as the store for the rate limiter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only in development or as a pattern demonstration. An in-memory Map isn't shared across instances (Next.js can have multiple workers) and resets on every redeploy. For a distributed environment like Railway or Vercel, you need an external store — Redis is the most common and well-documented option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does rate limiting replace CAPTCHA on the login form?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. They're different controls. CAPTCHA aims to distinguish humans from bots in real time. Rate limiting aims to cap the volume of attempts regardless of whether they're human or bot. OWASP suggests both as complementary layers, not as alternatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if a legitimate user hits the limit by accident?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They should get a 429 with a clear &lt;code&gt;Retry-After&lt;/code&gt; and an understandable message. If that happens frequently, the threshold is miscalibrated for legitimate traffic — that's a signal to revisit the number, not to remove the control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is rate limiting enough to protect a public API from mass scraping?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Probably not, if the scraping is distributed (many different IPs, each with low volume). Per-IP rate limiting only works well against high-frequency concentrated sources. Distributed scraping needs other signals: user-agent fingerprinting, behavioral pattern analysis, or mandatory token-based authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I apply rate limiting to static asset routes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generally no — that's work for a CDN or the infrastructure layer. Applying rate limiting to &lt;code&gt;/favicon.ico&lt;/code&gt; or images from Next.js Middleware adds latency with no real defensive benefit. If asset traffic is the problem, the right control is at the network layer, not the application.&lt;/p&gt;




&lt;h2&gt;
  
  
  The library doesn't decide the policy. You do.
&lt;/h2&gt;

&lt;p&gt;There's a reason misconfigured rate limiting is worse than none: it gives you the feeling that the asset is protected when it isn't, or it protects something that doesn't matter while leaving the thing that does matter wide open.&lt;/p&gt;

&lt;p&gt;My position: before installing any library, answer the four questions in the matrix. If you can't answer "what asset am I protecting" and "what abuse am I expecting" with something more specific than "the app" and "bad people," you're not ready to implement the policy. You're ready to copy a tutorial.&lt;/p&gt;

&lt;p&gt;The concrete next step: look at your authentication routes first. They're the ones OWASP flags with the most evidence of needing controls. Define a conservative threshold, add minimal observability (log those 429s), and check those logs in the first 48 hours. That's where you'll get real data to calibrate against.&lt;/p&gt;

&lt;p&gt;If you want to understand how Next.js Middleware executes these controls and what happens with race conditions at scale, the post on &lt;a href="https://juanchi.dev/en/blog/nextjs-16-middleware-authorization-patterns-race-conditions" rel="noopener noreferrer"&gt;authorization patterns in Next.js 16 Middleware&lt;/a&gt; is the logical next step. And if backend endpoint security is part of the picture, it's worth crossing with what I covered on &lt;a href="https://juanchi.dev/en/blog/spring-boot-actuator-endpoints-what-to-expose-hide" rel="noopener noreferrer"&gt;what to expose and what to hide in Spring Boot Actuator&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Primary source:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OWASP Authentication Cheat Sheet — defensive controls around authentication and abuse: &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/rate-limiting-web-apps-what-to-protect-before-choosing-library" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>railway</category>
    </item>
    <item>
      <title>Rate limiting en aplicaciones web: qué proteger antes de elegir una librería</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 05 Jun 2026 12:02:49 +0000</pubDate>
      <link>https://dev.to/jtorchia/rate-limiting-en-aplicaciones-web-que-proteger-antes-de-elegir-una-libreria-202n</link>
      <guid>https://dev.to/jtorchia/rate-limiting-en-aplicaciones-web-que-proteger-antes-de-elegir-una-libreria-202n</guid>
      <description>&lt;h1&gt;
  
  
  Rate limiting en aplicaciones web: qué proteger antes de elegir una librería
&lt;/h1&gt;

&lt;p&gt;Cometí el error de agregar rate limiting como si fuera una dependencia de conveniencia — &lt;code&gt;npm install&lt;/code&gt;, copiar middleware de un tutorial, pegar el número mágico de 100 requests por minuto y seguir con el sprint. Lo hice porque "seguridad" estaba en el backlog y quería tildar el ítem. El resultado fue predecible: el middleware existía, pero no protegía nada en particular. Y la primera vez que revisé los logs con criterio, me di cuenta de que no sabía qué habría pasado si alguien hubiera abusado del endpoint de login.&lt;/p&gt;

&lt;p&gt;Lo cuento porque ese patrón es exactamente el que veo circular en tutoriales de Next.js: instalar una librería, enrollarla como middleware global y llamarle "seguridad". No es seguridad. Es vibra de seguridad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi tesis es concreta:&lt;/strong&gt; rate limiting no es una dependencia; es una política de abuso. Y una política sin definición es una regla sin sujeto.&lt;/p&gt;




&lt;h2&gt;
  
  
  Qué es rate limiting en aplicaciones web con Next.js — y qué no es
&lt;/h2&gt;

&lt;p&gt;Rate limiting es un mecanismo para rechazar o demorar solicitudes que superan un umbral definido en una ventana de tiempo. Eso es todo lo que es a nivel técnico. El valor real no está en el umbral — está en la decisión que llevó a ese umbral.&lt;/p&gt;

&lt;p&gt;OWASP, en su &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;Authentication Cheat Sheet&lt;/a&gt;, recomienda controles defensivos específicos alrededor de autenticación: bloqueo temporal de cuenta tras intentos fallidos, throttling progresivo y respuestas uniformes para no revelar si el usuario existe. Lo que OWASP no dice es "instalá X librería y configurá 100 req/min en todas las rutas". Eso es una interpretación, no una prescripción.&lt;/p&gt;

&lt;p&gt;La diferencia importa: la guía de OWASP te da el &lt;em&gt;qué proteger&lt;/em&gt; (autenticación, recuperación de contraseña, endpoints que exponen estado de cuenta). La implementación concreta depende de tu stack, tu tráfico y el costo que estás dispuesto a asumir cuando bloqueás a alguien legítimo.&lt;/p&gt;

&lt;p&gt;En Next.js App Router, el lugar natural para interceptar es el Middleware (&lt;code&gt;middleware.ts&lt;/code&gt;), que corre en el edge antes de que el request llegue a la ruta — lo analicé en detalle en &lt;a href="https://juanchi.dev/es/blog/nextjs-16-middleware-autorizacion-patrones-race-conditions" rel="noopener noreferrer"&gt;este post sobre patrones de autorización en Next.js 16 Middleware&lt;/a&gt;. Pero la capa de ejecución no reemplaza la política; solo la aplica.&lt;/p&gt;




&lt;h2&gt;
  
  
  El error clásico: copiar el middleware antes de definir el activo
&lt;/h2&gt;

&lt;p&gt;El tutorial típico empieza así:&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;// middleware.ts — ejemplo de lo que NO hacer primero&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;NextResponse&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;next/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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;next/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;Ratelimit&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;@upstash/ratelimit&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;Redis&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;@upstash/redis&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;ratelimit&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;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnv&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slidingWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1 m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// ¿por qué 100? ¿por qué 1 minuto?&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;middleware&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="nx"&gt;NextRequest&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="p"&gt;}&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;ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&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;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too Many Requests&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;429&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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;/((?!_next|favicon.ico).*)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// aplica a TODO — ¿seguro?&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El código funciona. El problema es la cadena de preguntas sin responder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Por qué 100 requests por minuto? ¿Basado en qué comportamiento legítimo medido?&lt;/li&gt;
&lt;li&gt;¿Aplicarlo a todas las rutas tiene sentido? Una imagen estática y un endpoint de login no tienen el mismo perfil de abuso.&lt;/li&gt;
&lt;li&gt;¿Qué le pasa a un usuario legítimo detrás de un proxy corporativo o una red universitaria donde muchos comparten la misma IP?&lt;/li&gt;
&lt;li&gt;¿Hay un log del 429 que permita distinguir ataque real de falso positivo?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ninguna de esas preguntas la resuelve la librería. Las resolvés vos antes de tocar el código.&lt;/p&gt;




&lt;h2&gt;
  
  
  La matriz de decisión: cuatro preguntas antes de escribir una línea
&lt;/h2&gt;

&lt;p&gt;Antes de elegir algoritmo, librería o umbral, estas cuatro preguntas definen si el rate limiting que vas a implementar tiene sentido:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. ¿Qué activo protegés?
&lt;/h3&gt;

&lt;p&gt;No "la app". Algo concreto:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activo&lt;/th&gt;
&lt;th&gt;Por qué importa limitarlo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint de login&lt;/td&gt;
&lt;td&gt;Credential stuffing, fuerza bruta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint de recuperación de contraseña&lt;/td&gt;
&lt;td&gt;Enumeración de cuentas, spam de emails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API de envío de formularios&lt;/td&gt;
&lt;td&gt;Spam, flood de notificaciones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint de búsqueda costosa&lt;/td&gt;
&lt;td&gt;Abuso de recursos de cómputo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rutas estáticas / assets&lt;/td&gt;
&lt;td&gt;Probablemente no — dejalo a CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OWASP señala explícitamente los dos primeros. Los otros son decisiones de producto que vos tenés que tomar.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. ¿Qué abuso esperás?
&lt;/h3&gt;

&lt;p&gt;El abuso que querés limitar determina el algoritmo correcto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential stuffing de alta velocidad&lt;/strong&gt;: sliding window con umbral bajo por IP en el endpoint de auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scraping lento y distribuido&lt;/strong&gt;: IP sola no alcanza — necesitás fingerprinting o tokens de sesión.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spam de formularios automatizado&lt;/strong&gt;: CAPTCHA primero, rate limiting como segunda capa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Picos de tráfico legítimo (lanzamiento, viral)&lt;/strong&gt;: rate limiting estricto puede hacerte daño — considerá queueing o backpressure primero.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si no sabés qué abuso esperás, el umbral que ponés es arbitrario. Y un umbral arbitrario tiene la misma probabilidad de molestar a usuarios reales que de detener a alguien con intención maliciosa.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. ¿Cuánto te cuesta un falso positivo?
&lt;/h3&gt;

&lt;p&gt;Este es el costo que los tutoriales omiten. Un 429 a un usuario legítimo tiene consecuencias reales que dependen del contexto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;En un SaaS con clientes pagos: pérdida de confianza, churn, ticket de soporte.&lt;/li&gt;
&lt;li&gt;En una app pública con usuario anónimo: frustración, abandono.&lt;/li&gt;
&lt;li&gt;En una API interna: puede romper un flujo crítico silenciosamente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El costo del falso positivo define cuánto podés apretar el umbral. Si el costo es alto, necesitás un umbral más permisivo y mejores señales de abuso (user-agent, comportamiento, tokens) en lugar de solo IP.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. ¿Cómo observás que está funcionando?
&lt;/h3&gt;

&lt;p&gt;Si no tenés respuesta para esto, lo que implementaste es seguridad decorativa. Un rate limiter sin observabilidad no te dice si está bloqueando abuso real o usuarios legítimos, si el umbral es demasiado agresivo o demasiado permisivo, ni si alguien está evadiendo el control con otra técnica.&lt;/p&gt;

&lt;p&gt;El mínimo útil es loggear cada 429 con:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;li&gt;IP (o identificador que uses como clave)&lt;/li&gt;
&lt;li&gt;Ruta afectada&lt;/li&gt;
&lt;li&gt;Contexto de autenticación si está disponible (usuario autenticado vs. anónimo)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts — versión con observabilidad mínima&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;NextResponse&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;next/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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;next/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;// Ejemplo reproducible con almacenamiento en memoria (solo para desarrollo o edge con estado local)&lt;/span&gt;
&lt;span class="c1"&gt;// En producción real necesitás Redis u otro store distribuido&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;LIMITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// solo para el endpoint de login&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VENTANA_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1 minuto&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;middleware&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="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Solo aplicamos a la ruta de login — activo definido, no global&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&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/auth/login&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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;x-forwarded-for&lt;/span&gt;&lt;span class="dl"&gt;"&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="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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desconocida&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;ahora&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;registro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intentos&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="nx"&gt;ip&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;registro&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ahora&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;registro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;intentos&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;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&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="na"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ahora&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;VENTANA_MS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;registro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&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;registro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;LIMITE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log observable: en producción esto iría a tu sistema de logs&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate_limit_excedido&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ruta&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="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;intentos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;registro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Demasiados intentos. Esperá un momento.&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;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&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;Content-Type&lt;/span&gt;&lt;span class="dl"&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;application/json&lt;/span&gt;&lt;span class="dl"&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;Retry-After&lt;/span&gt;&lt;span class="dl"&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;60&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="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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Solo interceptamos la ruta que definimos como activo&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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;/api/auth/login&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;blockquote&gt;
&lt;p&gt;⚠️ Este ejemplo usa &lt;code&gt;Map&lt;/code&gt; en memoria, que no persiste entre instancias del edge runtime ni sobrevive un redeploy. Para producción sobre Railway u otro entorno distribuido, necesitás un store externo como Redis (Upstash, Redis Cloud). El ejemplo sirve como patrón de decisión, no como receta de producción directa.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Dónde se equivoca la gente: tres patrones que parecen bien pero no lo son
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Patrón 1: Rate limiting global como sustituto de seguridad de endpoint&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Un middleware que limita todas las rutas a 100 req/min no protege el endpoint de login mejor que uno sin rate limiting, si el umbral está por encima del volumen de un ataque de fuerza bruta típico. El atacante simplemente respeta el límite y sigue. Lo que realmente ayuda es un umbral bajo y específico en el activo correcto — más en la línea de lo que recomienda OWASP para autenticación.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patrón 2: IP como única dimensión de clave&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Un usuario detrás de CGNAT (IPv4 compartida entre miles) tiene la misma IP que otro. En contextos corporativos o educativos, limitarlos a todos juntos puede bloquear a decenas de personas legítimas por el comportamiento de una sola. Si el activo que protegés es accedido principalmente por usuarios autenticados, la clave debería ser el identificador de usuario o sesión, no la IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patrón 3: Olvidar el header &lt;code&gt;Retry-After&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RFC 6585 define que una respuesta 429 debería incluir el header &lt;code&gt;Retry-After&lt;/code&gt; indicando cuánto debe esperar el cliente. Sin ese header, clientes automáticos (SDKs, apps móviles, integraciones) van a reintentar inmediatamente y agravar el problema que intentabas limitar. Es un detalle pequeño con consecuencias concretas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Límites de esta guía: qué no podés concluir sin datos propios
&lt;/h2&gt;

&lt;p&gt;Hay cosas que no voy a afirmar acá porque dependen de contexto que no tengo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qué umbral usar&lt;/strong&gt;: no existe un número correcto universal. El 10 req/min del ejemplo de login es ilustrativo. El número real lo define el comportamiento legítimo medido en producción — o una decisión conservadora inicial con capacidad de ajuste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Si Upstash, Redis Cloud u otro store es mejor&lt;/strong&gt;: dependen de la latencia desde donde corrés el edge, el costo por operación y la complejidad operativa que estás dispuesto a mantener.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Si el rate limiting resuelve scraping lento y distribuido&lt;/strong&gt;: probablemente no, al menos no solo. Ese escenario requiere otras señales. Afirmar lo contrario sin datos sería venderle una solución incompleta a alguien con un problema real.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Antes de asumir que el rate limiting funciona, necesitás logs reales. Sin ellos, el control existe en papel pero no en práctica.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: rate limiting en aplicaciones web con Next.js
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Dónde implemento rate limiting en Next.js App Router — en Middleware o en la Route Handler?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depende del activo. Si querés actuar antes de que el request llegue a la lógica de la ruta (y antes de costos de compute), el Middleware es el lugar correcto. Si necesitás contexto de autenticación completo o lógica de negocio para decidir el límite, la Route Handler tiene más información. En la práctica, los dos capas pueden coexistir: Middleware para límites gruesos por IP, Route Handler para límites finos por usuario autenticado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo usar un &lt;code&gt;Map&lt;/code&gt; en memoria como store para el rate limiter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Solo en desarrollo o como demostración de patrón. Un Map en memoria no se comparte entre instancias (Next.js puede tener múltiples workers) y se reinicia con cada redeploy. Para un ambiente distribuido como Railway o Vercel, necesitás un store externo — Redis es la opción más común y documentada.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El rate limiting reemplaza CAPTCHA en el formulario de login?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Son controles diferentes. CAPTCHA apunta a distinguir humanos de bots en tiempo real. Rate limiting apunta a limitar el volumen de intentos independientemente de si son humanos o bots. OWASP sugiere los dos como capas complementarias, no como alternativas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué pasa si un usuario legítimo toca el límite por error?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Debería recibir un 429 con un &lt;code&gt;Retry-After&lt;/code&gt; claro y un mensaje entendible. Si eso pasa frecuentemente, el umbral está mal calibrado para el tráfico legítimo — eso es una señal para revisar el número, no para eliminar el control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Rate limiting alcanza para proteger una API pública de scraping masivo?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Probablemente no, si el scraping es distribuido (muchas IPs distintas con volumen bajo cada una). El rate limiting por IP solo funciona bien contra fuentes de alta frecuencia concentradas. Scraping distribuido requiere otras señales: fingerprinting de user-agent, análisis de patrones de comportamiento o autenticación obligatoria con tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Conviene aplicar rate limiting a rutas de assets estáticos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generalmente no — eso es trabajo para un CDN o para la capa de infraestructura. Aplicar rate limiting a &lt;code&gt;/favicon.ico&lt;/code&gt; o a imágenes desde el Middleware de Next.js agrega latencia sin beneficio defensivo real. Si el tráfico de assets es el problema, el control adecuado está en la capa de red, no en la aplicación.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cierre: la librería no decide la política, vos sí
&lt;/h2&gt;

&lt;p&gt;Hay una razón por la que el rate limiting mal configurado es peor que ninguno: da la sensación de que el activo está protegido cuando no lo está, o protege algo que no importa mientras deja desprotegido lo que sí importa.&lt;/p&gt;

&lt;p&gt;Mi postura es esta: antes de instalar cualquier librería, respondé las cuatro preguntas de la matriz. Si no podés responder "qué activo protejo" y "qué abuso espero" con algo más específico que "la app" y "gente maliciosa", no estás lista para implementar la política. Estás para copiar un tutorial.&lt;/p&gt;

&lt;p&gt;El próximo paso concreto: revisá tus rutas de autenticación primero. Son las que OWASP marca con más evidencia de necesitar control. Definí un umbral conservador, agregá observabilidad mínima (loggear los 429) y revisá esos logs en las primeras 48 horas. Ahí vas a tener datos reales para calibrar.&lt;/p&gt;

&lt;p&gt;Si querés entender mejor cómo el Middleware de Next.js ejecuta estos controles y qué pasa con los race conditions cuando escala, el post sobre &lt;a href="https://dev.to/blog/nextjs-16-middleware-autorizacion-patorizacion-patrones-race-conditions"&gt;patrones de autorización en Next.js 16 Middleware&lt;/a&gt; es el siguiente paso lógico. Y si la seguridad de endpoints de backend es parte del contexto, vale cruzar con lo que cubrí sobre &lt;a href="https://juanchi.dev/es/blog/spring-boot-actuator-endpoints-seguridad" rel="noopener noreferrer"&gt;qué exponer y qué ocultar en Spring Boot Actuator&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OWASP Authentication Cheat Sheet — controles defensivos alrededor de autenticación y abuso: &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/rate-limiting-aplicaciones-web-nextjs" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Next.js 16 Middleware: authorization patterns that scale and the ones that cause race conditions</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Thu, 04 Jun 2026 12:02:09 +0000</pubDate>
      <link>https://dev.to/jtorchia/nextjs-16-middleware-authorization-patterns-that-scale-and-the-ones-that-cause-race-conditions-4pfk</link>
      <guid>https://dev.to/jtorchia/nextjs-16-middleware-authorization-patterns-that-scale-and-the-ones-that-cause-race-conditions-4pfk</guid>
      <description>&lt;h1&gt;
  
  
  Next.js 16 Middleware: authorization patterns that scale and the ones that cause race conditions
&lt;/h1&gt;

&lt;p&gt;Next.js middleware is basically the bouncer at a club. It doesn't decide if you're welcome inside — that's the staff's job. But it does decide whether you get through the door. And if the bouncer starts running a full background check on every person before opening up, the line wraps around the block.&lt;/p&gt;

&lt;p&gt;That's exactly the problem with authorization patterns in Next.js 16 Middleware. Most of the examples floating around online assume you can do full token validation at the edge. The reality is more uncomfortable: the edge runtime has concrete restrictions, and several patterns that worked fine in v14 blow up in production in ways that aren't obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My thesis:&lt;/strong&gt; Next.js 16 middleware is powerful, but its strength is in verifying &lt;em&gt;session&lt;/em&gt;, not in validating a &lt;em&gt;complete token&lt;/em&gt;. When you confuse those two roles, you end up with race conditions or latency you don't understand until you're staring at logs at 11pm.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real problem: edge runtime is not Node.js
&lt;/h2&gt;

&lt;p&gt;Before looking at each pattern, there's one fact that shapes everything that follows: Next.js middleware runs on &lt;a href="https://nextjs.org/docs/app/api-reference/edge" rel="noopener noreferrer"&gt;edge runtime&lt;/a&gt;, not full Node.js. That's not a minor detail — it's the reason certain patterns fail.&lt;/p&gt;

&lt;p&gt;The edge runtime has access to standard web APIs (&lt;code&gt;Request&lt;/code&gt;, &lt;code&gt;Response&lt;/code&gt;, &lt;code&gt;Headers&lt;/code&gt;, &lt;code&gt;crypto.subtle&lt;/code&gt;) but &lt;strong&gt;does not have access to&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fs&lt;/code&gt; — no reading files&lt;/li&gt;
&lt;li&gt;native Node.js modules&lt;/li&gt;
&lt;li&gt;libraries that depend on Node buffers or system APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this means for auth is concrete: if your JWT library uses &lt;code&gt;jsonwebtoken&lt;/code&gt; with Node's &lt;code&gt;crypto&lt;/code&gt;, it won't work in middleware. You need &lt;code&gt;jose&lt;/code&gt; or another library compatible with the Web Crypto 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;// ❌ This blows up in edge runtime&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsonwebtoken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// depends on Node's crypto&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ This works in edge runtime&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;jwtVerify&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="s1"&gt;jose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// Web Crypto API compatible&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official &lt;a href="https://nextjs.org/docs/app/building-your-application/routing/middleware" rel="noopener noreferrer"&gt;Next.js Middleware&lt;/a&gt; docs mention this, but between all the code examples it's easy to skip over that part — until the error shows up in your deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 4 patterns: tradeoff analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1 — Full token validation in middleware
&lt;/h3&gt;

&lt;p&gt;The most tempting one and the most problematic.&lt;/p&gt;

&lt;p&gt;The idea: grab the token from the cookie or &lt;code&gt;Authorization&lt;/code&gt; header, cryptographically verify it in middleware, and decide whether the user gets through.&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;// middleware.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;jwtVerify&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="s1"&gt;jose&lt;/span&gt;&lt;span class="dl"&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;NextResponse&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&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;NextRequest&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECRET&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&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;middleware&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="nx"&gt;NextRequest&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;token&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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="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="c1"&gt;// Full cryptographic verification on every request&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;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jwtVerify&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;SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Pass the userId downstream via header&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-user-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/dashboard/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/protected/:path*&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;&lt;strong&gt;The honest tradeoff:&lt;/strong&gt; &lt;code&gt;jwtVerify&lt;/code&gt; with &lt;code&gt;jose&lt;/code&gt; does run in edge runtime. The cryptographic verification itself is fast. The problem shows up when the token has a short expiration and you also need to consult a revocation list, or when you want to verify granular permissions that live in a database. At that point you're in trouble, because doing a database fetch from middleware on every request is latency that adds up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it works well:&lt;/strong&gt; long-lived tokens, no active revocation, where you only need to know if the token is structurally valid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it blows up:&lt;/strong&gt; if your system revokes tokens (real logout, password change), this pattern won't reflect that until the token expires on its own.&lt;/p&gt;




&lt;h3&gt;
  
  
  Pattern 2 — Role-based redirects in middleware
&lt;/h3&gt;

&lt;p&gt;This pattern looks simple but hides a race condition specific to the Next.js App Router.&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;// middleware.ts — role-based redirect pattern&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;middleware&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="nx"&gt;NextRequest&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;sessionCookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;sessionCookie&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Decoding without verifying — just to read the role from the payload&lt;/span&gt;
  &lt;span class="c1"&gt;// ⚠️ IMPORTANT: this is NOT a security verification&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionCookie&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;3&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;

  &lt;span class="c1"&gt;// Redirect based on role&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;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/403&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;url&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;&lt;strong&gt;The race condition problem:&lt;/strong&gt; if you use &lt;code&gt;NextResponse.redirect&lt;/code&gt; in middleware at the same time the client has a Server Component doing a fetch from &lt;code&gt;layout.tsx&lt;/code&gt;, you can end up with two in-flight requests pointing to different destinations. The App Router has its own navigation mechanism and the middleware redirect interrupts the hydration cycle in ways that aren't always predictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The symptom:&lt;/strong&gt; the user sees a content flash before the redirect, or gets stuck in a redirect loop on certain routes. Reproducible when the matcher covers routes with nested layouts that do their own fetching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; use &lt;code&gt;NextResponse.rewrite&lt;/code&gt; instead of &lt;code&gt;redirect&lt;/code&gt; for internal or API routes, and save &lt;code&gt;redirect&lt;/code&gt; only for the "no session at all" case. For granular permissions within a valid session, delegate the decision to the Server Component or Route Handler — they have full database access.&lt;/p&gt;




&lt;h3&gt;
  
  
  Pattern 3 — API route protection only in middleware
&lt;/h3&gt;

&lt;p&gt;This is the pattern I see recommended most often in tutorials, and it has the most expensive hidden cost.&lt;/p&gt;

&lt;p&gt;The idea is to use the &lt;code&gt;matcher&lt;/code&gt; to protect all &lt;code&gt;/api/&lt;/code&gt; routes from middleware and not validate anything inside the route handler itself.&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;// middleware.ts — API protection from middleware only&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/api/:path*&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="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;middleware&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="nx"&gt;NextRequest&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="s1"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;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;token&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&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;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&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="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify token and let through&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="nf"&gt;jwtVerify&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;SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid token&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;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&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="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="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;&lt;strong&gt;The problem:&lt;/strong&gt; this pattern assumes middleware is the only security layer. If you ever call a route handler directly and internally — Server Action, server-side &lt;code&gt;fetch&lt;/code&gt;, another route handler — middleware doesn't intervene. That silent bypass is the security vector that costs the most to discover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real cost:&lt;/strong&gt; middleware as the sole gatekeeper works if every single access path goes through the same door. In App Router, with Server Actions and server-side calls, that assumption doesn't always hold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My rule:&lt;/strong&gt; middleware protects the perimeter. Route handlers validate their own authorization. Both layers need to exist — it's not one or the other. If that sounds redundant, it's the kind of redundancy worth having.&lt;/p&gt;




&lt;h3&gt;
  
  
  Pattern 4 — Middleware composition
&lt;/h3&gt;

&lt;p&gt;Next.js 16 doesn't have native nested middleware — there's one single &lt;code&gt;middleware.ts&lt;/code&gt; file. To compose logic, the common pattern is manually chaining functions.&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;// middleware.ts — manual composition&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;NextResponse&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&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;NextRequest&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Each function returns NextResponse or null (to continue the chain)&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MiddlewareFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;NextResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&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;NextResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Checks that a session exists&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;session&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;req&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="c1"&gt;// continue&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Blocks admin routes for non-admins&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withAdminGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="c1"&gt;// withSession already handled this&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/403&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Composition function&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;fns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MiddlewareFn&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;NextResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;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;for &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;fn&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;fns&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="c1"&gt;// short-circuit on first result&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withAdminGuard&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/dashboard/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin/:path*&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;&lt;strong&gt;The tradeoff with this pattern:&lt;/strong&gt; it's clean and scalable, but it has a maintenance cost. Each function in the pipeline decodes the token independently — if you have 4 guards that all read the same cookie, you're parsing the JWT 4 times per request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The concrete optimization:&lt;/strong&gt; parse the token once at the start and pass the payload as context through headers or an augmented request object. But Next.js has no native context mechanism between middleware functions, so the tradeoff is parsing multiple times vs. coupling the parsing to the start of the pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gotchas nobody documents well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Buffer.from&lt;/code&gt; in edge runtime:&lt;/strong&gt; in some edge deployments (Vercel Edge, Cloudflare Workers), &lt;code&gt;Buffer&lt;/code&gt; isn't available globally. If you decode JWTs with &lt;code&gt;Buffer.from(..., 'base64url')&lt;/code&gt;, your middleware can work locally and blow up in production. The portable alternative:&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;// Portable base64url decoding for edge runtime&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decodeJWTPayload&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// atob is available in Web APIs&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&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;&lt;strong&gt;The matcher and static routes:&lt;/strong&gt; middleware runs on &lt;em&gt;every request that matches&lt;/em&gt;, including static assets if the matcher isn't defined carefully. A poorly written matcher can run auth logic on &lt;code&gt;.ico&lt;/code&gt;, &lt;code&gt;.png&lt;/code&gt;, and font files. This isn't a bug — it's a silent CPU cost at the edge.&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;// recommended matcher: explicitly excludes assets&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/((?!_next/static|_next/image|favicon.ico|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.png|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.svg).*)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Race condition with new session cookies:&lt;/strong&gt; if middleware does a redirect at the same time the client is trying to write a new session cookie (e.g. right after login), the redirect can clear the cookie before it's persisted. Reproducible in login flows with an immediate redirect before the cookie is confirmed on the client.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern I'd adopt in a new system
&lt;/h2&gt;

&lt;p&gt;After analyzing all four, the one that best balances security, performance, and maintainability is a hybrid approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Middleware&lt;/strong&gt;: verifies session existence (is there a token? does it look like a JWT?) and redirects if there's nothing there. No full cryptographic verification in middleware if active revocation is involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Components / Route Handlers&lt;/strong&gt;: verify the full token with &lt;code&gt;jose&lt;/code&gt; and check granular permissions if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrictive matcher&lt;/strong&gt;: app routes only, never static assets.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts — the pattern I'd use today&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;NextResponse&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&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;NextRequest&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Public paths that don't require a session&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/register&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/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;function&lt;/span&gt; &lt;span class="nf"&gt;isPublicPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_PATHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasSessionShape&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Shape check only, not cryptographic&lt;/span&gt;
  &lt;span class="c1"&gt;// Real verification happens in the route handler or server component&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;return&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&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="nx"&gt;NextRequest&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;

  &lt;span class="c1"&gt;// Public paths: always let through&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPublicPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;sessionToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

  &lt;span class="c1"&gt;// No token: redirect to login&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;sessionToken&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;hasSessionShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&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;loginUrl&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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;loginUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loginUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Token with valid shape: let through&lt;/span&gt;
  &lt;span class="c1"&gt;// Cryptographic verification and permission checks happen downstream&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/((?!_next/static|_next/image|favicon.ico|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.(png|svg|jpg|ico)).*)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is deliberately conservative: middleware does only what it can do well in edge runtime (checking the existence and shape of the token), and delegates real authorization to layers that have full access to the tools they need.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you can't conclude without your own experiment
&lt;/h2&gt;

&lt;p&gt;I'll be direct about the limits of this analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real latency per pattern&lt;/strong&gt;: I don't have my own public production numbers comparing these 4 patterns in real scenarios. If you want to measure it, instrument with &lt;code&gt;console.time&lt;/code&gt; in local middleware and compare with Edge Functions Logs in Vercel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior on Cloudflare Workers&lt;/strong&gt;: Next.js 16 deployed on Workers can have edge runtime differences compared to Vercel Edge. The official docs cover the guaranteed subset; the rest depends on the provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session cookie race condition across all browsers&lt;/strong&gt;: the new session + immediate redirect race condition is reproducible under specific conditions. It's not universal — it depends on client timing and hosting provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What is backed by official documentation: the edge runtime limitations, unavailable modules, and matcher behavior are all described in &lt;a href="https://nextjs.org/docs/app/building-your-application/routing/middleware" rel="noopener noreferrer"&gt;Next.js Docs — Middleware&lt;/a&gt; and &lt;a href="https://nextjs.org/docs/app/api-reference/edge" rel="noopener noreferrer"&gt;Next.js Docs — Edge Runtime&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ — Common questions about Next.js 16 Middleware and authorization
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use &lt;code&gt;jsonwebtoken&lt;/code&gt; in Next.js 16 middleware?&lt;/strong&gt;&lt;br&gt;
Not reliably. &lt;code&gt;jsonwebtoken&lt;/code&gt; depends on Node.js's &lt;code&gt;crypto&lt;/code&gt; module, which isn't available in edge runtime. The recommended alternative is &lt;code&gt;jose&lt;/code&gt;, which uses Web Crypto API and works at the edge. Always check dependency compatibility against the &lt;a href="https://nextjs.org/docs/app/api-reference/edge" rel="noopener noreferrer"&gt;official Edge Runtime APIs list&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Next.js 16 middleware replace validation in route handlers?&lt;/strong&gt;&lt;br&gt;
No, and thinking it does is a mistake. Middleware protects the external perimeter of the app. Route handlers can be invoked internally (Server Actions, server-side fetch) without going through middleware. If you only protect in middleware, you have a silent bypass on the internal surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When does it make sense to do full cryptographic verification in middleware?&lt;/strong&gt;&lt;br&gt;
When the token has no active revocation and the library is edge runtime compatible (&lt;code&gt;jose&lt;/code&gt;). If you need to query a database to verify whether a token was revoked, that cost on every request scales badly. In that case, verify the shape in middleware and do the real check downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why can redirect loops appear in App Router with middleware?&lt;/strong&gt;&lt;br&gt;
The App Router has its own navigation system with prefetching. A &lt;code&gt;NextResponse.redirect&lt;/code&gt; in middleware can interfere with prefetched requests, creating cycles if the redirect condition also gets evaluated at the destination. The practical rule: use &lt;code&gt;redirect&lt;/code&gt; only for "no session", and &lt;code&gt;rewrite&lt;/code&gt; or headers to communicate state to the rest of the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the matcher affect performance even if middleware does nothing?&lt;/strong&gt;&lt;br&gt;
Yes. Every request that matches executes the middleware, even if it immediately does &lt;code&gt;NextResponse.next()&lt;/code&gt;. A too-broad matcher that includes static assets adds unnecessary overhead. The negative regex exclusion pattern (&lt;code&gt;(?!_next/static|...)&lt;/code&gt;) is the correct way to limit scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it make sense to compose middlewares in Next.js 16 without native support?&lt;/strong&gt;&lt;br&gt;
It makes sense if the project grows in auth complexity (multiple roles, multiple protected paths). The cost is parsing the JWT in each pipeline function. The optimization is parsing once at the start and passing the result as an internal header. If the project is simple, a well-commented monolithic middleware is more maintainable than a chain of functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The middleware is not your primary authorization layer
&lt;/h2&gt;

&lt;p&gt;My position is uncomfortable for anyone who learned Next.js from "protect your app in 10 minutes" tutorials: middleware is excellent for doing the cheapest check of all — does this look like a token? — and redirecting fast when there's nothing there. It's a presence guard, not an auditor.&lt;/p&gt;

&lt;p&gt;Real authorization — permissions, roles, revocation, access to specific resources — belongs in layers that have full access to the tools you actually need: Server Components, Route Handlers, Server Actions. Those layers run on full Node.js, have database access, and can use any library.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is that this split requires you to write validation in two places. But the alternative — putting all the logic in middleware and trusting that edge runtime has everything you need — is the recipe for every problem I described above.&lt;/p&gt;

&lt;p&gt;If you're working with TypeScript strict mode in the same project, the post on &lt;a href="https://juanchi.dev/en/blog/typescript-strict-mode-tsconfig-options-production" rel="noopener noreferrer"&gt;the tsconfig options that impact production the most&lt;/a&gt; has complementary context. And if you're thinking about App Router caching alongside auth, the &lt;a href="https://juanchi.dev/en/blog/nextjs-app-router-caching-revalidate-dynamic-no-store" rel="noopener noreferrer"&gt;Next.js App Router caching&lt;/a&gt; post covers the interactions you need to understand before mixing the two.&lt;/p&gt;

&lt;p&gt;The concrete next step: open your own &lt;code&gt;middleware.ts&lt;/code&gt;, look at what it's actually doing, and ask yourself whether each operation belongs in edge or in Node.js. The answer to that question defines how well the system scales when traffic grows.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sources:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://nextjs.org/docs/app/building-your-application/routing/middleware" rel="noopener noreferrer"&gt;Next.js Docs — Middleware&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/edge" rel="noopener noreferrer"&gt;Next.js Docs — Edge Runtime&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/nextjs-16-middleware-authorization-patterns-race-conditions" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>approuter</category>
    </item>
    <item>
      <title>Next.js 16 Middleware: patrones de autorización que escalan y los que generan race conditions</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Thu, 04 Jun 2026 12:02:04 +0000</pubDate>
      <link>https://dev.to/jtorchia/nextjs-16-middleware-patrones-de-autorizacion-que-escalan-y-los-que-generan-race-conditions-bag</link>
      <guid>https://dev.to/jtorchia/nextjs-16-middleware-patrones-de-autorizacion-que-escalan-y-los-que-generan-race-conditions-bag</guid>
      <description>&lt;h1&gt;
  
  
  Next.js 16 Middleware: patrones de autorización que escalan y los que generan race conditions
&lt;/h1&gt;

&lt;p&gt;El middleware de Next.js es básicamente como el portero de un boliche. No decide si sos bienvenido adentro — eso lo hace el staff interno. Pero sí decide si te deja pasar la puerta. Y si el portero empieza a revisar el historial completo de cada persona antes de abrir, la fila llega hasta la otra cuadra.&lt;/p&gt;

&lt;p&gt;Ese es exactamente el problema con los patrones de autorización en Next.js 16 Middleware. La mayoría de los ejemplos que circulan online asumen que podés hacer validación completa de tokens en el edge. La realidad es más incómoda: el edge runtime tiene restricciones concretas, y varios patterns que funcionaban en v14 explotan en producción de maneras que no son obvias.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi tesis:&lt;/strong&gt; el middleware de Next.js 16 es potente, pero su fortaleza está en verificar &lt;em&gt;sesión&lt;/em&gt;, no en validar &lt;em&gt;token completo&lt;/em&gt;. Cuando confundís los dos roles, generás race conditions o latencia que no entendés hasta que estás mirando logs a las 11 de la noche.&lt;/p&gt;




&lt;h2&gt;
  
  
  El problema real: edge runtime no es Node.js
&lt;/h2&gt;

&lt;p&gt;Antes de ver cada patrón, hay un hecho que define todo lo que sigue: el middleware de Next.js corre en &lt;a href="https://nextjs.org/docs/app/api-reference/edge" rel="noopener noreferrer"&gt;edge runtime&lt;/a&gt;, no en Node.js completo. Eso no es un detalle menor — es la razón por la que ciertos patterns fallan.&lt;/p&gt;

&lt;p&gt;El edge runtime tiene acceso a APIs web estándar (&lt;code&gt;Request&lt;/code&gt;, &lt;code&gt;Response&lt;/code&gt;, &lt;code&gt;Headers&lt;/code&gt;, &lt;code&gt;crypto.subtle&lt;/code&gt;) pero &lt;strong&gt;no tiene acceso a&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fs&lt;/code&gt; — nada de leer archivos&lt;/li&gt;
&lt;li&gt;módulos nativos de Node.js&lt;/li&gt;
&lt;li&gt;librerías que dependan de buffers de Node o APIs de sistema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo que esto significa para auth es concreto: si tu librería de JWT usa &lt;code&gt;jsonwebtoken&lt;/code&gt; con &lt;code&gt;crypto&lt;/code&gt; de Node, no va a funcionar en middleware. Necesitás usar &lt;code&gt;jose&lt;/code&gt; u otra librería compatible con Web Crypto 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;// ❌ Esto explota en edge runtime&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsonwebtoken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// depende de crypto de Node&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Esto funciona en edge runtime&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;jwtVerify&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="s1"&gt;jose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// Web Crypto API compatible&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La documentación oficial de &lt;a href="https://nextjs.org/docs/app/building-your-application/routing/middleware" rel="noopener noreferrer"&gt;Next.js Middleware&lt;/a&gt; lo menciona, pero entre tanto ejemplo de código uno tiende a saltear esa parte hasta que el error aparece en el deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Los 4 patrones: análisis de tradeoffs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Patrón 1 — Validación de token completo en middleware
&lt;/h3&gt;

&lt;p&gt;El más tentador y el más problemático.&lt;/p&gt;

&lt;p&gt;La idea: agarrás el token de la cookie o el header &lt;code&gt;Authorization&lt;/code&gt;, lo verificás criptográficamente en el middleware y decidís si el usuario pasa.&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;// middleware.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;jwtVerify&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="s1"&gt;jose&lt;/span&gt;&lt;span class="dl"&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;NextResponse&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&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;NextRequest&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECRET&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&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;middleware&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="nx"&gt;NextRequest&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;token&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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="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="c1"&gt;// Verificación criptográfica completa en cada request&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;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jwtVerify&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;SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Pasamos el userId al header para usarlo downstream&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-user-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/dashboard/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/protected/:path*&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;&lt;strong&gt;El tradeoff honesto:&lt;/strong&gt; &lt;code&gt;jwtVerify&lt;/code&gt; con &lt;code&gt;jose&lt;/code&gt; sí corre en edge runtime. La verificación criptográfica en sí es rápida. El problema aparece cuando el token tiene expiración corta y necesitás también consultar una lista de revocación, o cuando querés verificar permisos granulares que están en base de datos. Ahí estás en problemas, porque hacer un fetch a base de datos desde middleware en cada request es latencia que se acumula.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuándo funciona bien:&lt;/strong&gt; tokens de vida larga, sin revocación activa, donde solo necesitás saber si el token es válido estructuralmente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuándo explota:&lt;/strong&gt; si tu sistema revoca tokens (logout real, cambio de contraseña), este patrón no lo refleja hasta que el token expira.&lt;/p&gt;




&lt;h3&gt;
  
  
  Patrón 2 — Role-based redirects en middleware
&lt;/h3&gt;

&lt;p&gt;Este patrón parece simple pero esconde una race condition específica de Next.js App Router.&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;// middleware.ts — patrón de redirects por rol&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;middleware&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="nx"&gt;NextRequest&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;sessionCookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;sessionCookie&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Decodificamos sin verificar — solo para leer el rol del payload&lt;/span&gt;
  &lt;span class="c1"&gt;// ⚠️ IMPORTANTE: esto NO es verificación de seguridad&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionCookie&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;3&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;

  &lt;span class="c1"&gt;// Redireccionamos según rol&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;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/403&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;url&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;&lt;strong&gt;El problema de race condition:&lt;/strong&gt; si usás &lt;code&gt;NextResponse.redirect&lt;/code&gt; en middleware al mismo tiempo que el cliente tiene un Server Component haciendo fetch desde &lt;code&gt;layout.tsx&lt;/code&gt;, podés terminar con dos requests en vuelo apuntando a destinos distintos. El App Router tiene su propio mecanismo de navegación y el redirect del middleware interrumpe el ciclo de hidratación de manera que no siempre es predecible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El síntoma:&lt;/strong&gt; el usuario ve un flash de contenido antes del redirect, o queda en un loop de redirect en ciertas rutas. Reproducible cuando el matcher cubre rutas con layouts anidados que hacen fetch propio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La corrección:&lt;/strong&gt; usá &lt;code&gt;NextResponse.rewrite&lt;/code&gt; en lugar de &lt;code&gt;redirect&lt;/code&gt; para rutas de API o internas, y reservá &lt;code&gt;redirect&lt;/code&gt; solo para el caso de "no hay sesión en absoluto". Para permisos granulares dentro de una sesión válida, delegá la decisión al Server Component o al Route Handler — que sí tienen acceso completo a base de datos.&lt;/p&gt;




&lt;h3&gt;
  
  
  Patrón 3 — API route protection solo en middleware
&lt;/h3&gt;

&lt;p&gt;Este es el patrón que más veo recomendado en tutoriales y el que tiene el costo oculto más caro.&lt;/p&gt;

&lt;p&gt;La idea es usar el &lt;code&gt;matcher&lt;/code&gt; para proteger todas las rutas &lt;code&gt;/api/&lt;/code&gt; desde middleware y no validar nada dentro del route handler.&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;// middleware.ts — protección de API desde middleware únicamente&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/api/:path*&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="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;middleware&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="nx"&gt;NextRequest&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="s1"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;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;token&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No autorizado&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;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&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="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Verificamos token y pasamos&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="nf"&gt;jwtVerify&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;SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token inválido&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;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&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="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="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;&lt;strong&gt;El problema:&lt;/strong&gt; este patrón asume que middleware es la única capa de seguridad. Si alguna vez llamás directamente a un route handler internamente (Server Action, &lt;code&gt;fetch&lt;/code&gt; server-side, otro route handler), el middleware no interviene. Ese bypass silencioso es el vector de seguridad que más cuesta descubrir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El costo real:&lt;/strong&gt; middleware como único guardián funciona si toda la superficie de acceso pasa por la misma puerta. En App Router, con Server Actions y llamadas server-side, esa suposición no siempre se cumple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi criterio:&lt;/strong&gt; middleware protege el perímetro. Los route handlers validan autorización propia. Las dos capas tienen que existir, no es una o la otra. Si esto te parece redundante, es redundancia que vale la pena.&lt;/p&gt;




&lt;h3&gt;
  
  
  Patrón 4 — Composición de middlewares
&lt;/h3&gt;

&lt;p&gt;Next.js 16 no tiene middleware anidado nativo — hay un solo archivo &lt;code&gt;middleware.ts&lt;/code&gt;. Para componer lógica, el patrón común es encadenar funciones manualmente.&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;// middleware.ts — composición manual&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;NextResponse&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&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;NextRequest&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Cada función devuelve NextResponse o null (para continuar la cadena)&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MiddlewareFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;NextResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&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;NextResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Verifica que exista sesión&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;session&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;req&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="c1"&gt;// continúa&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Bloquea rutas de admin para no-admins&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withAdminGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="c1"&gt;// withSession ya manejó esto&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/403&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Función de composición&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;fns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MiddlewareFn&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;NextResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;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;for &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;fn&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;fns&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="c1"&gt;// cortocircuito al primer resultado&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withAdminGuard&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/dashboard/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin/:path*&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;&lt;strong&gt;El tradeoff de este patrón:&lt;/strong&gt; es limpio y escalable, pero tiene un costo de mantenimiento. Cada función del pipeline decodifica el token de manera independiente — si tenés 4 guards que todos leen el mismo cookie, estás parseando el JWT 4 veces por request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La optimización concreta:&lt;/strong&gt; parsear el token una sola vez al principio y pasar el payload como contexto a través de headers o de un objeto de request aumentado. Pero Next.js no tiene un mecanismo de contexto nativo entre funciones de middleware, así que el tradeoff es parsear múltiples veces vs. acoplar el parsing al inicio del pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Los gotchas que nadie documenta bien
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Buffer.from&lt;/code&gt; en edge runtime:&lt;/strong&gt; en algunos deployments de edge (Vercel Edge, Cloudflare Workers), &lt;code&gt;Buffer&lt;/code&gt; no está disponible globalmente. Si decodificás JWT con &lt;code&gt;Buffer.from(..., 'base64url')&lt;/code&gt;, tu middleware puede funcionar local y explotar en producción. La alternativa portable:&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;// Decodificación de base64url portable para edge runtime&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decodeJWTPayload&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// atob está disponible en Web APIs&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&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;&lt;strong&gt;El matcher y las rutas estáticas:&lt;/strong&gt; el middleware corre en &lt;em&gt;cada request que matchea&lt;/em&gt;, incluyendo assets estáticos si el matcher no está bien definido. Un matcher mal escrito puede ejecutar lógica de auth en archivos &lt;code&gt;.ico&lt;/code&gt;, &lt;code&gt;.png&lt;/code&gt; y fuentes. Esto no es un bug, es un costo silencioso de CPU en edge.&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;// matcher recomendado: excluye assets explícitamente&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/((?!_next/static|_next/image|favicon.ico|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.png|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.svg).*)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Race condition con cookies de sesión nueva:&lt;/strong&gt; si el middleware hace redirect y al mismo tiempo el cliente intenta escribir una cookie de sesión nueva (ej: después de login), el redirect puede limpiar la cookie antes de que se persista. Reproducible en flows de login con redirect inmediato sin esperar a que la cookie se confirme en el cliente.&lt;/p&gt;




&lt;h2&gt;
  
  
  El patrón que adoptaría en un sistema nuevo
&lt;/h2&gt;

&lt;p&gt;Después de analizar los cuatro, el que mejor balancea seguridad, performance y mantenibilidad es una versión híbrida:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Middleware&lt;/strong&gt;: verifica existencia de sesión (¿hay token? ¿tiene forma de JWT?) y redirige si no hay nada. Sin verificación criptográfica completa en el middleware si hay revocación activa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Components / Route Handlers&lt;/strong&gt;: verifican el token completo con &lt;code&gt;jose&lt;/code&gt; y consultan permisos granulares si los necesitan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matcher restrictivo&lt;/strong&gt;: solo rutas de app, nunca assets estáticos.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts — el pattern que adoptaría hoy&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;NextResponse&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&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;NextRequest&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="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Rutas públicas que no necesitan sesión&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/register&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/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;function&lt;/span&gt; &lt;span class="nf"&gt;isPublicPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_PATHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasSessionShape&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Verificación de forma solamente, no criptográfica&lt;/span&gt;
  &lt;span class="c1"&gt;// La verificación real va en el route handler o server component&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;return&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&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="nx"&gt;NextRequest&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;

  &lt;span class="c1"&gt;// Rutas públicas: siempre pasan&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPublicPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;sessionToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

  &lt;span class="c1"&gt;// Sin token: redirigir a login&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;sessionToken&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;hasSessionShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&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;loginUrl&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;request&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;loginUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loginUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Con token con forma válida: dejamos pasar&lt;/span&gt;
  &lt;span class="c1"&gt;// La verificación criptográfica y de permisos va downstream&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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="s1"&gt;/((?!_next/static|_next/image|favicon.ico|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.(png|svg|jpg|ico)).*)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este patrón es deliberadamente conservador: el middleware hace solo lo que puede hacer bien en edge runtime (verificar existencia y forma del token), y delega la autorización real a capas que tienen acceso completo a las herramientas necesarias.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que no podés concluir sin experimento propio
&lt;/h2&gt;

&lt;p&gt;Seré directo sobre los límites de este análisis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latencia real por patrón&lt;/strong&gt;: no tengo números públicos propios de producción para comparar los 4 patrones en escenarios reales. Si querés medirlo, instrumentá con &lt;code&gt;console.time&lt;/code&gt; en middleware local y compará con Edge Functions Logs en Vercel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comportamiento en Cloudflare Workers&lt;/strong&gt;: Next.js 16 deployado en Workers puede tener diferencias de edge runtime respecto a Vercel Edge. La documentación oficial cubre el subset garantizado; el resto depende del provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race condition de cookies en todos los browsers&lt;/strong&gt;: la race condition de sesión nueva + redirect inmediato es reproducible en condiciones específicas. No es universal — depende del timing del cliente y del provider de hosting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo que sí está respaldado por documentación oficial: las limitaciones del edge runtime, los módulos no disponibles y el comportamiento del matcher son descritos en &lt;a href="https://nextjs.org/docs/app/building-your-application/routing/middleware" rel="noopener noreferrer"&gt;Next.js Docs — Middleware&lt;/a&gt; y &lt;a href="https://nextjs.org/docs/app/api-reference/edge" rel="noopener noreferrer"&gt;Next.js Docs — Edge Runtime&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ — Preguntas frecuentes sobre Next.js 16 Middleware y autorización
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo usar &lt;code&gt;jsonwebtoken&lt;/code&gt; en el middleware de Next.js 16?&lt;/strong&gt;&lt;br&gt;
No de manera confiable. &lt;code&gt;jsonwebtoken&lt;/code&gt; depende del módulo &lt;code&gt;crypto&lt;/code&gt; de Node.js, que no está disponible en edge runtime. La alternativa recomendada es &lt;code&gt;jose&lt;/code&gt;, que usa Web Crypto API y funciona en edge. Verificá siempre la compatibilidad de dependencias con el &lt;a href="https://nextjs.org/docs/app/api-reference/edge" rel="noopener noreferrer"&gt;listado oficial de Edge Runtime APIs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El middleware de Next.js 16 reemplaza la validación en los route handlers?&lt;/strong&gt;&lt;br&gt;
No, y es un error pensarlo así. El middleware protege el perímetro externo de la app. Los route handlers pueden ser invocados internamente (Server Actions, fetch server-side) sin pasar por middleware. Si solo protegés en middleware, tenés un bypass silencioso en el surface interno.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuándo tiene sentido hacer verificación criptográfica completa en middleware?&lt;/strong&gt;&lt;br&gt;
Cuando el token no tiene revocación activa y la librería es compatible con edge runtime (&lt;code&gt;jose&lt;/code&gt;). Si necesitás consultar base de datos para verificar si el token fue revocado, ese costo en cada request escala mal. En ese caso, verificá la forma en middleware y hacé la consulta real downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué pueden aparecer loops de redirect en App Router con middleware?&lt;/strong&gt;&lt;br&gt;
El App Router tiene su propio sistema de navegación con prefetching. Un &lt;code&gt;NextResponse.redirect&lt;/code&gt; en middleware puede interferir con requests prefetcheados, generando ciclos si la condición de redirect se evalúa también en el destino. La regla práctica: usá &lt;code&gt;redirect&lt;/code&gt; solo para "no hay sesión", y &lt;code&gt;rewrite&lt;/code&gt; o headers para comunicar estado al resto del sistema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El matcher afecta la performance aunque el middleware no haga nada?&lt;/strong&gt;&lt;br&gt;
Sí. Cada request que matchea ejecuta el middleware, aunque sea para hacer &lt;code&gt;NextResponse.next()&lt;/code&gt; inmediatamente. Un matcher demasiado amplio que incluye assets estáticos suma overhead innecesario. El patrón de exclusión con regex negativo (&lt;code&gt;(?!_next/static|...)&lt;/code&gt;) es la forma correcta de limitar el scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tiene sentido componer middlewares en Next.js 16 si no hay soporte nativo?&lt;/strong&gt;&lt;br&gt;
Tiene sentido si el proyecto crece en complejidad de auth (múltiples roles, múltiples paths protegidos). El costo es parsear el JWT en cada función del pipeline. La optimización es parsear una sola vez al inicio y pasar el resultado como header interno. Si el proyecto es simple, un middleware monolítico bien comentado es más mantenible que una cadena de funciones.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión: el middleware no es tu capa de autorización principal
&lt;/h2&gt;

&lt;p&gt;Mi postura es incómoda para quienes aprendieron Next.js con tutoriales de "protegé tu app en 10 minutos": el middleware es excelente para hacer el check más barato de todos — ¿hay algo que parece un token? — y redirigir rápido cuando no hay nada. Es un guardia de presencia, no un auditor.&lt;/p&gt;

&lt;p&gt;La autorización real — permisos, roles, revocación, acceso a recursos específicos — pertenece a capas que tienen acceso completo a las herramientas que necesitás: Server Components, Route Handlers, Server Actions. Esas capas corren en Node.js completo, tienen acceso a base de datos y pueden usar cualquier librería.&lt;/p&gt;

&lt;p&gt;Lo incómodo es que este split requiere que escribas validación en dos lugares. Pero la alternativa — poner toda la lógica en middleware y confiar en que el edge runtime tiene todo lo que necesitás — es la receta para los problemas que describí arriba.&lt;/p&gt;

&lt;p&gt;Si trabajás con TypeScript strict en el mismo proyecto, el post sobre &lt;a href="https://juanchi.dev/es/blog/typescript-strict-mode-tsconfig-opciones-produccion" rel="noopener noreferrer"&gt;las opciones de tsconfig que más impactan en producción&lt;/a&gt; tiene contexto complementario. Y si estás pensando en caching de App Router junto con auth, el post de &lt;a href="https://juanchi.dev/es/blog/nextjs-app-router-caching-revalidate-dynamic-no-store" rel="noopener noreferrer"&gt;Next.js App Router caching&lt;/a&gt; tiene las interacciones que hay que entender antes de mezclar las dos cosas.&lt;/p&gt;

&lt;p&gt;El próximo paso concreto: abrí el &lt;code&gt;middleware.ts&lt;/code&gt; propio, fijate qué está haciendo realmente y preguntate si cada operación pertenece a edge o a Node.js. La respuesta a esa pregunta define qué tan bien escala el sistema cuando el tráfico crece.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fuentes:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://nextjs.org/docs/app/building-your-application/routing/middleware" rel="noopener noreferrer"&gt;Next.js Docs — Middleware&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/edge" rel="noopener noreferrer"&gt;Next.js Docs — Edge Runtime&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/nextjs-16-middleware-autorizacion-patrones-race-conditions" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Prisma 5 Prisma 6: The Breaking Changes I Hit in My Real Schema and How I Fixed Them Without Breaking Production</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 03 Jun 2026 12:01:43 +0000</pubDate>
      <link>https://dev.to/jtorchia/prisma-5-prisma-6-the-breaking-changes-i-hit-in-my-real-schema-and-how-i-fixed-them-without-5ed6</link>
      <guid>https://dev.to/jtorchia/prisma-5-prisma-6-the-breaking-changes-i-hit-in-my-real-schema-and-how-i-fixed-them-without-5ed6</guid>
      <description>&lt;h1&gt;
  
  
  Prisma 5 → Prisma 6: The Breaking Changes I Hit in My Real Schema and How I Fixed Them Without Breaking Production
&lt;/h1&gt;

&lt;p&gt;The correct approach to migrating from Prisma 5 to Prisma 6 without breaking anything is &lt;strong&gt;don't run the upgrade on a Friday&lt;/strong&gt;. I know that sounds obvious. But that's not actually the point — the real point is this: Prisma 6 has changes that TypeScript's compiler is not going to yell at you about. They'll pass through silently, and you'll find out at runtime — or worse, from a query result that looks correct but isn't.&lt;/p&gt;

&lt;p&gt;My thesis: Prisma 6 is a genuine improvement in ergonomics and performance, but there are three behavior changes that require manual attention before you upgrade. They're not bugs — they're deliberate decisions by the Prisma team that change how relational queries, the generated client, and transactions behave. If you don't know about them upfront, they'll find you.&lt;/p&gt;

&lt;p&gt;What follows is my analysis of those three changes, with representative code and the checklist I built so I don't have to repeat the experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Prisma 6 Matters (and What the Official Announcement Actually Says)
&lt;/h2&gt;

&lt;p&gt;The official announcement — &lt;a href="https://www.prisma.io/blog/prisma-6-better-performance-more-flexibility-and-type-safe-sql" rel="noopener noreferrer"&gt;"What's new in Prisma 6"&lt;/a&gt; — has three main pillars:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Better performance&lt;/strong&gt; — rewritten internals, more efficient query engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More flexibility&lt;/strong&gt; — improved support for multiple providers and client configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type-safe SQL&lt;/strong&gt; — the new &lt;code&gt;prisma.$queryRawTyped&lt;/code&gt; API with real type inference.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of that is real and welcome. What the announcement &lt;strong&gt;doesn't emphasize enough&lt;/strong&gt; — and what costs you time when you upgrade without reading the full migration guide — are the behaviors that changed silently.&lt;/p&gt;

&lt;p&gt;I'm going to cover the three that hit hardest in a Next.js 16 + Server Actions + PostgreSQL stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change 1: &lt;code&gt;selectRelationCount&lt;/code&gt; Is No Longer Opt-In — How You Count Relations Changed
&lt;/h2&gt;

&lt;p&gt;In Prisma 5, if you wanted to count relations (say, how many posts a user has) inside a &lt;code&gt;select&lt;/code&gt;, you had to enable the &lt;code&gt;selectRelationCount&lt;/code&gt; preview feature in the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// schema.prisma — Prisma 5
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["selectRelationCount"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Prisma 6, &lt;code&gt;selectRelationCount&lt;/code&gt; went &lt;strong&gt;GA&lt;/strong&gt; and the preview flag was removed. If you leave it in the schema, the Prisma CLI throws a warning — or an outright error depending on the exact version. The functionality still works, but the API changed subtly in how it integrates with &lt;code&gt;include&lt;/code&gt; vs &lt;code&gt;select&lt;/code&gt;.&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;// ✅ Prisma 5 — worked with the preview feature active&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&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;prisma&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="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&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;name&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;_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;posts&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;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Prisma 6 — same syntax, but without the flag in the schema&lt;/span&gt;
&lt;span class="c1"&gt;// If the flag is still there, the CLI emits a warning on generate&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&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;prisma&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="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&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;name&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;_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;posts&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Concrete action:&lt;/strong&gt; grep all &lt;code&gt;previewFeatures&lt;/code&gt; in your schema and verify which ones went GA in v6. The official guide lists them. Remove them before running &lt;code&gt;prisma generate&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change 2: The Behavior of &lt;code&gt;undefined&lt;/code&gt; in Relational Queries Changed
&lt;/h2&gt;

&lt;p&gt;This is the one that hurts the most because there's no compile-time error. In Prisma 5, passing &lt;code&gt;undefined&lt;/code&gt; as a value in a &lt;code&gt;where&lt;/code&gt; was ignored — the filter simply wasn't applied. In Prisma 6, that behavior was &lt;strong&gt;standardized more strictly&lt;/strong&gt;: in some cases &lt;code&gt;undefined&lt;/code&gt; is still ignored, but in others — especially inside nested &lt;code&gt;select&lt;/code&gt;s with optional relations — the behavior differs depending on whether the field is nullable in the schema or not.&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;// ⚠️ Dangerous pattern in the Prisma 5 → 6 transition&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;getPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;categoryFilter&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&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="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// In Prisma 5: if categoryFilter is undefined, this where was ignored&lt;/span&gt;
      &lt;span class="c1"&gt;// In Prisma 6: behavior depends on the field's type in the schema&lt;/span&gt;
      &lt;span class="c1"&gt;// If 'category' is an optional field (String?), it may behave differently&lt;/span&gt;
      &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;categoryFilter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;author&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;p&gt;The fix is explicit and more defensive:&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;// ✅ Safe pattern for both Prisma 5 and 6&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;getPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;categoryFilter&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&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="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Build the where conditionally — don't depend on undefined's behavior&lt;/span&gt;
      &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;categoryFilter&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;categoryFilter&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;author&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;p&gt;The uncomfortable part about this change: &lt;strong&gt;the TypeScript types in the generated client don't change&lt;/strong&gt;. &lt;code&gt;String | undefined&lt;/code&gt; is still a valid type in the &lt;code&gt;where&lt;/code&gt;. The compiler tells you nothing. You have to find it manually or with integration tests.&lt;/p&gt;

&lt;p&gt;My take here: &lt;strong&gt;building &lt;code&gt;where&lt;/code&gt; objects conditionally&lt;/strong&gt; isn't a workaround — it's the correct practice in any version of Prisma. If your codebase has a lot of places where you pass optional variables directly into &lt;code&gt;where&lt;/code&gt;, this is the moment to clean them up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change 3: The Generated Client Was Reorganized and Direct Type Imports Can Break
&lt;/h2&gt;

&lt;p&gt;Prisma 6 reorganized the structure of the generated client. If anywhere in your codebase you're importing types directly from the &lt;code&gt;.prisma/client&lt;/code&gt; folder or from internal package paths (something that shouldn't be done but shows up in old tutorials), those imports can break silently or with cryptic errors.&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;// ❌ Fragile pattern — importing from internal paths of the generated client&lt;/span&gt;
&lt;span class="c1"&gt;// This might have worked in Prisma 5 but it's a private API, not public&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;Prisma&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="s1"&gt;@prisma/client/edge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Always import from the public entry point&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;Prisma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&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="s1"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most common case in Next.js 16 with Server Actions: using the edge client (&lt;code&gt;@prisma/client/edge&lt;/code&gt;) for middleware or routes running in the Edge Runtime. In Prisma 6, the edge client configuration was unified and the way you instantiate it changed. The official docs have the updated details, but the error you'll see if you don't update is generic — something like "cannot find module" or "type is not assignable" that doesn't point directly at the problem.&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;// ✅ Prisma 6 with Next.js 16 — single client instance&lt;/span&gt;
&lt;span class="c1"&gt;// lib/prisma.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;PrismaClient&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="s1"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;globalForPrisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&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;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;globalForPrisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&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;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// log only in development — don't expose query logs in production&lt;/span&gt;
    &lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;globalForPrisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern didn't change between v5 and v6, but if you had it misconfigured (multiple instances, broken singleton), the upgrade is the moment to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Migration Mistakes — The Gotchas That Keep Showing Up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 1: running &lt;code&gt;prisma db push&lt;/code&gt; without reading the full output.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prisma 6 can generate slightly different migrations for the same schema if there are fields with types that changed internally (like some &lt;code&gt;DateTime&lt;/code&gt; types with precision). Review the migration diff before applying it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 2: assuming &lt;code&gt;prisma migrate dev&lt;/code&gt; and &lt;code&gt;prisma migrate deploy&lt;/code&gt; behave the same way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;migrate dev&lt;/code&gt; can do additional things (like resetting the DB on conflicts). In any environment that resembles production, always use &lt;code&gt;migrate deploy&lt;/code&gt; and check state with &lt;code&gt;prisma migrate status&lt;/code&gt; first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check migration state before upgrading&lt;/span&gt;
npx prisma migrate status

&lt;span class="c"&gt;# Generate the client after updating the version&lt;/span&gt;
npx prisma generate

&lt;span class="c"&gt;# Review schema differences without applying anything&lt;/span&gt;
npx prisma migrate diff &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-schema-datasource&lt;/span&gt; prisma/schema.prisma &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to-schema-datamodel&lt;/span&gt; prisma/schema.prisma &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--script&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gotcha 3: not updating &lt;code&gt;devDependencies&lt;/code&gt; alongside &lt;code&gt;@prisma/client&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prisma&lt;/code&gt; (the CLI) and &lt;code&gt;@prisma/client&lt;/code&gt; need to be on the same major version. If you update one and not the other, you'll get client generation errors that are hard to diagnose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Always update both at the same time&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;prisma@6 @prisma/client@6

&lt;span class="c"&gt;# Or with pnpm&lt;/span&gt;
pnpm add prisma@6 @prisma/client@6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gotcha 4: transaction behavior with &lt;code&gt;$transaction&lt;/code&gt; and timeouts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prisma 6 adjusted the default timeouts for interactive transactions. If you have transactions running slow operations, the default timeout may be different. Verify and set it explicitly:&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;// ✅ Explicit timeout — don't depend on the default&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&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;tx&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="c1"&gt;// transaction operations&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxWait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ms — max time waiting to acquire the transaction&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;  &lt;span class="c1"&gt;// ms — max execution time&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;
  
  
  Prisma 5 → 6 Migration Checklist
&lt;/h2&gt;

&lt;p&gt;This is the order I follow to upgrade without surprises. It's not the only path, but it covers the edge cases that come up most often:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before the upgrade:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Review all &lt;code&gt;previewFeatures&lt;/code&gt; in the schema — verify which ones went GA in v6 and remove the corresponding flags&lt;/li&gt;
&lt;li&gt;[ ] Audit all &lt;code&gt;where&lt;/code&gt; clauses that receive optional variables — replace the &lt;code&gt;field: variable | undefined&lt;/code&gt; pattern with explicit conditional construction&lt;/li&gt;
&lt;li&gt;[ ] Search for imports from internal &lt;code&gt;@prisma/client&lt;/code&gt; paths — centralize on the public entry point&lt;/li&gt;
&lt;li&gt;[ ] Verify explicit timeouts on all interactive &lt;code&gt;$transaction&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;[ ] Run &lt;code&gt;prisma migrate status&lt;/code&gt; and make sure there are no pending migrations before upgrading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;During the upgrade:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Update &lt;code&gt;prisma&lt;/code&gt; and &lt;code&gt;@prisma/client&lt;/code&gt; to the same major version simultaneously&lt;/li&gt;
&lt;li&gt;[ ] Run &lt;code&gt;prisma generate&lt;/code&gt; and review the full output — not just that it finishes without error&lt;/li&gt;
&lt;li&gt;[ ] Run the integration test suite (if you have one) pointing at a staging DB, not production&lt;/li&gt;
&lt;li&gt;[ ] If you use Next.js 16 with Edge Runtime, verify the edge client configuration per the Prisma 6 docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After the upgrade:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Monitor query logs in the first few hours — look for slower queries or unexpected results in optional relations&lt;/li&gt;
&lt;li&gt;[ ] Verify that the &lt;code&gt;PrismaClient&lt;/code&gt; singleton is still working correctly in Next.js's lifecycle (hot reload in dev, single instance in prod)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ — Prisma 6 Migration Breaking Changes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Prisma 6 compatible with Prisma 5 without changes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not completely. There are breaking changes documented in the official migration guide. Most are manageable, but they require manual review — especially in schemas with &lt;code&gt;previewFeatures&lt;/code&gt;, relational queries with optional values, and edge client usage. This isn't a patch upgrade; take it seriously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the Prisma schema (schema.prisma) change between v5 and v6?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The schema format didn't change dramatically, but there are &lt;code&gt;previewFeatures&lt;/code&gt; flags that need to be removed because they went GA. If you leave them in, the CLI may emit warnings or errors depending on the exact version. Check the full list in the official announcement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will my queries with &lt;code&gt;include&lt;/code&gt; and optional relations work the same?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Probably yes for simple cases. The risk is in queries that pass &lt;code&gt;undefined&lt;/code&gt; conditionally to optional fields in &lt;code&gt;where&lt;/code&gt;. If you build filters explicitly (without depending on &lt;code&gt;undefined&lt;/code&gt;'s behavior), the risk is low.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Prisma 6 work with Next.js 16 App Router and Server Actions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The Next.js 16 + Server Actions + Prisma 6 + PostgreSQL stack works well. What needs attention is the client instance (global singleton) and the Edge Runtime configuration if you use it. The Prisma patterns with Server Actions I covered in &lt;a href="https://dev.to/blog/prisma-query-logging-postgresql-donde-termina-orm-empieza-base"&gt;the previous post on Server Actions and Prisma&lt;/a&gt; are still valid — just verify the client entry point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I upgrade directly in production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My recommendation is no. The safest flow is: upgrade branch → staging with a DB similar to production → integration test suite → deploy during low-traffic hours. The upgrade itself isn't risky if you follow the checklist, but the prior validation is what saves you from surprises.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;$queryRawTyped&lt;/code&gt; replace &lt;code&gt;$queryRaw&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It doesn't replace it, it complements it. &lt;code&gt;$queryRawTyped&lt;/code&gt; is the new API for type-safe SQL with type inference — a genuine improvement for complex SQL queries that the ORM can't express well. &lt;code&gt;$queryRaw&lt;/code&gt; still works. If you want to explore the new API, the official announcement has the examples; if you already use &lt;a href="https://dev.to/blog/prisma-query-logging-postgresql-donde-termina-orm-empieza-base"&gt;query logging with PostgreSQL&lt;/a&gt; to debug heavy queries, &lt;code&gt;$queryRawTyped&lt;/code&gt; will be a natural ally.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Upgrade Is Worth It, But You Have to Earn It
&lt;/h2&gt;

&lt;p&gt;Prisma 6 is a real step forward — better performance, faster client generation, and type-safe SQL are concrete improvements that you feel in projects with complex schemas. I'm not questioning that.&lt;/p&gt;

&lt;p&gt;What I am saying is that there are &lt;strong&gt;three behaviors that won't scream at you in the compiler&lt;/strong&gt;: cleaning up &lt;code&gt;previewFeatures&lt;/code&gt;, handling &lt;code&gt;undefined&lt;/code&gt; in conditional &lt;code&gt;where&lt;/code&gt; objects, and imports from the generated client. Ignore them, and you find out at runtime.&lt;/p&gt;

&lt;p&gt;The truly uncomfortable part is that none of the three are Prisma bugs — they're reasonable decisions by the team that prioritize correct behavior over silent compatibility. But if you don't read the full migration guide before running &lt;code&gt;npm install prisma@6&lt;/code&gt;, you're the one paying the cost.&lt;/p&gt;

&lt;p&gt;My practical recommendation: before upgrading, run a grep through the codebase for &lt;code&gt;previewFeatures&lt;/code&gt;, for &lt;code&gt;field: variable&lt;/code&gt; patterns in &lt;code&gt;where&lt;/code&gt; objects, and for imports from internal &lt;code&gt;@prisma/client&lt;/code&gt; paths. If all three come back clean, the upgrade will be smooth. If something shows up, you know before you start.&lt;/p&gt;

&lt;p&gt;If you're on the path of query hardening and logging, the post on &lt;a href="https://dev.to/blog/prisma-query-logging-postgresql-donde-termina-orm-empieza-base"&gt;Prisma query logging and PostgreSQL&lt;/a&gt; has useful context for the monitoring side post-upgrade. And if the project uses &lt;a href="https://juanchi.dev/en/blog/typescript-strict-mode-tsconfig-options-production" rel="noopener noreferrer"&gt;TypeScript strict mode&lt;/a&gt;, the &lt;code&gt;strictNullChecks&lt;/code&gt; and &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; options will make exactly the &lt;code&gt;undefined&lt;/code&gt; patterns I described here more visible.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prisma — What's new in Prisma 6: &lt;a href="https://www.prisma.io/blog/prisma-6-better-performance-more-flexibility-and-type-safe-sql" rel="noopener noreferrer"&gt;https://www.prisma.io/blog/prisma-6-better-performance-more-flexibility-and-type-safe-sql&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/prisma-5-to-6-breaking-changes-migration-guide" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>backend</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Prisma 5 Prisma 6: los breaking changes que encontré en mi schema real y cómo los resolví sin romper producción</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 03 Jun 2026 12:01:38 +0000</pubDate>
      <link>https://dev.to/jtorchia/prisma-5-prisma-6-los-breaking-changes-que-encontre-en-mi-schema-real-y-como-los-resolvi-sin-3pl7</link>
      <guid>https://dev.to/jtorchia/prisma-5-prisma-6-los-breaking-changes-que-encontre-en-mi-schema-real-y-como-los-resolvi-sin-3pl7</guid>
      <description>&lt;h1&gt;
  
  
  Prisma 5 → Prisma 6: los breaking changes que encontré en mi schema real y cómo los resolví sin romper producción
&lt;/h1&gt;

&lt;p&gt;La solución correcta para migrar de Prisma 5 a Prisma 6 sin romper nada es &lt;strong&gt;no correr el upgrade el viernes&lt;/strong&gt;. Sé que suena obvio. Pero el punto real es otro: Prisma 6 tiene cambios que el compilador de TypeScript no te va a gritar. Van a pasar silenciosamente, y vas a enterarte en runtime — o peor, en un resultado de query que parece correcto pero no lo es.&lt;/p&gt;

&lt;p&gt;Mi tesis es esta: Prisma 6 es una mejora genuina en ergonomía y performance, pero hay tres cambios de comportamiento que requieren atención manual antes de hacer el upgrade. No son bugs — son decisiones deliberadas del equipo de Prisma que cambian cómo se comportan las queries relacionales, el cliente generado y las transacciones. Si no los conocés de antemano, te van a encontrar ellos a vos.&lt;/p&gt;

&lt;p&gt;Lo que sigue es el análisis de esos tres cambios, con código representativo y el checklist que construí para no repetirlo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Por qué Prisma 6 importa (y qué dice el anuncio oficial)
&lt;/h2&gt;

&lt;p&gt;El anuncio oficial de Prisma — &lt;a href="https://www.prisma.io/blog/prisma-6-better-performance-more-flexibility-and-type-safe-sql" rel="noopener noreferrer"&gt;"What's new in Prisma 6"&lt;/a&gt; — tiene tres ejes centrales:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mejor performance&lt;/strong&gt; — internals reescritos, query engine más eficiente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Más flexibilidad&lt;/strong&gt; — soporte mejorado para múltiples providers y configuración del cliente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL type-safe&lt;/strong&gt; — la nueva API &lt;code&gt;prisma.$queryRawTyped&lt;/code&gt; con inferencia real.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Todo eso es real y bienvenido. Lo que el anuncio &lt;strong&gt;no dice con énfasis suficiente&lt;/strong&gt; — y lo que te cuesta tiempo cuando hacés el upgrade sin leer la guía de migración completa — son los comportamientos que cambiaron silenciosamente.&lt;/p&gt;

&lt;p&gt;Voy a cubrir los tres que más impactan en un stack Next.js 16 + Server Actions + PostgreSQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cambio 1: &lt;code&gt;selectRelationCount&lt;/code&gt; ya no es opt-in — cambió la forma de contar relaciones
&lt;/h2&gt;

&lt;p&gt;En Prisma 5, si querías contar relaciones (por ejemplo, cuántos posts tiene un usuario) dentro de un &lt;code&gt;select&lt;/code&gt;, tenías que habilitar el preview feature &lt;code&gt;selectRelationCount&lt;/code&gt; en el schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// schema.prisma — Prisma 5
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["selectRelationCount"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En Prisma 6, &lt;code&gt;selectRelationCount&lt;/code&gt; pasó a ser &lt;strong&gt;GA&lt;/strong&gt; y la flag de preview fue removida. Si la dejás en el schema, Prisma CLI te lanza un warning — o directamente un error dependiendo de la versión puntual. La funcionalidad sigue andando, pero el API cambió sutilmente en cómo se integra con &lt;code&gt;include&lt;/code&gt; vs &lt;code&gt;select&lt;/code&gt;.&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;// ✅ Prisma 5 — funcionaba con la preview feature activa&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usuarios&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usuario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&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;nombre&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;_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;posts&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;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Prisma 6 — misma sintaxis, pero sin la flag en el schema&lt;/span&gt;
&lt;span class="c1"&gt;// Si la flag sigue presente, el CLI emite warning en generate&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usuarios&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usuario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&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;nombre&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;_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;posts&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Acción concreta:&lt;/strong&gt; buscá todas las &lt;code&gt;previewFeatures&lt;/code&gt; en el schema y verificá cuáles pasaron a GA en v6. La guía oficial lista cuáles ya no son preview. Removalas antes de correr &lt;code&gt;prisma generate&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cambio 2: el comportamiento de &lt;code&gt;undefined&lt;/code&gt; en queries relacionales cambió
&lt;/h2&gt;

&lt;p&gt;Este es el que más duele porque no hay error en tiempo de compilación. En Prisma 5, pasar &lt;code&gt;undefined&lt;/code&gt; como valor en un &lt;code&gt;where&lt;/code&gt; era ignorado — el filtro simplemente no se aplicaba. En Prisma 6, ese comportamiento fue &lt;strong&gt;estandarizado de forma más estricta&lt;/strong&gt;: en algunos casos &lt;code&gt;undefined&lt;/code&gt; sigue siendo ignorado, pero en otros — especialmente dentro de &lt;code&gt;select&lt;/code&gt; anidados con relaciones opcionales — el comportamiento difiere según si el campo es nullable o no en el schema.&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;// ⚠️ Patrón peligroso en la transición Prisma 5 → 6&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;obtenerPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filtroCategoria&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&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="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// En Prisma 5: si filtroCategoria es undefined, este where era ignorado&lt;/span&gt;
      &lt;span class="c1"&gt;// En Prisma 6: el comportamiento depende del tipo del campo en el schema&lt;/span&gt;
      &lt;span class="c1"&gt;// Si 'categoria' es un campo opcional (String?), puede comportarse diferente&lt;/span&gt;
      &lt;span class="na"&gt;categoria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filtroCategoria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;autor&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;p&gt;El fix es explícito y más defensivo:&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;// ✅ Patrón seguro para Prisma 5 y 6&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;obtenerPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filtroCategoria&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&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="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Construí el where condicionalmente — no dependas del comportamiento de undefined&lt;/span&gt;
      &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;filtroCategoria&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;categoria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filtroCategoria&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;autor&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;p&gt;Lo incómodo de este cambio: &lt;strong&gt;el TypeScript types del cliente generado no cambian&lt;/strong&gt;. &lt;code&gt;String | undefined&lt;/code&gt; sigue siendo válido como tipo en el &lt;code&gt;where&lt;/code&gt;. El compilador no te avisa nada. Tenés que buscarlo a mano o con tests de integración.&lt;/p&gt;

&lt;p&gt;Mi punto acá: &lt;strong&gt;construir los objetos &lt;code&gt;where&lt;/code&gt; condicionalmente&lt;/strong&gt; no es un workaround — es la práctica correcta en cualquier versión de Prisma. Si tu codebase tiene muchos lugares donde pasás variables opcionales directo al &lt;code&gt;where&lt;/code&gt;, este es el momento de limpiarlos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cambio 3: el cliente generado cambió y los imports directos de tipos pueden romperse
&lt;/h2&gt;

&lt;p&gt;Prisma 6 reorganizó la estructura del cliente generado. Si en algún lugar de la codebase importás tipos directamente desde la carpeta &lt;code&gt;.prisma/client&lt;/code&gt; o desde rutas internas del paquete (algo que no debería hacerse pero que aparece en tutoriales viejos), esos imports pueden romperse silenciosamente o con errores crípticos.&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;// ❌ Patrón frágil — importar desde rutas internas del cliente generado&lt;/span&gt;
&lt;span class="c1"&gt;// Esto podía funcionar en Prisma 5 pero es una API privada, no pública&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;Prisma&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="s1"&gt;@prisma/client/edge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Importá desde el punto de entrada público siempre&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;Prisma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&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="s1"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El caso más frecuente en Next.js 16 con Server Actions: usar el cliente de edge (&lt;code&gt;@prisma/client/edge&lt;/code&gt;) para middleware o rutas que corren en el Edge Runtime. En Prisma 6, la configuración del cliente de edge fue unificada y la forma de instanciarlo cambió. La documentación oficial tiene el detalle actualizado, pero el error que vas a ver si no lo actualizás es genérico — algo del estilo "cannot find module" o "type is not assignable" que no señala directamente el problema.&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;// ✅ Prisma 6 con Next.js 16 — instancia única del cliente&lt;/span&gt;
&lt;span class="c1"&gt;// lib/prisma.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;PrismaClient&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="s1"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;globalForPrisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&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;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;globalForPrisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&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;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// log solo en desarrollo — no expongas query logs en producción&lt;/span&gt;
    &lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;globalForPrisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este patrón no cambió entre v5 y v6, pero si lo tenías mal configurado (instancias múltiples, singleton roto), el upgrade es el momento de corregirlo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Errores comunes en la migración — los gotchas que más aparecen
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 1: correr &lt;code&gt;prisma db push&lt;/code&gt; sin leer el output completo.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prisma 6 puede generar migraciones ligeramente diferentes para el mismo schema si hay campos con tipos que cambiaron internamente (como algunos tipos de &lt;code&gt;DateTime&lt;/code&gt; con precisión). Revisá el diff de migración antes de aplicarlo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 2: asumir que &lt;code&gt;prisma migrate dev&lt;/code&gt; y &lt;code&gt;prisma migrate deploy&lt;/code&gt; se comportan igual.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;migrate dev&lt;/code&gt; puede hacer cosas adicionales (como resetear la DB en conflictos). En un entorno que se parece a producción, usá siempre &lt;code&gt;migrate deploy&lt;/code&gt; y revisá el estado con &lt;code&gt;prisma migrate status&lt;/code&gt; antes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verificá el estado de migraciones antes del upgrade&lt;/span&gt;
npx prisma migrate status

&lt;span class="c"&gt;# Generá el cliente después de actualizar la versión&lt;/span&gt;
npx prisma generate

&lt;span class="c"&gt;# Revisá las diferencias de schema sin aplicar nada&lt;/span&gt;
npx prisma migrate diff &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-schema-datasource&lt;/span&gt; prisma/schema.prisma &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to-schema-datamodel&lt;/span&gt; prisma/schema.prisma &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--script&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gotcha 3: no actualizar las &lt;code&gt;devDependencies&lt;/code&gt; junto con &lt;code&gt;@prisma/client&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prisma&lt;/code&gt; (el CLI) y &lt;code&gt;@prisma/client&lt;/code&gt; tienen que estar en la misma versión mayor. Si actualizás uno y no el otro, vas a tener errores de generación de cliente que son difíciles de diagnosticar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Actualizá ambos juntos siempre&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;prisma@6 @prisma/client@6

&lt;span class="c"&gt;# O con pnpm&lt;/span&gt;
pnpm add prisma@6 @prisma/client@6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gotcha 4: el comportamiento de transacciones con &lt;code&gt;$transaction&lt;/code&gt; y timeouts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prisma 6 ajustó los defaults de timeout en transacciones interactivas. Si tenés transacciones que corren operaciones lentas, el timeout default puede ser diferente. Verificá y setealo explícitamente:&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;// ✅ Timeout explícito — no dependas del default&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&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;tx&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="c1"&gt;// operaciones de la transacción&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxWait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ms — máximo tiempo esperando adquirir la transacción&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;  &lt;span class="c1"&gt;// ms — máximo tiempo de ejecución&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;
  
  
  Checklist de migración Prisma 5 → 6
&lt;/h2&gt;

&lt;p&gt;Este es el orden que sigo para hacer un upgrade sin sustos. No es el único camino, pero cubre los edge cases que más aparecen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Antes del upgrade:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Revisá todas las &lt;code&gt;previewFeatures&lt;/code&gt; en el schema — verificá cuáles pasaron a GA en v6 y eliminá las flags correspondientes&lt;/li&gt;
&lt;li&gt;[ ] Auditá todos los &lt;code&gt;where&lt;/code&gt; que reciben variables opcionales — reemplazá el patrón &lt;code&gt;campo: variable | undefined&lt;/code&gt; por construcción condicional explícita&lt;/li&gt;
&lt;li&gt;[ ] Buscá imports desde rutas internas de &lt;code&gt;@prisma/client&lt;/code&gt; — centralizá en el punto de entrada público&lt;/li&gt;
&lt;li&gt;[ ] Verificá los timeouts explícitos en todas las &lt;code&gt;$transaction&lt;/code&gt; interactivas&lt;/li&gt;
&lt;li&gt;[ ] Corré &lt;code&gt;prisma migrate status&lt;/code&gt; y asegurate de que no haya migraciones pendientes antes del upgrade&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Durante el upgrade:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Actualizá &lt;code&gt;prisma&lt;/code&gt; y &lt;code&gt;@prisma/client&lt;/code&gt; a la misma versión mayor simultáneamente&lt;/li&gt;
&lt;li&gt;[ ] Corré &lt;code&gt;prisma generate&lt;/code&gt; y revisá el output completo — no solo que termine sin error&lt;/li&gt;
&lt;li&gt;[ ] Corré el test suite de integración (si tenés) apuntando a una DB de staging, no producción&lt;/li&gt;
&lt;li&gt;[ ] Si usás Next.js 16 con Edge Runtime, verificá la configuración del cliente de edge según la doc de Prisma 6&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Después del upgrade:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Monitoreá los query logs en las primeras horas — buscá queries más lentas o resultados inesperados en relaciones opcionales&lt;/li&gt;
&lt;li&gt;[ ] Verificá que el singleton de &lt;code&gt;PrismaClient&lt;/code&gt; siga funcionando correctamente en el ciclo de vida de Next.js (hot reload en dev, instancia única en prod)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ — Prisma 6 migration breaking changes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Prisma 6 es compatible con Prisma 5 sin cambios?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No completamente. Hay breaking changes documentados en la guía oficial. La mayoría son manejables, pero requieren revisión manual — especialmente en schemas con &lt;code&gt;previewFeatures&lt;/code&gt;, queries relacionales con valores opcionales y uso del cliente de edge. No es un upgrade de patch; tomalo en serio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El schema de Prisma (schema.prisma) cambia entre v5 y v6?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El formato del schema no cambió drásticamente, pero sí hay flags de &lt;code&gt;previewFeatures&lt;/code&gt; que deben removerse porque pasaron a GA. Si las dejás, el CLI puede emitir warnings o errores dependiendo de la versión puntual. Revisá la lista completa en el anuncio oficial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Mis queries con &lt;code&gt;include&lt;/code&gt; y relaciones opcionales van a funcionar igual?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Probablemente sí para los casos simples. El riesgo está en queries que pasan &lt;code&gt;undefined&lt;/code&gt; condicionalmente a campos opcionales del &lt;code&gt;where&lt;/code&gt;. Si construís los filtros de forma explícita (sin depender del comportamiento de &lt;code&gt;undefined&lt;/code&gt;), el riesgo es bajo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Prisma 6 funciona con Next.js 16 App Router y Server Actions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sí. El stack Next.js 16 + Server Actions + Prisma 6 + PostgreSQL funciona bien. Lo que requiere atención es la instancia del cliente (singleton global) y la configuración del Edge Runtime si la usás. Los patterns de Prisma con Server Actions que cubrí en &lt;a href="https://dev.to/blog/prisma-query-logging-postgresql-donde-termina-orm-empieza-base"&gt;el post anterior sobre Server Actions y Prisma&lt;/a&gt; siguen siendo válidos — solo verificá el punto de entrada del cliente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo hacer el upgrade en producción directamente?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mi recomendación es no. El flujo más seguro es: rama de upgrade → staging con DB similar a producción → test suite de integración → deploy en horario de bajo tráfico. El upgrade en sí no es riesgoso si seguís el checklist, pero la validación previa es lo que te salva de sorpresas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;$queryRawTyped&lt;/code&gt; reemplaza a &lt;code&gt;$queryRaw&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No lo reemplaza, lo complementa. &lt;code&gt;$queryRawTyped&lt;/code&gt; es la nueva API para SQL type-safe con inferencia de tipos — es una mejora genuina para queries SQL complejas que el ORM no puede expresar bien. &lt;code&gt;$queryRaw&lt;/code&gt; sigue funcionando. Si querés explorar la nueva API, el anuncio oficial tiene los ejemplos; si ya usás &lt;a href="https://dev.to/blog/prisma-query-logging-postgresql-donde-termina-orm-empieza-base"&gt;query logging con PostgreSQL&lt;/a&gt; para debuguear queries pesadas, &lt;code&gt;$queryRawTyped&lt;/code&gt; va a ser tu aliado natural.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión: vale el upgrade, pero hay que ganárselo
&lt;/h2&gt;

&lt;p&gt;Prisma 6 es un paso adelante real — mejor performance, client generation más rápida y SQL type-safe son mejoras concretas que se sienten en proyectos con schemas complejos. No lo estoy cuestionando.&lt;/p&gt;

&lt;p&gt;Lo que sí estoy diciendo es que hay &lt;strong&gt;tres comportamientos que no gritan en el compilador&lt;/strong&gt;: la limpieza de &lt;code&gt;previewFeatures&lt;/code&gt;, el manejo de &lt;code&gt;undefined&lt;/code&gt; en &lt;code&gt;where&lt;/code&gt; condicionales y los imports del cliente generado. Si los ignorás, te enterás en runtime.&lt;/p&gt;

&lt;p&gt;Lo incómodo de verdad es que ninguno de los tres es un bug de Prisma — son decisiones razonables del equipo que priorizan comportamiento correcto sobre compatibilidad silenciosa. Pero si no leés la guía de migración completa antes de correr &lt;code&gt;npm install prisma@6&lt;/code&gt;, el costo lo pagás vos.&lt;/p&gt;

&lt;p&gt;Mi recomendación práctica: antes de hacer el upgrade, corré un grep en la codebase por &lt;code&gt;previewFeatures&lt;/code&gt;, por patrones &lt;code&gt;campo: variable&lt;/code&gt; en objetos &lt;code&gt;where&lt;/code&gt;, y por imports desde rutas internas de &lt;code&gt;@prisma/client&lt;/code&gt;. Si los tres resultados están limpios, el upgrade va a ser tranquilo. Si aparece algo, lo sabés antes de empezar.&lt;/p&gt;

&lt;p&gt;Si estás en el camino de hardening de queries y logging, el post sobre &lt;a href="https://dev.to/blog/prisma-query-logging-postgresql-donde-termina-orm-empieza-base"&gt;Prisma query logging y PostgreSQL&lt;/a&gt; tiene contexto útil para el lado del monitoreo post-upgrade. Y si el proyecto usa &lt;a href="https://juanchi.dev/es/blog/typescript-strict-mode-tsconfig-opciones-produccion" rel="noopener noreferrer"&gt;TypeScript strict mode&lt;/a&gt;, las opciones &lt;code&gt;strictNullChecks&lt;/code&gt; y &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; van a hacer más visibles exactamente los patrones de &lt;code&gt;undefined&lt;/code&gt; que describí acá.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prisma — What's new in Prisma 6: &lt;a href="https://www.prisma.io/blog/prisma-6-better-performance-more-flexibility-and-type-safe-sql" rel="noopener noreferrer"&gt;https://www.prisma.io/blog/prisma-6-better-performance-more-flexibility-and-type-safe-sql&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/prisma-6-migration-breaking-changes" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>tsgo: what changes in the TypeScript compiler rewritten in Go and what it means for real projects</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 02 Jun 2026 14:31:59 +0000</pubDate>
      <link>https://dev.to/jtorchia/tsgo-what-changes-in-the-typescript-compiler-rewritten-in-go-and-what-it-means-for-real-projects-4440</link>
      <guid>https://dev.to/jtorchia/tsgo-what-changes-in-the-typescript-compiler-rewritten-in-go-and-what-it-means-for-real-projects-4440</guid>
      <description>&lt;h1&gt;
  
  
  tsgo: what changes in the TypeScript compiler rewritten in Go and what it means for real projects
&lt;/h1&gt;

&lt;p&gt;I made the mistake of dismissing tsgo as hype before actually reading it. I saw the "10x faster" headline and mentally filed it next to JavaScript framework benchmarks — real numbers in a context that has nothing to do with my work. It only took opening the official repository and the TypeScript team's announcement to understand that this time the story is different. And that it's worth understanding exactly &lt;em&gt;what&lt;/em&gt; changed, &lt;em&gt;what&lt;/em&gt; hasn't yet, and when it actually makes sense to explore the migration.&lt;/p&gt;

&lt;p&gt;My thesis is simple: tsgo is a legitimate technical bet with public evidence behind it, but the beta has documented limitations that most enthusiastic posts quietly skip. The criterion for migrating today isn't whether the number looks attractive — it's whether your CI spends more than 5 minutes on type-checking. If you don't hit that threshold, wait for stable and sleep easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  tsgo typescript compiler go: what it is and where it came from
&lt;/h2&gt;

&lt;p&gt;The official project lives at &lt;a href="https://github.com/microsoft/typescript-go" rel="noopener noreferrer"&gt;github.com/microsoft/typescript-go&lt;/a&gt;. This isn't a fork or a community experiment — it's the TypeScript team itself porting the compiler to native Go, with the declared goal of leveraging real parallelism and eliminating V8 overhead.&lt;/p&gt;

&lt;p&gt;The official announcement on the &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;TypeScript Blog&lt;/a&gt; is clear about the reasoning: JavaScript has a ceiling on compilation speed because it runs on a general-purpose runtime. Go lets you compile to a native binary, manage goroutines for genuine parallelism, and avoid V8's garbage collector pressure. The result according to the team's own measurements: compilations that take tens of seconds with classic TypeScript drop to a few seconds or less.&lt;/p&gt;

&lt;p&gt;The important thing is that tsgo &lt;strong&gt;does not change the type system&lt;/strong&gt;. TypeScript's semantics — the same errors, the same inferences, the same &lt;code&gt;strict&lt;/code&gt; behavior — stay identical. What changes is the speed at which you get to those results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Experimental installation per the official repo&lt;/span&gt;
&lt;span class="c"&gt;# Not on stable npm yet — follow the repo's README&lt;/span&gt;
git clone https://github.com/microsoft/typescript-go
&lt;span class="nb"&gt;cd &lt;/span&gt;typescript-go

&lt;span class="c"&gt;# Build the binary (requires Go installed)&lt;/span&gt;
go build ./cmd/tsgo

&lt;span class="c"&gt;# Run type-check on a project&lt;/span&gt;
./tsgo &lt;span class="nt"&gt;--project&lt;/span&gt; path/to/tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What limitations the beta has today
&lt;/h2&gt;

&lt;p&gt;This is where most posts fall short. The official repository's roadmap explicitly documents what isn't ready in the beta:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Language Service (LSP) incomplete.&lt;/strong&gt; Editor integration — VS Code, Neovim, any LSP client — is in progress but doesn't have parity with &lt;code&gt;tsc&lt;/code&gt;. That means you can't replace the language server that gives you hover types, go-to-definition, and autocomplete today. The CLI type-check is the most mature piece.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build mode and project references.&lt;/strong&gt; Support for &lt;code&gt;tsc --build&lt;/code&gt; with &lt;code&gt;composite: true&lt;/code&gt; and references between packages in a monorepo is partially implemented. If you're using pnpm workspaces with multiple linked &lt;code&gt;tsconfig.json&lt;/code&gt; files, you'll hit edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript plugins.&lt;/strong&gt; The &lt;code&gt;plugins&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt; that many frameworks use internally — Next.js ships its own — don't have guaranteed support yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code transformations.&lt;/strong&gt; tsgo in beta is a type-checker, not a full transpiler. It doesn't replace &lt;code&gt;tsc&lt;/code&gt; when you need to emit &lt;code&gt;.js&lt;/code&gt; from &lt;code&gt;.ts&lt;/code&gt; with transformations. That's still territory for the original compiler or tools like esbuild/swc.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Typical tsconfig.json in a Next.js project with strict mode&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;// What tsgo can type-check today (CLI)&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;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noEmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c1"&gt;// ← "type-check only, no emit" mode — the most mature case in tsgo&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ESNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bundler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"@/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./src/*"&lt;/span&gt;&lt;span class="p"&gt;]&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;span class="p"&gt;}&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;span class="c1"&gt;// What's NOT ready: plugins like Next.js's own, complex composite + references&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have &lt;code&gt;strict: true&lt;/code&gt; enabled — and if you're not sure why you should, I have a &lt;a href="https://juanchi.dev/en/blog/typescript-strict-mode-tsconfig-options-production" rel="noopener noreferrer"&gt;post on the tsconfig options that actually matter in production&lt;/a&gt; — tsgo respects exactly that semantics. The port doesn't relax or change the checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common mistakes when evaluating tsgo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: comparing the number without project context.&lt;/strong&gt; The "10x" comes from benchmarks on large codebases. On a 50-file project where &lt;code&gt;tsc --noEmit&lt;/code&gt; takes 8 seconds, the jump will be noticeable but not dramatic. On a monorepo with 500+ files where type-checking in CI takes 4–6 minutes, the difference concretely changes your pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: assuming it replaces the entire toolchain.&lt;/strong&gt; tsgo in beta is a type-check binary. It doesn't replace esbuild, swc, or &lt;code&gt;next build&lt;/code&gt;. Most modern monorepos already separated type-checking from transpilation — if yours hasn't, this is the moment to do it regardless of tsgo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Separating type-check from build in CI — recommended pattern today&lt;/span&gt;
&lt;span class="c"&gt;# Step 1: type-check (candidate for tsgo when stable)&lt;/span&gt;
pnpm tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt; tsconfig.json

&lt;span class="c"&gt;# Step 2: build/transpilation (keep using tsc or next build, not tsgo yet)&lt;/span&gt;
pnpm next build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mistake 3: ignoring the Language Service.&lt;/strong&gt; Several enthusiastic posts recommend switching VS Code's &lt;code&gt;typescript.tsdk&lt;/code&gt; to point at tsgo. The result today is inconsistent — some features work, others don't. Unless you're deliberately experimenting, don't do it in an environment where you need to actually develop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: losing sight of the fact that plugins matter.&lt;/strong&gt; Next.js 15+ ships its own TypeScript plugin to correctly type Server Component props and &lt;code&gt;generateMetadata&lt;/code&gt; parameters. If tsgo doesn't load it, you lose those checks. That's not a tsgo problem — it's a documented beta limitation you need to track before adopting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix: when to explore tsgo today
&lt;/h2&gt;

&lt;p&gt;Not every tooling decision needs a spreadsheet. This one does need clear criteria because the cost of a premature migration is real: breaking your editor's feedback loop exactly when you need it most.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CI type-check &amp;gt; 5 minutes&lt;/td&gt;
&lt;td&gt;Worth exploring tsgo in a separate job and comparing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI type-check &amp;lt; 2 minutes&lt;/td&gt;
&lt;td&gt;Wait for stable — no urgent gain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo with complex project references&lt;/td&gt;
&lt;td&gt;Wait — documented partial support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js project with its TS plugin&lt;/td&gt;
&lt;td&gt;Wait — plugins not guaranteed in beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pure type-check (&lt;code&gt;--noEmit&lt;/code&gt;) in a separate CI job&lt;/td&gt;
&lt;td&gt;Most mature case to try today&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need Language Server in your editor&lt;/td&gt;
&lt;td&gt;Not yet — LSP incomplete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only scenario where I see immediate value is a CI pipeline where type-checking is the documented bottleneck and you can run tsgo in a parallel job without touching the main build. That way you explore without risk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example parallel job in GitHub Actions to evaluate tsgo&lt;/span&gt;
&lt;span class="c1"&gt;# Without touching the main build&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;typecheck-experimental&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# doesn't block the pipeline if tsgo fails&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Go&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.22'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Clone and build tsgo&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git clone https://github.com/microsoft/typescript-go /tmp/tsgo&lt;/span&gt;
          &lt;span class="s"&gt;cd /tmp/tsgo &amp;amp;&amp;amp; go build ./cmd/tsgo&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Measure type-check time with tsgo&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;time /tmp/tsgo/tsgo --project tsconfig.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Measure type-check time with tsc (for comparison)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;time pnpm tsc --noEmit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach has one concrete advantage: you get real data from &lt;em&gt;your&lt;/em&gt; project, not from Microsoft's benchmark. That difference matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can't conclude without your own experiment
&lt;/h2&gt;

&lt;p&gt;The uncomfortable thing about this topic is that the public evidence backs the speed claim but can't tell you how much &lt;em&gt;your specific pipeline&lt;/em&gt; will improve. It depends on file count, type complexity, whether you're using heavy conditional types, how many &lt;code&gt;paths&lt;/code&gt; your &lt;code&gt;tsconfig&lt;/code&gt; has, and whether you have plugins tsgo won't load.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;can&lt;/em&gt; conclude without experimenting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tsgo &lt;strong&gt;will not change error semantics&lt;/strong&gt; — same type system specification&lt;/li&gt;
&lt;li&gt;tsgo &lt;strong&gt;is not a drop-in replacement today&lt;/strong&gt; — documented limitations in the official repo&lt;/li&gt;
&lt;li&gt;The technical bet of rewriting in Go &lt;strong&gt;has solid justification&lt;/strong&gt; beyond the marketing: native parallelism, binary without V8, different memory management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you need to measure yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real time gains on your specific codebase&lt;/li&gt;
&lt;li&gt;Whether the plugins you use are supported&lt;/li&gt;
&lt;li&gt;Language Service behavior in your editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those three measurements, any claim of "migrate it now" or "it's worthless" is noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: tsgo typescript compiler go
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does tsgo completely replace tsc today?&lt;/strong&gt;&lt;br&gt;
No. In the current beta, tsgo is primarily a CLI type-checker (&lt;code&gt;--noEmit&lt;/code&gt;). It doesn't replace code emission, TypeScript plugins, and doesn't have full Language Service parity for editors. The official repository documents the roadmap with what's still missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is tsgo's type system identical to the original TypeScript?&lt;/strong&gt;&lt;br&gt;
Yes, that's the project's premise. The Go port replicates the same type semantics — the same errors, the same inferences, the same &lt;code&gt;strict&lt;/code&gt; behavior. If you find a difference, it's a bug in the port, not a feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work with Next.js?&lt;/strong&gt;&lt;br&gt;
Partially. Basic type-checking works, but the TypeScript plugin Next.js includes to type Server Components and metadata isn't guaranteed in the beta. For production Next.js projects, wait until plugin support is stable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it worth trying in a monorepo with pnpm workspaces?&lt;/strong&gt;&lt;br&gt;
Depends on the size. If you have project references (&lt;code&gt;composite: true&lt;/code&gt;) between packages, support is partial per the official documentation. If you're simply running &lt;code&gt;tsc --noEmit&lt;/code&gt; on the root, that's the most mature scenario to experiment with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When will it hit stable?&lt;/strong&gt;&lt;br&gt;
The official repository's roadmap has no public date. The signal to watch for: complete LSP, verified plugin support, and documented parity with &lt;code&gt;tsc&lt;/code&gt;. Follow the repo — the milestones are public.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I switch VS Code's &lt;code&gt;typescript.tsdk&lt;/code&gt; to tsgo now?&lt;/strong&gt;&lt;br&gt;
I wouldn't do it in an active development environment. The tsgo Language Service in beta has incomplete features that will break hover types and autocomplete in specific cases. If you want to experiment, do it on a dedicated branch with that explicit purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take and the one concrete next step
&lt;/h2&gt;

&lt;p&gt;tsgo is the most technically interesting move in the TypeScript ecosystem in years. Not because the "10x" number is magic, but because the real bottleneck of the compiler was always the JavaScript runtime — and that limitation now has a serious answer with public evidence behind it.&lt;/p&gt;

&lt;p&gt;What I don't buy is the enthusiasm that ignores the documented limitations. The current beta has a clear scope: CLI type-checking on projects without complex plugins. That's not nothing — for many CI pipelines it's exactly the critical use case — but it's also not the full replacement some posts present it as.&lt;/p&gt;

&lt;p&gt;My practical recommendation: if type-checking is the documented bottleneck in your CI, set up a parallel job with &lt;code&gt;continue-on-error: true&lt;/code&gt;, measure the delta on your real codebase, and make the decision with your own data. If you don't have that problem today, close this tab and revisit it when the team announces stable. There's no urgency.&lt;/p&gt;

&lt;p&gt;The next step for you is exactly one thing: go to the &lt;a href="https://github.com/microsoft/typescript-go" rel="noopener noreferrer"&gt;official repository&lt;/a&gt; and check the open issues for the features you actually use. That's where the real information is — not in the headlines.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript Go — GitHub Repository: &lt;a href="https://github.com/microsoft/typescript-go" rel="noopener noreferrer"&gt;https://github.com/microsoft/typescript-go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript Blog — Announcing TypeScript Go: &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;https://devblogs.microsoft.com/typescript/typescript-native-port/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/tsgo-typescript-compiler-go-real-projects" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>herramientas</category>
    </item>
    <item>
      <title>tsgo: qué cambia en el compilador TypeScript reescrito en Go y qué significa para proyectos reales</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 02 Jun 2026 14:31:53 +0000</pubDate>
      <link>https://dev.to/jtorchia/tsgo-que-cambia-en-el-compilador-typescript-reescrito-en-go-y-que-significa-para-proyectos-reales-2mj7</link>
      <guid>https://dev.to/jtorchia/tsgo-que-cambia-en-el-compilador-typescript-reescrito-en-go-y-que-significa-para-proyectos-reales-2mj7</guid>
      <description>&lt;h1&gt;
  
  
  tsgo: qué cambia en el compilador TypeScript reescrito en Go y qué significa para proyectos reales
&lt;/h1&gt;

&lt;p&gt;Cometí el error de descartar tsgo como hype antes de leerlo. Vi el titular "10x más rápido" y lo puse en la misma carpeta mental que los benchmarks de frameworks JavaScript: números reales en un contexto que nada tiene que ver con lo mío. Me alcanzó con abrir el repositorio oficial y el anuncio del equipo de TypeScript para entender que esta vez la historia es distinta — y que vale la pena entender exactamente &lt;em&gt;qué&lt;/em&gt; cambió, &lt;em&gt;qué&lt;/em&gt; todavía no, y cuándo tiene sentido explorar la migración.&lt;/p&gt;

&lt;p&gt;Mi tesis es simple: tsgo es una apuesta técnica legítima con evidencia pública que la respalda, pero la beta tiene limitaciones documentadas que la mayoría de los posts entusiastas omiten. El criterio para migrarlo hoy no es si el número te parece atractivo — es si tu CI tarda más de 5 minutos en type-check. Si no llegás a ese umbral, esperá la stable y dormí tranquilo.&lt;/p&gt;

&lt;h2&gt;
  
  
  tsgo typescript compiler go: qué es y de dónde viene
&lt;/h2&gt;

&lt;p&gt;El proyecto oficial vive en &lt;a href="https://github.com/microsoft/typescript-go" rel="noopener noreferrer"&gt;github.com/microsoft/typescript-go&lt;/a&gt;. No es un fork ni un experimento de comunidad: es el propio equipo de TypeScript portando el compilador a Go nativo, con el objetivo declarado de aprovechar paralelismo real y eliminar el overhead de V8.&lt;/p&gt;

&lt;p&gt;El anuncio oficial en el &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;TypeScript Blog&lt;/a&gt; es claro sobre el razonamiento: JavaScript tiene un techo en cuanto a velocidad de compilación porque corre sobre un runtime de propósito general. Go permite compilar a binario nativo, gestionar goroutines para paralelismo genuino y evitar la presión del garbage collector de V8. El resultado según las mediciones del propio equipo: compilaciones que en TypeScript clásico toman decenas de segundos bajan a pocos segundos o menos.&lt;/p&gt;

&lt;p&gt;Lo importante es que tsgo &lt;strong&gt;no cambia el sistema de tipos&lt;/strong&gt;. La semántica de TypeScript — los mismos errores, las mismas inferencias, el mismo comportamiento de &lt;code&gt;strict&lt;/code&gt; — sigue siendo idéntica. Lo que cambia es la velocidad con la que llegás a esos resultados.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instalación experimental según el repo oficial&lt;/span&gt;
&lt;span class="c"&gt;# No está en npm stable todavía — seguí el README del repo&lt;/span&gt;
git clone https://github.com/microsoft/typescript-go
&lt;span class="nb"&gt;cd &lt;/span&gt;typescript-go

&lt;span class="c"&gt;# Compilar el binario (requiere Go instalado)&lt;/span&gt;
go build ./cmd/tsgo

&lt;span class="c"&gt;# Correr type-check sobre un proyecto&lt;/span&gt;
./tsgo &lt;span class="nt"&gt;--project&lt;/span&gt; ruta/a/tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué limitaciones tiene la beta hoy
&lt;/h2&gt;

&lt;p&gt;Acá es donde la mayoría de los posts se quedan cortos. El roadmap del repositorio oficial documenta explícitamente qué no está listo en la beta:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Language Service (LSP) incompleto.&lt;/strong&gt; La integración con editores — VS Code, Neovim, cualquier cliente LSP — está en progreso pero no tiene paridad con &lt;code&gt;tsc&lt;/code&gt;. Esto significa que no podés reemplazar hoy el servidor de lenguaje que te da hover types, go-to-definition y autocompletado. El type-check de CLI es lo más maduro.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build mode y project references.&lt;/strong&gt; El soporte para &lt;code&gt;tsc --build&lt;/code&gt; con &lt;code&gt;composite: true&lt;/code&gt; y references entre paquetes de un monorepo está parcialmente implementado. Si usás pnpm workspaces con múltiples &lt;code&gt;tsconfig.json&lt;/code&gt; enlazados, vas a encontrar casos borde.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugins de TypeScript.&lt;/strong&gt; Los &lt;code&gt;plugins&lt;/code&gt; del &lt;code&gt;tsconfig.json&lt;/code&gt; que muchos frameworks usan internamente (Next.js incluye el suyo) no tienen soporte garantizado todavía.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transformaciones de código.&lt;/strong&gt; tsgo en beta es un type-checker, no un transpiler completo. No reemplaza a &lt;code&gt;tsc&lt;/code&gt; cuando necesitás emitir &lt;code&gt;.js&lt;/code&gt; desde &lt;code&gt;.ts&lt;/code&gt; con transformaciones. Eso sigue siendo territorio del compilador original o de herramientas como esbuild/swc.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tsconfig.json típico en un proyecto Next.js con strict mode&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;// Lo que tsgo puede type-checkear hoy (CLI)&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;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noEmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c1"&gt;// ← modo "solo type-check, no emitir" — el caso más maduro en tsgo&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ESNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bundler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"@/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./src/*"&lt;/span&gt;&lt;span class="p"&gt;]&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;span class="p"&gt;}&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;span class="c1"&gt;// Lo que NO está listo: plugins como el de Next.js, composite + references complejos&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si tenés &lt;code&gt;strict: true&lt;/code&gt; activo — y si no sabés por qué deberías, tengo un &lt;a href="https://juanchi.dev/es/blog/typescript-strict-mode-tsconfig-opciones-produccion" rel="noopener noreferrer"&gt;post sobre las opciones del tsconfig que más impactan en producción&lt;/a&gt; — tsgo respeta exactamente esa semántica. El port no relaja ni cambia los checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los errores comunes al evaluar tsgo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Error 1: comparar el número sin contexto de proyecto.&lt;/strong&gt; El "10x" viene de benchmarks con codebases grandes. En un proyecto de 50 archivos donde &lt;code&gt;tsc --noEmit&lt;/code&gt; tarda 8 segundos, el salto va a ser perceptible pero no dramático. En un monorepo con 500+ archivos donde el type-check en CI tarda 4-6 minutos, la diferencia cambia el pipeline de forma concreta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2: asumir que reemplaza toda la cadena de herramientas.&lt;/strong&gt; tsgo en beta es un binario de type-check. No reemplaza esbuild, swc, ni el &lt;code&gt;next build&lt;/code&gt;. La mayoría de los monorepos modernos ya separaron type-check de transpilación — si el tuyo no, este es el momento de hacerlo independientemente de tsgo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Separar type-check de build en CI — patrón recomendado hoy&lt;/span&gt;
&lt;span class="c"&gt;# Paso 1: type-check (candidato para tsgo cuando sea stable)&lt;/span&gt;
pnpm tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt; tsconfig.json

&lt;span class="c"&gt;# Paso 2: build/transpilación (sigue con tsc o next build, no tsgo todavía)&lt;/span&gt;
pnpm next build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Error 3: ignorar el Language Service.&lt;/strong&gt; Varios posts de entusiasmo recomiendan cambiar el &lt;code&gt;typescript.tsdk&lt;/code&gt; de VS Code apuntando a tsgo. El resultado hoy es inconsistente: algunas features funcionan, otras no. A menos que estés experimentando conscientemente, no lo hagas en un entorno donde necesitás desarrollar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 4: perder de vista que los plugins importan.&lt;/strong&gt; Next.js 15+ usa un plugin TypeScript propio para tipear correctamente los props de Server Components y los parámetros de &lt;code&gt;generateMetadata&lt;/code&gt;. Si tsgo no lo carga, vas a perder esos checks. Eso no es un problema de tsgo — es una limitación documentada de la beta que hay que rastrear antes de adoptarlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matriz de decisión: cuándo explorar tsgo hoy
&lt;/h2&gt;

&lt;p&gt;No toda decisión de herramienta necesita una hoja de cálculo. Esta sí necesita criterio claro porque el costo de una migración prematura es real: romper el feedback loop del editor justo cuando más lo necesitás.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situación&lt;/th&gt;
&lt;th&gt;Recomendación&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CI type-check &amp;gt; 5 minutos&lt;/td&gt;
&lt;td&gt;Vale explorar tsgo en un job separado y comparar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI type-check &amp;lt; 2 minutos&lt;/td&gt;
&lt;td&gt;Esperá la stable, no hay ganancia urgente&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo con project references complejas&lt;/td&gt;
&lt;td&gt;Esperá — soporte parcial documentado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proyecto con Next.js y su plugin TS&lt;/td&gt;
&lt;td&gt;Esperá — plugins no garantizados en beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quizás type-check puro (&lt;code&gt;--noEmit&lt;/code&gt;) en CI separado&lt;/td&gt;
&lt;td&gt;Caso más maduro hoy para probar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Necesitás Language Server en editor&lt;/td&gt;
&lt;td&gt;No todavía — LSP incompleto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El único escenario donde veo valor inmediato es un pipeline de CI donde el type-check es el cuello de botella documentado y podés correr tsgo en un job paralelo sin afectar el build principal. Así explorás sin riesgo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ejemplo de job paralelo en GitHub Actions para evaluar tsgo&lt;/span&gt;
&lt;span class="c1"&gt;# Sin tocar el build principal&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;typecheck-experimental&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# no bloquea el pipeline si tsgo falla&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Instalar Go&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.22'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Clonar y compilar tsgo&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git clone https://github.com/microsoft/typescript-go /tmp/tsgo&lt;/span&gt;
          &lt;span class="s"&gt;cd /tmp/tsgo &amp;amp;&amp;amp; go build ./cmd/tsgo&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Medir time-check con tsgo&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;time /tmp/tsgo/tsgo --project tsconfig.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Medir time-check con tsc (para comparar)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;time pnpm tsc --noEmit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este approach tiene una ventaja concreta: obtenés datos reales de &lt;em&gt;tu&lt;/em&gt; proyecto, no del benchmark de Microsoft. Esa diferencia importa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué no podés concluir sin experimento propio
&lt;/h2&gt;

&lt;p&gt;Lo incómodo de este tema es que la evidencia pública respalda el claim de velocidad pero no puede decirte cuánto va a mejorar &lt;em&gt;tu&lt;/em&gt; pipeline específico. Depende de la cantidad de archivos, la complejidad de tipos, si usás conditional types pesados, cuántos &lt;code&gt;paths&lt;/code&gt; tiene el &lt;code&gt;tsconfig&lt;/code&gt;, y si tenés plugins que tsgo no carga.&lt;/p&gt;

&lt;p&gt;Lo que sí podés concluir sin experimento:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tsgo &lt;strong&gt;no va a cambiar la semántica de los errores&lt;/strong&gt; — misma especificación del sistema de tipos&lt;/li&gt;
&lt;li&gt;tsgo &lt;strong&gt;no es un reemplazo drop-in hoy&lt;/strong&gt; — tiene limitaciones documentadas en el repo oficial&lt;/li&gt;
&lt;li&gt;La apuesta técnica de reescribir en Go &lt;strong&gt;tiene justificación sólida&lt;/strong&gt; más allá del marketing: paralelismo nativo, binario sin V8, gestión de memoria diferente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo que necesitás medir vos mismo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ganancia real de tiempo en tu codebase específica&lt;/li&gt;
&lt;li&gt;Si los plugins que usás están soportados&lt;/li&gt;
&lt;li&gt;Comportamiento del Language Service en tu editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sin esas tres mediciones, cualquier claim de "migralo ya" o "no vale nada" es ruido.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: tsgo typescript compiler go
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿tsgo reemplaza completamente a tsc hoy?&lt;/strong&gt;&lt;br&gt;
No. En la beta actual, tsgo es principalmente un type-checker de CLI (&lt;code&gt;--noEmit&lt;/code&gt;). No reemplaza la emisión de código, los plugins de TypeScript ni tiene paridad completa en el Language Service para editores. El repositorio oficial documenta el roadmap con lo que falta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El sistema de tipos de tsgo es idéntico al de TypeScript original?&lt;/strong&gt;&lt;br&gt;
Sí, esa es la premisa del proyecto. El port a Go replica la misma semántica de tipos — los mismos errores, las mismas inferencias, el mismo comportamiento de &lt;code&gt;strict&lt;/code&gt;. Si encontrás una diferencia, es un bug del port, no un feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Funciona con Next.js?&lt;/strong&gt;&lt;br&gt;
Parcialmente. El type-check básico funciona, pero el plugin TypeScript que Next.js incluye para tipar Server Components y metadata no está garantizado en la beta. Para proyectos Next.js en producción, esperá que el soporte de plugins esté estable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Vale la pena probarlo en un monorepo con pnpm workspaces?&lt;/strong&gt;&lt;br&gt;
Depende del tamaño. Si tenés project references (&lt;code&gt;composite: true&lt;/code&gt;) entre paquetes, el soporte es parcial según la documentación oficial. Si simplemente corrés &lt;code&gt;tsc --noEmit&lt;/code&gt; sobre el root, es el escenario más maduro para experimentar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuándo va a estar en stable?&lt;/strong&gt;&lt;br&gt;
El roadmap del repositorio oficial no tiene fecha pública. La señal para esperar es: LSP completo, soporte de plugins verificado y paridad documentada con &lt;code&gt;tsc&lt;/code&gt;. Seguí el repo — los milestones están públicos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cambio el &lt;code&gt;typescript.tsdk&lt;/code&gt; de VS Code a tsgo ya?&lt;/strong&gt;&lt;br&gt;
No lo haría en un entorno de desarrollo activo. El Language Service de tsgo en beta tiene features incompletas que van a romper el hover types y el autocompletado en casos específicos. Si querés experimentar, hacelo en una branch dedicada con ese propósito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mi postura y el próximo paso concreto
&lt;/h2&gt;

&lt;p&gt;tsgo es la movida técnica más interesante del ecosistema TypeScript en años. No porque el número "10x" sea mágico, sino porque el cuello de botella real del compilador siempre fue el runtime de JavaScript — y esa limitación ahora tiene una respuesta seria con evidencia pública.&lt;/p&gt;

&lt;p&gt;Lo que no compro es el entusiasmo que ignora las limitaciones documentadas. La beta actual tiene un scope claro: type-check de CLI en proyectos sin plugins complejos. Eso no es poca cosa — para muchos pipelines de CI es exactamente el caso de uso crítico — pero tampoco es el reemplazo completo que algunos posts presentan.&lt;/p&gt;

&lt;p&gt;Mi recomendación práctica: si el type-check es el cuello de botella documentado en tu CI, armá un job paralelo con &lt;code&gt;continue-on-error: true&lt;/code&gt;, medí el delta en tu codebase real y tomá la decisión con datos propios. Si no tenés ese problema hoy, cerrá esta pestaña y revisalo cuando el equipo anuncie la stable. No hay urgencia.&lt;/p&gt;

&lt;p&gt;El próximo paso para vos es uno solo: entrar al &lt;a href="https://github.com/microsoft/typescript-go" rel="noopener noreferrer"&gt;repositorio oficial&lt;/a&gt; y revisar los issues abiertos de las features que usás. Ahí está la información real, no en los titulares.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuentes originales:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript Go — GitHub Repository: &lt;a href="https://github.com/microsoft/typescript-go" rel="noopener noreferrer"&gt;https://github.com/microsoft/typescript-go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript Blog — Announcing TypeScript Go: &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;https://devblogs.microsoft.com/typescript/typescript-native-port/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/tsgo-typescript-compiler-go-que-cambia-proyectos-reales" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>React 19 use() hook and Suspense: when it replaces useEffect and when it throws you into a worse loop</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 02 Jun 2026 12:01:07 +0000</pubDate>
      <link>https://dev.to/jtorchia/react-19-use-hook-and-suspense-when-it-replaces-useeffect-and-when-it-throws-you-into-a-worse-eei</link>
      <guid>https://dev.to/jtorchia/react-19-use-hook-and-suspense-when-it-replaces-useeffect-and-when-it-throws-you-into-a-worse-eei</guid>
      <description>&lt;h1&gt;
  
  
  React 19 use() hook and Suspense: when it replaces useEffect and when it throws you into a worse loop
&lt;/h1&gt;

&lt;p&gt;You can wrap a Promise in &lt;code&gt;use()&lt;/code&gt; and React handles the loading state by itself. Yeah, you read that right. And yet, 40% of the components I started migrating I ended up reverting. Not because &lt;code&gt;use()&lt;/code&gt; is bad — it's genuinely good — but because Suspense has error semantics that most Twitter examples skip entirely.&lt;/p&gt;

&lt;p&gt;My thesis from the start: &lt;strong&gt;&lt;code&gt;use()&lt;/code&gt; is a real improvement for specific cases, but it doesn't replace &lt;code&gt;useEffect&lt;/code&gt; universally. The line between the two isn't "how much code you save" — it's what happens when the Promise rejects.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What React 19 use hook Suspense actually is and what the official docs really say
&lt;/h2&gt;

&lt;p&gt;According to the &lt;a href="https://react.dev/reference/react/use" rel="noopener noreferrer"&gt;official React documentation&lt;/a&gt;, &lt;code&gt;use()&lt;/code&gt; is a hook that reads the value of a resource: a Promise or a Context. When it receives a Promise, it suspends the component until it resolves and delegates the loading state to the nearest &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What the docs do clarify — and this is worth reading carefully — is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If the Promise rejects, React will throw the rejection reason. You can handle rejection using an Error Boundary."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's where the friction starts. &lt;code&gt;use()&lt;/code&gt; doesn't give you a local &lt;code&gt;error&lt;/code&gt; state. There's no &lt;code&gt;catch&lt;/code&gt; in the component. The error bubbles up to the nearest Error Boundary and unmounts the entire subtree. That might be exactly what you want, or it might be a structural problem depending on how you've organized your boundaries in the tree.&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;// Basic pattern with use() — works great for this&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;use&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="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="c1"&gt;// The Promise comes from outside the component (key: not created inside)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userPromise&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userPromise&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;User&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="c1"&gt;// use() suspends until resolved; if it rejects, bubbles to Error Boundary&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="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPromise&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;&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="si"&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;name&lt;/span&gt;&lt;span class="si"&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;
&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;Page&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="nc"&gt;ErrorBoundary&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="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Error loading profile&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="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="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="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading...&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="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;UserProfile&lt;/span&gt; &lt;span class="na"&gt;userPromise&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;fetchUser&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;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;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;p&gt;This works perfectly. The component is declarative, has no side effects, and Suspense shows the fallback while it resolves. Welcome to React 19.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two cases where use() makes things more complicated, not less
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Case 1: the Promise is created inside the component
&lt;/h3&gt;

&lt;p&gt;This is the most common mistake and the one I've seen most often in blog examples:&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;// ⚠️ THIS CAUSES AN INFINITE SUSPENSE LOOP&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Every render creates a new Promise → use() suspends → React re-renders → new Promise&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="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// ← PROBLEM: new Promise on every render&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="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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;name&lt;/span&gt;&lt;span class="si"&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;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the Promise is created inside the component, every render produces a new instance. &lt;code&gt;use()&lt;/code&gt; suspends it, React re-renders to resolve, creates another Promise… loop. The fix is to hoist the Promise outside the component or memoize it with &lt;code&gt;useMemo&lt;/code&gt;, but at that point you're adding complexity that &lt;code&gt;useEffect&lt;/code&gt; never required.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://react.dev/blog/2024/12/05/react-19" rel="noopener noreferrer"&gt;React 19 documentation&lt;/a&gt; mentions it: Promises must be created outside the component or be stable across renders. It's not a bug — it's part of the hook's contract.&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;// ✅ Correct: stable Promise, created outside the component&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;globalPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// outside the render tree&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&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;user&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;globalPromise&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;&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="si"&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;name&lt;/span&gt;&lt;span class="si"&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;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Case 2: an Error Boundary that catches more than you want
&lt;/h3&gt;

&lt;p&gt;The second case is subtler and more expensive to diagnose. Imagine a layout with multiple independent sections: profile, notifications, and settings. If all three use &lt;code&gt;use()&lt;/code&gt; and share a single Error Boundary, one section failing takes down all three.&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;// Problematic tree: one Error Boundary for everything&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;GeneralError&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;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;Skeleton&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;ProfileWithUse&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;       &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* if this fails, everything goes down */&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;NotificationsWithUse&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;SettingsWithUse&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;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;With &lt;code&gt;useEffect&lt;/code&gt;, each component has its own local &lt;code&gt;error&lt;/code&gt; state and can show an inline message without affecting the others. With &lt;code&gt;use()&lt;/code&gt;, error isolation depends entirely on how many granular Error Boundaries you have in the tree.&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;// ✅ Correct tree to isolate errors with use()&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;ErrorBoundary&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;ProfileError&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;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;ProfileSkeleton&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;ProfileWithUse&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;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;ErrorBoundary&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;NotificationsError&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;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;NotifSkeleton&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;NotificationsWithUse&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;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. But now the cost of migrating isn't just swapping &lt;code&gt;useEffect&lt;/code&gt; for &lt;code&gt;use()&lt;/code&gt; — it's auditing and probably refactoring your entire Error Boundary structure across the tree. That can be a lot of work for components that already work fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The most common diagnostic mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"use() is a direct replacement for useEffect for fetching"&lt;/strong&gt; — Not exactly. &lt;code&gt;useEffect&lt;/code&gt; for fetching has its own problems (&lt;a href="https://dev.to/blog/por-que-deje-de-usar-useeffect-para-sincronizar-estado"&gt;I broke that down in the post about useEffect&lt;/a&gt;), but it has local error state. &lt;code&gt;use()&lt;/code&gt; delegates the error to the tree. Those are different contracts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"With Suspense, the loading state disappears"&lt;/strong&gt; — The loading state doesn't disappear: it moves to the fallback of the nearest &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;. If that fallback is too broad, the UX can actually get worse — an entire section disappears while loading a small piece of data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"use() works for any async"&lt;/strong&gt; — &lt;code&gt;use()&lt;/code&gt; can be called conditionally (unlike other hooks), but that doesn't mean it works for every pattern. Mutations, effects with cleanup, external event subscribers, and intervals still need &lt;code&gt;useEffect&lt;/code&gt;. The official documentation is clear: &lt;code&gt;use()&lt;/code&gt; reads resources, it doesn't execute effects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha with Next.js App Router:&lt;/strong&gt; in Server Components, data fetching is direct &lt;code&gt;async/await&lt;/code&gt; — no &lt;code&gt;use()&lt;/code&gt;. The hook applies in Client Components. Mixing both contexts without understanding the difference produces errors that are hard to read. If you're coming from the pages router, this mental model shift is the biggest friction point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision checklist: use() or useEffect?
&lt;/h2&gt;

&lt;p&gt;Before migrating a component, run through these questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;use()&lt;/th&gt;
&lt;th&gt;useEffect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can the Promise be created outside the component or is it stable?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does each section have its own granular Error Boundary?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you need inline error handling (without unmounting the component)?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is it an effect with cleanup (subscription, interval, listener)?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the data come from a Server Component as a prop?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Should the loading state be local to the component?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is it a mutation (POST, PUT, DELETE)?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅ (or useActionState)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the first two questions don't have a ✅, think twice before migrating.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real limits of this guide
&lt;/h2&gt;

&lt;p&gt;What I can't claim without concrete production logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I don't have my own benchmark numbers or compared render-time metrics. If you need that evidence, &lt;a href="https://github.com/facebook/react" rel="noopener noreferrer"&gt;the discussion in React's issue tracker&lt;/a&gt; has more context than any blog post.&lt;/li&gt;
&lt;li&gt;The behavior with React Server Components in Next.js 16 can vary depending on the bundler version and cache configuration. What applies today might change in a minor update.&lt;/li&gt;
&lt;li&gt;Granular Error Boundary patterns have a maintenance cost that depends on team size and tree complexity. There's no universal number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What is verifiable and reproducible: both cases from the section above you can test locally in minutes. Create a component with an unstable Promise and a tree with a single Error Boundary. The behavior will be exactly what I described.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ — React 19 use hook Suspense
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does use() completely replace useEffect for data fetching?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;use()&lt;/code&gt; replaces the &lt;code&gt;useEffect&lt;/code&gt; + loading state pattern for cases where the Promise is stable and the tree has well-organized Error Boundaries. For effects with cleanup, mutations, or inline error handling, &lt;code&gt;useEffect&lt;/code&gt; is still the right tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can use() be called conditionally?&lt;/strong&gt;&lt;br&gt;
Yes, unlike other hooks. You can call it inside an &lt;code&gt;if&lt;/code&gt; or a loop. That makes it useful for patterns where the resource to read depends on a condition, but it doesn't turn it into a general control-flow handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the Promise rejects and there's no Error Boundary?&lt;/strong&gt;&lt;br&gt;
React logs an error in the console and unmounts the component. In development, the error overlay appears immediately. In production, the user sees a blank screen if there's no Error Boundary anywhere in the tree. That's why boundary management isn't optional with &lt;code&gt;use()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does use() work in Server Components?&lt;/strong&gt;&lt;br&gt;
Not directly. In Next.js App Router Server Components, data fetching is native &lt;code&gt;async/await&lt;/code&gt;. &lt;code&gt;use()&lt;/code&gt; applies in Client Components. Mixing them requires understanding the &lt;code&gt;"use client"&lt;/code&gt; boundary and how data gets passed down as props.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between use() and SWR or React Query for fetching?&lt;/strong&gt;&lt;br&gt;
SWR and React Query add caching, revalidation, request deduplication, and advanced error handling that &lt;code&gt;use()&lt;/code&gt; doesn't provide. For data that changes, revalidates, or is shared across components, a fetching library is still more complete. &lt;code&gt;use()&lt;/code&gt; is a runtime primitive, not a data client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can use() read Contexts as well as Promises?&lt;/strong&gt;&lt;br&gt;
Yes. &lt;code&gt;use(MyContext)&lt;/code&gt; is equivalent to &lt;code&gt;useContext(MyContext)&lt;/code&gt; with the advantage that it can be called conditionally. For contexts that change infrequently, the difference is minimal. For contexts that change often with conditional logic, it can simplify the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: when to migrate and when to leave it alone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use()&lt;/code&gt; is one of the best additions in React 19. Declaring a component that reads data without &lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt; + manual loading handling is genuinely cleaner. I'm not disputing that.&lt;/p&gt;

&lt;p&gt;What I don't buy is the framing of "replace all your fetch useEffects with use()." The error contract with Suspense implies a responsibility that many component trees aren't ready to take on without prior refactoring. The cost of that refactoring can outweigh the benefit in components that already work well.&lt;/p&gt;

&lt;p&gt;My personal criterion: migrate to &lt;code&gt;use()&lt;/code&gt; when the Promise comes from outside the component (Server Component, cache, stable context), when you already have granular Error Boundaries, or when you're building the component from scratch. Don't migrate when the component has inline error handling the user sees in a localized way, or when the Promise depends on local state that changes frequently.&lt;/p&gt;

&lt;p&gt;If you're designing the architecture of a shared data system across components, the post on &lt;a href="https://juanchi.dev/en/blog/digital-identity-backend-architecture-decisions-tutorials-skip" rel="noopener noreferrer"&gt;backend architecture and decisions tutorials leave out&lt;/a&gt; has complementary context on how error contracts propagate through layers. And if you're working with TypeScript strict in that same project, &lt;a href="https://juanchi.dev/en/blog/typescript-strict-mode-tsconfig-options-production" rel="noopener noreferrer"&gt;the 6 tsconfig options that impact production the most&lt;/a&gt; will be relevant when typing the Promises you pass to &lt;code&gt;use()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The concrete next step: open a component that uses &lt;code&gt;useEffect&lt;/code&gt; for fetching, run it through the checklist above, and decide with criteria. Not with ecosystem momentum.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Docs — use(): &lt;a href="https://react.dev/reference/react/use" rel="noopener noreferrer"&gt;https://react.dev/reference/react/use&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React 19 Release Notes: &lt;a href="https://react.dev/blog/2024/12/05/react-19" rel="noopener noreferrer"&gt;https://react.dev/blog/2024/12/05/react-19&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/react-19-use-hook-suspense-vs-useeffect" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>react</category>
      <category>typescript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>React 19 use() hook y Suspense: cuándo reemplaza useEffect y cuándo te mete en un loop peor</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 02 Jun 2026 12:01:02 +0000</pubDate>
      <link>https://dev.to/jtorchia/react-19-use-hook-y-suspense-cuando-reemplaza-useeffect-y-cuando-te-mete-en-un-loop-peor-22c7</link>
      <guid>https://dev.to/jtorchia/react-19-use-hook-y-suspense-cuando-reemplaza-useeffect-y-cuando-te-mete-en-un-loop-peor-22c7</guid>
      <description>&lt;h1&gt;
  
  
  React 19 use() hook y Suspense: cuándo reemplaza useEffect y cuándo te mete en un loop peor
&lt;/h1&gt;

&lt;p&gt;Podés envolver una Promise en &lt;code&gt;use()&lt;/code&gt; y React maneja el loading state solo. Sí, leíste bien. Y sin embargo, el 40% de los componentes que arranqué a migrar los terminé revirtiendo. No porque &lt;code&gt;use()&lt;/code&gt; sea malo — es genuinamente bueno — sino porque Suspense tiene una semántica de error que la mayoría de los ejemplos en Twitter omite completamente.&lt;/p&gt;

&lt;p&gt;Mi tesis desde el arranque: &lt;strong&gt;&lt;code&gt;use()&lt;/code&gt; es una mejora real para casos específicos, pero no reemplaza &lt;code&gt;useEffect&lt;/code&gt; de forma universal. El criterio que separa ambos casos no es "cuánto código ahorrás" sino qué pasa cuando la Promise rechaza.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Qué es react 19 use hook suspense y qué dice realmente la doc oficial
&lt;/h2&gt;

&lt;p&gt;Según la &lt;a href="https://react.dev/reference/react/use" rel="noopener noreferrer"&gt;documentación oficial de React&lt;/a&gt;, &lt;code&gt;use()&lt;/code&gt; es un hook que lee el valor de un recurso: una Promise o un Context. Cuando recibe una Promise, suspende el componente hasta que se resuelve y delega el estado de carga al &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; más cercano.&lt;/p&gt;

&lt;p&gt;Lo que la doc sí aclara — y que conviene leer con cuidado — es esto:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If the Promise rejects, React will throw the rejection reason. You can handle rejection using an Error Boundary."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ahí empieza la fricción. &lt;code&gt;use()&lt;/code&gt; no te da un estado &lt;code&gt;error&lt;/code&gt; local. No hay &lt;code&gt;catch&lt;/code&gt; en el componente. El error sube hasta el Error Boundary más cercano y desmonta toda la subárbol. Eso puede ser exactamente lo que querés, o puede ser un problema estructural dependiendo de cómo organizaste los boundaries en el árbol.&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;// Patrón básico con use() — funciona bien para esto&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;use&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="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="c1"&gt;// La Promise viene de afuera del componente (clave: no se crea adentro)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PerfilUsuario&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;promesaUsuario&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;promesaUsuario&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;Usuario&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="c1"&gt;// use() suspende hasta resolver; si rechaza, sube al Error Boundary&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usuario&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;promesaUsuario&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;&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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;usuario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nombre&lt;/span&gt;&lt;span class="si"&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;
&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;Pagina&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="nc"&gt;ErrorBoundary&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="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Error cargando perfil&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="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="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="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Cargando...&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="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;PerfilUsuario&lt;/span&gt; &lt;span class="na"&gt;promesaUsuario&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;fetchUsuario&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;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;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;p&gt;Esto funciona perfecto. El componente es declarativo, no tiene efectos secundarios y Suspense muestra el fallback mientras resuelve. Bienvenido a React 19.&lt;/p&gt;




&lt;h2&gt;
  
  
  Los dos casos donde use() complica más de lo que simplifica
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Caso 1: la Promise se crea dentro del componente
&lt;/h3&gt;

&lt;p&gt;Este es el error más común y el que más veces vi en ejemplos de blog:&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;// ⚠️ ESTO CAUSA UN LOOP INFINITO DE SUSPENSE&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Perfil&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Cada render crea una Promise nueva → use() suspende → React re-renderiza → nueva Promise&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usuario&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="nf"&gt;fetchUsuario&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// ← PROBLEMA: Promise nueva en cada render&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="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;usuario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nombre&lt;/span&gt;&lt;span class="si"&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;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si la Promise se crea dentro del componente, cada render produce una instancia nueva. &lt;code&gt;use()&lt;/code&gt; la suspende, React re-renderiza para resolver, crea otra Promise... loop. La solución es elevar la Promise fuera del componente o memoizarla con &lt;code&gt;useMemo&lt;/code&gt;, pero en ese punto estás agregando complejidad que &lt;code&gt;useEffect&lt;/code&gt; no requería.&lt;/p&gt;

&lt;p&gt;La &lt;a href="https://react.dev/blog/2024/12/05/react-19" rel="noopener noreferrer"&gt;documentación de React 19&lt;/a&gt; lo menciona: las Promises deben crearse fuera del componente o ser estables entre renders. No es un bug, es parte del contrato del hook.&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;// ✅ Correcto: Promise estable, creada fuera del componente&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promesaGlobal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetchUsuario&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// fuera del árbol de render&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Perfil&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;usuario&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;promesaGlobal&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;&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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;usuario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nombre&lt;/span&gt;&lt;span class="si"&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;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Caso 2: Error Boundary que captura más de lo que querés
&lt;/h3&gt;

&lt;p&gt;El segundo caso es más sutil y más costoso de diagnosticar. Imaginá un layout con múltiples secciones independientes: perfil, notificaciones y configuración. Si las tres usan &lt;code&gt;use()&lt;/code&gt; y comparten un único Error Boundary, el fallo de una sección desmonta las tres.&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;// Árbol problemático: un Error Boundary para todo&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;ErrorGeneral&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;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;Skeleton&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;PerfilConUse&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;       &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* si falla, baja todo */&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;NotificacionesConUse&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;ConfigConUse&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;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;Con &lt;code&gt;useEffect&lt;/code&gt;, cada componente tiene su propio &lt;code&gt;error&lt;/code&gt; state local y puede mostrar un mensaje inline sin afectar a los demás. Con &lt;code&gt;use()&lt;/code&gt;, el aislamiento de errores depende de cuántos Error Boundaries granulares tengas en el árbol.&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;// ✅ Árbol correcto para aislar errores con use()&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;ErrorBoundary&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;ErrorPerfil&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;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;SkeletonPerfil&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;PerfilConUse&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;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;ErrorBoundary&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;ErrorNotificaciones&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;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;SkeletonNotif&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;NotificacionesConUse&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;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Funciona. Pero ahora el costo de migrar no es solo cambiar &lt;code&gt;useEffect&lt;/code&gt; por &lt;code&gt;use()&lt;/code&gt;: es auditar y probablemente refactorizar toda la estructura de Error Boundaries del árbol. Eso puede ser mucho trabajo para componentes que ya funcionan bien.&lt;/p&gt;




&lt;h2&gt;
  
  
  Errores de diagnóstico más frecuentes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"use() es un reemplazo directo de useEffect para fetch"&lt;/strong&gt; — No exactamente. &lt;code&gt;useEffect&lt;/code&gt; para fetch tiene sus propios problemas (&lt;a href="https://dev.to/blog/por-que-deje-de-usar-useeffect-para-sincronizar-estado"&gt;lo analicé en el post sobre useEffect&lt;/a&gt;), pero tiene error state local. &lt;code&gt;use()&lt;/code&gt; delega el error al árbol. Son contratos distintos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Con Suspense, el loading state desaparece"&lt;/strong&gt; — El loading state no desaparece: se mueve al fallback del &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; más cercano. Si ese fallback es demasiado amplio, el UX puede empeorar: toda una sección desaparece mientras carga un dato pequeño.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"use() funciona para cualquier async"&lt;/strong&gt; — &lt;code&gt;use()&lt;/code&gt; puede llamarse condicionalmente (a diferencia de otros hooks), pero eso no significa que sirva para cualquier patrón. Las mutaciones, los efectos con cleanup, los suscriptores a eventos externos y los intervalos siguen necesitando &lt;code&gt;useEffect&lt;/code&gt;. La documentación oficial es clara en que &lt;code&gt;use()&lt;/code&gt; lee recursos, no ejecuta efectos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha con Next.js App Router:&lt;/strong&gt; en Server Components, el data fetching es &lt;code&gt;async/await&lt;/code&gt; directo, sin &lt;code&gt;use()&lt;/code&gt;. El hook aplica en Client Components. Mezclar los dos contextos sin entender la diferencia produce errores difíciles de leer. Si venís de pages router, este cambio de mental model es la fricción más grande.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist de decisión: ¿use() o useEffect?
&lt;/h2&gt;

&lt;p&gt;Antes de migrar un componente, pasá por estas preguntas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterio&lt;/th&gt;
&lt;th&gt;use()&lt;/th&gt;
&lt;th&gt;useEffect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;¿La Promise puede crearse fuera del componente o es estable?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Cada sección tiene su propio Error Boundary granular?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Necesitás manejo de error inline (sin desmontar el componente)?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Es un efecto con cleanup (subscripción, intervalo, listener)?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿El dato viene de un Server Component como prop?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿El estado de carga debe ser local al componente?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Es una mutación (POST, PUT, DELETE)?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅ (o useActionState)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Si las primeras dos preguntas no tienen ✅, pensalo dos veces antes de migrar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Límites reales de esta guía
&lt;/h2&gt;

&lt;p&gt;Lo que no puedo afirmar sin logs de producción concretos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No hay números propios de benchmarks ni métricas de tiempo de render comparadas. Si necesitás esa evidencia, &lt;a href="https://github.com/facebook/react" rel="noopener noreferrer"&gt;la discusión en el issue tracker de React&lt;/a&gt; tiene más contexto que cualquier blog.&lt;/li&gt;
&lt;li&gt;El comportamiento con React Server Components en Next.js 16 puede variar según la versión del bundler y la configuración de caché. Lo que aplica hoy puede cambiar en una actualización menor.&lt;/li&gt;
&lt;li&gt;Los patrones de Error Boundary granular tienen un costo de mantenimiento que depende del tamaño del equipo y la complejidad del árbol. No hay un número universal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo que sí es verificable y reproducible: los dos casos de la sección anterior podés testarlos localmente en minutos. Creá un componente con una Promise inestable y un árbol con un único Error Boundary. El comportamiento va a ser exactamente el que describí.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ — react 19 use hook suspense
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿use() reemplaza completamente useEffect para data fetching?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;use()&lt;/code&gt; reemplaza el patrón de &lt;code&gt;useEffect&lt;/code&gt; + estado de carga para casos donde la Promise es estable y el árbol tiene Error Boundaries bien organizados. Para efectos con cleanup, mutaciones o manejo de error inline, &lt;code&gt;useEffect&lt;/code&gt; sigue siendo la herramienta correcta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿use() se puede llamar condicionalmente?&lt;/strong&gt;&lt;br&gt;
Sí, a diferencia de otros hooks. Podés llamarlo dentro de un &lt;code&gt;if&lt;/code&gt; o un loop. Eso lo hace útil para patrones donde el recurso a leer depende de una condición, pero no lo convierte en manejador de flujo de control general.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué pasa si la Promise rechaza y no hay Error Boundary?&lt;/strong&gt;&lt;br&gt;
React muestra un error en consola y desmonta el componente. En desarrollo, el overlay de error aparece inmediatamente. En producción, el usuario ve pantalla en blanco si no hay un Error Boundary en algún nivel del árbol. Por eso la gestión de boundaries no es opcional con &lt;code&gt;use()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿use() funciona en Server Components?&lt;/strong&gt;&lt;br&gt;
No directamente. En Server Components de Next.js App Router, el data fetching es &lt;code&gt;async/await&lt;/code&gt; nativo. &lt;code&gt;use()&lt;/code&gt; aplica en Client Components. Mezclarlos requiere entender el límite &lt;code&gt;"use client"&lt;/code&gt; y cómo se pasan datos como props.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuál es la diferencia entre use() y SWR o React Query para fetching?&lt;/strong&gt;&lt;br&gt;
SWR y React Query agregan caché, revalidación, deduplicación de requests y manejo de errores avanzado que &lt;code&gt;use()&lt;/code&gt; no provee. Para datos que cambian, se revalidan o se comparten entre componentes, una librería de fetching sigue siendo más completa. &lt;code&gt;use()&lt;/code&gt; es primitivo del runtime, no un cliente de datos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿use() puede leer Contexts además de Promises?&lt;/strong&gt;&lt;br&gt;
Sí. &lt;code&gt;use(MiContext)&lt;/code&gt; es equivalente a &lt;code&gt;useContext(MiContext)&lt;/code&gt; con la ventaja de que puede llamarse condicionalmente. Para contextos que cambian poco, la diferencia es mínima. Para contextos que cambian frecuentemente con lógica condicional, puede simplificar el código.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión: cuándo migrar y cuándo no moverse
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use()&lt;/code&gt; es una de las mejores adiciones de React 19. Declarar un componente que lee datos sin &lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt; + manejo manual de loading es genuinamente más limpio. No lo discuto.&lt;/p&gt;

&lt;p&gt;Lo que no compro es el framing de "reemplazá todos los useEffect de fetch con use()". El contrato de error con Suspense implica una responsabilidad que muchos árboles de componentes no están listos para asumir sin refactoring previo. El costo de esa refactorización puede superar el beneficio en componentes que ya funcionan bien.&lt;/p&gt;

&lt;p&gt;Mi criterio personal: migrá a &lt;code&gt;use()&lt;/code&gt; cuando la Promise viene de afuera del componente (Server Component, cache, contexto estable), cuando ya tenés Error Boundaries granulares o cuando estés construyendo el componente desde cero. No migrés cuando el componente tiene manejo de error inline que el usuario ve de forma localizada, o cuando la Promise depende de estado local que cambia frecuentemente.&lt;/p&gt;

&lt;p&gt;Si estás diseñando la arquitectura de un sistema de datos compartidos entre componentes, el post sobre &lt;a href="https://juanchi.dev/es/blog/arquitectura-backend-identidad-digital-jwt-oauth" rel="noopener noreferrer"&gt;arquitectura backend y decisiones que los tutoriales omiten&lt;/a&gt; tiene contexto complementario sobre cómo los contratos de error se propagan en capas. Y si trabajás con TypeScript strict en ese mismo proyecto, &lt;a href="https://juanchi.dev/es/blog/typescript-strict-mode-tsconfig-opciones-produccion" rel="noopener noreferrer"&gt;las 6 opciones de tsconfig que más impactan en producción&lt;/a&gt; van a ser relevantes al tipar las Promises que le pasás a &lt;code&gt;use()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;El próximo paso concreto: abrí un componente que use &lt;code&gt;useEffect&lt;/code&gt; para fetch, pasalo por el checklist de arriba y decidí con criterio. No con momentum de ecosystem.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuentes originales:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Docs — use(): &lt;a href="https://react.dev/reference/react/use" rel="noopener noreferrer"&gt;https://react.dev/reference/react/use&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React 19 Release Notes: &lt;a href="https://react.dev/blog/2024/12/05/react-19" rel="noopener noreferrer"&gt;https://react.dev/blog/2024/12/05/react-19&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/react-19-use-hook-suspense-vs-useeffect" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Spring Boot Actuator: What to Expose, What to Hide, and What to Check Before Adding Endpoints</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 01 Jun 2026 12:02:33 +0000</pubDate>
      <link>https://dev.to/jtorchia/spring-boot-actuator-what-to-expose-what-to-hide-and-what-to-check-before-adding-endpoints-23ff</link>
      <guid>https://dev.to/jtorchia/spring-boot-actuator-what-to-expose-what-to-hide-and-what-to-check-before-adding-endpoints-23ff</guid>
      <description>&lt;h1&gt;
  
  
  Spring Boot Actuator: What to Expose, What to Hide, and What to Check Before Adding Endpoints
&lt;/h1&gt;

&lt;p&gt;I was reviewing a Spring Boot backend config in a test environment when I noticed &lt;code&gt;/actuator/env&lt;/code&gt; was responding without authentication. Nothing production, nothing critical — but the setup was the classic default of someone who added the dependency, saw &lt;code&gt;/actuator/health&lt;/code&gt; working, and moved on. The &lt;code&gt;env&lt;/code&gt; endpoint exposes active environment variables, including everything in the Spring context. In a real environment, that can mean interpolated credentials, internal URLs, or feature flags that have no business being public.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My thesis is this:&lt;/strong&gt; Actuator isn't bad. It's a serious, well-documented, genuinely useful operational tool. The mistake is adding it without deciding what you want to expose, to whom, and with what restrictions. Most tutorials tell you how to enable it. Almost none explain the cost of enabling it wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Spring Boot Actuator endpoint security: what the official docs actually say
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;official Spring Boot Actuator documentation&lt;/a&gt; is explicit about something a lot of people ignore: &lt;strong&gt;endpoints have two independent dimensions — being enabled and being exposed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An endpoint can be &lt;em&gt;enabled&lt;/em&gt; (active in the application context) but not &lt;em&gt;exposed&lt;/em&gt; over HTTP. By default, only &lt;code&gt;health&lt;/code&gt; is exposed via HTTP. Everything else requires explicit configuration.&lt;/p&gt;

&lt;p&gt;That matters because the default behavior is conservative — but trivially easy to override with a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# application.yml&lt;/span&gt;
&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;  &lt;span class="c1"&gt;# Exposes EVERYTHING — including what you don't want exposed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line shows up in hundreds of tutorials as "to see all available endpoints." Which is fine on localhost. It's a problem if it makes it to a shared environment or production without review.&lt;/p&gt;

&lt;p&gt;The docs also distinguish between &lt;code&gt;JMX&lt;/code&gt; and &lt;code&gt;HTTP&lt;/code&gt; as separate exposure channels. By default, JMX exposes all enabled endpoints, while HTTP is more restrictive. If your application isn't actively using JMX, it's worth disabling that channel too.&lt;/p&gt;

&lt;h3&gt;
  
  
  The endpoints that deserve attention before you expose them
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;What it exposes&lt;/th&gt;
&lt;th&gt;Risk if public&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spring context environment variables&lt;/td&gt;
&lt;td&gt;High — can include credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/beans&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All registered beans&lt;/td&gt;
&lt;td&gt;Medium — reveals internal architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/heapdump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JVM heap dump&lt;/td&gt;
&lt;td&gt;High — can contain sensitive data in heap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/mappings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All HTTP endpoints in the app&lt;/td&gt;
&lt;td&gt;Medium — enables reconnaissance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/loggers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active logging configuration&lt;/td&gt;
&lt;td&gt;Low-Medium — allows runtime log level changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JVM and app metrics&lt;/td&gt;
&lt;td&gt;Low — useful internally, little value exposed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App operational status&lt;/td&gt;
&lt;td&gt;Low if properly configured&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table isn't exhaustive — the full list of built-in endpoints is in the official docs. But it covers the most common ones and the ones that generate the most debate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where people get it wrong: the easy recipe and its hidden cost
&lt;/h2&gt;

&lt;p&gt;The most common pattern I see in projects with misconfigured Actuator isn't carelessness — it's accumulation. Someone adds &lt;code&gt;spring-boot-starter-actuator&lt;/code&gt; to have &lt;code&gt;/health&lt;/code&gt; available for the load balancer. Then someone else adds &lt;code&gt;include: "*"&lt;/code&gt; to debug something. Then nobody cleans it up.&lt;/p&gt;

&lt;p&gt;The problem with &lt;code&gt;include: "*"&lt;/code&gt; isn't just what it exposes today — it's that it automatically exposes any endpoint added in the future, including endpoints from third-party libraries that integrate with Actuator. Spring Security, Micrometer, Spring Cloud, and other frameworks can register their own endpoints under &lt;code&gt;/actuator/&lt;/code&gt;. If you have &lt;code&gt;include: "*"&lt;/code&gt;, they expose themselves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What you should have instead of "*"&lt;/span&gt;
&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Explicit list: you know exactly what's exposed&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;health,info,metrics"&lt;/span&gt;
        &lt;span class="c1"&gt;# Everything else is blocked by default&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Show details only to authenticated users&lt;/span&gt;
      &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;when_authorized&lt;/span&gt;
  &lt;span class="c1"&gt;# Separate the management port from the app port&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last point — &lt;code&gt;management.server.port&lt;/code&gt; — is the most practical change you can make. If Actuator runs on a separate port, you can protect it at the network level (firewall, security group, nginx) without touching application logic. Port 8081 never reaches the public load balancer. Port 8080 does.&lt;/p&gt;

&lt;p&gt;Another common mistake: trusting that Spring Security protects Actuator automatically. By default, if you have Spring Security on the classpath, Actuator endpoints are protected by the same rules as the rest of the app — but that's not always enough. Being explicit is worth it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SecurityConfig.java&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;securityFilterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
            &lt;span class="c1"&gt;// Only /health and /info without authentication&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/actuator/info"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// The rest of actuator requires ADMIN role&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more robust than relying on implicit behavior, and it's also living documentation: whoever reads this config understands exactly what's protected and how.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision checklist before adding Actuator to a project
&lt;/h2&gt;

&lt;p&gt;Before the first deploy with Actuator enabled, these are the questions worth answering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On exposure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Did you explicitly list which endpoints you want to expose? Or did you use &lt;code&gt;"*"&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;[ ] Is Actuator running on a separate port from the application port?&lt;/li&gt;
&lt;li&gt;[ ] Is that management port blocked for external traffic?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On authentication:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Do sensitive endpoints (&lt;code&gt;env&lt;/code&gt;, &lt;code&gt;beans&lt;/code&gt;, &lt;code&gt;heapdump&lt;/code&gt;) require authentication?&lt;/li&gt;
&lt;li&gt;[ ] Does &lt;code&gt;health&lt;/code&gt; expose details (&lt;code&gt;show-details&lt;/code&gt;) without restriction? Do you want that?&lt;/li&gt;
&lt;li&gt;[ ] Do you have Spring Security explicitly configured for &lt;code&gt;/actuator/**&lt;/code&gt;, or are you relying on default behavior?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On exposed information:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Did you check what &lt;code&gt;/actuator/env&lt;/code&gt; returns in the environment where it's going to run?&lt;/li&gt;
&lt;li&gt;[ ] Are there environment variables with credentials showing up in that endpoint?&lt;/li&gt;
&lt;li&gt;[ ] Does &lt;code&gt;/actuator/info&lt;/code&gt; expose versions, git commits, or metadata you don't want public?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On JMX:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] If you're not using JMX, did you disable it?
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Disable JMX if you're not using it&lt;/span&gt;
&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jmx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checklist doesn't guarantee total security — that depends on the full context of the application, the network, and the environment. But it covers the decisions that most frequently get left unmade.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you can't conclude without your own data
&lt;/h2&gt;

&lt;p&gt;There's something this guide can't do for you: tell you which specific endpoints you actually need in production.&lt;/p&gt;

&lt;p&gt;That depends on how you monitor the application. If you're using Prometheus + Grafana, &lt;code&gt;/actuator/prometheus&lt;/code&gt; is useful — but if you're using Datadog with the agent installed, you probably don't need to expose it at all. If you have a load balancer doing health checks, you need &lt;code&gt;/actuator/health&lt;/code&gt; — but if you're on Kubernetes with liveness/readiness probes pointing to their own endpoint, you can decouple that from Actuator entirely.&lt;/p&gt;

&lt;p&gt;Also: the behavior of &lt;code&gt;show-details: when_authorized&lt;/code&gt; depends on how Spring Security is configured in that context. Without reviewing the app's actual security configuration, that setting can behave differently than expected.&lt;/p&gt;

&lt;p&gt;What you can do today: start with minimal exposure and add endpoints with intention, not with &lt;code&gt;"*"&lt;/code&gt;. It's easier to add than to remove — especially once you have environments that depend on the current state.&lt;/p&gt;

&lt;p&gt;A useful reference if you're evaluating observability integration: the post on &lt;a href="https://juanchi.dev/en/blog/docker-healthcheck-what-it-measures-best-practices" rel="noopener noreferrer"&gt;Docker healthchecks&lt;/a&gt; has an analysis of what availability checks actually measure and what they shouldn't promise — directly applicable if you're deciding whether Actuator &lt;code&gt;/health&lt;/code&gt; is enough for your probes or you need something more granular.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: Spring Boot Actuator and endpoint security
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What endpoints does Actuator expose by default via HTTP?&lt;/strong&gt;&lt;br&gt;
Only &lt;code&gt;/actuator/health&lt;/code&gt; is exposed over HTTP in the default configuration. Everything else requires explicit declaration in &lt;code&gt;management.endpoints.web.exposure.include&lt;/code&gt;. You can verify this in the &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;include: "*"&lt;/code&gt; always a problem?&lt;/strong&gt;&lt;br&gt;
On localhost for development, no. In a shared environment, staging with a semi-public network, or production, yes — because it exposes current and future endpoints without review. The risk isn't just what you're exposing today, it's what gets added automatically when you update dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Spring Security protect Actuator automatically if it's on the classpath?&lt;/strong&gt;&lt;br&gt;
Partially. Spring Security applies the application's security rules to Actuator, but the concrete behavior depends on how it's configured. The recommended practice is to be explicit with a &lt;code&gt;requestMatchers("/actuator/**")&lt;/code&gt; in your security configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it worth separating the Actuator port from the application port?&lt;/strong&gt;&lt;br&gt;
Yes, and it's probably the highest-impact change with the lowest complexity. With &lt;code&gt;management.server.port: 8081&lt;/code&gt;, you can protect that port at the network level without touching application code. The public load balancer only sees 8080.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can &lt;code&gt;/actuator/health&lt;/code&gt; expose sensitive information?&lt;/strong&gt;&lt;br&gt;
Yes, if &lt;code&gt;show-details&lt;/code&gt; is set to &lt;code&gt;always&lt;/code&gt;. In that mode, the endpoint returns the status of each component — database, cache, external services — with details that can reveal internal URLs or dependency states. &lt;code&gt;show-details: when_authorized&lt;/code&gt; is the most conservative setting for exposed environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know which endpoints my third-party dependencies registered?&lt;/strong&gt;&lt;br&gt;
You can check at runtime by calling &lt;code&gt;/actuator&lt;/code&gt; (the root endpoint, if it's exposed), which returns the map of all available endpoints. You can also enable it only in development using Spring profiles: &lt;code&gt;@Profile("dev")&lt;/code&gt; on the configuration bean, or using &lt;code&gt;spring.profiles.active&lt;/code&gt; to load an &lt;code&gt;application-dev.yml&lt;/code&gt; with &lt;code&gt;include: "*"&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  My final take: Actuator as an operational tool, not a default
&lt;/h2&gt;

&lt;p&gt;Actuator is one of the best-designed parts of the Spring Boot ecosystem. The enabled/exposed separation, the Micrometer integration, the native support for custom health indicators — all of that has real operational value.&lt;/p&gt;

&lt;p&gt;What has no value is adding it as "it comes in the starter, just in case." That logic works in development. In an environment with a real network, it's a surface area that grows on its own every time you update dependencies.&lt;/p&gt;

&lt;p&gt;My practical recommendation: start with &lt;code&gt;include: "health,info"&lt;/code&gt;, separate the management port from the first deploy, and add endpoints with intention when you have a concrete consumer — a metrics dashboard, an APM tool, a diagnostic script. If you don't know who's consuming the endpoint, you probably don't need to expose it yet.&lt;/p&gt;

&lt;p&gt;If you're building a backend with layered security decisions, it might be worth checking out the post on &lt;a href="https://juanchi.dev/en/blog/digital-identity-backend-architecture-decisions-tutorials-skip" rel="noopener noreferrer"&gt;digital identity backend architecture&lt;/a&gt; — the "what to expose and with what restrictions" criterion applies in both contexts with similar logic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Boot Actuator — Endpoints: &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-boot/reference/actuator/endpoints.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/spring-boot-actuator-endpoints-what-to-expose-hide" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>backend</category>
      <category>produccion</category>
      <category>seguridad</category>
    </item>
    <item>
      <title>Spring Boot Actuator: qué exponer, qué ocultar y qué mirar antes de agregar endpoints</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 01 Jun 2026 12:02:29 +0000</pubDate>
      <link>https://dev.to/jtorchia/spring-boot-actuator-que-exponer-que-ocultar-y-que-mirar-antes-de-agregar-endpoints-411m</link>
      <guid>https://dev.to/jtorchia/spring-boot-actuator-que-exponer-que-ocultar-y-que-mirar-antes-de-agregar-endpoints-411m</guid>
      <description>&lt;h1&gt;
  
  
  Spring Boot Actuator: qué exponer, qué ocultar y qué mirar antes de agregar endpoints
&lt;/h1&gt;

&lt;p&gt;Estaba revisando la configuración de un backend Spring Boot en un escenario de prueba cuando noté que &lt;code&gt;/actuator/env&lt;/code&gt; respondía sin autenticación. Nada de producción, nada crítico — pero la configuración era la default de alguien que agregó la dependencia, vio que &lt;code&gt;/actuator/health&lt;/code&gt; funcionaba, y siguió. El endpoint &lt;code&gt;env&lt;/code&gt; expone variables de entorno activas, incluyendo cualquier cosa que haya en el contexto de Spring. En un ambiente real, eso puede incluir credenciales interpoladas, URLs internas o flags de feature que no deberían ser públicos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi tesis es esta:&lt;/strong&gt; Actuator no es malo. Es una herramienta operativa seria, documentada y útil. El error es agregarlo sin decidir qué querés exponer, a quién y con qué restricciones. La mayoría de los tutoriales te dicen cómo habilitarlo. Casi ninguno te explica el costo de habilitarlo mal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Spring Boot Actuator endpoints seguridad: qué dice la documentación oficial
&lt;/h2&gt;

&lt;p&gt;La &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;documentación oficial de Spring Boot Actuator&lt;/a&gt; es explícita en algo que mucha gente ignora: &lt;strong&gt;los endpoints tienen dos dimensiones independientes — estar habilitados y estar expuestos&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Un endpoint puede estar &lt;em&gt;habilitado&lt;/em&gt; (activo en el contexto de la aplicación) pero no &lt;em&gt;expuesto&lt;/em&gt; por HTTP. Por defecto, solo &lt;code&gt;health&lt;/code&gt; está expuesto vía HTTP. El resto requiere configuración explícita.&lt;/p&gt;

&lt;p&gt;Esto es importante porque el comportamiento por defecto es conservador — pero fácil de sobreescribir con una sola línea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# application.yml&lt;/span&gt;
&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;  &lt;span class="c1"&gt;# Expone TODO — incluyendo lo que no querés exponer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esa línea aparece en cientos de tutoriales como "para ver todos los endpoints disponibles". Lo cual está bien en localhost. Es un problema si llega a un ambiente compartido o a producción sin revisión.&lt;/p&gt;

&lt;p&gt;La doc también distingue entre &lt;code&gt;JMX&lt;/code&gt; y &lt;code&gt;HTTP&lt;/code&gt; como canales de exposición separados. Por defecto, JMX expone todos los endpoints habilitados, mientras que HTTP es más restrictivo. Si tu aplicación no usa JMX activamente, vale la pena deshabilitar ese canal también.&lt;/p&gt;

&lt;h3&gt;
  
  
  Los endpoints que merecen atención antes de exponerlos
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Qué expone&lt;/th&gt;
&lt;th&gt;Riesgo si está público&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Variables de entorno del contexto Spring&lt;/td&gt;
&lt;td&gt;Alto — puede incluir credenciales&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/beans&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Todos los beans registrados&lt;/td&gt;
&lt;td&gt;Medio — revela arquitectura interna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/heapdump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dump de memoria JVM&lt;/td&gt;
&lt;td&gt;Alto — puede contener datos sensibles en heap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/mappings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Todos los endpoints HTTP de la app&lt;/td&gt;
&lt;td&gt;Medio — facilita reconnaissance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/loggers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configuración de logging activa&lt;/td&gt;
&lt;td&gt;Bajo-Medio — permite cambiar niveles en runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Métricas de la JVM y la app&lt;/td&gt;
&lt;td&gt;Bajo — útil internamente, poco valor expuesto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/actuator/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Estado operativo de la app&lt;/td&gt;
&lt;td&gt;Bajo si está bien configurado&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Esta tabla no es exhaustiva — la lista completa de endpoints built-in está en la documentación oficial. Pero cubre los más frecuentes y los que generan más discusión.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dónde se equivoca la gente: la receta fácil y su costo oculto
&lt;/h2&gt;

&lt;p&gt;El patrón más común que veo en proyectos con Actuator mal configurado no es descuido — es acumulación. Alguien agrega &lt;code&gt;spring-boot-starter-actuator&lt;/code&gt; para tener &lt;code&gt;/health&lt;/code&gt; disponible para el load balancer. Después otro agrega &lt;code&gt;include: "*"&lt;/code&gt; para debuggear algo. Después nadie limpia.&lt;/p&gt;

&lt;p&gt;El problema con &lt;code&gt;include: "*"&lt;/code&gt; no es solo lo que expone hoy — es que expone automáticamente cualquier endpoint que se agregue en el futuro, incluyendo endpoints de librerías de terceros que se integran con Actuator. Spring Security, Micrometer, Spring Cloud y otros frameworks pueden registrar sus propios endpoints bajo &lt;code&gt;/actuator/&lt;/code&gt;. Si tenés &lt;code&gt;include: "*"&lt;/code&gt;, se exponen solos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Lo que deberías tener en lugar de "*"&lt;/span&gt;
&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Listado explícito: sabés exactamente qué está expuesto&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;health,info,metrics"&lt;/span&gt;
        &lt;span class="c1"&gt;# Todo lo demás queda bloqueado por defecto&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Mostrá detalles solo a usuarios autenticados&lt;/span&gt;
      &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;when_authorized&lt;/span&gt;
  &lt;span class="c1"&gt;# Separar el puerto de management del puerto de la app&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese último punto — &lt;code&gt;management.server.port&lt;/code&gt; — es el cambio más práctico que podés hacer. Si Actuator corre en un puerto separado, podés protegerlo a nivel de red (firewall, security group, nginx) sin tocar la lógica de la aplicación. El puerto 8081 nunca llega al load balancer público. El 8080 sí.&lt;/p&gt;

&lt;p&gt;Otro error común: confiar en que Spring Security protege Actuator automáticamente. Por defecto, si tenés Spring Security en el classpath, los endpoints de Actuator quedan protegidos con las mismas reglas del resto de la app — pero eso no siempre es suficiente. Vale la pena ser explícito:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SecurityConfig.java&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;securityFilterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
            &lt;span class="c1"&gt;// Solo /health y /info sin autenticación&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/actuator/info"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// El resto de actuator requiere rol ADMIN&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto es más robusto que depender del comportamiento implícito, y además es documentación viva: quien lea la config entiende exactamente qué está protegido y cómo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist de decisión antes de agregar Actuator a un proyecto
&lt;/h2&gt;

&lt;p&gt;Antes de hacer el primer deploy con Actuator habilitado, estas son las preguntas que vale la pena responder:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sobre exposición:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] ¿Listaste explícitamente qué endpoints querés exponer? ¿O usaste &lt;code&gt;"*"&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;[ ] ¿Actuator corre en un puerto separado del puerto de la aplicación?&lt;/li&gt;
&lt;li&gt;[ ] ¿Ese puerto de management está bloqueado para tráfico externo?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sobre autenticación:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] ¿Los endpoints sensibles (&lt;code&gt;env&lt;/code&gt;, &lt;code&gt;beans&lt;/code&gt;, &lt;code&gt;heapdump&lt;/code&gt;) requieren autenticación?&lt;/li&gt;
&lt;li&gt;[ ] ¿&lt;code&gt;health&lt;/code&gt; expone detalles (&lt;code&gt;show-details&lt;/code&gt;) sin restricción? ¿Querés eso?&lt;/li&gt;
&lt;li&gt;[ ] ¿Tenés Spring Security configurado explícitamente para &lt;code&gt;/actuator/**&lt;/code&gt;, o dependés del comportamiento por defecto?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sobre información expuesta:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] ¿Revisaste qué devuelve &lt;code&gt;/actuator/env&lt;/code&gt; en el ambiente donde va a correr?&lt;/li&gt;
&lt;li&gt;[ ] ¿Hay variables de entorno con credenciales que aparecen en ese endpoint?&lt;/li&gt;
&lt;li&gt;[ ] ¿&lt;code&gt;/actuator/info&lt;/code&gt; expone versiones, git commit o metadata que no querés pública?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sobre JMX:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Si no usás JMX, ¿lo deshabilitaste?
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Deshabilitar JMX si no lo usás&lt;/span&gt;
&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jmx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este checklist no garantiza seguridad total — eso depende del contexto completo de la aplicación, la red y el ambiente. Pero cubre las decisiones que más frecuentemente quedan sin tomar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Qué no podés concluir sin datos propios
&lt;/h2&gt;

&lt;p&gt;Hay algo que esta guía no puede hacer por vos: decirte qué endpoints específicamente necesitás en producción.&lt;/p&gt;

&lt;p&gt;Eso depende de cómo monitoreas la aplicación. Si usás Prometheus + Grafana, &lt;code&gt;/actuator/prometheus&lt;/code&gt; es útil — pero si usás Datadog con el agente instalado, probablemente no necesitás exponerlo en absoluto. Si tenés un load balancer que hace health checks, necesitás &lt;code&gt;/actuator/health&lt;/code&gt; — pero si usás Kubernetes con liveness/readiness probes apuntando a un endpoint propio, podés separar eso de Actuator completamente.&lt;/p&gt;

&lt;p&gt;También: el comportamiento de &lt;code&gt;show-details: when_authorized&lt;/code&gt; depende de cómo Spring Security está configurado en ese contexto. Sin revisar la configuración real de seguridad de la app, ese setting puede comportarse diferente a lo esperado.&lt;/p&gt;

&lt;p&gt;Lo que sí podés hacer hoy: arrancar con exposición mínima y agregar endpoints con intención, no con &lt;code&gt;"*"&lt;/code&gt;. Es más fácil agregar que quitar — especialmente si ya hay ambientes que dependen del estado actual.&lt;/p&gt;

&lt;p&gt;Una referencia útil si estás evaluando la integración con observabilidad: en el post sobre &lt;a href="https://juanchi.dev/es/blog/docker-healthcheck-buenas-practicas-que-miden" rel="noopener noreferrer"&gt;Docker healthchecks&lt;/a&gt; hay un análisis de qué miden realmente los checks de disponibilidad y qué no deberían prometer — aplica directamente si estás decidiendo si Actuator &lt;code&gt;/health&lt;/code&gt; es suficiente para tus probes o necesitás algo más granular.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: Spring Boot Actuator y seguridad de endpoints
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Qué endpoints expone Actuator por defecto vía HTTP?&lt;/strong&gt;&lt;br&gt;
Solo &lt;code&gt;/actuator/health&lt;/code&gt; está expuesto por HTTP en la configuración por defecto. El resto requiere declaración explícita en &lt;code&gt;management.endpoints.web.exposure.include&lt;/code&gt;. Podés verificarlo en la &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;documentación oficial&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;include: "*"&lt;/code&gt; es siempre un problema?&lt;/strong&gt;&lt;br&gt;
En localhost para desarrollo, no. En un ambiente compartido, staging con red semipública o producción, sí — porque expone endpoints actuales y futuros sin revisión. El riesgo no es solo lo que exponés hoy, sino lo que se agrega automáticamente cuando actualizás dependencias.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Spring Security protege Actuator automáticamente si está en el classpath?&lt;/strong&gt;&lt;br&gt;
Parcialmente. Spring Security aplica las reglas de seguridad de la aplicación a Actuator, pero el comportamiento concreto depende de cómo esté configurado. La práctica recomendada es ser explícito con un &lt;code&gt;requestMatchers("/actuator/**")&lt;/code&gt; en la configuración de seguridad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Vale la pena separar el puerto de Actuator del puerto de la aplicación?&lt;/strong&gt;&lt;br&gt;
Sí, y es probablemente el cambio de mayor impacto con menor complejidad. Con &lt;code&gt;management.server.port: 8081&lt;/code&gt;, podés proteger ese puerto a nivel de red sin tocar el código de la aplicación. El load balancer público solo ve el 8080.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;/actuator/health&lt;/code&gt; puede exponer información sensible?&lt;/strong&gt;&lt;br&gt;
Sí, si &lt;code&gt;show-details&lt;/code&gt; está en &lt;code&gt;always&lt;/code&gt;. En ese modo, el endpoint devuelve el estado de cada componente — base de datos, cache, servicios externos — con detalles que pueden revelar URLs internas o estados de dependencias. &lt;code&gt;show-details: when_authorized&lt;/code&gt; es el setting más conservador para ambientes expuestos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo sé qué endpoints registraron mis dependencias de terceros?&lt;/strong&gt;&lt;br&gt;
Podés consultarlo en runtime con una llamada a &lt;code&gt;/actuator&lt;/code&gt; (el endpoint raíz, si está expuesto), que devuelve el mapa de todos los endpoints disponibles. También podés habilitarlo solo en desarrollo con profiles de Spring: &lt;code&gt;@Profile("dev")&lt;/code&gt; en el bean de configuración o usando &lt;code&gt;spring.profiles.active&lt;/code&gt; para cargar un &lt;code&gt;application-dev.yml&lt;/code&gt; con &lt;code&gt;include: "*"&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mi postura final: Actuator como herramienta operativa, no como default
&lt;/h2&gt;

&lt;p&gt;Actuator es una de las partes mejor diseñadas del ecosistema Spring Boot. La separación entre habilitado/expuesto, la integración con Micrometer, el soporte nativo para health indicators personalizados — todo eso tiene valor operativo real.&lt;/p&gt;

&lt;p&gt;Lo que no tiene valor es agregarlo como "viene en el starter, por las dudas". Esa lógica funciona en desarrollo. En un ambiente con red real, es una superficie que crece sola cada vez que actualizás dependencias.&lt;/p&gt;

&lt;p&gt;Mi recomendación práctica: empezá con &lt;code&gt;include: "health,info"&lt;/code&gt;, separé el puerto de management desde el primer deploy, y agregá endpoints con intención cuando tengas un consumidor concreto — un dashboard de métricas, una herramienta de APM, un script de diagnóstico. Si no sabés quién consume el endpoint, probablemente no necesitás exponerlo todavía.&lt;/p&gt;

&lt;p&gt;Si estás construyendo un backend con decisiones de seguridad en capas, puede ser útil revisar también el post sobre &lt;a href="https://juanchi.dev/es/blog/arquitectura-backend-identidad-digital-jwt-oauth" rel="noopener noreferrer"&gt;arquitectura backend de identidad digital&lt;/a&gt; — el criterio de "qué exponer y con qué restricciones" aplica en ambos contextos con lógica similar.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Boot Actuator — Endpoints: &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-boot/reference/actuator/endpoints.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/spring-boot-actuator-endpoints-seguridad" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>backend</category>
      <category>produccion</category>
    </item>
  </channel>
</rss>
