<?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: TheKitBase</title>
    <description>The latest articles on DEV Community by TheKitBase (@thekitbase).</description>
    <link>https://dev.to/thekitbase</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3953758%2F8055ca87-ed42-4f3d-8088-c7ef6426cba6.png</url>
      <title>DEV Community: TheKitBase</title>
      <link>https://dev.to/thekitbase</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thekitbase"/>
    <language>en</language>
    <item>
      <title>How to Cut Your AI/LLM Costs in 2026: Caching, Cheaper Models, and Multi-Agent Routing</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Thu, 23 Jul 2026 06:57:17 +0000</pubDate>
      <link>https://dev.to/thekitbase/how-to-cut-your-aillm-costs-in-2026-caching-cheaper-models-and-multi-agent-routing-41dp</link>
      <guid>https://dev.to/thekitbase/how-to-cut-your-aillm-costs-in-2026-caching-cheaper-models-and-multi-agent-routing-41dp</guid>
      <description>&lt;p&gt;AI features ship fast and then the bill arrives. The good news: most LLM spend is avoidable waste - the same prompt paid for a thousand times, a frontier model doing work a cheap one could handle, tokens generated that nobody reads. Here are six levers that cut real money, ordered by how much they typically save, with numbers and code you can paste today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 1: Prompt caching (the biggest single win)
&lt;/h2&gt;

&lt;p&gt;Most AI apps resend the same large prefix on every request - a system prompt, tool definitions, a knowledge base, few-shot examples. That prefix is often 80% of your input tokens, and you are paying full price for it every single time. Prompt caching bills the cached prefix at roughly one tenth of the input price on a hit. Writing to the cache costs about 1.25x, so the break-even is two requests - after that it is close to 90% off the cached portion.&lt;/p&gt;

&lt;p&gt;The rule is that caching is a prefix match: anything before your cache breakpoint must be byte-identical across requests. Put the stable content first (frozen system prompt, deterministic tool list) and the volatile content (the user's actual question, timestamps, per-request ids) last. A single &lt;code&gt;datetime.now()&lt;/code&gt; interpolated into the system prompt invalidates the entire cache on every request - the most common reason a cache hit rate sits at zero.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fastest audit: log &lt;code&gt;cache_read_input_tokens&lt;/code&gt; on every response. If it is zero across repeated requests with the same system prompt, a silent invalidator (a timestamp, a random id, unsorted JSON) is in your prefix. Find it and you just cut input cost by up to 90%.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Lever 2: Right-size the model
&lt;/h2&gt;

&lt;p&gt;The reflex to route everything to the smartest, most expensive model is the second biggest source of waste. Model tiers differ by 5x or more in price, and most requests in a real app - classification, extraction, short answers, routing - do not need the top tier. The trick is matching the model to the job, not to your ambition.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Rough price / 1M tokens (in / out)&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small (Haiku-class)&lt;/td&gt;
&lt;td&gt;~$1 / ~$5&lt;/td&gt;
&lt;td&gt;Classification, extraction, routing, short replies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid (Sonnet-class)&lt;/td&gt;
&lt;td&gt;~$3 / ~$15&lt;/td&gt;
&lt;td&gt;Most product features, tool-calling agents, coding help&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontier (Opus-class)&lt;/td&gt;
&lt;td&gt;~$5 / ~$25&lt;/td&gt;
&lt;td&gt;Hard reasoning, long-horizon agents, the few requests that need it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prices move, so check current rates - but the ratios hold: the small tier is roughly 5x cheaper than frontier for input and output alike. And do not sleep on open-source models. Llama, Mistral, DeepSeek, and Qwen are available through the same gateway, and for high-volume, well-scoped tasks (summarization, extraction, internal tools) they can undercut hosted frontier models dramatically. Route the boring 80% of your traffic to a cheap or open model and reserve the expensive tier for the requests that actually need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 3: Multi-agent routing (the cascade pattern)
&lt;/h2&gt;

&lt;p&gt;Levers 1 and 2 combine into the highest-leverage architecture: a cheap model classifies each incoming request, and only the genuinely hard ones escalate to an expensive model. This is the cascade - a fast, cheap triage step in front of your real work. In most apps the classifier costs a rounding error and diverts the majority of traffic away from the frontier tier.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateText&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;ai&lt;/span&gt;&lt;span class="dl"&gt;'&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;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userMessage&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="c1"&gt;// 1. A cheap model triages the request&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;difficulty&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;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic/claude-haiku-4.5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Classify as SIMPLE or HARD. Reply with one word only.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Route to the tier that fits&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&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="s1"&gt;HARD&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;anthropic/claude-sonnet-4.6&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;anthropic/claude-haiku-4.5&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;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;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userMessage&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same idea scales up: specialized agents per job (a cheap extractor, a mid-tier writer, a frontier planner) instead of one expensive generalist doing everything. You pay top-tier prices only for the steps that are genuinely top-tier work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 4: Response and semantic caching
&lt;/h2&gt;

&lt;p&gt;Prompt caching (Lever 1) discounts the shared prefix. Response caching skips the model call entirely when the same request comes in again. For static-knowledge queries - FAQs, policy lookups, translations, extraction from identical documents - cache the whole response at the gateway and serve repeats for free.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateText&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;ai&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;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;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai/gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarize our refund policy in two sentences.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;providerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cacheControl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max-age=3600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// serve repeats for 1 hour&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 cache key is the model plus the prompt plus the generation parameters, so identical requests hit and anything that varies (a real conversation) misses and is billed normally. Do not cache user-specific conversations; do cache the repetitive, deterministic lookups that make up a surprising share of most apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 5: Gateway-level cost controls
&lt;/h2&gt;

&lt;p&gt;Routing every call through a single gateway is what makes the levers above governable instead of theoretical. From one place you get spend attribution by feature and user, automatic failover to a cheaper provider or model when the primary is down or rate-limited, per-user rate limits so one abuser cannot run up your bill, and budget alerts before you blow the month.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gateway&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;ai&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;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;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic/claude-sonnet-4.6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;providerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;order&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;anthropic&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;bedrock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;       &lt;span class="c1"&gt;// failover order&lt;/span&gt;
      &lt;span class="na"&gt;models&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;openai/gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;            &lt;span class="c1"&gt;// fallback model if the primary fails&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="c1"&gt;// per-user tracking and rate limits&lt;/span&gt;
      &lt;span class="na"&gt;tags&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;feature:support&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;env:prod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// cost attribution&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;Because the gateway charges provider list price with zero markup, you get all of this - failover, tracking, per-user limits, budgets - without paying more per token than calling the provider directly. Tags turn a mystery invoice into a per-feature cost breakdown, which is how you find the one endpoint quietly burning half your budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 6: Token hygiene
&lt;/h2&gt;

&lt;p&gt;The unglamorous lever that adds up. You pay per token in and per token out, so trim both.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use structured output (a schema) instead of asking for JSON in prose - shorter, parseable, and no wasted tokens explaining the format.&lt;/li&gt;
&lt;li&gt;Cap output with &lt;code&gt;maxOutputTokens&lt;/code&gt;. A model with no ceiling will happily write three paragraphs where you needed one sentence.&lt;/li&gt;
&lt;li&gt;Trim the context you resend - old tool results and stale history are pure cost. Summarize or drop them.&lt;/li&gt;
&lt;li&gt;For anything not real-time (nightly reports, bulk classification, embeddings backfills), use the Batch API - it runs asynchronously at roughly half price.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;None of these are exotic. Stacked, they routinely take an AI feature from alarming to boring on the cost side. A representative before-and-after for a support assistant handling a mix of simple and hard questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Relative cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Everything to the frontier model, full prefix every request&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ prompt caching on the system prompt and tools&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ cascade routing (cheap triage, escalate only hard ones)&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ response caching on repeated lookups + output caps&lt;/td&gt;
&lt;td&gt;~10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The exact numbers depend on your traffic mix, but the shape is real: the same product, an order of magnitude cheaper to run, with no drop in quality on the requests that actually matter - because the frontier model still handles those. The waste you cut was never doing useful work.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://thekitbase.app/templates/keel-saas-starter-kit" rel="noopener noreferrer"&gt;Keel&lt;/a&gt;, our AI SaaS starter kit, is built cost-aware from the start - gateway routing, model fallbacks, and a streaming assistant wired so you can swap tiers with a string. Start from an architecture that already respects your bill.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>webdev</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Tool Calling with the AI SDK in Next.js: Build an Assistant That Actually Does Things</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Thu, 23 Jul 2026 06:55:20 +0000</pubDate>
      <link>https://dev.to/thekitbase/tool-calling-with-the-ai-sdk-in-nextjs-build-an-assistant-that-actually-does-things-3kh2</link>
      <guid>https://dev.to/thekitbase/tool-calling-with-the-ai-sdk-in-nextjs-build-an-assistant-that-actually-does-things-3kh2</guid>
      <description>&lt;p&gt;A chat box that only answers questions is a demo. The moment your AI feature has to look something like a user's orders, book a slot, or update a record, you need tool calling - letting the model call functions you define, read the results, and keep going until the task is done. This guide builds that loop with the Vercel AI SDK v6 in Next.js, and it is shorter than you would expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;You give the model a set of tools - each one a description, an input schema, and a function to run. The model decides which tools to call and with what arguments; the SDK runs your functions, feeds the results back, and loops until the model produces a final answer. You never write the orchestration loop by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: one package, gateway model strings
&lt;/h2&gt;

&lt;p&gt;Install the &lt;code&gt;ai&lt;/code&gt; package. For the model, pass a plain provider/model string and it routes through the Vercel AI Gateway automatically - no provider SDK, no API key juggling, and you can swap models by changing the string. Gateway slugs use dots for versions (&lt;code&gt;anthropic/claude-sonnet-4.6&lt;/code&gt;), which is the one naming gotcha to remember.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/chat/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateText&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;ai&lt;/span&gt;&lt;span class="dl"&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;POST&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;Request&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;prompt&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="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;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic/claude-sonnet-4.6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// routes through AI Gateway&lt;/span&gt;
    &lt;span class="nx"&gt;prompt&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&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="nx"&gt;text&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;
  
  
  Defining a tool
&lt;/h2&gt;

&lt;p&gt;A tool is a description (so the model knows when to reach for it), an &lt;code&gt;inputSchema&lt;/code&gt; built with Zod (so arguments are validated and typed), and an &lt;code&gt;execute&lt;/code&gt; function (your actual code). Be prescriptive in the description about when to call it - not just what it does.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tool&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;ai&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;z&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;zod&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;getOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Look up a customer's recent orders. Call this whenever the user asks about order status, history, or a specific purchase.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The customer id from the session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;How many orders to return&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;execute&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;customerId&lt;/span&gt;&lt;span class="p"&gt;,&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;5&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;orders&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&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="nx"&gt;customerId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&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;orders&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;In AI SDK v6 the field is &lt;code&gt;inputSchema&lt;/code&gt;, not &lt;code&gt;parameters&lt;/code&gt;. If you are following an older tutorial that uses &lt;code&gt;parameters&lt;/code&gt;, it will not typecheck - the rename is the single most common upgrade error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The multi-step loop
&lt;/h2&gt;

&lt;p&gt;One tool call is rarely the whole task. The model might call &lt;code&gt;getOrders&lt;/code&gt;, read the result, then decide it also needs to check inventory before answering. &lt;code&gt;stopWhen&lt;/code&gt; lets the SDK run that back-and-forth automatically - it keeps looping (call tool, feed result back, let the model continue) until the model stops calling tools or you hit the step cap.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stepCountIs&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;ai&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;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;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic/claude-sonnet-4.6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// cap the loop at 5 steps&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;checkInventory&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Where is my last order and is a replacement in stock?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// result.text is the final answer after all tool calls resolved&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;log&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;stepCountIs(5)&lt;/code&gt; is your safety belt. Without a cap, a confused model could loop indefinitely. Five is a reasonable default for most assistants; raise it for genuinely multi-hop tasks, lower it for simple lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming the whole thing to the UI
&lt;/h2&gt;

&lt;p&gt;For a chat interface you want tokens and tool activity to stream, not arrive in one blob. Swap &lt;code&gt;generateText&lt;/code&gt; for &lt;code&gt;streamText&lt;/code&gt; - same tools, same &lt;code&gt;stopWhen&lt;/code&gt; - and pipe it to the client. The user sees the assistant think, call a tool, and answer in real time.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stepCountIs&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;ai&lt;/span&gt;&lt;span class="dl"&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;POST&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;Request&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;messages&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="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="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic/claude-sonnet-4.6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;checkInventory&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;messages&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUIMessageStreamResponse&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;
  
  
  Gating the dangerous tools
&lt;/h2&gt;

&lt;p&gt;Read-only tools (look up orders, search docs) can run automatically. Tools with side effects - refund a payment, delete a record, send an email - should not fire without a human in the loop. The cleanest pattern is to make the &lt;code&gt;execute&lt;/code&gt; function return a proposal instead of performing the action, then confirm on the client before a second call actually commits it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate every argument at the boundary - the model's output is untrusted input, so treat &lt;code&gt;customerId&lt;/code&gt; like anything else a user could send.&lt;/li&gt;
&lt;li&gt;For destructive actions, have &lt;code&gt;execute&lt;/code&gt; return what it would do and surface a confirm step in the UI before committing.&lt;/li&gt;
&lt;li&gt;Scope your tools to the current session - never let the model pass in a &lt;code&gt;customerId&lt;/code&gt; that is not the authenticated user.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  From tools to a reusable agent
&lt;/h2&gt;

&lt;p&gt;Once you have a set of tools you reuse across routes, wrap them in an agent instead of re-declaring the config each time. The AI SDK's &lt;code&gt;ToolLoopAgent&lt;/code&gt; pattern bundles the model, tools, and &lt;code&gt;stopWhen&lt;/code&gt; into one object you can call from anywhere and type end-to-end. Same loop, less repetition.&lt;/p&gt;

&lt;p&gt;The whole point of tool calling: your assistant stops being a search box with personality and becomes a teammate that can actually operate your product - safely, because you define exactly what it can touch.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://thekitbase.app/templates/keel-saas-starter-kit" rel="noopener noreferrer"&gt;Keel&lt;/a&gt;, our AI SaaS starter kit, ships with a streaming assistant, tool-calling wired through the AI Gateway, and the auth and billing around it - so you start from a working agent instead of a blank route handler.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Next.js 16 Cache Components: use cache, PPR, and When to Reach for Each</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Thu, 23 Jul 2026 06:46:38 +0000</pubDate>
      <link>https://dev.to/thekitbase/nextjs-16-cache-components-use-cache-ppr-and-when-to-reach-for-each-503e</link>
      <guid>https://dev.to/thekitbase/nextjs-16-cache-components-use-cache-ppr-and-when-to-reach-for-each-503e</guid>
      <description>&lt;p&gt;Next.js 16 shipped Cache Components - the feature that finally lets a single route mix static HTML, cached data, and per-request dynamic content without splitting it into separate pages. It is Partial Prerendering (PPR) made stable, plus a new &lt;code&gt;use cache&lt;/code&gt; directive that replaces the old &lt;code&gt;unstable_cache&lt;/code&gt; and the awkward route-segment config flags. This guide covers what changed, the three content types you now think in, and the runtime-data rule that trips up almost everyone on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;If you were using the experimental PPR flag, it is gone. Cache Components is a single config switch, and it turns on the whole model - static shell, cached segments, and streamed dynamic content in one route.&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;// next.config.ts&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;NextConfig&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&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;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&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="c1"&gt;// replaces experimental.ppr&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="nx"&gt;nextConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it is on, every piece of your route falls into one of three buckets. The whole mental model is learning which bucket each component belongs in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three content types
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static&lt;/strong&gt; - synchronous code, imports, and pure markup. Prerendered at build time and served instantly from the CDN. Your header, nav, and layout shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cached&lt;/strong&gt; - async data that does not need to be fresh on every request. Marked with &lt;code&gt;use cache&lt;/code&gt;. Think product lists, blog posts, dashboard stats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic&lt;/strong&gt; - runtime data that must be fresh (cookies, headers, per-user state). Wrapped in Suspense so it streams in after the shell paints.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&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;cookies&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/headers&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;cacheLife&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/cache&lt;/span&gt;&lt;span class="dl"&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;DashboardPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Static - instant from the CDN */&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;header&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Dashboard&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;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&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;/* Cached - fast, revalidates hourly */&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;Stats&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;/* Dynamic - streams in with fresh data */&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;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;NotificationsSkeleton&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;Notifications&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Stats&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;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hours&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;stats&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&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="nc"&gt;StatsDisplay&lt;/span&gt; &lt;span class="na"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Notifications&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;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;userId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notifications&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="nx"&gt;userId&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NotificationList&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff: the user sees the static shell and cached stats instantly, and the personalized notifications stream in a beat later - instead of the whole page blocking on the slowest query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The use cache directive
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use cache&lt;/code&gt; works at three levels - the whole file, a single component, or a plain data function. Put it at the top of whatever scope you want cached, and Next.js generates the cache key automatically from the function's arguments and closure variables. No manual key arrays like the old unstable_cache required.&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;// Function level - cache a query&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;getPosts&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;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Component level - cache a whole subtree&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;PricingTable&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;use cache&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;plans&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plans&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Plans&lt;/span&gt; &lt;span class="na"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;plans&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Controlling lifetime with cacheLife
&lt;/h2&gt;

&lt;p&gt;By default a &lt;code&gt;use cache&lt;/code&gt; block is stale after 5 minutes and revalidates after 15. Override that with a built-in profile or an inline config. The built-in profiles are default, minutes, hours, days, weeks, and max.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&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/cache&lt;/span&gt;&lt;span class="dl"&gt;'&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;getData&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;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// serve stale for 1 hour while revalidating&lt;/span&gt;
    &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// refresh in the background every 2 hours&lt;/span&gt;
    &lt;span class="na"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// hard expiry after 1 day&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Invalidation: cacheTag, updateTag, revalidateTag
&lt;/h2&gt;

&lt;p&gt;Tag your cached data, then invalidate by tag from a Server Action. The one distinction that matters: &lt;code&gt;updateTag&lt;/code&gt; refreshes within the same request (the user who just edited sees fresh data immediately), while &lt;code&gt;revalidateTag&lt;/code&gt; is background-only (the next visitor sees fresh data). Reach for &lt;code&gt;updateTag&lt;/code&gt; after a mutation the current user made; reach for &lt;code&gt;revalidateTag&lt;/code&gt; for everyone else's writes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&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/cache&lt;/span&gt;&lt;span class="dl"&gt;'&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;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&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;product-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;id&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&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="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In a Server Action:&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&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;updateTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&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/cache&lt;/span&gt;&lt;span class="dl"&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;editProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// same request sees the change&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// everyone else's next load is fresh&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The rule that trips everyone up
&lt;/h2&gt;

&lt;p&gt;You cannot call &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or read &lt;code&gt;searchParams&lt;/code&gt; inside a &lt;code&gt;use cache&lt;/code&gt; block. It makes sense once you say it out loud - cached output has to be identical for everyone, and those APIs are per-request. The fix is to read the dynamic value outside the cached function and pass it in as an argument, where it becomes part of the cache key automatically.&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;// Wrong - runtime API inside use cache&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;CachedProfile&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;use cache&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;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;// Error&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;div&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;session&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;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Right - read outside, pass in as an argument&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;ProfilePage&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;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;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CachedProfile&lt;/span&gt; &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CachedProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;sessionId&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&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;data&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;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// sessionId is part of the key&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;div&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;data&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;div&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 you genuinely cannot refactor (a compliance wrapper, say), &lt;code&gt;use cache: private&lt;/code&gt; is the escape hatch - it permits runtime APIs and caches per-user. Use it sparingly; it is not a shared cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from the old world
&lt;/h2&gt;

&lt;p&gt;If you are coming from an earlier Next.js app, most of the migration is deleting things. Here is the direct mapping.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old&lt;/th&gt;
&lt;th&gt;Next.js 16 replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;experimental.ppr&lt;/td&gt;
&lt;td&gt;cacheComponents: true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;export const dynamic = 'force-dynamic'&lt;/td&gt;
&lt;td&gt;Remove - it is the default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;export const dynamic = 'force-static'&lt;/td&gt;
&lt;td&gt;'use cache' + cacheLife('max')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;export const revalidate = N&lt;/td&gt;
&lt;td&gt;cacheLife({ revalidate: N })&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unstable_cache()&lt;/td&gt;
&lt;td&gt;'use cache' directive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The unstable_cache migration is the big one, and it is mostly deletion: no more manual key arrays, tags move from an options object into &lt;code&gt;cacheTag()&lt;/code&gt; calls, and revalidate moves into &lt;code&gt;cacheLife()&lt;/code&gt;. The same cookies/headers restriction that applied to unstable_cache applies to &lt;code&gt;use cache&lt;/code&gt;, so any code that already respected it ports cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations worth knowing up front
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js runtime only - Cache Components does not run on the Edge runtime.&lt;/li&gt;
&lt;li&gt;No static export - you need a server, so &lt;code&gt;next export&lt;/code&gt; is out.&lt;/li&gt;
&lt;li&gt;Non-deterministic values (Math.random, Date.now) run once at build time inside a cached block. For per-request randomness, call &lt;code&gt;connection()&lt;/code&gt; from next/server to defer to request time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to reach for each
&lt;/h2&gt;

&lt;p&gt;The decision is almost always about freshness. Does the data change per-user or per-request? Dynamic, wrapped in Suspense. Does it change occasionally and can be a few minutes stale? Cached with &lt;code&gt;use cache&lt;/code&gt; and a cacheLife that matches your tolerance. Never changes between deploys? Leave it static. Get those three buckets right and you get CDN-fast first paint with correct, fresh data streaming in - which used to require picking one or the other.&lt;/p&gt;




&lt;p&gt;Every &lt;a href="https://thekitbase.app/templates" rel="noopener noreferrer"&gt;TheKitBase&lt;/a&gt; template is built on Next.js 16 with this rendering model already wired up - static shells, cached data layers, and streamed dynamic content, so you inherit the fast-by-default architecture instead of retrofitting it.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>The Best Free Next.js Templates in 2026 (Sorted by Use Case)</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:14:16 +0000</pubDate>
      <link>https://dev.to/thekitbase/the-best-free-nextjs-templates-in-2026-sorted-by-use-case-85b</link>
      <guid>https://dev.to/thekitbase/the-best-free-nextjs-templates-in-2026-sorted-by-use-case-85b</guid>
      <description>&lt;p&gt;There are a lot of free Next.js templates out there, and most "best free templates" lists are just a wall of links with no guidance. This one is sorted by what you're actually building, with the genuinely good free options in each category and an honest note on where free stops being enough. Every pick here is free to use; the premium upgrades are called out where they save real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find free templates first
&lt;/h2&gt;

&lt;p&gt;Before any list, know the three sources that cover most needs and are always current because their maintainers keep them updated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The official Next.js examples repo&lt;/strong&gt; - dozens of minimal starters for specific integrations (auth, CMS, databases, i18n), maintained alongside the framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Vercel Templates directory&lt;/strong&gt; - a large gallery of free and paid templates you can deploy in one click&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadcn/ui&lt;/strong&gt; - not a template but a component collection you copy into your project; the fastest way to a good-looking UI from scratch&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  For marketing sites and landing pages
&lt;/h2&gt;

&lt;p&gt;Free marketing starters get you a hero, features, and a footer, which is enough to validate an idea. What they rarely give you is a complete, cohesive multi-page site with blog, pricing, and auth designed as one system. For a quick landing page, the Vercel and Tailwind free starters are fine; when you need a finished marketing site, that's where a premium template earns its price.&lt;/p&gt;

&lt;h2&gt;
  
  
  For dashboards and admin panels
&lt;/h2&gt;

&lt;p&gt;shadcn/ui ships an excellent free dashboard example, and it's the best free starting point for an admin UI - charts, tables, and a sidebar you can build on. The gap with free dashboards is depth: you typically get one or two screens, not the 9-12 cohesive pages a real admin panel needs (billing, team, settings, reports). Free gets you the shell; a production template gets you the whole app.&lt;/p&gt;

&lt;h2&gt;
  
  
  For e-commerce
&lt;/h2&gt;

&lt;p&gt;Next.js Commerce, Vercel's open-source storefront, is the standout free e-commerce starter - a real, deployable store wired for a headless backend. It's an excellent foundation if you're comfortable connecting a commerce backend. For a simpler, static storefront with a working cart and no backend to configure, a focused template will be faster to launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  For docs and content
&lt;/h2&gt;

&lt;p&gt;Nextra is the go-to free documentation framework on Next.js - MDX, search, and a clean default theme, all open-source. For blogs and content sites, the official Next.js blog examples and any of the many free MDX starters will do the job. Docs is one area where free options are genuinely strong.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI apps
&lt;/h2&gt;

&lt;p&gt;Vercel's open-source AI Chatbot template is the best free starting point for anything with a streaming AI interface - it's the reference implementation for the AI SDK and stays current with it. Open SaaS (on the Wasp framework) is a capable free full-stack option with AI examples. Both are great to learn from; neither is a complete, polished SaaS with the product shell around the AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  For portfolios
&lt;/h2&gt;

&lt;p&gt;Portfolios are the most abundant category of free template - GitHub is full of open-source Next.js portfolio starters, and many are genuinely good. If you want something that stands out from the common grey-on-white developer portfolio, that's where a designed premium template is worth it, but for a solid free portfolio you're spoiled for choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  When free is enough (and when it isn't)
&lt;/h2&gt;

&lt;p&gt;Free templates are perfect for learning, prototyping, side projects, and anything where your time is worth less than the money saved. They stop being enough when you're shipping something people pay for and your time matters: free templates are usually shallow (a few screens, not a whole product), inconsistent (assembled from parts rather than designed as a system), and yours to finish. A good premium template is the finished system - which is the difference between a weekend of setup and an afternoon of shipping.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rule of thumb: use free templates to learn and prototype; buy a premium one the moment your time is worth more than the price. A $79 template that saves a week is one of the cheapest hours-back trades in software.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates" rel="noopener noreferrer"&gt;When free stops being enough - see TheKitBase templates from $39&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>resources</category>
    </item>
    <item>
      <title>The Ultimate Next.js SaaS Tech Stack for 2026</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:10:27 +0000</pubDate>
      <link>https://dev.to/thekitbase/the-ultimate-nextjs-saas-tech-stack-for-2026-15mn</link>
      <guid>https://dev.to/thekitbase/the-ultimate-nextjs-saas-tech-stack-for-2026-15mn</guid>
      <description>&lt;p&gt;The hardest part of starting a SaaS in 2026 isn't writing code - it's choosing the stack. Every layer has five defensible options and a dozen strong opinions. This is an opinionated, boringly reliable stack for building a Next.js SaaS: the choices that let you ship fast, scale sanely, and not rewrite everything in six months. Where there's a real fork, I'll name the alternative and when to take it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Strong alternative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js (App Router)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript (strict)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS v4&lt;/td&gt;
&lt;td&gt;CSS Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Components&lt;/td&gt;
&lt;td&gt;shadcn/ui&lt;/td&gt;
&lt;td&gt;Build your own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Auth.js&lt;/td&gt;
&lt;td&gt;Clerk (managed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Postgres (Neon) + Drizzle&lt;/td&gt;
&lt;td&gt;Supabase / Prisma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;Dodo / Lemon Squeezy (MoR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Vercel AI SDK + Claude&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;td&gt;Postmark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Framework, language, and styling
&lt;/h2&gt;

&lt;p&gt;Next.js with the App Router is the default for a reason: server components, server actions, and a single codebase for your marketing site and app. TypeScript in strict mode is non-negotiable for anything you'll maintain - the compile-time safety pays for itself the first time you rename a field. Tailwind CSS v4 for styling, with its &lt;code&gt;@theme&lt;/code&gt; token system, makes an entire product reskinnable from a handful of variables and keeps styles colocated with markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth and database
&lt;/h2&gt;

&lt;p&gt;For auth, Auth.js (formerly NextAuth) is the flexible, self-hosted default; reach for Clerk when you'd rather pay to never think about auth UI, sessions, and organizations again. For data, Postgres is the safe forever-choice - Neon and Supabase both give you serverless Postgres with generous free tiers. Pair it with Drizzle (typed, SQL-first, light) or Prisma (batteries-included, more magic). The through-line: pick managed Postgres and a typed query layer, and you won't outgrow it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payments
&lt;/h2&gt;

&lt;p&gt;Stripe is the default and the most capable, and the right pick if you're comfortable handling sales tax and VAT yourself (or with a tax add-on). If you'd rather offload tax and compliance entirely, a merchant-of-record provider like Dodo Payments or Lemon Squeezy handles the tax paperwork globally in exchange for a slightly higher fee - often worth it for a small team selling internationally. Decide based on whether you want to own tax compliance or rent it.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI, email, and hosting
&lt;/h2&gt;

&lt;p&gt;If your SaaS has an AI feature, the Vercel AI SDK is the standard way to build it - one API across providers, first-class streaming, and an easy swap between models. Point it at Claude for strong reasoning and a clean developer experience. For transactional email (receipts, invites, password resets), Resend is the modern default with a great DX and React-based email templates. And for hosting, Vercel is the path of least resistance for Next.js - zero-config deploys, preview URLs, and edge/serverless handled for you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; Vercel AI SDK + Claude - streaming, provider-agnostic, swap models in one line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; Resend - transactional email with React templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Vercel - zero-config Next.js deploys and preview URLs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics:&lt;/strong&gt; Vercel Analytics or PostHog when you need product analytics + flags&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The best stack is the one you don't have to think about. Every pick here is chosen to be reliable and swappable, not clever - so you spend your energy on the product, not on infrastructure decisions you'll second-guess at 2am.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates/keel-saas-starter-kit" rel="noopener noreferrer"&gt;Skip the setup - TheKitBase templates ship on exactly this stack&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>saas</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Next.js vs Astro in 2026: Which Should You Actually Use?</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:03:34 +0000</pubDate>
      <link>https://dev.to/thekitbase/nextjs-vs-astro-in-2026-which-should-you-actually-use-ke3</link>
      <guid>https://dev.to/thekitbase/nextjs-vs-astro-in-2026-which-should-you-actually-use-ke3</guid>
      <description>&lt;p&gt;Next.js and Astro get pitted against each other constantly, which is a little unfair because they were built for different jobs. Next.js is a full-stack React framework for building applications; Astro is a content-first framework for building fast, mostly-static sites. They overlap in the middle - marketing sites, blogs, docs - which is exactly where the choice gets interesting. Here's how they actually differ and a clear rule for deciding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core difference: apps vs. content
&lt;/h2&gt;

&lt;p&gt;Next.js assumes you're building an application - interactivity, authentication, dashboards, data that changes per user. It ships React, runs server components, supports server actions and dynamic rendering, and gives you a full backend surface. Astro assumes you're building content - articles, marketing pages, documentation - and optimizes ruthlessly for that: it ships zero JavaScript by default and only hydrates the interactive "islands" you explicitly mark.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Next.js&lt;/th&gt;
&lt;th&gt;Astro&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Built for&lt;/td&gt;
&lt;td&gt;Full-stack apps&lt;/td&gt;
&lt;td&gt;Content-first sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default JS shipped&lt;/td&gt;
&lt;td&gt;React runtime&lt;/td&gt;
&lt;td&gt;Zero (islands opt-in)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rendering&lt;/td&gt;
&lt;td&gt;SSR, SSG, ISR, RSC&lt;/td&gt;
&lt;td&gt;Static-first, SSR available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI framework&lt;/td&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;Any (React, Vue, Svelte) or none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Dashboards, SaaS, auth'd apps&lt;/td&gt;
&lt;td&gt;Blogs, docs, marketing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Performance: Astro's home turf
&lt;/h2&gt;

&lt;p&gt;For a content site with little interactivity, Astro is hard to beat on raw performance because it ships almost no JavaScript - the browser gets HTML and CSS and nothing to parse and execute. Next.js has closed much of the gap with React Server Components, which keep non-interactive components off the client, and a well-built Next.js content site scores excellently. But if your page is genuinely mostly static content, Astro's zero-JS default gives it a structural head start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Next.js pulls ahead
&lt;/h2&gt;

&lt;p&gt;The moment your product becomes an application - users log in, data is personalized, state is complex, you need a real backend - Next.js is the stronger choice. The App Router, server components, server actions, and the enormous React ecosystem are built for exactly this. Astro can do SSR and can embed React islands, but you'd be fighting its content-first grain to build a full SaaS in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule for picking
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Building a blog, docs site, or marketing site that's mostly content - &lt;strong&gt;Astro&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Building an app with auth, dashboards, and dynamic data - &lt;strong&gt;Next.js&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Building a SaaS whose marketing site and app should share one codebase and stack - &lt;strong&gt;Next.js&lt;/strong&gt; for both keeps it simple.&lt;/li&gt;
&lt;li&gt;Not sure and you might grow into an app - &lt;strong&gt;Next.js&lt;/strong&gt; is the safer long-term bet.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;It's not really a rivalry - it's a fork in the road. Content site: reach for Astro. Application: reach for Next.js. The trap is picking Astro because it benchmarks faster, then spending months bending it into an app it was never meant to be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates" rel="noopener noreferrer"&gt;Building an app? Start from a production Next.js foundation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Cursor vs Claude Code vs Copilot: The 2026 AI Coding Tools Compared</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Tue, 14 Jul 2026 12:59:24 +0000</pubDate>
      <link>https://dev.to/thekitbase/cursor-vs-claude-code-vs-copilot-the-2026-ai-coding-tools-compared-2fg</link>
      <guid>https://dev.to/thekitbase/cursor-vs-claude-code-vs-copilot-the-2026-ai-coding-tools-compared-2fg</guid>
      <description>&lt;p&gt;AI-assisted coding stopped being a novelty and became the default way most developers work. By 2026 three tools lead the space, and they're not really the same kind of thing: Cursor is an AI-first editor, Claude Code is an agentic terminal tool, and GitHub Copilot is an assistant that lives inside your existing IDE. They overlap enough to compete and differ enough that the right pick depends on how you actually work. Here's an honest breakdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quick version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Form factor&lt;/th&gt;
&lt;th&gt;Best at&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;AI-first editor (VS Code fork)&lt;/td&gt;
&lt;td&gt;Inline editing + in-context chat&lt;/td&gt;
&lt;td&gt;Multiple (incl. Claude, GPT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;Agentic terminal / CLI&lt;/td&gt;
&lt;td&gt;Autonomous multi-file changes&lt;/td&gt;
&lt;td&gt;Claude (Opus / Sonnet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;Plugin for your existing IDE&lt;/td&gt;
&lt;td&gt;Completions inside your current setup&lt;/td&gt;
&lt;td&gt;Multiple (incl. Claude, GPT)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Cursor - the AI-first editor
&lt;/h2&gt;

&lt;p&gt;Cursor is a fork of VS Code rebuilt around AI. You get everything familiar about VS Code plus deep AI integration: inline edits, a chat that knows your codebase, and an agent mode that can make multi-file changes while you watch. Because it's the editor itself, the AI has rich context about what you're looking at, and the feedback loop is tight - highlight code, describe a change, accept the diff.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best for developers who want AI woven into the editor, not bolted on&lt;/li&gt;
&lt;li&gt;Strong inline editing and codebase-aware chat&lt;/li&gt;
&lt;li&gt;You switch editors (from VS Code) to use it&lt;/li&gt;
&lt;li&gt;Model choice, with usage-based and subscription pricing tiers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Claude Code - the agentic terminal tool
&lt;/h2&gt;

&lt;p&gt;Claude Code takes a different shape entirely: it lives in your terminal and works agentically. You describe a task in natural language and it explores the codebase, makes changes across many files, runs commands, and iterates - closer to delegating to a capable pair than autocompleting your typing. It's editor-agnostic (your files are just files), which makes it a strong fit for larger, multi-step changes and for developers who live in the terminal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best for autonomous, multi-file tasks and refactors you can describe end to end&lt;/li&gt;
&lt;li&gt;Editor-agnostic - works alongside whatever you already use&lt;/li&gt;
&lt;li&gt;Terminal-native, scriptable, and strong at planning across a codebase&lt;/li&gt;
&lt;li&gt;Runs on Claude models; available via subscription and API-based usage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  GitHub Copilot - the assistant in your IDE
&lt;/h2&gt;

&lt;p&gt;Copilot is the incumbent and the least disruptive to adopt: it's a plugin for the IDE you already use (VS Code, JetBrains, and others), so nothing about your setup changes. It started as autocomplete and has grown chat and agent capabilities, plus tight integration with GitHub itself - pull requests, issues, and code review. If you don't want to change editors and you live in the GitHub ecosystem, it's the path of least resistance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best for staying in your current IDE with zero workflow change&lt;/li&gt;
&lt;li&gt;Deep GitHub integration (PRs, issues, review)&lt;/li&gt;
&lt;li&gt;Completions, chat, and agent mode across many editors&lt;/li&gt;
&lt;li&gt;Predictable per-seat subscription pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Want AI as the center of your editor and you'll switch from VS Code - &lt;strong&gt;Cursor&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Want to delegate whole multi-file tasks from the terminal - &lt;strong&gt;Claude Code&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Want to keep your exact IDE and GitHub workflow - &lt;strong&gt;Copilot&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Honestly, many developers run two: a completion/edit tool in the editor and an agentic tool for big changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Whichever you pick, the same rule applies: AI tools accelerate work within a structure far better than they invent structure from nothing. Start from a real, well-architected foundation and any of these tools becomes dramatically more useful - the coherence is already there for them to build on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates" rel="noopener noreferrer"&gt;Give your AI tool a real foundation - browse TheKitBase templates&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Serif Is the New Sans: Editorial Typography Comes to SaaS</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:14:36 +0000</pubDate>
      <link>https://dev.to/thekitbase/serif-is-the-new-sans-editorial-typography-comes-to-saas-4iaj</link>
      <guid>https://dev.to/thekitbase/serif-is-the-new-sans-editorial-typography-comes-to-saas-4iaj</guid>
      <description>&lt;p&gt;For about ten years, every SaaS site looked typographically identical: a clean, neutral sans-serif for everything, usually Inter or something close to it. Safe, legible, and completely interchangeable. In 2026 that's changing - the products that look most distinctive are leading with a characterful serif for their headlines, and it's the single fastest way a site now signals taste. Here's why the serif revival happened and how to use one without looking like a wedding invitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why sans-everywhere got boring
&lt;/h2&gt;

&lt;p&gt;There was nothing wrong with Inter. That was the problem. When every startup uses the same neutral sans for headlines and body, typography stops carrying any personality - it becomes a delivery mechanism for words and nothing more. Once a look becomes universal, it stops signaling "modern" and starts signaling "default." The serif revival is a reaction to that sameness: a display serif immediately makes a page feel like someone made a choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new serifs aren't your grandfather's serifs
&lt;/h2&gt;

&lt;p&gt;This isn't Times New Roman. The serifs driving the trend are modern, variable, and full of character - faces like Fraunces, Instrument Serif, GT Sectra, and the new wave of expressive display serifs. They have high contrast, distinctive details, and optical sizing that lets them go from elegant at large sizes to sturdy at small ones. Used big, they carry a page. The move is editorial - closer to a magazine masthead than a corporate letterhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pair without looking dated
&lt;/h2&gt;

&lt;p&gt;The winning formula is contrast: a characterful serif for display, a clean sans or mono for everything functional. The serif provides personality where it's read slowly (headlines, hero, pull quotes); the sans provides clarity where it's read fast (body, UI, labels). Mixing them is what makes it feel intentional rather than nostalgic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Display serif + clean body sans + mono for data */&lt;/span&gt;
&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Fraunces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c"&gt;/* headlines only */&lt;/span&gt;
  &lt;span class="py"&gt;--font-body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="s1"&gt;"Hanken Grotesk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--font-mono&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="s1"&gt;"Geist Mono"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;monospace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* labels, prices, code */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.display&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.label&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-mono&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;
  
  
  The rules that keep it tasteful
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Serif for display only&lt;/strong&gt; - headlines, hero, quotes. Never set body text in a display serif.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximize the contrast&lt;/strong&gt; - a big elegant serif over a small clean sans reads as considered; two similar faces read as an accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use optical sizing&lt;/strong&gt; if the face has it, so the serif looks right large and small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One serif, used with intent&lt;/strong&gt; - a second decorative face is one accessory too many.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the serif be the personality&lt;/strong&gt;, and keep everything around it quiet.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Typography is the cheapest way to make a product look like it has a point of view. Swapping a neutral sans headline for a characterful serif - and keeping the body clean - changes the entire personality of a page without touching the layout.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates/agency-studio" rel="noopener noreferrer"&gt;See Agency Studio - editorial serif typography done right&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>typography</category>
      <category>webdev</category>
      <category>css</category>
      <category>design</category>
    </item>
    <item>
      <title>Your Shadows Are Flat Gray (That's the Tell)</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:12:29 +0000</pubDate>
      <link>https://dev.to/thekitbase/your-shadows-are-flat-gray-thats-the-tell-3b04</link>
      <guid>https://dev.to/thekitbase/your-shadows-are-flat-gray-thats-the-tell-3b04</guid>
      <description>&lt;p&gt;Here's a detail almost nobody consciously notices but everybody feels: shadows. Put a default card next to a carefully designed one and the designed one looks more expensive, even if the layout is identical. A big part of that difference is the shadow. The default &lt;code&gt;box-shadow&lt;/code&gt; everyone reaches for is flat, gray, and a single layer - and it's one of the clearest tells that a design was defaulted rather than considered. Here's what's wrong with it and how to fix it in a few lines of CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why flat gray shadows look cheap
&lt;/h2&gt;

&lt;p&gt;Real shadows in the physical world aren't pure gray, and they aren't a single hard edge. They pick up the color of the surface they fall on, they get softer and fainter the further they travel, and they're really a stack of overlapping shadows at different distances. The stock &lt;code&gt;box-shadow: 0 4px 6px rgba(0,0,0,0.1)&lt;/code&gt; violates all of that - one flat, evenly-gray blur - so it reads as artificial. Your eye doesn't articulate why; it just clocks the element as flatter and cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tinted shadows: carry the color
&lt;/h2&gt;

&lt;p&gt;The single highest-leverage fix is to stop using gray. Instead of black at low opacity, tint the shadow toward the background or the accent color. On a warm paper background, a warm-tinted shadow feels natural; a cool gray one feels pasted on. The shadow should belong to the scene it's in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Default - flat, gray, pasted-on */&lt;/span&gt;
&lt;span class="nt"&gt;box-shadow&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt; &lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="err"&gt;6&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c"&gt;/* Tinted - shadow carries the surface/accent hue */&lt;/span&gt;
&lt;span class="nt"&gt;box-shadow&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt; &lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="err"&gt;14&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;80&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;40&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;10&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;12&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c"&gt;/* warm surface */&lt;/span&gt;
&lt;span class="c"&gt;/* or tinted toward a brand accent on colored buttons */&lt;/span&gt;
&lt;span class="nt"&gt;box-shadow&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="err"&gt;20&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;-6px&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;229&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;64&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;42&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;35&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Layer them for real depth
&lt;/h2&gt;

&lt;p&gt;The second upgrade is to stack multiple shadows. A close, tight shadow grounds the element; a larger, softer one gives it height. Together they mimic how light actually falls, and the element reads as genuinely lifted off the page rather than stickered on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Layered, tinted elevation - close + far */&lt;/span&gt;
&lt;span class="nt"&gt;box-shadow&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt; &lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="err"&gt;2&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;20&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;17&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;11&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;06&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt; &lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;20&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;17&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;11&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;06&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt; &lt;span class="err"&gt;16&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="err"&gt;32&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;-8px&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;20&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;17&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;11&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;10&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use it with restraint
&lt;/h2&gt;

&lt;p&gt;Better shadows are not more shadows. Elevation should mean something - a card that floats, a menu that pops, a button you can press. If everything on the page has a heavy shadow, nothing reads as elevated and the whole thing looks heavy. Reserve the richest shadows for the elements that are genuinely lifted, and let everything else sit flat.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tinted, layered shadows are one of those details that never gets mentioned in a testimonial but quietly does half the work of making a product feel premium. Swap gray for a hue that belongs to your palette and stack two layers - it's ten minutes and it changes how expensive the whole UI feels.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates/keel-saas-starter-kit" rel="noopener noreferrer"&gt;See Keel - tinted, layered shadows throughout the "Gilt" design system&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>css</category>
      <category>design</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Bento Grids Ate SaaS Design in 2026 (And How to Build One)</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:09:16 +0000</pubDate>
      <link>https://dev.to/thekitbase/bento-grids-ate-saas-design-in-2026-and-how-to-build-one-3h89</link>
      <guid>https://dev.to/thekitbase/bento-grids-ate-saas-design-in-2026-and-how-to-build-one-3h89</guid>
      <description>&lt;p&gt;Scroll any SaaS landing page shipped in the last year and you'll hit one: a grid of cards in different sizes, one big feature anchoring the corner, smaller ones filling in around it, each with its own little visual. The bento grid went from an Apple keynote flourish to the default feature section in about eighteen months. It's everywhere because it solves a real problem - and it's easy to get wrong. Here's why it took over, how to build one, and the rules that separate a bento grid from a cluttered mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a bento grid actually is
&lt;/h2&gt;

&lt;p&gt;The name comes from the Japanese bento box - a single tray divided into compartments of different sizes, each holding something different, the whole thing composed to feel balanced. In UI terms it's an asymmetric grid: cards that span different numbers of rows and columns, so a primary item gets a large cell and supporting items get smaller ones. Apple popularized it on hardware and OS feature pages, and SaaS marketing adopted it wholesale because it fixed the thing everyone hated about feature sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it beat the 3-column grid
&lt;/h2&gt;

&lt;p&gt;The old feature section was three identical columns of icon + title + paragraph. Everything the same size means everything reads as equally important, which means nothing stands out, and the eye glazes over. A bento grid encodes hierarchy into the layout itself - the biggest cell is your headline feature, and the reader knows that instantly, before reading a word.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual hierarchy&lt;/strong&gt; - cell size signals importance, so your best feature leads without a label saying so&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scannable&lt;/strong&gt; - varied shapes give the eye anchors instead of a wall of equal boxes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room for visuals&lt;/strong&gt; - a large cell can hold a real product shot or animation, not just an icon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Density without clutter&lt;/strong&gt; - you can show more features in less vertical space&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It just looks modern&lt;/strong&gt; - the asymmetry reads as designed, not defaulted&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building one with CSS grid
&lt;/h2&gt;

&lt;p&gt;You don't need a library. CSS grid with column and row spans does the whole thing. Define a grid, then let specific cells span extra tracks - the featured cell takes two columns, the rest fall into place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.bento&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;grid-auto-rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* the hero feature - twice as wide and tall */&lt;/span&gt;
&lt;span class="nc"&gt;.bento&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.featured&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;grid-column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;grid-row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* a wide-but-short supporting cell */&lt;/span&gt;
&lt;span class="nc"&gt;.bento&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.wide&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;grid-column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;640px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.bento&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c"&gt;/* stack on mobile */&lt;/span&gt;
  &lt;span class="nc"&gt;.bento&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;grid-column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;grid-row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&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;That's the entire mechanism. The design work is deciding which cells get the spans and what goes inside each one - the CSS is trivial once the composition is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that keep it from looking messy
&lt;/h2&gt;

&lt;p&gt;A bento grid fails when it becomes a random assortment of boxes. What makes it feel composed rather than chaotic is restraint and rhythm.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One clear hero cell&lt;/strong&gt; - exactly one item should dominate. Two large cells fight each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit the size variations&lt;/strong&gt; - two or three cell sizes, not six. Too many breaks the rhythm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each cell communicates in under five seconds&lt;/strong&gt; - title, one line, one visual. If it needs a paragraph, it's a doc, not a bento cell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the gaps and radii consistent&lt;/strong&gt; across every cell so the grid reads as one object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collapse cleanly to a single column on mobile&lt;/strong&gt; - the spans should reset, not overflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The bento grid works because it does the reader's prioritizing for them: the layout itself says "look here first." A three-column grid makes every feature shout at the same volume; a bento grid gives one feature the mic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates/ai-saas-landing" rel="noopener noreferrer"&gt;See AI SaaS Landing - ships a bento feature grid with custom visuals&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>design</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Warm Dark Mode Is the New Dark Mode: 2026 SaaS Design Trends</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:57:26 +0000</pubDate>
      <link>https://dev.to/thekitbase/warm-dark-mode-is-the-new-dark-mode-2026-saas-design-trends-13o9</link>
      <guid>https://dev.to/thekitbase/warm-dark-mode-is-the-new-dark-mode-2026-saas-design-trends-13o9</guid>
      <description>&lt;p&gt;Look closely at the dark mode on the SaaS products that feel most premium right now and you'll notice something: none of them are actually black. The pure &lt;code&gt;#000&lt;/code&gt; background that defined the first wave of dark mode is quietly being replaced by warm dark - near-black backgrounds with a hint of temperature, grays that lean slightly warm, and accents chosen to glow against them rather than vibrate. It's a small shift with a big effect on how expensive a product feels. Here's what changed and how to build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why pure black is a mistake
&lt;/h2&gt;

&lt;p&gt;Pure &lt;code&gt;#000&lt;/code&gt; seems like the obvious choice for dark mode, and it's usually the wrong one. On OLED screens, black next to bright white text creates harsh contrast that causes halation - text appears to smear or glow for tired eyes. Pure black also flattens depth: with nothing darker to recede to, shadows and elevation stop working, so every surface looks like it's on the same plane. And it reads as cheap, because it's the default a framework gives you before anyone has made a design decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "warm dark" actually means
&lt;/h2&gt;

&lt;p&gt;Warm dark starts the background a few points above black and nudges the hue toward warmth instead of the default cool blue-gray. The surfaces layer up from there, so elevation reads. Text is off-white rather than pure white, which softens the contrast without losing legibility. The whole palette shares a subtle temperature, which is what makes it feel considered rather than generated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Cool default dark (the framework default) */&lt;/span&gt;
&lt;span class="nt"&gt;--bg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="err"&gt;#000000&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--surface&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#0&lt;/span&gt;&lt;span class="nt"&gt;a0a0a&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--text&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="nf"&gt;#ffffff&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;/* Warm dark - near-black with temperature, off-white text */&lt;/span&gt;
&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="m"&gt;#0b0b0c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* not black - a hair of warmth */&lt;/span&gt;
  &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#151412&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* layers up so elevation reads */&lt;/span&gt;
  &lt;span class="py"&gt;--color-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;#262320&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* warm hairline, not cool gray */&lt;/span&gt;
  &lt;span class="py"&gt;--color-ink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="m"&gt;#f7f3ea&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* off-white, softer than #fff */&lt;/span&gt;
  &lt;span class="py"&gt;--color-muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="m"&gt;#a8a29a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* warm-leaning muted text */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Accents that glow instead of vibrate
&lt;/h2&gt;

&lt;p&gt;On a warm near-black, saturated cool accents (electric blue, pure violet) tend to vibrate and feel harsh. Warm accents - amber, gold, coral, terracotta - sit naturally against the background and read as a glow rather than a clash. This is why so many premium 2026 dark themes have converged on warm accent colors: they're working with the background temperature, not against it. If you want a cool accent, desaturate it slightly and it'll settle down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other trends riding alongside it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subtle film grain or noise texture&lt;/strong&gt; over flat backgrounds, to kill banding and add analog warmth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editorial typography&lt;/strong&gt; - a characterful display face paired with a clean UI face, instead of one neutral sans everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tinted shadows&lt;/strong&gt; - shadows that carry a hint of the accent hue rather than flat black&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flash-free theme switching as table stakes&lt;/strong&gt; - a blocking inline script so there's no white flash on load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restraint with the accent&lt;/strong&gt; - one confident color used sparingly beats a rainbow of tints&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The tell of a considered dark theme in 2026 is that the "black" isn't black and the grays aren't gray - everything shares a quiet temperature. It's the difference between a theme someone designed and a theme a framework defaulted to.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates/keel-saas-starter-kit" rel="noopener noreferrer"&gt;See Keel - "Gilt" warm dark theme, reskin from two CSS variables&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>design</category>
      <category>tailwindcss</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vibe Coding a Next.js SaaS in 2026: What Actually Works</title>
      <dc:creator>TheKitBase</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:54:10 +0000</pubDate>
      <link>https://dev.to/thekitbase/vibe-coding-a-nextjs-saas-in-2026-what-actually-works-35k</link>
      <guid>https://dev.to/thekitbase/vibe-coding-a-nextjs-saas-in-2026-what-actually-works-35k</guid>
      <description>&lt;p&gt;"Vibe coding" started as a half-joke - describe what you want, let the AI write it, keep the good vibes, ship. In 2026 it's a real workflow that real products are built with. It's also wildly oversold. The honest version is more useful than either the hype or the backlash: AI-assisted building is genuinely transformative for some parts of shipping a SaaS and actively dangerous for others. Here's where the line actually falls, from someone who builds Next.js products for a living.&lt;/p&gt;

&lt;h2&gt;
  
  
  What vibe coding is genuinely great at
&lt;/h2&gt;

&lt;p&gt;For the first 80% of a UI, AI is astonishing. Describe a pricing page, a settings form, a dashboard layout, and you get a working, decent-looking first draft in seconds. It's a phenomenal accelerator for the parts of building that are pattern-heavy and low-stakes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First-draft UI&lt;/strong&gt; - layouts, forms, marketing sections, empty states&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boilerplate&lt;/strong&gt; - types, mock data, repetitive CRUD, config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unblocking&lt;/strong&gt; - a rough version of something you're not sure how to start&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactors with a clear shape&lt;/strong&gt; - rename, extract, convert patterns across files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning&lt;/strong&gt; - explaining an unfamiliar API in the context of your own code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it falls apart
&lt;/h2&gt;

&lt;p&gt;The trouble starts exactly where software gets hard: the parts that require holding the whole system in your head. Vibe coding optimizes for something that looks done, and "looks done" and "is done" diverge fast in the places that matter most.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Architecture&lt;/strong&gt; - AI will happily build the same logic five different ways across five files; coherence is on you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth and security&lt;/strong&gt; - subtly wrong auth looks identical to correct auth until it's exploited&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State and edge cases&lt;/strong&gt; - the empty, loading, error, and race-condition states it quietly skips&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming and real-time UX&lt;/strong&gt; - the "feels smooth" details are exactly what a quick prompt drops&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowing what good looks like&lt;/strong&gt; - AI gives you an answer; it can't tell you it's the wrong answer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The hybrid that actually ships
&lt;/h2&gt;

&lt;p&gt;The teams shipping real products with AI aren't vibe coding from an empty folder. They're starting from a solid foundation - a real architecture, real patterns, tested core logic - and using AI to move fast within it. The foundation constrains the AI toward coherence; the AI accelerates the work the foundation left to you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start from a real structure&lt;/strong&gt;, not a blank create-next-app - established patterns keep AI output coherent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let AI draft components and content&lt;/strong&gt;; you own architecture, data flow, and the security boundary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the hard-won logic&lt;/strong&gt; (auth, billing, streaming) tested, so AI edits can't silently break it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review like it's a junior PR&lt;/strong&gt; - fast, but never blind merge&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Vibe coding isn't a replacement for knowing how to build - it's a multiplier on it. On a good foundation it's a rocket. From an empty folder with no taste to steer it, it's a fast way to build something that looks finished and falls over in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is exactly why starting from a production-grade template pays off in the AI era rather than despite it: a real Next.js foundation - typed data, tested session and billing logic, a coherent design system - is the structure that makes AI-assisted building safe. You spend your prompts on your product, not on re-deriving auth and layout for the fifth time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thekitbase.app/templates" rel="noopener noreferrer"&gt;Browse TheKitBase templates - production-grade Next.js foundations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
