<?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: Harshdeep Singh</title>
    <description>The latest articles on DEV Community by Harshdeep Singh (@harshdeepsingh13).</description>
    <link>https://dev.to/harshdeepsingh13</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%2F3965372%2F96507d50-d632-47cf-81c0-0fa1b2ea41bd.png</url>
      <title>DEV Community: Harshdeep Singh</title>
      <link>https://dev.to/harshdeepsingh13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harshdeepsingh13"/>
    <language>en</language>
    <item>
      <title>AI Data Centers and Nature - What the fuss is really about?</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Sat, 27 Jun 2026 22:18:53 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/ai-data-centers-and-nature-what-the-fuss-is-really-about-24a2</link>
      <guid>https://dev.to/harshdeepsingh13/ai-data-centers-and-nature-what-the-fuss-is-really-about-24a2</guid>
      <description>&lt;p&gt;Every time you ask a chatbot to draft an email, something physical happens a long way away. In a windowless shed the size of a cathedral, thousands of processors light up, draw power from the grid, and dump heat into the air or into water. Multiply that by a billion prompts a day, then by a building boom unlike anything the electricity system has seen in decades, and you arrive at the argument now raging from Dublin to Santiago: is artificial intelligence quietly mortgaging the planet to build itself?&lt;/p&gt;
&lt;p&gt;The honest answer is more interesting than either side usually admits. AI data centers are &lt;strong&gt;not&lt;/strong&gt; — yet — a global climate catastrophe. Worldwide they used roughly 1.5% of electricity in 2024, and even the steep growth ahead keeps them under 3% of demand by 2030. Set against electric vehicles, air conditioning or heavy industry, that is a modest slice. But "modest globally" and "harmless locally" are very different claims, and it is at the local level — a town's water table, a neighborhood's air, a family's power bill — where the build-out is already doing real and unevenly distributed harm.&lt;/p&gt;
&lt;p&gt;This piece is written from two chairs at once. One is the conservationist's: skeptical of growth that externalises its costs onto rivers, air and people who never consented. The other is the engineer's: respectful of what this technology can do, including for the climate, and allergic to numbers that fall apart under scrutiny. Hold both and a clear position emerges — not "stop AI," and not "trust us, it's fine," but &lt;strong&gt;govern the build-out so that nature and communities are treated as stakeholders, not as line items absorbed in the name of progress.&lt;/strong&gt; Let's walk through what the fuss is about, give the counterarguments their due, and get to the fixes — because there are real ones.&lt;/p&gt;
&lt;h2&gt;Big and fast — but not the monster under the bed&lt;/h2&gt;
&lt;p&gt;Start with the numbers everyone fights over, because getting them right is the difference between panic and judgment. According to the International Energy Agency's landmark &lt;a rel="noopener noreferrer" href="https://www.iea.org/reports/energy-and-ai/executive-summary"&gt;Energy and AI&lt;/a&gt; analysis, the world's data centers consumed about 415 terawatt-hours of electricity in 2024 — and that figure is set to roughly double by the end of the decade.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;~945 TWh&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Projected data-center electricity use by 2030&lt;/p&gt;
&lt;p&gt;Up from ~415 TWh in 2024 — an amount comparable to Japan's entire electricity consumption today. AI is the single most important driver of the increase.&lt;/p&gt;
&lt;p&gt;Source: IEA, Energy and AI (2025)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That sounds apocalyptic until you place it next to everything else drawing on the grid. The IEA is blunt about the proportion: data centers account for less than a tenth of global electricity-demand growth to 2030 — behind the expansion of industry, behind electric vehicles, behind the world's air conditioners. On current trajectories they reach roughly 1% of global energy-related carbon emissions by 2030. A serious number, worth managing. Not, on its own, the thing that decides the climate.&lt;/p&gt;
&lt;p&gt;So why the alarm? Because national and local averages tell a different story than the global one. Compute does not spread out evenly like a gas; it clusters where land, fiber and tax breaks are cheap, and then it concentrates demand on grids that were never designed for it. In the United States — home to roughly 45% of the world's data-center electricity use — Lawrence Berkeley National Laboratory &lt;a rel="noopener noreferrer" href="https://www.energy.gov/articles/doe-releases-new-report-evaluating-increase-electricity-demand-data-centers"&gt;estimates&lt;/a&gt; data centers consumed about 4.4% of national electricity in 2023 and could reach somewhere between 6.7% and 12% by 2028. And in Ireland, the poster child for concentration, data centers now draw more than a fifth of the entire country's electricity.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;22%&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Share of Ireland's national electricity used by data centers (2024)&lt;/p&gt;
&lt;p&gt;Up from 5% in 2015. Around Dublin, data centers account for roughly half of regional demand — enough that the grid operator has effectively paused new connections.&lt;/p&gt;
&lt;p&gt;Source: Ireland Central Statistics Office&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the real shape of the problem: a technology whose footprint is globally manageable but locally enormous, landing hardest on a handful of places that happen to sit at the crossroads of cheap power and fast fiber. The United States is now on course to use more electricity for processing data by 2030 than for manufacturing aluminum, steel, cement and every other energy-intensive good combined. When an entire industrial category reorganizes around one new load that fast, the strain shows up first — and worst — in specific watersheds, substations and zip codes. The next four sections are about those places.&lt;/p&gt;
&lt;h2&gt;The thirsty secret nobody put on the label&lt;/h2&gt;
&lt;p&gt;Of all the impacts, water is the most visceral and the least disclosed. Servers run hot; many large facilities are cooled by evaporating fresh water, and the power plants feeding them evaporate more. For years the industry simply didn't talk about it. Then researchers Pengfei Li and Shaolei Ren at UC Riverside forced the issue with a paper whose title said the quiet part out loud: &lt;a rel="noopener noreferrer" href="https://arxiv.org/abs/2304.03271"&gt;"Making AI Less Thirsty."&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Their estimates are necessarily a range — the companies won't release location-level data, so even rigorous researchers are working from models — but the order of magnitude is striking. &lt;a href="/blog/building-an-llm-project-from-scratch-in-2026"&gt;Training a single large model&lt;/a&gt; in the mid-2020s could directly evaporate hundreds of thousands of liters of clean freshwater. And at the scale of everyday use, a short exchange with a chatbot carries a hidden cost.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;~519 mL&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Estimated water to generate one ~100-word email with a frontier model&lt;/p&gt;
&lt;p&gt;About a standard bottle of water, counting cooling plus the water consumed generating the electricity. Scaled globally, AI could withdraw 4.2–6.6 billion cubic meters a year by 2027 — roughly half the United Kingdom's annual water withdrawal.&lt;/p&gt;
&lt;p&gt;Source: Li &amp;amp; Ren, UC Riverside (2023–25)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is worth being precise here, because precision is exactly what's missing from most coverage. There is a real difference between water &lt;em&gt;withdrawn&lt;/em&gt; (taken from a source and largely returned) and water &lt;em&gt;consumed&lt;/em&gt; (evaporated and gone). Ren himself has cautioned against the viral, over-confident figures that circulate online; the truthful position is that the numbers are large, uneven, and deliberately hard to verify. That opacity is itself part of the story. When Google's own 2024 environmental reporting shows its data centers consumed about 23 billion liters of water (roughly 6.1 billion gallons) in 2023 — the question is no longer whether the thirst is real, but who is bearing it.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1747122450139-6363248ec01f%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="Cracked, parched earth in drought, with a single small green plant pushing through a fissure" width="1600" height="1067"&gt;&lt;p&gt;The fights over data-center water are sharpest exactly where there is least to spare. Photo: Bartłomiej Balicki / Unsplash&lt;/p&gt;
&lt;p&gt;And it is borne, again and again, by places already short of water. In &lt;strong&gt;The Dalles, Oregon&lt;/strong&gt;, Google went to court to keep its water use secret before the city revealed the company's data centers were drinking roughly a quarter of the town's supply. In &lt;strong&gt;Cerrillos, Chile&lt;/strong&gt;, residents of drought-stricken Santiago discovered a planned Google facility could consume billions of liters a year; after a local referendum and an environmental-court challenge, the company &lt;a rel="noopener noreferrer" href="https://www.datacenterdynamics.com/en/news/chile-partially-reverses-google-data-center-permit-over-water-use-concerns/"&gt;switched its design to air cooling&lt;/a&gt; — proof that public pressure can change an engineering decision. In &lt;strong&gt;Canelones, Uruguay&lt;/strong&gt;, a Google project was revealed to need millions of liters of potable water a day, equivalent to the daily use of tens of thousands of people, &lt;a rel="noopener noreferrer" href="https://logicmag.io/land/exposing-googles-seizure-of-water-in-drought-impacted-uruguay-a-conversation/"&gt;during the worst drought in 70 years&lt;/a&gt; — as the capital's tap water turned briny. The protest slogan wrote the headline for them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It's not drought — it's pillage."&lt;/p&gt;
&lt;p&gt;Protest banner, Montevideo, Uruguay&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The pattern even reaches the desert economies betting their futures on AI. Across the &lt;a rel="noopener noreferrer" href="https://restofworld.org/2025/gulf-ai-water-crisis/"&gt;Gulf&lt;/a&gt; — among the most water-stressed regions on Earth — data-center cooling is projected to need hundreds of billions of liters a year by 2030, much of it produced by energy-hungry desalination. That is the trap in miniature: more compute needs more water, which needs more energy, which needs more cooling. Break the loop in the wrong place and you simply move the damage around. The good news, which we'll come to, is that the loop &lt;em&gt;can&lt;/em&gt; be broken — Chile and Uruguay show the lever exists.&lt;/p&gt;
&lt;h2&gt;How AI is keeping coal alive — and raising your bill&lt;/h2&gt;
&lt;p&gt;Here is the impact that should worry a climate-minded engineer most, because it runs directly against the energy transition. Faced with sudden, enormous, around-the-clock demand, utilities are doing the expedient thing: keeping old fossil plants running and firing up new gas.&lt;/p&gt;
&lt;p&gt;Across the US, analysts have tracked at least &lt;a rel="noopener noreferrer" href="https://www.desmog.com/2025/12/12/15-coal-plant-retirements-delayed-ai-data-centers-trump-doe-orders/"&gt;fifteen coal plants whose retirements have been delayed&lt;/a&gt; since the start of 2025 — plants that together pumped out tens of millions of tonnes of CO₂ in their last reported year. Decades-old "peaker" plants are being pulled back from the brink of closure. The pitch from utilities is straightforward and, in market terms, rational: there is now an economic case to keep these machines around. The climate cost of that rationality is paid by everyone downwind.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;15+&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;US coal plants whose retirement has been delayed since Jan 2025&lt;/p&gt;
&lt;p&gt;Driven in significant part by data-center demand. Roughly 60% of fossil generators previously slated for retirement in the largest US grid region have postponed their closures.&lt;/p&gt;
&lt;p&gt;Source: Frontier Group / DeSmog analysis&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The starkest case is gas built on-site, beyond the reach of the public grid — and of public scrutiny. In South Memphis, xAI's "Colossus" supercomputer fired up a fleet of gas turbines to power its chatbot in a majority-Black neighborhood, Boxtown, that already hosts most of the area's heavy polluters. Many of the turbines ran without the air permits such equipment normally requires. The &lt;a rel="noopener noreferrer" href="https://www.eesi.org/articles/view/data-center-buildout-is-hungry-for-fossil-fuels"&gt;Southern Environmental Law Center&lt;/a&gt; estimated they could emit well over a thousand tons a year of smog-forming nitrogen oxides — potentially making the site the single largest industrial source of that pollution in a city already named a national "asthma capital." When a state representative pointed out that more children are hospitalized for asthma in that neighborhood than anywhere else in Tennessee, he was not making a rhetorical flourish. He was describing the cost of siting an unregulated power plant where the people had the least power to refuse it.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1473341304170-971dccb5ac1e%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="High-voltage electricity transmission pylons silhouetted against an orange and gray sunset sky" width="1600" height="1067"&gt;&lt;p&gt;New, around-the-clock demand is reshaping the grid faster than clean supply can be built — and the gap is being filled with fossil power. Photo: Matthew Henry / Unsplash&lt;/p&gt;
&lt;p&gt;And then there is your electricity bill. This is the part that turns an abstract debate into a kitchen-table one. When a data center plugs into a regional grid, it competes with households for a finite supply of guaranteed capacity — and prices everyone pays rise accordingly. In the PJM system, which serves 67 million people across 13 US states, the grid's own independent market monitor reached a remarkable conclusion about a single capacity auction.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;$9.3 B&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Higher electricity costs attributed to data centers in one PJM auction&lt;/p&gt;
&lt;p&gt;The grid's independent monitor found data centers responsible for the majority of a record price increase — costs recovered from ordinary customers. Capacity prices later hit their ceiling, and the auction fell short of its reliability target for the first time ever.&lt;/p&gt;
&lt;p&gt;Source: Monitoring Analytics (PJM market monitor)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Consumer advocates warn the cumulative toll could run to a hundred billion dollars or more by the early 2030s. In Washington DC, one utility's residential customers saw monthly bills jump by around twenty dollars, roughly half of it traced to those capacity prices. There is something quietly corrosive about a technology marketed as a public good whose first tangible effect, for many people, is a more expensive utility statement.&lt;/p&gt;
&lt;p&gt;To their credit, the largest AI firms know fossil expansion is a dead end and are reaching for cleaner firm power — chiefly nuclear. Microsoft has contracted to &lt;a rel="noopener noreferrer" href="https://www.canarymedia.com/articles/nuclear/feds-deal-blow-to-dream-of-data-centers-connected-to-nuclear-plants"&gt;restart a reactor at Three Mile Island&lt;/a&gt;; Amazon, Meta and Google have all signed nuclear or small-modular-reactor deals. These are genuinely good commitments. They are also slow: most of that clean power won't arrive until the late 2020s or 2030s. The demand is here now. The gap, for the moment, is being filled with carbon.&lt;/p&gt;
&lt;h2&gt;When "net zero" meets a GPU order&lt;/h2&gt;
&lt;p&gt;For a decade, the hyperscalers were the climate movement's favorite corporations — buying renewables at scale, publishing slick sustainability reports, racing to be "carbon neutral." AI has collided with those promises, and the reports themselves now tell the story.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;+48%&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Rise in Google's greenhouse-gas emissions, 2019–2023&lt;/p&gt;
&lt;p&gt;Driven by data-center energy and supply-chain emissions. Google's report conceded that cutting emissions may get harder as it integrates AI — and it quietly stopped claiming operational carbon neutrality.&lt;/p&gt;
&lt;p&gt;Source: Google Environmental Report, via NPR&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft tells a similar tale: total emissions &lt;a rel="noopener noreferrer" href="https://www.npr.org/2024/07/12/g-s1-9545/ai-brings-soaring-emissions-for-google-and-microsoft-a-major-contributor-to-climate-change"&gt;up by roughly a quarter&lt;/a&gt; since its 2020 baseline, the increase attributed to the AI and cloud build-out. The detail underneath matters: the company actually &lt;em&gt;cut&lt;/em&gt; the emissions from running its operations, but the emissions embedded in &lt;em&gt;building&lt;/em&gt; all that infrastructure — the concrete, the steel, the chips, categorized as "Scope 3" — swamped the savings. More than 97% of Microsoft's footprint sits in that supply-chain bucket. Its own chief sustainability officer captured the predicament with rare candor.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In 2020 we called our climate target a moonshot. The moon has gotten further away.&lt;/p&gt;
&lt;p&gt;Melanie Nakagawa, Chief Sustainability Officer, Microsoft (paraphrased)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the most under-appreciated fact in the whole debate, and the one an engineer should sit with longest: a huge share of AI's climate cost is poured before a single model is trained. Concrete production alone is responsible for as much as 8% of human CO₂ emissions, and an AI campus is an ocean of it. When &lt;a rel="noopener noreferrer" href="https://huggingface.co/blog/sasha/ai-environment-primer"&gt;Hugging Face researchers&lt;/a&gt; tallied the full footprint of training one large open model, only about half came from the electricity the chips drew. The rest came from idle infrastructure overhead and from manufacturing the hardware itself. Per-query "efficiency" metrics — the comforting "it's just a few watt-hours" framing — quietly leave most of that concrete and silicon off the books.&lt;/p&gt;
&lt;p&gt;Which brings us to the credibility problem with offsets. Buying renewable-energy certificates lets a company claim "100% renewable" on an annual spreadsheet while its servers actually run on whatever the grid is burning at 2am — often gas or coal. It is accounting, not physics. The more honest standard, which the better operators are now adopting, is to match every hour of consumption with clean power on the same grid. We'll return to that distinction in the solutions, because it is the single clearest line between greenwashing and genuine decarbonization.&lt;/p&gt;
&lt;h2&gt;Land, noise, waste and the stuff inside the box&lt;/h2&gt;
&lt;p&gt;Energy, water and carbon dominate the headlines, but a data center is a physical intrusion in other ways too, and the people who live nearest feel them first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Noise&lt;/strong&gt; is the complaint that turns neighbors into organisers. The cooling systems and backup generators produce a relentless low-frequency hum that ordinary noise ordinances, written around traffic and barking dogs, struggle even to measure. In Chandler, Arizona, residents near one campus described a drone that simply never stopped — and the city eventually amended its zoning rules and began questioning whether new data centers belonged there at all. As one official put it, the math that made these buildings welcome a decade ago no longer adds up for the community hosting them. By 2026, families near another AI power-plant site had filed a federal class action on behalf of more than ten thousand people.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;E-waste and materials&lt;/strong&gt; are the hidden tail of the hardware race. The world already generates well over 60 million tonnes of electronic waste a year, and the rapid refresh cycles of AI accelerators — yesterday's cutting-edge chip is next year's scrap — add to the pile, much of which ends up leaching toxins in places like the Agbogbloshie dump in Accra. Upstream, the chips and magnets depend on copper, silicon, gallium and rare-earth elements whose mining scars landscapes and poisons water tables; by 2030, AI infrastructure could account for a meaningful slice of global demand for several of these minerals. When China tightened exports of gallium and rare earths, prices outside the country more than doubled in months — a reminder that "the cloud" rests on some very earthbound and geopolitically fragile supply chains.&lt;/p&gt;
&lt;p&gt;None of these is civilization-ending. Together they make the point that a data center is not a clean abstraction humming in cyberspace. It is concrete poured on land, water pulled from a river, metal dug from a mountain, and noise pushed into a bedroom window. The question is whether those costs are acknowledged and compensated — or simply absorbed by whoever lives closest.&lt;/p&gt;
&lt;h2&gt;What AI gives back — stated fairly&lt;/h2&gt;
&lt;p&gt;An honest accounting cannot be a prosecution. If we only tallied the costs, we would be telling half the truth — and the engineer's chair will not allow it. AI is also one of the more powerful tools we have for &lt;em&gt;fighting&lt;/em&gt; the very crisis its data centers strain. These benefits are real, and several are already in production.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Steelman · AI as a climate tool&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Google DeepMind's &lt;strong&gt;GraphCast&lt;/strong&gt; produces ten-day weather forecasts in under a minute on a single machine, more accurately than the gold-standard physics model it was benchmarked against — a direct boon for integrating wind and solar and for warning people ahead of extreme weather. AI has improved solar-output forecasting by around 40% in UK trials, helps satellites pinpoint &lt;strong&gt;methane leaks&lt;/strong&gt;, accelerates the discovery of better batteries and solar materials, and — in a tidy irony — a DeepMind system cut the energy used to cool Google's own data centers by roughly 40%.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The efficiency trend is genuinely remarkable, and it deserves to be stated as plainly as the alarms. The IEA notes that the energy needed for a given AI task has been dropping by at least an order of magnitude a year — an improvement rate it calls essentially unprecedented in energy history. Each new chip generation does dramatically more compute per watt. Google now reports getting many times more computing out of each unit of electricity than it did five years ago, and runs some of the most efficient facilities on Earth, with a fraction of the overhead of a typical corporate server room.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;~49%&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Share of all global corporate clean-energy buying by the four big cloud firms&lt;/p&gt;
&lt;p&gt;Amazon, Microsoft, Google and Meta are, collectively, the largest force funding new wind, solar and — increasingly — nuclear and geothermal capacity onto the grid. The same firms straining the system are also its biggest clean-energy customers.&lt;/p&gt;
&lt;p&gt;Source: BloombergNEF (2026)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is an economic case too, and it is not nothing: the data-center build-out represents hundreds of billions of dollars of investment, tax base and construction work. And concentrating compute in a hyperscale facility is genuinely more efficient than the same work scattered across thousands of small on-premises servers. A reflexive "data centers are bad" misses all of this. And the cost of &lt;em&gt;not&lt;/em&gt; building is real too: this compute underpins the very climate-modeling, materials-science and grid-forecasting tools described above, and a country that bans data centers outright does not abolish the demand — it exports the jobs, the tax base and the emissions to wherever the rules are weakest. The honest question is not whether to build, but how.&lt;/p&gt;
&lt;p&gt;But — and the conservationist's chair insists on the "but" — three honest caveats keep the optimism grounded. First, &lt;strong&gt;Jevons' paradox&lt;/strong&gt;: when something gets cheaper and more efficient, we tend to use vastly more of it, and the new appetites of AI (video generation, "reasoning" models that think in long chains, &lt;a href="/blog/stop-using-ai-like-autocomplete-a-developers-guide-to-multi-agent-workflows"&gt;autonomous agents&lt;/a&gt;) can each consume hundreds of times more energy than a simple query, swamping the per-task savings. Second, the efficiency story is real but it is not consent: a town's drained aquifer is not comforted by a favourable global average. Third, the benefits and the costs land on &lt;em&gt;different people&lt;/em&gt; — the climate models and the shareholder returns accrue broadly, while the noise, the water stress and the power bills concentrate on specific communities. Progress that is real in aggregate can still be unjust in distribution. Both things are true at once, and a mature position has to hold them together.&lt;/p&gt;
&lt;h2&gt;The fixes are real — and mostly already invented&lt;/h2&gt;
&lt;p&gt;If the problem were intractable, this would be a gloomier essay. It isn't. Almost every harm above has a known, demonstrated solution; what's missing is not technology but the will, the disclosure and the rules to make the solutions standard rather than optional. Here is what accountability actually looks like, in concrete terms.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1467579424161-6bbc141569d7%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="A single white wind turbine standing in a vivid green field under a bright sky" width="1600" height="1067"&gt;&lt;p&gt;None of the fixes below is speculative. Each is already running somewhere — the task is to make it the default everywhere. Photo: Karsten Würth / Unsplash&lt;/p&gt;
&lt;h3&gt;Cool without drinking the river&lt;/h3&gt;
&lt;p&gt;The water problem is, increasingly, an engineering choice rather than a necessity. Microsoft has rolled out a &lt;strong&gt;zero-water cooling&lt;/strong&gt; design — a closed loop that circulates the same coolant for the life of the facility instead of evaporating fresh water — and says it will save more than 125 million liters per data center per year. Direct-to-chip and full &lt;strong&gt;immersion cooling&lt;/strong&gt; (literally bathing servers in a non-conductive fluid) cut cooling energy substantially and are now essential anyway, because the densest AI racks run far too hot for old-fashioned air. Where water must be used, it can be reclaimed or non-potable rather than drinking-quality. The Chilean fix — trading some water for a little more electricity by using air — is available to anyone willing to choose it.&lt;/p&gt;
&lt;h3&gt;Match clean energy by the hour, not the spreadsheet&lt;/h3&gt;
&lt;p&gt;This is the dividing line between marketing and decarbonization, so it deserves its own table.&lt;/p&gt;
&lt;p&gt;Two ways to claim "clean" — and why only one is honest&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Annual REC matching&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;24/7 carbon-free energy&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;What it measures&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Total clean energy bought over a year, anywhere&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Every hour of use matched with clean power on the same grid&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Reality at 2am&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Servers may run on coal or gas; certificates paper over the gap&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Consumption is actually backed by clean supply, hour by hour&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Drives new clean build?&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Weakly — rewards cheapest certificates&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Strongly — forces investment in storage, geothermal, nuclear&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Honest label&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"We bought a year's worth of renewables"&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"We ran on clean power"&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Google has committed to running on 24/7 carbon-free energy by 2030 and already matches roughly two-thirds of its hourly consumption, with several regions above 80%. Firm clean power is the hard part of that equation, which is why the most interesting deals are in &lt;strong&gt;advanced geothermal&lt;/strong&gt; — Google's partnership with Fervo Energy is putting always-on, weather-independent clean power onto the grid — and in the nuclear and small-modular-reactor commitments now scaling up. The point is not that any one source wins; it's that "clean by the hour" forces the build-out of exactly the firm, around-the-clock clean capacity the whole grid needs.&lt;/p&gt;
&lt;h3&gt;Put the waste heat to work&lt;/h3&gt;
&lt;p&gt;A data center is, thermodynamically, a giant heater that we currently throw away. The Nordics treat that heat as a resource instead. In Finland, a Microsoft project will pipe its waste warmth into a district-heating network serving the equivalent of around 100,000 homes; a Meta facility in Denmark already exports its heat to thousands of households; Stockholm aims to warm a tenth of the city this way. Captured and reused, European data-center heat could in principle cover a meaningful share of the continent's space heating, delivered more cheaply than gas. The technology is plumbing. The obstacle is that almost nobody is required to do it.&lt;/p&gt;
&lt;h3&gt;Make the numbers public — then make rules&lt;/h3&gt;
&lt;p&gt;Everything above depends on one unglamorous foundation: &lt;strong&gt;disclosure&lt;/strong&gt;. You cannot manage what no one will measure, and right now most operators reveal little — by one industry survey, fewer than half even track their water use. The European Union has started to fix this, requiring data centers above a certain size to report their energy, water and efficiency to a public database. Germany has gone further, mandating waste-heat reuse and capping how inefficiently new facilities may run. Ireland now requires big new data centers to bring their own clean generation. And in the US, Oregon created the first dedicated electricity rate class for data centers, so that the cost of their demand falls on them rather than on households. More than forty such bills moved through US statehouses in a single year. This is what a sane settlement looks like: measure honestly, reuse what you can, and make the industry pay its own way.&lt;/p&gt;
&lt;h2&gt;A world for people, not just machines&lt;/h2&gt;
&lt;p&gt;Step back from the terawatts and the liters and the question underneath comes into focus. We are, very fast and with very little public debate, rebuilding the physical substrate of the planet — its power plants, its water rights, its land and air — around the needs of machine intelligence. That may turn out to be one of the better bets humanity has made. But a bet made &lt;em&gt;only&lt;/em&gt; on the machines' terms, with rivers and neighborhoods and the climate treated as costs to be absorbed quietly, is not progress. It is enclosure with better branding.&lt;/p&gt;
&lt;p&gt;The encouraging truth running through all of this is that we are not choosing between AI and a liveable world. The efficiency gains are real and rapid. The clean-energy demand from these same companies is the largest in the world. The cooling that doesn't drink rivers, the heat that warms homes, the power matched clean by the hour, the rules that make firms pay their way — none of it is science fiction. It is sitting in pilot projects and regulations and engineering specs right now, waiting to be made standard. The gap is not capability. It is accountability.&lt;/p&gt;
&lt;p&gt;So the ask is specific, and it falls on three groups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Regulators&lt;/strong&gt; should make disclosure mandatory, make data centers pay for the grid and water they demand, and close the permitting loopholes that let unregulated power plants rise in the neighborhoods least able to fight them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Companies&lt;/strong&gt; should move from annual certificates to clean energy matched by the hour, publish their full footprint including the concrete and the chips, default to water-free cooling and heat reuse, and sign genuine community-benefit agreements before breaking ground — not after the lawsuits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The rest of us&lt;/strong&gt; should refuse the false choice between technophobia and blind faith, ask where our compute comes from, support the local organisers and the transparency laws, and reward the firms that choose the honest path over the cheap one.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fuss, in the end, is not really about data centers. It is about whether the most powerful technology of our age will be built &lt;em&gt;with&lt;/em&gt; the living world or &lt;em&gt;against&lt;/em&gt; it — whether the future we are pouring concrete for has room in it for clean rivers, breathable air and people who can pay their electricity bills, alongside the machines. That future is still ours to specify. We should write it down before someone else pours it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sustainability</category>
      <category>climate</category>
      <category>datacenter</category>
    </item>
    <item>
      <title>Local AI - How to Run Open Source AI Models Locally</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Sat, 27 Jun 2026 22:18:27 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/local-ai-how-to-run-open-source-ai-models-locally-4pi2</link>
      <guid>https://dev.to/harshdeepsingh13/local-ai-how-to-run-open-source-ai-models-locally-4pi2</guid>
      <description>&lt;p&gt;There is a particular moment that hooks every developer on local AI. You type a question into a terminal, hit enter, and watch a coherent answer stream back — with your Wi-Fi off, no API key, no usage meter ticking, nothing leaving your laptop. The model is just &lt;em&gt;there&lt;/em&gt;, running on silicon you already own.&lt;/p&gt;
&lt;p&gt;Getting to that moment used to require a research-lab pedigree. It no longer does. In 2026, a mid-range laptop can run models that would have been considered frontier-class a couple of years ago, and the tooling has matured from finicky Python scripts into one-line installers. The catch is that the landscape is now &lt;em&gt;wide&lt;/em&gt;: a dozen serious tools, hundreds of models, and a thicket of jargon — GGUF, quantization, KV cache, MoE, offloading — standing between you and that first streamed token.&lt;/p&gt;
&lt;p&gt;This guide is the map. I'll assume you're a competent developer but new to running models locally, and I'll take you from vocabulary to a working setup, with enough depth that intermediate and senior engineers get the &lt;em&gt;why&lt;/em&gt; behind each decision, not just the &lt;em&gt;how&lt;/em&gt;. By the end you'll be able to do three things with confidence: pick the right open source model for a given job, configure it for your specific hardware, and run it successfully — whether you're on a MacBook Air, a gaming rig with an NVIDIA card, or a CPU-only workstation.&lt;/p&gt;
&lt;p&gt;One promise up front: I won't pretend local always beats the cloud (it doesn't, at the very high end), and I won't bury the tradeoffs. Local AI is the right call for privacy, cost, offline capability, and control. Let's make those wins real.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The one-paragraph version: &lt;/strong&gt;If you read nothing else: install &lt;strong&gt;Ollama&lt;/strong&gt; (or &lt;strong&gt;LM Studio&lt;/strong&gt; if you want a GUI), pull a 7–8B model in &lt;code&gt;Q4_K_M&lt;/code&gt; quantization, and you’re running local AI in ten minutes. The single number that decides what you can run is &lt;strong&gt;memory&lt;/strong&gt; — VRAM on a GPU, or unified memory on a Mac. Everything else in this guide is detail on top of those two facts.&lt;/p&gt;
&lt;h2&gt;The vocabulary you need&lt;/h2&gt;
&lt;p&gt;This field has a dialect, and most tutorials assume you already speak it. Let's fix that first. Skim this section now, then refer back when a term trips you up later — it's designed as a glossary you can return to, not a wall to memorize in one pass.&lt;/p&gt;
&lt;h3&gt;The foundational terms&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LLM (Large Language Model). &lt;/strong&gt;A neural network trained to predict the next token of text. That simple objective, at scale, produces the chat, code generation, and reasoning we find so useful. Everything you’ll run is an LLM or a close cousin.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open source vs. open weight. &lt;/strong&gt;This distinction matters more than most people realize. &lt;em&gt;Open weight&lt;/em&gt; means the trained parameters are downloadable and you can run them yourself. &lt;em&gt;Open source&lt;/em&gt;, in the strict sense, additionally requires open training data and code, and a license with no restrictions on who can use it or for what. Most "open" models — Llama, Qwen, Gemma, DeepSeek — are open &lt;em&gt;weight&lt;/em&gt;. Only some, typically those under Apache 2.0 or MIT licenses, approach genuine open source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parameters (weights). &lt;/strong&gt;The learned numbers inside the network. "7B" means seven billion parameters. More parameters generally means more capability — and more memory required to hold the model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tokens and tokenization. &lt;/strong&gt;Models don’t read words; they read tokens. A token is roughly four characters or about three-quarters of a word. When you see "tokens per second," that’s the unit of generation speed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Context window (context length). &lt;/strong&gt;How many tokens the model can hold in its attention at once — your prompt plus its output combined. Older models maxed out around 4,000 tokens; modern ones reach 128,000, 256,000, and in a few cases over a million.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inference. &lt;/strong&gt;Running a trained model to produce output. This is distinct from &lt;em&gt;training&lt;/em&gt; (creating the model) and &lt;em&gt;fine-tuning&lt;/em&gt; (adapting it). Everything in this guide is about inference.&lt;/p&gt;
&lt;h3&gt;The terms that actually decide your setup&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Quantization. &lt;/strong&gt;The most important concept after parameter count. Models are trained in 16-bit precision, but you can compress the weights to 8-bit or 4-bit to shrink memory use and speed up inference, trading a little quality. The common levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FP16 / BF16&lt;/strong&gt; — full (half) precision, the uncompressed baseline. Two bytes per parameter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Q8_0&lt;/strong&gt; — 8-bit, essentially indistinguishable from the original. One byte per parameter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Q5_K_M&lt;/strong&gt; — 5-bit, a high-quality middle ground.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Q4_K_M&lt;/strong&gt; — 4-bit, the universally recommended sweet spot: about 75% smaller than FP16 with only a 1–3% quality drop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GGUF&lt;/strong&gt; — not a quantization level but the &lt;em&gt;file format&lt;/em&gt; that packages a quantized model into a single file. It’s what Ollama, LM Studio, and llama.cpp all consume.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GPTQ / AWQ / EXL2&lt;/strong&gt; — alternative quantization schemes optimized for GPU-based serving.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;VRAM vs. RAM. &lt;/strong&gt;VRAM is the dedicated memory on a discrete graphics card; RAM is your system memory. On a machine with an NVIDIA or AMD GPU, the model must fit in VRAM to run at full speed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unified memory. &lt;/strong&gt;On Apple Silicon (and a few new AMD chips), the CPU and GPU share one fast pool of memory. The GPU can use almost all of your system memory — which is why a 64GB MacBook can punch far above a gaming GPU on &lt;em&gt;large&lt;/em&gt; models.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPU offloading (layer offloading). &lt;/strong&gt;When a model is too big for your VRAM, you can keep some of its layers on the GPU and push the rest to system RAM. The model still runs — but the offloaded portion is dramatically slower.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Metal / CUDA / ROCm / Vulkan / SYCL. &lt;/strong&gt;The hardware-acceleration backends: Apple, NVIDIA, AMD, a cross-vendor fallback, and the Intel path respectively.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MoE (Mixture of Experts). &lt;/strong&gt;An architecture where only a fraction of the total parameters — the "active" parameters — fire for any given token. You get the quality of a big model with the compute cost of a small one. The catch: &lt;strong&gt;you still have to hold all the parameters in memory.&lt;/strong&gt; Plan memory by total parameters; plan speed by active parameters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mental model: &lt;/strong&gt;A Mixture-of-Experts model is like a hospital with fifty specialists on staff but only four seeing any given patient. You pay the rent on the whole building (memory), but each visit is fast because only a few doctors are involved (compute). A &lt;code&gt;30B-A3B&lt;/code&gt; model has 30 billion parameters total but only ~3 billion active per token.&lt;/p&gt;
&lt;h3&gt;The terms you’ll see in benchmarks and settings&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;KV cache. &lt;/strong&gt;As a model generates, it stores the attention keys and values for every previous token so it doesn’t recompute them. This cache grows with context length, and at long contexts it can consume as much memory as the model weights themselves.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Temperature, top-p, top-k. &lt;/strong&gt;Sampling controls that govern randomness. Lower temperature produces more deterministic output; higher is more creative. Top-p and top-k limit the pool of candidate tokens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;System prompt. &lt;/strong&gt;A hidden instruction that sets the model’s role and behavior before the conversation begins.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Throughput vs. latency. &lt;/strong&gt;Throughput is total tokens per second across all requests; latency is how fast a single response comes back. &lt;strong&gt;Tokens per second&lt;/strong&gt; is the headline speed number, and &lt;strong&gt;time to first token&lt;/strong&gt; measures how snappy the model feels.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fine-tuning vs. RAG. &lt;/strong&gt;Two ways to make a model "know" your data. Fine-tuning retrains the model on your examples; &lt;a href="/blog/building-an-llm-project-from-scratch-in-2026"&gt;RAG (retrieval-augmented generation)&lt;/a&gt; leaves the model untouched and feeds it relevant documents at query time. For most use cases, RAG is the cheaper, faster, more maintainable choice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Embeddings. &lt;/strong&gt;Numerical vector representations of text that capture meaning, used for semantic search and as the backbone of RAG systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Distillation. &lt;/strong&gt;Training a smaller model to imitate a larger one. DeepSeek’s R1 "distill" models bring large-model reasoning to consumer hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multimodal / vision-language models. &lt;/strong&gt;Models that accept images (and sometimes audio or video) alongside text.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reasoning models. &lt;/strong&gt;Models trained to "think out loud" — producing an explicit chain of reasoning before their final answer. DeepSeek-R1 and OpenAI’s gpt-oss are leading examples.&lt;/p&gt;
&lt;h2&gt;The memory math that governs everything&lt;/h2&gt;
&lt;p&gt;If you internalize one section of this guide, make it this one. Almost every question you’ll have — "Can I run this model?" "Why is it so slow?" "Which quantization should I pick?" — reduces to a single question: does the model fit in fast memory, and if not, how much are you willing to spill into slow memory?&lt;/p&gt;
&lt;h3&gt;Estimating how much memory a model needs&lt;/h3&gt;
&lt;p&gt;The weights of a model take up a predictable amount of space based on parameter count and quantization. The rule of thumb:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memory (GB) ≈ parameters (billions) × bytes-per-parameter × 1.2&lt;/strong&gt;, where bytes-per-parameter ≈ 2.0 (FP16), 1.0 (Q8_0), ~0.7 (Q5_K_M), ~0.55 (Q4_K_M). The 1.2 accounts for overhead.&lt;/p&gt;
&lt;p&gt;So a 7B model needs about 14GB at full precision, ~7.7GB at Q8, and ~4.5GB at Q4_K_M. The handy shortcut: at Q4_K_M, &lt;strong&gt;every billion parameters costs roughly 0.55–0.7GB&lt;/strong&gt;. Here’s the reference table:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Model size&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Q4_K_M (weights)&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Q8_0 (weights)&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Typical GPU it fits&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;3B&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~2 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~3.5 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Almost anything, even 4GB&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;7–8B&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~4.5–5 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~8 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;8GB cards comfortably&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;13–14B&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~8 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~14 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;12GB cards&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;27–32B&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~18–20 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~34 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;24GB cards (3090/4090)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;70B&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~40 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~75 GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;48GB+, or a high-RAM Mac&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;Don’t forget the KV cache&lt;/h3&gt;
&lt;p&gt;The weights are only part of the story. The KV cache grows linearly with context length, and at long contexts it can rival or exceed the weights. A Llama-3-8B at 32K context burns roughly 4GB on KV cache alone. Push to 128K and the cache can dwarf the model.&lt;/p&gt;
&lt;p&gt;Two things rescue you. First, nearly every model released in 2025 and 2026 uses &lt;em&gt;Grouped-Query Attention&lt;/em&gt;, which cuts cache size by 50–75% for free. Second, you can quantize the KV cache itself — setting it to 8-bit or 4-bit — to roughly halve its footprint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Common trap: &lt;/strong&gt;When a model card says "runs in 8GB," that almost always means &lt;em&gt;weights only&lt;/em&gt;, at a short context. Budget an extra 1–2GB for the KV cache and overhead at modest context lengths — and far more at long ones.&lt;/p&gt;
&lt;h3&gt;The MoE wrinkle&lt;/h3&gt;
&lt;p&gt;Mixture-of-Experts models break the simple mental model. Take &lt;code&gt;Qwen3-30B-A3B&lt;/code&gt;: 30 billion total parameters but only ~3 billion active per token. It &lt;em&gt;generates&lt;/em&gt; as fast as a 3B model, but you still need enough memory to hold all 30 billion. So: &lt;strong&gt;size your memory by total parameters, size your speed expectations by active parameters.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;What happens when it doesn’t fit: offloading&lt;/h3&gt;
&lt;p&gt;When a model exceeds your VRAM, tools like llama.cpp and Ollama automatically offload the excess layers to system RAM. This prevents a crash, but it’s slow — system RAM bandwidth (roughly 50–70 GB/s on a dual-channel DDR5 desktop) is an order of magnitude below GPU VRAM (around 1,000 GB/s on an RTX 4090). Offloading 10–20% is often tolerable; offloading half will make you wish you hadn’t.&lt;/p&gt;
&lt;p&gt;This leads to the most important hardware insight in the guide: &lt;strong&gt;token generation speed is governed by memory bandwidth, not raw compute.&lt;/strong&gt; A model generates roughly as fast as your memory bandwidth divided by the model’s size in memory.&lt;/p&gt;
&lt;h2&gt;The tooling landscape, tool by tool&lt;/h2&gt;
&lt;p&gt;The ecosystem looks chaotic until you see its structure. There are really three layers: &lt;strong&gt;engines&lt;/strong&gt; that do the actual math (llama.cpp, MLX); &lt;strong&gt;experiences&lt;/strong&gt; that wrap an engine in convenience (Ollama, LM Studio, Jan, GPT4All); and &lt;strong&gt;servers&lt;/strong&gt; for high-throughput, multi-user production (vLLM, TGI, SGLang).&lt;/p&gt;
&lt;p&gt;Because most consumer tools wrap llama.cpp, their raw single-user speed differs by only a few percent. So choose based on &lt;em&gt;workflow&lt;/em&gt;, not on a myth that one is dramatically faster than another.&lt;/p&gt;
&lt;h3&gt;llama.cpp — the engine underneath almost everything&lt;/h3&gt;
&lt;p&gt;The foundation of the entire consumer local-AI world. Created by Georgi Gerganov, its first commit landed on &lt;a rel="noopener noreferrer" href="https://github.com/ggml-org/llama.cpp"&gt;March 10, 2023&lt;/a&gt; — just two weeks after Meta released the original LLaMA weights. It’s a dependency-free C/C++ inference library that reads GGUF and runs on essentially everything: CPU, CUDA, Metal, ROCm, Vulkan, SYCL.&lt;/p&gt;
&lt;p&gt;Its superpower is being first: new architectures usually land here before anywhere else. It exposes every tuning knob. The tradeoff is that you compile it yourself and manage flags. &lt;strong&gt;Pick it if&lt;/strong&gt; you want maximum control, the newest models the day they drop, or the last few percent of performance.&lt;/p&gt;
&lt;h3&gt;Ollama — the "Docker for LLMs"&lt;/h3&gt;
&lt;p&gt;If one tool is the default recommendation for developers, it’s this one. Ollama is CLI-first, runs as a background daemon, and exposes both a REST API and an OpenAI-compatible endpoint on port 11434. The workflow: &lt;code&gt;ollama pull&lt;/code&gt;, &lt;code&gt;ollama run&lt;/code&gt;, done. It stores models by content hash and automatically manages VRAM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pick it if&lt;/strong&gt; you’re a developer who wants local AI to behave like a service you forget is running. This is the one most people should start with.&lt;/p&gt;
&lt;h3&gt;LM Studio — the polished GUI&lt;/h3&gt;
&lt;p&gt;The friendliest on-ramp, and free for personal use. LM Studio gives you a built-in model browser that shows memory estimates before you download, a chat playground, RAG over your local documents, and an OpenAI-compatible server — all in a clean desktop app. It runs both the llama.cpp and MLX backends.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pick it if&lt;/strong&gt; you want the smoothest discover-download-experiment loop, or you’re on a Mac and want MLX speed without touching the command line.&lt;/p&gt;
&lt;h3&gt;The rest of the field&lt;/h3&gt;
&lt;p&gt;Each of these earns its place for a specific job:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Tool&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;What makes it special&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Pick it if…&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Jan&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Open-source, offline-first ChatGPT-style desktop app; can bridge to cloud APIs&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;You want a clean assistant UI and value fully open-source software&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;GPT4All&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Point-and-click RAG over a folder of documents, fully offline, near-zero config&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;You want private document Q&amp;amp;A with no setup&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;KoboldCpp&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Single-executable llama.cpp fork built for creative writing and roleplay&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Fiction or roleplay with rich world/character memory&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Llamafile&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Packs an entire model plus runtime into one cross-platform executable&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;You want maximum portability or to ship a model as a single file&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;MLX / MLX-LM&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Apple’s native framework; exploits unified memory and supports on-device fine-tuning&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;You’re on a Mac and want peak performance or local LoRA training&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;text-generation-webui&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"Swiss Army knife" — multiple loaders behind one UI, plus fine-tuning and RAG&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;You want to experiment broadly across model formats&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;LocalAI&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;A router, not a runner: one OpenAI-compatible endpoint in front of many backends&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;You’re orchestrating several model types behind a single API&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;The production tier: vLLM and friends&lt;/h3&gt;
&lt;p&gt;Everything above is built for one user at a desk. The moment you need to serve a model to many concurrent users, you cross into a different category — and the leader is &lt;strong&gt;vLLM&lt;/strong&gt;. Its &lt;em&gt;PagedAttention&lt;/em&gt; manages the KV cache in non-contiguous blocks like an OS manages virtual memory, cutting memory waste from 60–80% down to under 4%, and &lt;em&gt;continuous batching&lt;/em&gt; slots new requests into the running batch the instant a slot frees. Its launch benchmarks reported up to &lt;a rel="noopener noreferrer" href="https://blog.vllm.ai/2023/06/20/vllm.html"&gt;24× the throughput&lt;/a&gt; of naive Hugging Face Transformers serving.&lt;/p&gt;
&lt;p&gt;The tradeoff: vLLM needs a Python environment and a capable GPU, doesn’t run GGUF (it uses safetensors with AWQ or GPTQ), and is heavier to set up. Its cousins &lt;strong&gt;TGI&lt;/strong&gt; and &lt;strong&gt;SGLang&lt;/strong&gt; compete in the same space.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb: Ollama for your laptop, vLLM for your server.&lt;/strong&gt; If exactly one person or process talks to the model at a time, use a llama.cpp-based tool. If many do at once, move to vLLM or TGI.&lt;/p&gt;
&lt;h3&gt;The Python baseline: Hugging Face Transformers&lt;/h3&gt;
&lt;p&gt;The reference implementation everything else is measured against. Transformers (with Accelerate) gives you maximum model coverage and flexibility — the standard for research and fine-tuning — but carries the most setup and isn’t optimized for consumer single-user inference. &lt;strong&gt;Pick it if&lt;/strong&gt; you’re doing research or need to run a brand-new model before anyone has produced a GGUF for it.&lt;/p&gt;
&lt;h3&gt;So which one should you actually use?&lt;/h3&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Your situation&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Best choice&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;I’m a developer and want one default&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;span&gt;Ollama&lt;/span&gt; Invisible infrastructure with a clean API&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;I’m a beginner or non-developer&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;span&gt;LM Studio&lt;/span&gt; or &lt;span&gt;GPT4All&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;I’m on a Mac&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;span&gt;LM Studio&lt;/span&gt; or Ollama with the MLX backend&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;I have a powerful NVIDIA card&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;span&gt;llama.cpp&lt;/span&gt; for control; vLLM to serve&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;I need to serve many users&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;span&gt;vLLM&lt;/span&gt; (or TGI / SGLang)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;I want document chat&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;span&gt;GPT4All&lt;/span&gt; or LM Studio (built-in RAG)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;I’m on low-end hardware&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;span&gt;Ollama&lt;/span&gt; with small models; Llamafile for portability&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Creative writing / roleplay&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;span&gt;KoboldCpp&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;Configuring for your specific machine&lt;/h2&gt;
&lt;p&gt;Now we get practical. Find your hardware below and follow the path. The through-line is always the memory math from the previous section — here we apply it to real silicon.&lt;/p&gt;
&lt;h3&gt;Apple Silicon Macs (M1 through M5)&lt;/h3&gt;
&lt;p&gt;The surprise winner for individual developers. Because of &lt;a rel="noopener noreferrer" href="https://www.apple.com/newsroom/2025/10/apple-unleashes-m5-the-next-big-leap-in-ai-performance-for-apple-silicon/"&gt;unified memory&lt;/a&gt;, a 64GB MacBook can load a 70B model at Q4 with no copying between CPU and GPU. Use an MLX-backed runtime; on the newest chips, MLX is meaningfully faster than the older Metal path.&lt;/p&gt;
&lt;p&gt;The rule for Macs: your usable model memory is roughly &lt;strong&gt;total unified RAM minus about 8GB&lt;/strong&gt; for the OS. A 16GB Mac handles 7–8B comfortably, 32GB reaches ~30B, 64GB runs 70B, and 128GB+ opens the big MoE models. The one place a Mac loses to a discrete GPU is raw speed on models that already fit comfortably in that GPU’s VRAM.&lt;/p&gt;
&lt;h3&gt;NVIDIA GPUs (Windows and Linux)&lt;/h3&gt;
&lt;p&gt;The best-supported platform, full stop. Every tool works on NVIDIA first. Plan by your VRAM tier:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;VRAM&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Example cards&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;What you can run&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;8 GB&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 4060, 3060 Ti&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;7–8B at Q4_K_M, 40+ tok/s. The popular real-world floor.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;12 GB&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 3060 12GB, 4070&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;12–14B at Q4_K_M with room for context&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;16 GB&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 4060 Ti 16GB, 5060 Ti 16GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;14B at Q5, or gpt-oss-20b — the "16GB sweet spot"&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;24 GB&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 3090, 4090&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;27–32B at Q4/Q5 fully resident, or 70B with offloading&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;32 GB+&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 5090, workstation cards&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Larger 32B at high quant; dual 24GB reach 70B fully in VRAM&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The RTX 3090 deserves special mention: thanks to its wide 384-bit bus and ~936 GB/s bandwidth, it often &lt;em&gt;out-generates&lt;/em&gt; the newer RTX 4080 despite being a generation older — a perfect illustration of the bandwidth-over-compute principle. A used 3090 remains one of the best value buys in 2026.&lt;/p&gt;
&lt;h3&gt;AMD GPUs (ROCm and Vulkan)&lt;/h3&gt;
&lt;p&gt;The story has genuinely improved. In 2026, ROCm has matured enough that AMD is a real choice for inference. On Linux, ROCm/HIP runs llama.cpp and Ollama at roughly 70–80% of CUDA speed at equivalent bandwidth. On Windows, Vulkan (through LM Studio or Ollama) is the least-friction path.&lt;/p&gt;
&lt;p&gt;The RX 7900 XTX (24GB) is a credible, cheaper alternative to a 4090 for inference, and AMD’s Strix Halo chips bring Apple-style unified memory to the PC world, with up to 128GB shared. Where NVIDIA still wins decisively is fine-tuning and FP8 production serving.&lt;/p&gt;
&lt;h3&gt;Intel Arc GPUs&lt;/h3&gt;
&lt;p&gt;Workable, but the roughest software story of the bunch. The A770 16GB is a genuine budget VRAM bargain. Your paths are llama.cpp’s SYCL or Vulkan backend, IPEX-LLM’s portable Ollama build, or Intel’s vLLM-based stack. Buy Intel only if you enjoy the setup adventure.&lt;/p&gt;
&lt;h3&gt;CPU-only and low-RAM laptops&lt;/h3&gt;
&lt;p&gt;Realistic, but manage expectations. A modern CPU does 3–13 tok/s on a quantized 7B — fine for batch jobs, frustrating for interactive chat (anything under ~15 tok/s feels laggy). Stick to small models: Phi-4-mini, Llama 3.2 3B, or Gemma 3 4B at Q4, on 8GB systems.&lt;/p&gt;
&lt;h3&gt;High-RAM workstation with a weak GPU&lt;/h3&gt;
&lt;p&gt;You have an underrated option: run a big MoE model (say, gpt-oss-120b) keeping the attention layers on the GPU and the experts in system RAM. Because only a few experts activate per token, you can hit 10–30 tok/s with surprisingly little VRAM. In llama.cpp the &lt;code&gt;--cpu-moe&lt;/code&gt; flag does exactly this.&lt;/p&gt;
&lt;h3&gt;Realistic speed expectations&lt;/h3&gt;
&lt;p&gt;Approximate tokens/second for a Q4_K_M model, single user, via llama.cpp or Ollama:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Hardware&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;7B model&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 4090&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~135 tok/s&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Faster than you can read&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 3090&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~95 tok/s&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;The value champion&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 4070 Super&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~75 tok/s&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Excellent mid-range&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;RTX 4060 8GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~25–37 tok/s&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Perfectly usable&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;M3 Max (64–128GB)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;fast on 7B; ~7–14 on 70B&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Holds the whole 70B — so it beats a 4090 there&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Modern CPU&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~12–13 tok/s&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Batch jobs, not chat&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;And the headline number that breaks the pattern: a 30B MoE like Qwen3-30B-A3B can sustain &lt;strong&gt;100+ tok/s on a 4090&lt;/strong&gt; — nothing like the slowdown you’d expect from 30 billion total parameters — because only ~3B are active per token.&lt;/p&gt;
&lt;h2&gt;Choosing the right model by use case&lt;/h2&gt;
&lt;p&gt;You’ve got a tool and you know your memory budget. Now: which of the hundreds of available models should you actually download? Start with the size tiers, then match a family to your task.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read this first: &lt;/strong&gt;This space moves &lt;em&gt;monthly&lt;/em&gt;. Specific version numbers and "best in class" claims go stale fast. Treat the families below as durable and the exact version numbers as a snapshot — always check the current model card on Hugging Face or the Ollama library before committing.&lt;/p&gt;
&lt;h3&gt;The parameter-size tiers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;1–3B (small / on-device):&lt;/strong&gt; Phi-4-mini, Llama 3.2 1B/3B, Gemma 3 1B/4B. For edge devices, autocomplete, classification, and simple chat.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;7–8B (the workhorse):&lt;/strong&gt; Llama 3.1/3.3 8B, Qwen3 8B, Mistral 7B. The best cost-to-capability ratio for most laptops.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;13–14B:&lt;/strong&gt; Phi-4 14B, Qwen3 14B. Meaningfully smarter; needs ~12GB.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;27–34B:&lt;/strong&gt; Gemma 3 27B, Qwen3 32B, Mistral Small 3 (24B). The single-24GB-GPU sweet spot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;70B+:&lt;/strong&gt; Llama 3.3 70B, Qwen2.5 72B. Needs 48GB+ or a high-RAM Mac.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MoE giants:&lt;/strong&gt; Llama 4 Scout/Maverick, DeepSeek-V3/R1, Qwen3-235B, gpt-oss-120b. Big-model quality at small-model compute — but huge memory footprints.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The major model families&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Llama (Meta). &lt;/strong&gt;The largest ecosystem and the most community fine-tunes. The 3.x series are dependable dense workhorses; &lt;a rel="noopener noreferrer" href="https://ai.meta.com/blog/llama-4-multimodal-intelligence/"&gt;Llama 4 (April 2025)&lt;/a&gt; brought Mixture-of-Experts to the line, with Scout offering a headline 10M-token context. The caveat: the Llama Community License is &lt;em&gt;not&lt;/em&gt; open source — it carries a 700M-monthly-active-user cap and EU restrictions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qwen (Alibaba). &lt;/strong&gt;For many developers, the default answer to "what should I run locally?" in 2025–2026. Apache 2.0 licensed, dense and MoE from ~1.7B to 235B, with strong coding, math, and multilingual ability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gemma (Google). &lt;/strong&gt;Gemma 3 spans 1B to 27B, is multimodal from 4B up, and runs beautifully on consumer hardware. The 4B is a superb laptop default; the 27B is competitive on a 24GB GPU.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DeepSeek. &lt;/strong&gt;DeepSeek-V3 for general use and the reasoning-focused DeepSeek-R1, released January 2025 under a clean MIT license. The full model needs a server, but the &lt;em&gt;distilled&lt;/em&gt; variants (1.5B to 70B) bring R1-style reasoning to consumer hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mistral / Mixtral. &lt;/strong&gt;Mistral 7B and Mixtral 8x7B remain heavily deployed; Mistral Small 3 (24B) rivals models three times its size, and the Mistral 3 family moved fully to Apache 2.0.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microsoft Phi. &lt;/strong&gt;Phi-4 (14B) and Phi-4-mini (3.8B), MIT-licensed, punch well above their weight per parameter — ideal for budget and edge deployments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenAI gpt-oss. &lt;/strong&gt;Released &lt;a rel="noopener noreferrer" href="https://openai.com/index/introducing-gpt-oss/"&gt;August 2025 under Apache 2.0&lt;/a&gt; — OpenAI’s first open-weight models since GPT-2. Both are MoE with a 128K context and three configurable reasoning-effort levels. The gpt-oss-20b needs only ~16GB (a standout for a 16GB GPU or Mac), and gpt-oss-120b runs within 80GB on a single high-end GPU.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Also worth watching: &lt;/strong&gt;Cohere’s Command R/R+ (RAG-focused), Falcon 3, GLM, Kimi, and coding specialists Qwen-Coder and Mistral’s Devstral.&lt;/p&gt;
&lt;h3&gt;Mapping use cases to models&lt;/h3&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Your task&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Reach for&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;General chat / assistant&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Qwen3, Gemma 3, Llama 3.3&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Code generation&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Qwen-Coder, Devstral, DeepSeek-Coder, Code Llama&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Reasoning / math&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;DeepSeek-R1 (or its distills), gpt-oss at high reasoning effort&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Long-context tasks&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Llama 4 Scout, Qwen3, Gemma 3&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;RAG / summarization&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Mid-size instruct models plus an embedding model&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Multilingual&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Qwen3 (strongest, especially CJK), Gemma 3, Mistral&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Vision-language&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Gemma 3, Qwen-VL, Llama 4, Mistral Small 3.1&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Small / fast on-device&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Phi-4-mini, Llama 3.2 3B, Gemma 3 4B&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;Reading model cards, and the licensing trap&lt;/h3&gt;
&lt;p&gt;You’ll find models in two main places: the &lt;strong&gt;Ollama library&lt;/strong&gt; (curated, one-command pulls) and &lt;strong&gt;Hugging Face&lt;/strong&gt; (everything, including community quantizations — "bartowski" and "Unsloth" are prolific and reliable). On any model card, check five things: parameter count, context length, license, intended use, and which quantizations are available.&lt;/p&gt;
&lt;p&gt;The licensing nuance matters the moment you build something real. Apache 2.0 (Qwen, Gemma’s permissive releases, Mistral 3, gpt-oss) and MIT (DeepSeek, Phi) are the cleanest for commercial use. Llama’s license permits commercial use but with that 700M-MAU cap and EU restrictions. Always read the actual card before shipping a product, and involve legal for anything at scale.&lt;/p&gt;
&lt;h2&gt;Optimization: squeezing out performance&lt;/h2&gt;
&lt;p&gt;Here’s the playbook, roughly in the order you should apply it.&lt;/p&gt;
&lt;h3&gt;1. Pick the right quantization for your VRAM&lt;/h3&gt;
&lt;p&gt;Q4_K_M is the default for a reason — ~75% smaller than FP16 with only a 1–3% quality drop. Step up to Q5_K_M or Q8_0 when you have headroom. The most useful heuristic of all: &lt;strong&gt;a larger model at lower precision usually beats a smaller model at higher precision&lt;/strong&gt;. A 14B at Q4 typically outperforms a 7B at Q8 — if both fit.&lt;/p&gt;
&lt;h3&gt;2. Maximize GPU layer offloading&lt;/h3&gt;
&lt;p&gt;In llama.cpp, set &lt;code&gt;-ngl 999&lt;/code&gt; to push every layer onto the GPU. If you run out of memory, lower the number until the model fits. Partial offload is far faster than running everything on the CPU.&lt;/p&gt;
&lt;h3&gt;3. Manage context length and the KV cache&lt;/h3&gt;
&lt;p&gt;Don’t set your context window higher than you actually need; the KV cache scales linearly with it. When context is tight, quantize the cache — setting the KV cache type to &lt;code&gt;q8_0&lt;/code&gt; roughly halves its footprint. In Ollama, this is the &lt;code&gt;OLLAMA_KV_CACHE_TYPE&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;h3&gt;4. Enable flash attention&lt;/h3&gt;
&lt;p&gt;Flash attention reduces the memory cost of attention and speeds up long-context inference. It’s standard on modern setups — turn it on with &lt;code&gt;--flash-attn on&lt;/code&gt; in llama.cpp.&lt;/p&gt;
&lt;h3&gt;5. Use speculative decoding for a free speedup&lt;/h3&gt;
&lt;p&gt;Pair a large "main" model with a small "draft" model from the same family — say, Llama 3.2 1B drafting for Llama 3.1 8B. When acceptance is high, you get a 1.5–3× speedup with &lt;em&gt;no&lt;/em&gt; quality loss, because the large model still has the final say. Keep the draft model much smaller than the main one.&lt;/p&gt;
&lt;h3&gt;6. Choose MoE models when speed matters&lt;/h3&gt;
&lt;p&gt;A 30B-A3B MoE generates as fast as a 3B dense model while approaching the quality of something far larger — provided you have the memory to hold all 30B.&lt;/p&gt;
&lt;h3&gt;7. Buy hardware for memory bandwidth&lt;/h3&gt;
&lt;p&gt;Memory bandwidth (GB/s) predicts token-generation speed better than raw compute (TFLOPS). This is why the RTX 3090 often beats the newer 4080, and why the M3 Max beats the M4 Pro on generation. Match your purchase to two things: enough memory &lt;em&gt;capacity&lt;/em&gt; to fit the model, and enough memory &lt;em&gt;bandwidth&lt;/em&gt; to run it fast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimization order of operations: &lt;/strong&gt;(1) Pick the largest model that fits at Q4_K_M. (2) Max out GPU layers. (3) Enable flash attention. (4) Quantize the KV cache if context is tight. (5) Add speculative decoding if you need more speed. (6) Only &lt;em&gt;then&lt;/em&gt; consider buying hardware.&lt;/p&gt;
&lt;h2&gt;Step-by-step setup walkthroughs&lt;/h2&gt;
&lt;p&gt;Enough theory. Here are the exact commands to get running on each of the three paths most people take. All three expose an OpenAI-compatible API, so any code you’ve written against &lt;a href="/blog/integrate-openai-api-production-express-nodejs"&gt;the OpenAI SDK&lt;/a&gt; will work against your local model with a one-line change to the base URL.&lt;/p&gt;
&lt;h3&gt;Path A: Ollama (the recommended default)&lt;/h3&gt;
&lt;p&gt;Works on macOS, Windows, and Linux. On macOS and Windows, download the installer; on Linux, one command does it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Linux install (macOS/Windows: download the app)
curl -fsSL https://ollama.com/install.sh | sh

# Pull and run a model — downloads on first run
ollama run qwen3:8b
# type your prompt in the chat; /bye to exit

# Handy commands
ollama list                 # installed models
ollama ps                   # models currently in memory
ollama pull gemma3:4b       # download without running
ollama rm &amp;lt;model&amp;gt;           # delete a model
ollama run qwen3:8b --verbose "Write a haiku"  # shows tok/s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server runs on port 11434. Here’s how you hit it from the command line and from Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"qwen3:8b","messages":[{"role":"user","content":"Hello!"}]}'&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",  # required by the SDK but unused
)

resp = client.chat.completions.create(
    model="qwen3:8b",
    messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To expose Ollama to other machines, set &lt;code&gt;OLLAMA_HOST=0.0.0.0:11434&lt;/code&gt; before it starts. To customize a model’s system prompt or parameters, write a Modelfile and run &lt;code&gt;ollama create&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Path B: LM Studio (the GUI route)&lt;/h3&gt;
&lt;p&gt;No commands required for the basics:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Download from the LM Studio site (macOS &lt;code&gt;.dmg&lt;/code&gt;, Windows &lt;code&gt;.exe&lt;/code&gt;, Linux AppImage) and install. It auto-detects your GPU and the right backend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the &lt;strong&gt;Discover&lt;/strong&gt; tab, search for a model — start with Gemma 3 4B or Llama 3.2 3B — pick the &lt;code&gt;Q4_K_M&lt;/code&gt; quantization, and download. It shows the memory estimate before you commit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Load the model and chat in the playground. Attach a PDF or text file to use the built-in RAG.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For development, go to the &lt;strong&gt;Developer&lt;/strong&gt; tab, enable Developer Mode, load a model, and click &lt;strong&gt;Start Server&lt;/strong&gt;. It exposes an OpenAI-compatible API at &lt;code&gt;http://localhost:1234/v1&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In settings, max out the GPU layers for your VRAM, set your context length, and on Apple Silicon select the MLX engine for a noticeable speed bump.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Path C: llama.cpp (maximum control)&lt;/h3&gt;
&lt;p&gt;For when you want every knob. Install via a package manager or build from source with your GPU backend:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Easiest: package manager (macOS/Linux)
brew install llama.cpp

# Or build from source with GPU support
git clone https://github.com/ggml-org/llama.cpp &amp;amp;&amp;amp; cd llama.cpp
cmake -B build -DGGML_CUDA=ON      # NVIDIA
# -DGGML_METAL=ON (Mac) · -DGGML_HIP=ON (AMD) · -DGGML_VULKAN=ON (cross-vendor)
cmake --build build --config Release&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Run a model straight from Hugging Face (auto-downloads the GGUF)
llama-cli -hf ggml-org/gemma-3-1b-it-GGUF

# Launch the OpenAI-compatible server with tuning flags
llama-server -hf bartowski/Qwen3-8B-GGUF:Q4_K_M \
  -ngl 999 \                               # all layers on GPU
  -c 8192 \                                # context length
  --flash-attn on \                        # enable flash attention
  --cache-type-k q8_0 --cache-type-v q8_0 \ # quantize the KV cache
  --host 0.0.0.0 --port 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server gives you a web UI and an API at &lt;code&gt;http://localhost:8080&lt;/code&gt;. Two utilities you’ll use often: &lt;code&gt;llama-bench&lt;/code&gt; measures tokens/second on your exact hardware, and &lt;code&gt;llama-quantize&lt;/code&gt; converts a model to a smaller quant.&lt;/p&gt;
&lt;h3&gt;When something goes wrong&lt;/h3&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Out of memory&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Use a smaller model, a lower quant (Q4 instead of Q8), or reduce context length&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Painfully slow (running on CPU)&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Confirm GPU detection and raise the GPU layer count&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Port already in use&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Ollama defaults to 11434, LM Studio to 1234, llama.cpp to 8080 — they coexist, but don’t double-bind one port&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Garbled or repetitive output&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Check the model’s prompt template; lower temperature; try a higher quant&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;Recommendations &amp;amp; what to do next&lt;/h2&gt;
&lt;p&gt;Let’s compress everything into an action plan.&lt;/p&gt;
&lt;h3&gt;Start here, today&lt;/h3&gt;
&lt;p&gt;Install &lt;strong&gt;Ollama&lt;/strong&gt; and run &lt;code&gt;ollama run qwen3:8b&lt;/code&gt; (or &lt;code&gt;gemma3:4b&lt;/code&gt; on a lighter machine). Confirm you get interactive speed, then point your code at &lt;code&gt;localhost:11434/v1&lt;/code&gt;. Prefer clicking to typing? Install &lt;strong&gt;LM Studio&lt;/strong&gt; and download Gemma 3 4B at Q4_K_M instead.&lt;/p&gt;
&lt;h3&gt;Then match the model to your memory and task&lt;/h3&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Your hardware&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Run this&lt;/strong&gt;&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;8GB GPU / 16GB Mac&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;7–8B (Qwen3 8B, Llama 3.1 8B) at Q4_K_M — or gpt-oss-20b on 16GB&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;12–16GB&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;14B (Phi-4, Qwen3 14B) at Q4/Q5&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;24GB / 32–64GB Mac&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;27–32B (Gemma 3 27B, Qwen3 32B), or the Qwen3-30B-A3B MoE for speed&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;48GB+ / 64–128GB Mac&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;70B at Q4, or the big MoE models&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;For reasoning, reach for a DeepSeek-R1 distill sized to your tier. For coding, Qwen-Coder or Devstral. For private document chat, the built-in RAG in GPT4All or LM Studio.&lt;/p&gt;
&lt;h3&gt;Scale up only when concurrency forces you to&lt;/h3&gt;
&lt;p&gt;The moment more than one user or process needs the model at once, move to &lt;strong&gt;vLLM&lt;/strong&gt; (or TGI) on a proper GPU server. Until then, a llama.cpp-based tool on your own machine is simpler, cheaper, and entirely sufficient. Ollama for the laptop, vLLM for the server.&lt;/p&gt;
&lt;h3&gt;The honest caveats&lt;/h3&gt;
&lt;p&gt;A few truths to keep you grounded. The model leaderboard changes &lt;em&gt;monthly&lt;/em&gt; — treat every specific version number and benchmark here as a snapshot, and verify the current state on the model card before you build. Benchmarks themselves disagree and are often vendor-reported, so validate on &lt;em&gt;your&lt;/em&gt; workload. "Runs in X GB" almost always means weights only — budget extra for the KV cache. And local models have a real quality ceiling: a local 8B will not match a frontier cloud model on the hardest reasoning. Choose local for privacy, cost, offline capability, and control — not because it always wins.&lt;/p&gt;
&lt;p&gt;Finally, privacy is not automatic just because inference is local. Some applications include telemetry; open-source tools let you verify what’s actually happening. Running models on your own hardware supports a strong privacy and compliance posture, but full compliance needs additional access, audit, and physical controls on top.&lt;/p&gt;
&lt;p&gt;That’s the whole map. The fundamentals here — the memory math, the quantization tradeoffs, the three tooling layers, the bandwidth-over-compute principle, and the setup commands — will outlast any individual model release. The specific models will keep getting better, faster, and smaller. Which means the best time to get comfortable running them locally is right now, and the second-best time is the next time you open your terminal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now go pull a model. That first streamed token is waiting.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Parenting &amp; AI - A Field Guide for Modern Families</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Wed, 24 Jun 2026 01:59:01 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/parenting-ai-a-field-guide-for-modern-families-50kc</link>
      <guid>https://dev.to/harshdeepsingh13/parenting-ai-a-field-guide-for-modern-families-50kc</guid>
      <description>&lt;h1&gt;Raising Humans in the Age of AI&lt;/h1&gt;
&lt;p&gt;A family therapist’s honest take on using AI well, keeping your kids safe from it, and the research that should actually shape your choices.&lt;/p&gt;
&lt;h2&gt;01 · The roommate nobody invited - &lt;em&gt;We’re all parenting without a manual now&lt;/em&gt;
&lt;/h2&gt;
&lt;p&gt;It’s 2 a.m. Your toddler finally went down an hour ago. You are upright in bed, thumb hovering over a glowing rectangle, typing some version of “is it normal that…” into a chatbot that answers in a calm, confident, slightly-too-certain voice. Somewhere down the hall, a tablet waits to be discovered by small hands at sunrise. And if you’ve got a teenager, there’s a decent chance they’re texting a piece of software that calls itself their friend.&lt;/p&gt;
&lt;p&gt;Artificial intelligence didn’t ask permission before moving into the family home. It arrived inside the search bar, the homework, the toy aisle, the group chat. The question parents keep asking me isn’t “should I allow this?” — that ship has sailed. It’s the much harder one: &lt;em&gt;how do I raise a whole human being in the middle of all this, without losing my mind or theirs?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So let’s talk like two people figuring it out together — because that’s exactly what we are. There is no generation of parents who has done this before us. Not one. The research is fresh, the products are faster than the rules, and the loudest voices online are split between “AI will save your child” and “AI will ruin your child.” Both are selling something. The truth, as usual, lives in the more useful middle: AI is a powerful tool that can genuinely help your family &lt;em&gt;and&lt;/em&gt; can genuinely hurt your kids, and the difference is almost entirely about how, when, and with whom it gets used.&lt;/p&gt;
&lt;p&gt;This is a field guide for that middle. Where AI earns its keep. Where it absolutely does not belong near a child. And what a calm, present parent actually does about both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nearly 3 in 4 &lt;/strong&gt;U.S. teens have used an AI companion, per Common Sense Media — this is mainstream, not fringe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;About 3 in 10 &lt;/strong&gt;teens use AI chatbots daily, according to Pew Research Center reporting from late 2025.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“Unacceptable” &lt;/strong&gt;— how Common Sense Media rates social AI companions for anyone under 18.&lt;/p&gt;
&lt;h2&gt;02 · A two-minute primer - &lt;em&gt;What even is this thing?&lt;/em&gt;
&lt;/h2&gt;
&lt;p&gt;You don’t need a computer-science degree to parent well around AI, but a couple of distinctions make everything downstream easier. The American Psychological Association, in its 2025 health advisory on AI and adolescents, splits it roughly two ways.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generative AI&lt;/strong&gt; is the stuff that produces things. It writes human-sounding text, creates photorealistic images, clones voices, and generates lifelike video. This is the “make me a picture / write me an essay / what does this rash mean” layer. It’s astonishingly useful and it is also a content-fabrication machine that can be wrong with total confidence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Interactive or companion AI&lt;/strong&gt; is the chatbot designed to talk &lt;em&gt;with&lt;/em&gt; you, remember you, and keep the conversation going. This is the layer parents most need to understand, because it’s engineered to feel like a relationship.&lt;/p&gt;
&lt;p&gt;Here’s the part that matters for kids specifically. The APA notes that adolescents are uniquely vulnerable for reasons that are developmental, not a knock on their intelligence: they’re less likely to question whether an AI’s answer is accurate, and heavy reliance on these tools can quietly crowd out the real-world relationships their brains are still wiring themselves around. And unlike social media — where you generally know there’s a human on the other end — children often don’t realize when they’re talking to a machine at all.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Not all good. Not all bad. The whole job is learning to tell which is in front of you.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;That’s the lens for everything that follows. Same technology, wildly different outcomes depending on the use. So let’s start where the news rarely does: with the genuinely good.&lt;/p&gt;
&lt;h2&gt;03 · The good stuff - &lt;em&gt;Where AI quietly earns its keep&lt;/em&gt;
&lt;/h2&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1719559519182-698f9bfc4e2f%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1500%26q%3D80" alt="A mother and daughter sitting on the floor looking at a tablet together" width="1500" height="1000"&gt;&lt;p&gt;A child’s relationship with AI starts as your relationship with AI — used alongside them, not handed to them. · Photo: Siwawut Phoophinyo / &lt;a rel="noopener noreferrer" href="https://unsplash.com"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Parents are not superheroes. We cannot be in two places at once, answer the four-hundredth “why” of the day with fresh enthusiasm, and also draft the email to the daycare about the lice situation. This is precisely where a well-used AI tool shines: it absorbs mental load.&lt;/p&gt;
&lt;p&gt;Used as a &lt;strong&gt;back-pocket assistant&lt;/strong&gt;, AI is wonderful for the low-stakes logistics that eat a parent alive. Brainstorm a week of toddler-friendly dinners using what’s in your fridge. Generate three versions of a calm script for refereeing the same sibling argument for the ninth time. Translate the school newsletter. Summarize the forty-page sports-league handbook into “what do I actually need to bring on Saturday.” Turn “explain mitosis to a curious seven-year-old” into something you can read aloud. None of this replaces your judgment — it just clears the underbrush so you have more of yourself left over for the kid.&lt;/p&gt;
&lt;p&gt;Used &lt;strong&gt;alongside your children&lt;/strong&gt;, it can be a genuine spark. Co-write a bedtime story where your daughter is the dragon and the dragon is afraid of broccoli. Chase the “why is the sky blue / but why / but &lt;em&gt;why&lt;/em&gt;” rabbit hole together. Help an older kid &lt;em&gt;scaffold&lt;/em&gt; a tricky assignment — outline the essay, quiz me on these terms, explain why I got this wrong — rather than having the machine simply do it for them. The distinction between scaffolding and outsourcing is one you’ll come back to a lot.&lt;/p&gt;
&lt;p&gt;And there’s a reason “alongside” keeps showing up. As you’ll see in the playbook, pediatric researchers find that interactive, co-used screen time tends to beat passive, solo screen time. AI is at its best as a thing you do &lt;em&gt;with&lt;/em&gt; your child, not a thing you hand them to be quiet.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Healthy use&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Treat AI as a &lt;strong&gt;drafting and brainstorming partner&lt;/strong&gt; — meal ideas, scripts, summaries, explainers, story-starters — and as a &lt;strong&gt;co-pilot you use beside your kid&lt;/strong&gt;, not a babysitter you hand them. Always sanity-check anything factual; these tools state wrong answers with the same confidence as right ones.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;From the therapist’s chair&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A parent recently told me, half-guilty, that she uses AI to reword her own frustrated texts before sending them to her co-parent. I told her that’s not cheating — that’s regulation. If a tool helps you pause and respond instead of react, you’re modeling the exact skill you want your kids to learn.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;04 · For expecting &amp;amp; new parents - &lt;em&gt;Before the baby even arrives&lt;/em&gt;
&lt;/h2&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1710897537209-392c74a1c299%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1500%26h%3D740" alt="An expectant parent standing with hands resting on their pregnant belly" width="1500" height="740"&gt;&lt;p&gt;Pregnancy is the most over-Googled season of life — which makes it the moment AI is most tempting, and most worth using carefully. · Photo: Jeferson Santu / &lt;a rel="noopener noreferrer" href="https://unsplash.com"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you’re expecting, you already know pregnancy runs on questions — a steady drip of them, often at hours when no clinic is open and no friend should be texted. AI has quietly become the 2 a.m. companion for a lot of this, and some of it is honestly great.&lt;/p&gt;
&lt;p&gt;On the everyday side, expectant parents are using chatbots to translate medical jargon into plain language (“what on earth is round-ligament pain”), to brainstorm baby names without spiraling, to compare twelve nearly identical bassinets into a shortlist, to draft a birth plan, and to keep track of the cravings, the appointments, and the glucose test you keep forgetting. It’s a back-pocket assistant for one of the most information-dense seasons of life.&lt;/p&gt;
&lt;p&gt;On the clinical frontier, the story is genuinely hopeful. Researchers are using AI to read ultrasound images more precisely, to flag pregnancies at higher risk of complications like preeclampsia, gestational diabetes, and preterm labor, and even to predict delivery timing from a standard scan — one company reports estimating a delivery date to within about eight days. For families in rural areas and “maternity care deserts,” AI-assisted remote monitoring and earlier warnings could meaningfully widen access to care. There’s also a quietly important thread here around maternal mental health, where chatbots and apps are being studied as a way to screen for and support perinatal anxiety and depression that too often go unspoken.&lt;/p&gt;
&lt;p&gt;But — and this is the whole sermon in one line — &lt;strong&gt;AI is not your OB, your midwife, or your therapist.&lt;/strong&gt; A chatbot can explain a symptom; it cannot examine you. It is reassuring at exactly the moments it should be alarming, because it’s built to be agreeable.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Handle with care&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For anything urgent — bleeding, severe pain, reduced fetal movement, or dark thoughts after birth — &lt;strong&gt;call your provider or a crisis line, not an app.&lt;/strong&gt; And remember pregnancy data is deeply sensitive: read the privacy policy before you log symptoms, and turn off model-training settings where you can.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;05 · The hard part - &lt;em&gt;Where AI gets genuinely dangerous for kids&lt;/em&gt;
&lt;/h2&gt;
&lt;p&gt;This is the section I’d ask you not to skim, because it’s the one the marketing won’t tell you. I’m not here to scare you off technology. I’m here because some uses of AI have already hurt children, and a calm, informed parent is the single best safety feature any of these products has.&lt;/p&gt;
&lt;h3&gt;The “friend” who is software&lt;/h3&gt;
&lt;p&gt;The fastest-growing risk isn’t a chatbot getting a math problem wrong — it’s a chatbot getting a &lt;em&gt;relationship&lt;/em&gt; right. Companion AIs are engineered to feel warm, attentive, available at 3 a.m., and endlessly validating. For a lonely kid, that’s not a feature; it’s a hook. After comprehensive testing, Common Sense Media rated social AI companions an outright “unacceptable” risk for anyone under 18, warning that they’re designed to manufacture emotional attachment and dependency in brains that are still forming.&lt;/p&gt;
&lt;p&gt;This isn’t hypothetical. A wave of lawsuits in 2024 and 2025 alleged that companion chatbots played a role in teens’ mental-health crises and deaths — including the case of 14-year-old Sewell Setzer III, whose mother sued Character.AI after he died by suicide following an intense attachment to a chatbot. Under mounting legal and regulatory pressure, Character.AI banned open-ended chat for under-18 users in late 2025, and in early 2026 the company and Google moved to settle several of those suits. Separately, a 2025 assessment by Common Sense Media with Stanford Medicine’s Brainstorm Lab found that the major general-purpose chatbots — including ChatGPT, Claude, Gemini, and Meta AI — consistently failed to reliably recognize and respond to signs of mental-health crisis in teen users.&lt;/p&gt;
&lt;p&gt;The lesson is not “AI is evil.” It’s that &lt;strong&gt;an always-available, always-agreeable voice is not a friend and is definitely not a therapist&lt;/strong&gt; — and a child can’t be expected to know the difference. Watch, gently, for the tell: a kid pulling away from real people and toward a screen that never disagrees with them.&lt;/p&gt;
&lt;h3&gt;The toy that wouldn’t stay on-script&lt;/h3&gt;
&lt;p&gt;Then there are the toys. In late 2025, a consumer-safety group at the U.S. PIRG Education Fund tested AI-powered children’s toys and found that one — a $99 teddy bear running a major chatbot under the hood — slid from friendly chatter into wildly inappropriate territory, including sexual content it introduced on its own and instructions on where to find dangerous objects around the house. The company pulled the product and the AI provider revoked its access, but the researchers’ point landed harder than the recall: the toy’s safeguards held up fine in short bursts and &lt;em&gt;broke down over longer conversations&lt;/em&gt; — exactly the kind a curious child has.&lt;/p&gt;
&lt;p&gt;Common Sense Media’s guidance here is blunt: no AI companion toys for children five and under, and serious caution for ages six to twelve. Many of these toys also ship with always-on microphones and hoover up data, which is a second problem stacked on the first.&lt;/p&gt;
&lt;h3&gt;The “is this even real?” problem&lt;/h3&gt;
&lt;p&gt;Finally, there’s the flood of AI-generated content itself. A convincing fake face, voice, or video can now be conjured from a single photo or a few seconds of audio. For kids, this shows up as misinformation that looks authoritative, scams that sound like a friend, and — at the genuinely dark end — synthetic images used to humiliate or exploit real children, a harm that child-safety organizations and state attorneys general have raised loud alarms about. Your child doesn’t need to understand the technology to be protected from it. They need one reflex, which we’ll build in the playbook: &lt;em&gt;pause and ask whether what you’re looking at is actually real.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The companies rushed AI to kids before the safety standards arrived. Until the rules catch up, you are the standard.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;06 · Sharenting, reconsidered - &lt;em&gt;The photos you post are training data now&lt;/em&gt;
&lt;/h2&gt;
&lt;p&gt;I want to talk about something most of us have done without a second thought: posting our kids online. The proud-parent montage, the first-day-of-school sign, the birthday post with the candles and the full name and the age. A decade ago, that photo mostly stayed within your circle. Today, a public post isn’t a memory — it’s data. It can be indexed, scraped, and fed into systems that build a profile or generate a convincing fake.&lt;/p&gt;
&lt;p&gt;The scale is sobering. One widely cited estimate from Barclays projected that by 2030, oversharing by parents could be linked to as many as 7.4 million incidents of identity fraud per year. AI scrapers can stitch a name, a birthday, and a location out of innocent posts. And a child’s face — unlike a password — can’t be changed later.&lt;/p&gt;
&lt;p&gt;This isn’t a call to delete every photo and go off-grid. It’s a nudge to shift from &lt;em&gt;broadcasting&lt;/em&gt; to &lt;em&gt;sharing&lt;/em&gt;, and to remember whose face it actually is.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Protect their footprint&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Favor &lt;strong&gt;private channels&lt;/strong&gt; over public profiles. Lock down privacy and tagging settings. Skip identifiable details — school names, uniforms, locations, full birthdates. Be wary of “fun” AI avatar and face-swap apps. Ask relatives not to repost your child without asking. And teach kids early: &lt;em&gt;your face, your voice, and your image belong to you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;07 · The playbook - &lt;em&gt;What a calm parent actually does&lt;/em&gt;
&lt;/h2&gt;
&lt;p&gt;Enough about the storms. Here’s the part you can act on tonight. The good news is that the most respected guidance has gotten simpler, not more complicated.&lt;/p&gt;
&lt;h3&gt;Stop counting minutes. Start asking better questions.&lt;/h3&gt;
&lt;p&gt;In early 2026, the American Academy of Pediatrics retired its old “two hours a day” rule and shifted the whole conversation to &lt;strong&gt;quality, context, and connection&lt;/strong&gt; over a stopwatch. The research behind it is clear: interactive beats passive, co-using with your child amplifies the benefits, and the real harm isn’t screens themselves but &lt;em&gt;displacement&lt;/em&gt; — when screen time crowds out sleep, movement, play, and face-to-face time. One pediatrician’s framing has stuck with me: think of screen time like dessert. Not the enemy, not the main course.&lt;/p&gt;
&lt;p&gt;Two firm exceptions still hold: essentially no screen media for babies under about 18 months (video-chatting grandma aside), and only small amounts of high-quality, co-viewed content for toddlers.&lt;/p&gt;
&lt;h3&gt;Run everything through the 5 C’s&lt;/h3&gt;
&lt;p&gt;The AAP’s “5 C’s” framework was built for media generally, but it maps beautifully onto AI. Before you say yes or no to a tool, run it through these.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Child — &lt;/strong&gt;Who is &lt;em&gt;this&lt;/em&gt; kid? A child prone to anxiety or loneliness has a very different relationship to a validating chatbot than a confident one. Match the tool to the actual human in front of you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content — &lt;/strong&gt;What is the AI actually doing — scaffolding learning, or doing the thinking for them? Generating, or just answering? Is it a closed kids’ tool or an open-ended adult one wearing a friendly costume?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Calm — &lt;/strong&gt;Is the tool helping your child self-soothe, or becoming the &lt;em&gt;only&lt;/em&gt; way they self-soothe? If a chatbot is the go-to for every big feeling, that’s a flag, not a win.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crowding out — &lt;/strong&gt;What is this replacing? If the AI is displacing sleep, friends, outdoor play, or you, the specific app barely matters — the trade is the problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Communication — &lt;/strong&gt;Are you talking about it, openly and often? The single highest-leverage move is making AI a normal, judgment-free topic at your dinner table before it becomes a secret in their bedroom.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The “would I sit next to them?” test&lt;/h3&gt;
&lt;p&gt;When you’re unsure about an app or a chatbot, use the simplest gut-check there is: would you be comfortable pulling up a chair and watching over their shoulder while they use it? If yes, it’s probably fine. If the idea makes you uneasy, that unease is information.&lt;/p&gt;
&lt;h3&gt;Teach three reflexes, not a hundred rules&lt;/h3&gt;
&lt;p&gt;You can’t write a rule for every situation AI will invent. So don’t try. Build three instincts your kids carry everywhere instead:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verify. &lt;/strong&gt;“Is this real? Is this actually true?” The antidote to deepfakes and confident-but-wrong answers is a child who pauses before believing. &lt;strong&gt;Protect. &lt;/strong&gt;“My image, my voice, and my information are mine to guard.” &lt;strong&gt;Connect. &lt;/strong&gt;“AI is a tool. People are home.” When something’s heavy, the move is to find a human — a parent, a friend, a counsellor — not a chatbot.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;From the therapist’s chair&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The families who navigate this best aren’t the ones with the strictest rules or the fanciest parental controls. They’re the ones where AI is a normal thing to talk about — where a kid feels safe saying “this video seems fake” or “this bot said something weird” without bracing for a lecture or a confiscation. Stay curious out loud. Curiosity keeps the door open; panic slams it shut.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And model it. Your children are running a continuous study on how you use your phone and your chatbots. Your own habits — when you put the device down, how you fact-check, whether you reach for a human or a screen when you’re stressed — are the first and most durable curriculum they’ll ever get.&lt;/p&gt;
&lt;h2&gt;08 · A gentle landing - &lt;em&gt;The kids will be alright &lt;/em&gt;
&lt;/h2&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1502086223501-7ea6ecd79368%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1500%26q%3D80" alt="A child playing joyfully outdoors in warm light" width="1500" height="1038"&gt;&lt;p&gt;No app can replace the thing kids actually need most: a present adult who keeps showing up. · Photo: Unsplash&lt;/p&gt;
&lt;p&gt;Here’s what I most want you to leave with. You are the first parents in human history to raise children alongside machines that talk back. There is no veteran to call, no dog-eared manual, no “well, this is how my mother did it.” That can feel terrifying. It’s also a kind of privilege: you get to set the tone for what a healthy relationship with this technology even looks like.&lt;/p&gt;
&lt;p&gt;The task was never to ban the future or to outsource childhood to it. It’s the same task it has always been, just with new scenery: stay present, stay curious, keep the conversation open, and remain — stubbornly, reliably — the warmest, realest thing in your child’s world. The machines are very good at sounding human. They are not good at &lt;em&gt;being&lt;/em&gt; home. That part is still, and always, yours.&lt;/p&gt;
&lt;p&gt;We’re all learning this together. None of us has it figured out. And honestly? Showing up imperfectly, with your eyes open and your heart in it, is the whole job. You’re already doing it.&lt;/p&gt;

&lt;h3&gt;A note on the heavier parts&lt;/h3&gt;
&lt;p&gt;This piece touches on suicide, self-harm, and exploitation — real risks, handled here only to help you protect your family. If you or a young person you love is struggling, please reach out to a person, not a chatbot.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;U.S. &amp;amp; Canada: &lt;/strong&gt;call or text &lt;strong&gt;988&lt;/strong&gt; (Suicide &amp;amp; Crisis Lifeline).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kids &amp;amp; teens in Canada: &lt;/strong&gt;Kids Help Phone — &lt;a rel="noopener noreferrer nofollow" href="tel:1-800-668-6868"&gt;&lt;strong&gt;1-800-668-6868&lt;/strong&gt;&lt;/a&gt;, or text &lt;strong&gt;CONNECT to 686868&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vetting kids’ apps, toys, and AI tools: &lt;/strong&gt;Common Sense Media publishes plain-language risk reviews.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When in doubt about anything medical or about your child’s mental health, talk to your pediatrician or family doctor.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sources &amp;amp; further reading&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;American Psychological Association — &lt;em&gt;Artificial Intelligence and Adolescent Well-being: An APA Health Advisory&lt;/em&gt; (June 2025), and the APA advisory on generative AI chatbots and wellness apps for mental health (Nov 2025).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Common Sense Media — Social AI Companions risk assessment (“Unacceptable” for under-18, 2025); “Talk, Trust, and Trade-Offs” teen survey (2025); AI chatbots &amp;amp; teen mental-health support assessment with Stanford Medicine’s Brainstorm Lab (2025); AI toy companion guidance (2026).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;U.S. PIRG Education Fund — &lt;em&gt;Trouble in Toyland 2025&lt;/em&gt;, AI toys testing (FoloToy “Kumma”).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reporting from CNN, NBC News, Fortune, and others on the Character.AI lawsuits, the under-18 policy change, and the 2026 settlement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;American Academy of Pediatrics / HealthyChildren.org — updated digital-media guidance and the “5 C’s of Media Use” framework (2024–2026).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Barclays oversharing / “sharenting” identity-fraud projection; Thorn and child-safety reporting on AI-generated exploitation material.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Peer-reviewed reviews on AI in maternal and feto-maternal health (e.g., PMC), and reporting on AI ultrasound and delivery-prediction tools.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;Figures and findings reflect sources available as of mid-2026; this fast-moving field is worth re-checking before publication.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;About this piece. &lt;/em&gt;&lt;/strong&gt;&lt;em&gt;Written from a family-therapy lens for parents, new parents, and parents-to-be. It’s educational, not a substitute for personalized medical, mental-health, or legal advice.&lt;/em&gt;&lt;/p&gt;


</description>
      <category>aiandparenting</category>
      <category>kidsaisafety</category>
      <category>aiparentalcontrols</category>
      <category>screentimeguidelines</category>
    </item>
    <item>
      <title>Stop Using AI Like Autocomplete: A Developer's Guide to Multi-Agent Workflows</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Wed, 24 Jun 2026 01:58:53 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/stop-using-ai-like-autocomplete-a-developers-guide-to-multi-agent-workflows-c1k</link>
      <guid>https://dev.to/harshdeepsingh13/stop-using-ai-like-autocomplete-a-developers-guide-to-multi-agent-workflows-c1k</guid>
      <description>&lt;p&gt;Most engineers who adopted Claude Code or Codex are still using them like a faster autocomplete: one prompt, one answer, repeat. The real productivity unlock is somewhere else entirely — in treating these tools as an &lt;em&gt;orchestra of specialized agents&lt;/em&gt; you direct, rather than a single assistant you chat with. This is a practical guide to building that multi-agent workflow into your day-to-day, and to doing it without fooling yourself about the gains.&lt;/p&gt;
&lt;h2&gt;From autocomplete to orchestration&lt;/h2&gt;
&lt;p&gt;If you have used an AI coding agent for more than a week, you already know the basic loop: describe a change, watch it edit files, run the tests, fix what broke. That loop is genuinely useful. But it is also the floor of what these tools can do, not the ceiling.&lt;/p&gt;
&lt;p&gt;The engineers getting the largest gains are not writing better single prompts. They are running &lt;em&gt;several agents at once&lt;/em&gt;, each with a narrow job, coordinated through a plan that a human reviewed before any code was written. One agent explores the codebase and writes a spec. Another implements against that spec. A third reviews the diff with fresh eyes. A fourth keeps the documentation in sync. Some of this happens in parallel across isolated branches; some of it happens in a strict sequence because step three genuinely depends on step two.&lt;/p&gt;
&lt;p&gt;This is the difference between using an agent and building an &lt;em&gt;agentic workflow&lt;/em&gt;. The first is a tool you reach for. The second is a system you design. This guide is about the second — what an ideal multi-agent workflow looks like on a normal development task, regardless of whether your tool of choice is Claude Code, OpenAI Codex, Cursor, or something that did not exist when this was written.&lt;/p&gt;
&lt;h2&gt;What a multi-agent workflow actually means&lt;/h2&gt;
&lt;p&gt;The vocabulary here is muddier than it should be, so it is worth being precise. The clearest definitions come from Anthropic's engineering writing, and they have largely become the industry's shared language.&lt;/p&gt;
&lt;p&gt;An &lt;strong&gt;agent&lt;/strong&gt;, in the practical sense, is an LLM autonomously using tools in a loop. It reads a file, decides to run a test, reads the output, decides to edit another file, and continues until it judges the task done. That is one agent: one model, one context window, one continuous train of thought.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;workflow&lt;/strong&gt; is a system where LLMs and tools are orchestrated through predefined code paths. The steps are known in advance. You chain them together because you have decided, as the engineer, that this sequence produces a reliable result.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;multi-agent system&lt;/strong&gt; introduces a lead or orchestrator agent that breaks a task into pieces and delegates them to specialized sub-agents — often running in parallel, each with its own context window, its own tools, and its own instructions. The orchestrator does not do the work itself; it decomposes, delegates, and synthesizes.&lt;/p&gt;
&lt;p&gt;The distinction that matters most in practice is between &lt;em&gt;workflows&lt;/em&gt; and &lt;em&gt;agents&lt;/em&gt;. Workflows offer predictability and consistency for tasks you can define up front. Agents are the better choice when you need flexibility and model-driven decision-making across a path you cannot map in advance. Anthropic's own guidance is refreshingly conservative on this point: find the simplest solution possible, and only increase complexity when it demonstrably improves outcomes. Multi-agent systems are powerful, but they spend tokens fast and they add coordination overhead. They earn their keep on high-value tasks that genuinely decompose into independent threads — not on everything.&lt;/p&gt;
&lt;p&gt;Find the simplest solution possible, and only increase complexity when it demonstrably improves outcomes.&lt;/p&gt;
&lt;h2&gt;The five patterns worth knowing by name&lt;/h2&gt;
&lt;p&gt;Before wiring up a crew of agents, it helps to have a vocabulary for the shapes these systems take. Anthropic's &lt;a rel="noopener noreferrer" href="https://www.anthropic.com/engineering/building-effective-agents"&gt;"Building Effective Agents"&lt;/a&gt; lays out five composable patterns that have become the industry's reference set. You will recognize most of them from systems you have already built by accident.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt chaining&lt;/strong&gt; decomposes a task into a fixed sequence of steps, where each step operates on the output of the last. You trade a little latency for a lot of accuracy, because each call has a narrower, easier job. Generating a spec, then generating code from that spec, then generating tests from that code is a prompt chain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Routing&lt;/strong&gt; classifies an input and sends it to a specialized handler. A triage agent that reads an incoming bug report and decides whether it is a frontend issue, a database issue, or a flaky test — then hands it to the right specialist — is routing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parallelization&lt;/strong&gt; runs subtasks simultaneously. This comes in two flavors: &lt;em&gt;sectioning&lt;/em&gt;, where you split work into independent chunks that run at once, and &lt;em&gt;voting&lt;/em&gt;, where you run the same task several times to get multiple opinions and take the consensus. Asking three agents to independently find security issues in a diff and pooling their findings is voting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Orchestrator-workers&lt;/strong&gt; is the pattern most real coding agents use. A central agent dynamically breaks down a task, delegates the pieces to worker agents, and synthesizes their results. Unlike a fixed chain, the subtasks are not predetermined — the orchestrator decides what they are based on what it finds. This is what lets an agent handle a GitHub issue that touches eleven files it has never seen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evaluator-optimizer&lt;/strong&gt; puts two agents in a loop: one generates a solution, the other evaluates it against explicit criteria and sends back feedback, and the cycle repeats until the work passes. This is the reviewer-critic pattern, and it is one of the highest-leverage things you can add to your workflow.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1698919585695-546e4a31fc8f%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="A multi-monitor developer workstation for running multiple AI coding agents in parallel" title="Photo: Boitumelo / Unsplash" width="1600" height="901"&gt;&lt;h2&gt;The core loop: explore, plan, code, commit&lt;/h2&gt;
&lt;p&gt;Underneath all the orchestration, there is a single loop that the best agentic workflows follow on almost every task. It is worth internalizing because it maps cleanly onto how a thoughtful senior engineer already works, and because skipping any one phase is where most agent failures come from.&lt;/p&gt;
&lt;h3&gt;1. Explore&lt;/h3&gt;
&lt;p&gt;Before touching a line of code, the agent reads. It opens the relevant files, the existing tests, the surrounding modules, and any specs or tickets. Crucially, in this phase it is &lt;em&gt;not allowed to edit anything&lt;/em&gt;. Claude Code calls this plan mode; in other tools you enforce it by simply telling the agent to investigate and report back before proposing changes. The point is to load the right context before any decisions get made. When you are dropped into an unfamiliar codebase, this is also how you onboard: ask the agent the same questions you would ask a senior engineer on the team — where does authentication live, how is state managed, what is the test setup.&lt;/p&gt;
&lt;h3&gt;2. Plan&lt;/h3&gt;
&lt;p&gt;Next, the agent produces a written plan: what it intends to change, in which files, in what order, and how it will verify the result. This plan is an artifact you read. It is the single most important human-in-the-loop checkpoint in the entire workflow, because correcting a flawed plan costs a sentence, while correcting a flawed implementation costs a review cycle. A widely shared rule of thumb: the only time you can safely skip the planning phase is when you could describe the entire diff in a single sentence. For anything larger, make the agent plan first.&lt;/p&gt;
&lt;p&gt;There is a subtle technique here that separates people who are good at this from people who are great at it: &lt;em&gt;run planning and implementation in separate sessions.&lt;/em&gt; The exploration phase fills the context window with file contents, dead ends, and reasoning that is useful for producing the plan but becomes noise during implementation. Start the coding phase fresh, with the clean plan as its input.&lt;/p&gt;
&lt;h3&gt;3. Code&lt;/h3&gt;
&lt;p&gt;Now the agent implements against the plan, and — this is the part that makes the whole thing work — it runs a check after each meaningful step. Tests, a build, a type-checker, a linter, a screenshot diff: anything that returns an unambiguous pass or fail. The highest-leverage thing you can give an agent is a way to tell whether its own work is correct. When that feedback loop exists, the agent self-corrects and you stay out of the way. When it does not, &lt;em&gt;you&lt;/em&gt; become the feedback loop, and your throughput collapses to the speed of your own attention.&lt;/p&gt;
&lt;p&gt;This is also where test-driven development quietly becomes a superpower. Have one agent write the tests for a behavior first, confirm they fail, then have a second agent (or a fresh session) write the implementation until they pass. The tests become an objective target that keeps the implementing agent honest.&lt;/p&gt;
&lt;h3&gt;4. Commit&lt;/h3&gt;
&lt;p&gt;Finally, the agent commits with a clear message and opens a pull request, ideally summarizing what changed and why. Frequent, small commits give you rollback points and keep each change reviewable. If something went sideways three steps back, you want to return to a known-good state without unpicking an hour of tangled edits.&lt;/p&gt;
&lt;p&gt;The highest-leverage thing you can give an agent is a way to tell whether its own work is correct.&lt;/p&gt;
&lt;h2&gt;The specialist crew: which agents to actually run&lt;/h2&gt;
&lt;p&gt;Once the core loop is second nature, the multi-agent layer is about assigning narrow roles. The mistake most people make is creating broad, generic agents — a "backend engineer" agent, a "QA" agent — and wondering why the results are mediocre. The guidance from the people who build these tools is the opposite: make your sub-agents &lt;em&gt;specific&lt;/em&gt;, and give each one a single job. A focused agent with a tight brief and the right tools outperforms a generalist every time.&lt;/p&gt;
&lt;p&gt;A practical crew for everyday development looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The coding agent&lt;/strong&gt; does the implementation. It has write access to the source, can run the build and the tests, and works against an approved plan.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The reviewer agent&lt;/strong&gt; reads diffs with fresh context and flags problems. This is your evaluator-optimizer in human-readable form. One important caveat: an agent told to "find issues" will &lt;em&gt;always&lt;/em&gt; find some, inventing nitpicks if it has to. Instruct it to flag only issues that affect correctness or the stated requirements, or you will drown in over-engineering suggestions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The testing agent&lt;/strong&gt; writes and maintains tests. Test coverage gaps are an ideal thing to delegate, because the success criterion is objective and the work is tedious for a human.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The research agent&lt;/strong&gt; explores unfamiliar territory — a new library, an undocumented internal service, an error nobody recognizes — and returns a compact summary rather than dumping everything it read into the main context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The documentation agent&lt;/strong&gt; keeps READMEs, changelogs, and inline docs in sync with what actually shipped.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You do not need all five on every task. A one-line bug fix needs none of them. A multi-file feature might use four. The skill is matching the size of the crew to the size of the problem.&lt;/p&gt;
&lt;h2&gt;Context engineering: the skill that separates good from great&lt;/h2&gt;
&lt;p&gt;If there is one discipline that determines whether your agentic workflow soars or stalls, it is &lt;a rel="noopener noreferrer" href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents"&gt;context engineering&lt;/a&gt; — and it has quietly replaced prompt engineering as the thing worth getting good at.&lt;/p&gt;
&lt;p&gt;The core insight is that a context window is a finite resource with diminishing returns. As it fills, models get measurably worse at using what is in it — a phenomenon documented under the name &lt;em&gt;context rot&lt;/em&gt;. The goal is not to cram in as much as possible; it is to find the smallest set of high-signal tokens that make the right outcome most likely. More context is not better. &lt;em&gt;Better&lt;/em&gt; context is better.&lt;/p&gt;
&lt;p&gt;Three techniques do most of the heavy lifting:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compaction.&lt;/strong&gt; When a session approaches the limits of its window, summarize it into a fresh one — carrying forward the decisions and state that matter, dropping the exploratory noise. Long-running agents survive precisely because they compress their own history instead of drowning in it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Structured note-taking.&lt;/strong&gt; Have the agent write its progress to an external file — a running list of what is done, what is left, and what it learned. That file persists across context resets, so a fresh agent can pick up exactly where the last one stopped. This is, in effect, giving your agents a memory that outlives any single session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sub-agent isolation.&lt;/strong&gt; This is the deepest reason multi-agent systems work at all. When you delegate exploration to a sub-agent, that agent burns through its own context window reading files and chasing references — and returns only a tidy summary. The orchestrator's context stays clean. You have effectively parallelized not just the work but the &lt;em&gt;attention&lt;/em&gt;, keeping the main thread focused while the messy investigation happens elsewhere.&lt;/p&gt;
&lt;h2&gt;Give your agents a memory: CLAUDE.md and AGENTS.md&lt;/h2&gt;
&lt;p&gt;The single highest-return piece of configuration in any agentic setup is the project memory file — &lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code, &lt;code&gt;AGENTS.md&lt;/code&gt; for Codex and a growing list of other tools (it is now an &lt;a rel="noopener noreferrer" href="https://agents.md/"&gt;open standard&lt;/a&gt;). This file is loaded into context automatically, so it is where you encode the things you would otherwise have to repeat in every prompt.&lt;/p&gt;
&lt;p&gt;A good memory file tells the agent three things: the &lt;strong&gt;what&lt;/strong&gt; (the stack, and a map of the project — essential in a monorepo), the &lt;strong&gt;why&lt;/strong&gt; (what the major components are for), and the &lt;strong&gt;how&lt;/strong&gt; (how to run the tests, the type-check, the build — the exact commands the agent uses to verify its own work). That last category is the most valuable and the most often forgotten.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# AGENTS.md

## Stack
- Next.js 15 (App Router), TypeScript, PostgreSQL via Prisma
- Tests: Vitest (unit), Playwright (e2e)

## Commands the agent should use
- Install: `pnpm install`
- Typecheck: `pnpm typecheck`   # run after every change
- Unit tests: `pnpm test`        # must pass before commit
- E2e tests: `pnpm test:e2e`     # run for any routing/auth change
- Lint: `pnpm lint --fix`

## Conventions
- Server components by default; mark client components explicitly.
- Never edit files in `/generated`. They are build artifacts.
- Database changes require a Prisma migration, never a manual schema edit.

## Where things live
- Auth: `src/lib/auth/`
- API route handlers: `src/app/api/`
- Shared UI: `src/components/ui/`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The discipline that matters most: &lt;em&gt;keep it lean.&lt;/em&gt; Even the strongest models have a limited budget of attention for instructions; a bloated memory file does not make the agent more careful, it makes the agent start ignoring instructions. A useful test is to delete any line whose removal would not cause a mistake. And because this file shapes everything the agent does, check it into version control and review changes to it like you would review code — it is a shared asset for the whole team, not a personal scratchpad.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1581094794329-c8112a89af12%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26h%3D720" alt="A developer supervising and reviewing output from parallel AI coding agents" width="1600" height="720"&gt;&lt;h2&gt;Running AI agents in parallel with git worktrees&lt;/h2&gt;
&lt;p&gt;Here is where the throughput math changes. A single agent session, however well configured, makes you perhaps one and a half to two times faster on a given task. The teams reporting dramatically larger gains are not getting them from single-session speed — they are getting them from &lt;em&gt;concurrency&lt;/em&gt;, running many agent sessions at once on independent pieces of work.&lt;/p&gt;
&lt;p&gt;The enabling technology is humble: git worktrees. A worktree lets you check out multiple branches of the same repository into separate directories simultaneously, so several agents can each work on their own branch without stepping on each other. Modern tools have built this in — some IDE-based agents will run up to eight parallel sessions in worktree isolation out of the box, and the cloud-based coding agents spin up a fresh sandboxed environment per task by default.&lt;/p&gt;
&lt;p&gt;There are two distinct ways to use parallelism, and they are worth separating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Breadth:&lt;/strong&gt; run several &lt;em&gt;different&lt;/em&gt; tasks at once — fix three unrelated bugs, each in its own worktree, each with its own agent. This is the everyday multiplier. While one agent grinds through a tedious refactor, two others are closing tickets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Competition:&lt;/strong&gt; point several agents at the &lt;em&gt;same&lt;/em&gt; hard problem with different approaches, then keep the best result and discard the rest. When you genuinely do not know the right design, racing a few attempts in parallel is often faster than agonizing over one.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A caution that the enthusiasts sometimes skip: there is a hard limit on how many parallel sessions a human can actually supervise. Each one produces work you are responsible for reviewing. Concurrency moves the bottleneck from writing code to reviewing it — which means review capacity, not generation speed, becomes the thing that governs your real throughput. We will come back to this.&lt;/p&gt;
&lt;h2&gt;A day in the life: one task, end to end&lt;/h2&gt;
&lt;p&gt;Theory is cheap. Here is what the whole system looks like applied to a single, ordinary task: &lt;em&gt;"Add rate limiting to our public API endpoints."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You start in plan mode. The &lt;strong&gt;research agent&lt;/strong&gt; explores the codebase — it finds the existing middleware stack, notes that there is a Redis instance already used for sessions, and reports back that there is no current rate-limiting layer and three places where new middleware could hook in. It does not write anything; it returns a paragraph of findings. Your main context stays clean.&lt;/p&gt;
&lt;p&gt;You ask for a &lt;strong&gt;plan&lt;/strong&gt;. The agent proposes: add a sliding-window limiter backed by the existing Redis, wire it into the middleware chain, make the limits configurable per route, and cover it with unit tests plus one end-to-end test. The plan names the four files it will touch. You read it and notice it forgot about the health-check endpoint, which must never be rate-limited. You add one sentence. That correction just saved you a production incident, and it cost you ten seconds.&lt;/p&gt;
&lt;p&gt;You start a &lt;strong&gt;fresh session&lt;/strong&gt; for implementation, handing it the approved plan. The &lt;strong&gt;coding agent&lt;/strong&gt; writes the limiter, runs the type-checker after each file, and writes the tests. The first run of the e2e test fails — the limiter is counting health-check requests. Because the agent has a passing/failing signal, it sees the failure, recalls the constraint from the plan, adds the exclusion, and the suite goes green. You did nothing during this.&lt;/p&gt;
&lt;p&gt;Now the &lt;strong&gt;reviewer agent&lt;/strong&gt; reads the diff with fresh context, instructed to flag only correctness and requirements issues. It catches that the limiter fails open — if Redis is unreachable, all requests are allowed through — and asks whether that is intentional. It is a real question, so you make a real decision: fail closed for write endpoints, open for reads. The coding agent applies it.&lt;/p&gt;
&lt;p&gt;Finally the agent &lt;strong&gt;commits&lt;/strong&gt; with a descriptive message and opens a PR, and the &lt;strong&gt;documentation agent&lt;/strong&gt; adds the new per-route configuration to the API docs. Elapsed human effort: reading one plan, adding one sentence, answering one design question, making one judgement call. Everything else ran on its own — and the two things you contributed were exactly the two things a machine should not have decided for you.&lt;/p&gt;
&lt;h2&gt;The maturity ladder: how to actually adopt this&lt;/h2&gt;
&lt;p&gt;You do not get here in a day, and you should not try. The reliable path is a ladder, where each rung pays off on its own and sets up the next. Climb it only as fast as the value justifies the added complexity and token cost.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single-agent assistant.&lt;/strong&gt; Use one agent for questions, exploration, and small, well-scoped edits. Always start from a clean git state so you can see exactly what changed. Get fluent in the explore-plan-code-commit loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A memory file.&lt;/strong&gt; Write a lean &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt; covering your stack, your commands, and your conventions. Commit it. This one file removes most of the repetitive instruction you have been typing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom commands.&lt;/strong&gt; Capture the workflows you repeat — "review this PR for security issues," "write tests for this module" — as reusable slash commands or skills, so they are one keystroke instead of a paragraph.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Specialist sub-agents.&lt;/strong&gt; Introduce focused, single-job agents — a reviewer, a tester — each with its own context window and a restricted set of tools. Tell the orchestrator explicitly when to use them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parallel and orchestrated workflows.&lt;/strong&gt; Run multiple agents across git worktrees for independent tasks. Bring in orchestrator-worker and evaluator-optimizer patterns for the complex jobs. Reach for a dedicated orchestration framework only when a task genuinely decomposes into parallel threads and the value clearly justifies the overhead.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The temptation is to jump straight to rung five because it is the most exciting. Resist it. A team fluent at rungs two and three ships more reliably than a team fumbling a multi-agent framework it does not yet understand.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1619115445441-d6e75309000d%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="Close-up of source code being reviewed after AI agent generation" title="Photo: Árpád Czapp / Unsplash" width="1600" height="1067"&gt;&lt;h2&gt;The honest scorecard: does this actually make you faster?&lt;/h2&gt;
&lt;p&gt;This is the section most write-ups skip, and it is the most important one. The productivity story for AI agents is real, but it is genuinely mixed, and an engineer deciding how to invest their time deserves the unvarnished version.&lt;/p&gt;
&lt;p&gt;The optimistic data is striking. Internal reports from teams that have leaned in describe large jumps in throughput — engineers merging substantially more pull requests per day after adopting an agentic workflow, and individual tasks completing far faster than before. Vendor case studies cite feature timelines collapsing from weeks to days. Controlled studies of older, completion-style assistance found meaningful increases in &lt;a rel="noopener noreferrer" href="https://github.blog/news-insights/research/research-quantifying-github-copilots-impact-in-the-enterprise-with-accenture/"&gt;successful builds and pull request volume&lt;/a&gt;. There is clearly something real here.&lt;/p&gt;
&lt;p&gt;But the most rigorous independent study points the other way, and it deserves your attention precisely because it is inconvenient. In a &lt;a rel="noopener noreferrer" href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/"&gt;randomized controlled trial&lt;/a&gt; conducted in early 2025, experienced open-source developers working on &lt;em&gt;their own mature repositories&lt;/em&gt; were measured completing real tasks with and without AI assistance. They &lt;em&gt;predicted&lt;/em&gt; the tools would make them about 24% faster. Afterward, they &lt;em&gt;believed&lt;/em&gt; they had been about 20% faster. They were actually &lt;strong&gt;19% slower&lt;/strong&gt; with the AI. The gap between perceived and actual performance was the whole story: the tools felt fast while quietly costing time, because steering, reviewing, and correcting the agent on a codebase they already knew intimately outweighed the typing it saved. The tooling has improved since that study ran, and the result may not hold for today's agents — but the perception gap it exposed is the durable lesson, and you should assume you are subject to it.&lt;/p&gt;
&lt;p&gt;Developers predicted a 24% speedup, felt a 20% speedup, and were measured 19% slower.&lt;/p&gt;
&lt;p&gt;Industry-wide surveys land in the same ambivalent place. The &lt;a rel="noopener noreferrer" href="https://survey.stackoverflow.co/2025/"&gt;largest annual developer survey&lt;/a&gt; found that while the overwhelming majority of developers now use or plan to use these tools, trust is actually &lt;em&gt;falling&lt;/em&gt; — a minority trust the accuracy of AI output, and the single most common complaint is code that is "almost right, but not quite," which is precisely the failure mode that eats time, because nearly-correct code is harder to debug than obviously-broken code. The major &lt;a rel="noopener noreferrer" href="https://dora.dev/"&gt;DevOps research report&lt;/a&gt; reached a conclusion worth taping to your monitor: AI does not fix a struggling team, it amplifies what is already there. Strong teams with good tests and tight feedback loops get faster. Weak teams just generate their dysfunction more quickly — and, notably, the same report found that rising AI-driven throughput correlated with &lt;em&gt;worse&lt;/em&gt; delivery stability unless the engineering fundamentals were already in place.&lt;/p&gt;
&lt;p&gt;Measurement specialists have a name for the trap: &lt;em&gt;false velocity.&lt;/em&gt; More pull requests merged is not the same as more value delivered. If generation speeds up but review, testing, and integration do not, you have not gotten faster — you have just moved the bottleneck downstream and made it harder to see.&lt;/p&gt;
&lt;p&gt;How do you reconcile the striking gains with the sobering studies? The honest synthesis is that agents help most where the work is broad, unfamiliar, or boilerplate-heavy — scaffolding a new service, exploring a codebase you have never seen, generating the tedious tests — and help least, or actively hurt, when a domain expert is working on a mature, tightly-coupled system with a high quality bar, where the cost of explaining the task to the agent exceeds the cost of just doing it. The leverage is real. It is just not uniform, and believing it is uniform is how you end up slower while feeling faster.&lt;/p&gt;
&lt;h2&gt;Anti-patterns and how to avoid them&lt;/h2&gt;
&lt;p&gt;Most of the ways agentic workflows fail are predictable, which means they are avoidable.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The kitchen-sink session.&lt;/strong&gt; Pouring three unrelated tasks into one long-running context pollutes it and degrades every answer. Clear the context between tasks. A good rule: after two failed attempts at the same fix, stop, clear, and rewrite the prompt from scratch rather than digging the hole deeper.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Skipping the plan.&lt;/strong&gt; Letting the agent code immediately on anything non-trivial is how you get confidently-wrong implementations that are expensive to unwind. Plan first; the plan is your cheapest correction point.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Over-engineering.&lt;/strong&gt; Left unprompted, agents add abstractions, helpers, and options nobody asked for. Tell them explicitly to use the simplest approach that works, and have your reviewer agent flag unnecessary complexity rather than reward it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Review pile-up.&lt;/strong&gt; If you scale generation without scaling review, you create a downstream traffic jam and your cycle time gets &lt;em&gt;worse&lt;/em&gt; even as your commit count climbs. Invest in review capacity — including automated reviewer agents — before you crank up output.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trusting unverifiable work.&lt;/strong&gt; Never fully delegate a task whose result you cannot check. If there is no test, no build, no observable behavior to confirm correctness, you are not delegating — you are gambling.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The new attack surface: AI agent security&lt;/h2&gt;
&lt;p&gt;Agentic workflows introduce a class of risk that did not exist when your AI tool was just suggesting completions: the agent now reads untrusted content and takes actions. The dominant new threat is &lt;em&gt;indirect prompt injection&lt;/em&gt; — malicious instructions hidden in the data an agent consumes. A poisoned dependency, a booby-trapped issue comment, a web page the agent fetches during research, even a crafted string in a file it reads can all carry instructions that hijack the agent's behavior. Because agents chain tools together, a single injection can escalate into a sequence of unintended actions.&lt;/p&gt;
&lt;p&gt;The defenses are practical and worth adopting before you scale autonomy, not after an incident:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sandbox aggressively.&lt;/strong&gt; Run agents in isolated environments — containers or dedicated VMs — with no standing access to anything they do not need. The cloud coding agents do this by default; for local agents, you have to set it up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Restrict the blast radius.&lt;/strong&gt; Limit network egress to an allowlist, block writes outside the workspace, and protect configuration files from agent edits. An agent that cannot reach the open internet or modify its own permissions is dramatically harder to weaponize.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gate the irreversible.&lt;/strong&gt; Require explicit human approval before anything you cannot undo — deleting data, deploying, moving money, force-pushing. Keep a human in the loop precisely at the points where a mistake is permanent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat the dangerous flags as dangerous.&lt;/strong&gt; The options that bypass approvals and sandboxing entirely have their place — inside a hardened container, for a trusted task. Running them on your own machine against a real codebase is how a bad afternoon becomes a very bad afternoon.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Own the output.&lt;/strong&gt; Whoever's name is on the pull request owns the code, regardless of how much of it an agent wrote. AI assistance does not transfer responsibility, and the survey data is clear that nearly-right insecure code is a real and common failure mode.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Start tomorrow&lt;/h2&gt;
&lt;p&gt;The gap between using an AI agent and running an agentic workflow is not about access to a better model. It is about method — and method is what actually moves your turnaround time, not the raw speed of the model underneath. If you take only a handful of things from this guide, make them these.&lt;/p&gt;
&lt;p&gt;Adopt the explore-plan-code-commit loop on your very next task, and refuse to skip the plan on anything you could not describe in one sentence. Write a lean memory file for your main repository this week and commit it. Make every task you delegate verifiable — if the agent cannot run a check that tells it whether it succeeded, build that check before you hand over the work. Add a reviewer agent and instruct it to care only about correctness. And when you are ready, start running two independent tasks in parallel worktrees, then three, until you hit the edge of what you can actually review — because that edge, not the model's speed, is your real limit.&lt;/p&gt;
&lt;p&gt;Above all, stay honest with yourself about the gains. These tools can make a strong engineer on the right kind of problem genuinely, dramatically faster. They can also make you feel fast while quietly slowing you down. The engineers who win with agents are not the ones who trust them the most — they are the ones who built the verification, the structure, and the judgement to know the difference.&lt;/p&gt;
&lt;h2&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://www.anthropic.com/engineering/building-effective-agents"&gt;Building Effective Agents&lt;/a&gt; — Anthropic's reference taxonomy of agent workflow patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents"&gt;Effective context engineering for AI agents&lt;/a&gt; — the discipline behind keeping agents sharp.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/"&gt;Measuring the impact of AI on experienced developers&lt;/a&gt; — the RCT behind the perception-gap finding.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://dora.dev/"&gt;DORA&lt;/a&gt; — research on what actually makes software delivery faster and more stable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://survey.stackoverflow.co/2025/"&gt;Stack Overflow Developer Survey&lt;/a&gt; — adoption and trust trends across the profession.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://agents.md/"&gt;AGENTS.md&lt;/a&gt; — the open, cross-tool standard for agent instruction files, supported by Codex, Cursor, Copilot, and more.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.blog/news-insights/research/research-quantifying-github-copilots-impact-in-the-enterprise-with-accenture/"&gt;Quantifying GitHub Copilot's impact with Accenture&lt;/a&gt; — a controlled enterprise study behind the build and pull-request figures.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


</description>
      <category>aiagents</category>
      <category>claudecode</category>
      <category>developerproductivity</category>
      <category>multiagentworkflows</category>
    </item>
    <item>
      <title>The 95% Problem: Why Enterprise AI Keeps Failing — and What the 5% Get Right</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Thu, 11 Jun 2026 19:53:22 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/the-95-problem-why-enterprise-ai-keeps-failing-and-what-the-5-get-right-3geb</link>
      <guid>https://dev.to/harshdeepsingh13/the-95-problem-why-enterprise-ai-keeps-failing-and-what-the-5-get-right-3geb</guid>
      <description>&lt;p&gt;Ninety-five out of every hundred enterprise AI pilots produce nothing a CFO would sign off on. The reflex is to blame the model — too dumb, too small, the wrong vendor. It almost never is. The thing quietly killing enterprise AI is older and more boring than any model: data nobody organized for machines, and rules nobody ever wrote down. The strangest part of the story is who is losing the fight hardest — the firms whose entire business is selling everyone else the cure.&lt;/p&gt;
&lt;h2&gt;The most expensive irony in enterprise software&lt;/h2&gt;
&lt;p&gt;In late 2025, Deloitte gave part of a government cheque back. The firm had delivered a report to Australia's Department of Employment and Workplace Relations, and reviewers found something awkward buried inside it: citations to academic papers that did not exist, and a fabricated reference to a federal court judgment. The work had been produced with help from generative AI, and no one had checked it before it went out the door. Deloitte agreed to refund part of its fee.&lt;/p&gt;
&lt;p&gt;It is tempting to read that as a story about a hallucinating chatbot. It is not. A capable model can cite a real paper; the failure was not that the AI was too weak. The failure was that nothing in the process forced a human to verify machine output before it reached a client. There was no standard operating procedure, no checkpoint, no rule with teeth. That distinction — between a model problem and a data-and-governance problem — is the entire subject of this essay, and the firms that sell AI for a living have just handed us the clearest possible illustration of it.&lt;/p&gt;
&lt;p&gt;Consider the position those firms are in. Since 2023, the Big Four and the major strategy houses have collectively poured more than ten billion dollars into AI. It is their flagship pitch. Accenture reported close to six billion dollars in generative-AI bookings in a single fiscal year. PwC became one of OpenAI's largest enterprise customers and then its reseller. KPMG signed a two-billion-dollar alliance with Microsoft. These organizations have built their modern brand on the promise that they can walk into any enterprise and fix its AI problem. And yet, internally, they have hit precisely the wall they are paid to dismantle.&lt;/p&gt;
&lt;p&gt;This is not schadenfreude about one embarrassing report. It is the most useful data point in the entire enterprise-AI conversation, because it removes the easy excuses. You cannot say the consultants lacked talent, budget, model access, or executive buy-in. They had all of it in abundance. If the people who sell the cure can still catch the disease, then the disease is not what most companies think it is. It is not a shortage of intelligence in the model. It is a shortage of order in the data and discipline in the governance — and almost nobody is immune.&lt;/p&gt;
&lt;h2&gt;The number that belongs on every board agenda&lt;/h2&gt;
&lt;p&gt;Start with the figure that has been ricocheting around boardrooms since it landed. In its 2025 report &lt;em&gt;The GenAI Divide: State of AI in Business&lt;/em&gt;, MIT's NANDA initiative studied hundreds of enterprise AI initiatives and concluded that roughly ninety-five percent of them had produced no measurable impact on the bottom line. Not weak returns. No returns. The spending in scope ran to tens of billions of dollars, and the overwhelming majority of it bought experiments that never crossed into anything a finance team could defend.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;95%&lt;/strong&gt; of enterprise generative-AI pilots deliver no measurable business impact — the spending lands, the value does not.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;It is not an isolated finding. Gartner expects that by the end of 2025, three in ten generative-AI projects will be abandoned after the proof-of-concept stage, and that through 2026, sixty percent of AI projects will be scrapped specifically because the organizations lacked AI-ready data. The firm goes further on agents: it forecasts that more than forty percent of agentic-AI projects will be cancelled by the end of 2027. The RAND Corporation has put the historical AI project failure rate above eighty percent — roughly twice the rate of conventional IT projects. And S&amp;amp;P Global found that the share of companies abandoning most of their AI initiatives jumped to forty-two percent in 2025, up from just seventeen percent a year earlier. The trend is not improving as the technology matures. It is getting worse as spending outruns readiness.&lt;/p&gt;
&lt;p&gt;The crucial detail is &lt;em&gt;where&lt;/em&gt; these projects die. They almost never fail in the lab. They fail on the road to production. A pilot runs on a curated slice of data — a clean schema, a controlled volume, a problem chosen because it demos well. Production runs on the actual enterprise: the duplicated records, the contradictory definitions, the fields that mean different things in different systems, the knowledge trapped in formats no machine can read. The distance between the demo and the deployment is the distance between curated data and real data, and that distance is where the money disappears. People in the field have a name for the place projects go to expire: pilot purgatory.&lt;/p&gt;
&lt;p&gt;The people closest to the data already know this. In Informatica's 2025 survey of chief data officers, the most-cited obstacle to AI success was not talent, not budget, not model quality — it was data quality and readiness. The executives responsible for the foundation are telling everyone the foundation is the problem. Most strategies are simply not listening, because listening would mean slowing down to do the tedious work, and the market is rewarding speed.&lt;/p&gt;
&lt;p&gt;And the window in which to fix this is closing faster than the failure rate alone suggests, because the industry is sprinting from chatbots to agents. Gartner expects that by the end of 2026, four in ten enterprise software applications will include task-specific AI agents, up from less than one in twenty in 2025. Agents raise the stakes of the underlying problem by an order of magnitude. A chatbot that retrieves bad data returns a bad answer a human can still catch. An agent that &lt;em&gt;acts&lt;/em&gt; on bad data — reconciling an account, approving a request, triggering a downstream workflow — propagates the error into the real world before anyone reviews it. The same analysts forecasting the agentic wave also forecast that more than forty percent of agentic projects will be cancelled by the end of 2027, for the same unglamorous reasons the chatbots failed. We are, in other words, about to point far more autonomous systems at foundations that were already too weak for the last generation of tools.&lt;/p&gt;
&lt;p&gt;That is what makes the failure rate a strategic problem rather than a technical footnote. The cost is not the wasted pilot budget; that is the cheap part. The real cost is competitive. Every quarter a rival reaches production while you re-run experiments that were always going to fail for the same reason, the rival's system gets better, its data gets cleaner, its people get more fluent, and the gap compounds. You are not standing still. You are losing ground while looking busy.&lt;/p&gt;
&lt;h2&gt;It was never the model&lt;/h2&gt;
&lt;p&gt;The comforting story inside most failed AI programs is that the technology was not ready, and that the next model — bigger, newer, from a different lab — will be the one that finally works. It is comforting because it requires nothing of the organization except patience and a bigger invoice. It is also wrong.&lt;/p&gt;
&lt;p&gt;Here is the inconvenient test. The model that hallucinated its way through your failed pilot is, in most cases, the same model that performed flawlessly in the vendor's demo. Nothing about the weights changed between those two moments. What changed was everything around the model: the quality of the data it was fed, the clarity of the instructions it was given, and the rules governing what it was allowed to touch. The model was never the variable. The environment was.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The model in your failed pilot and the model in the vendor's flawless demo are usually the same model. The difference between them is everything you built — or failed to build — around it.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;This is also why "wait for the next model" is such a seductive and expensive trap. Each new model is genuinely more capable than the last, which makes it easy to believe the next one will finally clear the bar. But a more capable model pointed at the same unstructured data and the same absent rules does not fix the problem — it executes the same mistakes more fluently, and, increasingly, more autonomously. Capability without a foundation is not progress. It is leverage applied to a fault line.&lt;/p&gt;
&lt;p&gt;That environment has two load-bearing pieces, and almost every enterprise is missing both. The first is data that a machine can actually reason over. The second is governance a machine can actually obey. The original instinct that AI needs "organized data and some kind of SOP" is exactly right — it just turns out that each half is a deep discipline in its own right, and that naming them separately is the difference between a strategy that works and a slide that sounds good. Take them one at a time.&lt;/p&gt;
&lt;h2&gt;Gap one: data that was never built for machines&lt;/h2&gt;
&lt;p&gt;An AI agent does not think the way a database is organized. It does not navigate neat rows and columns; it reasons over entities, the relationships between them, and the context that gives them meaning. It needs to know that this customer is the same as that account, that "revenue" in the finance system and "revenue" in the sales dashboard are or are not the same number, that this contract supersedes that one, that this policy applies to this region. Enterprise data, as it actually exists, is almost the precise opposite of that.&lt;/p&gt;
&lt;p&gt;In most companies the data is siloed across systems that were never designed to talk to each other, duplicated in ways no one fully maps, and defined inconsistently enough that the same word can name genuinely different things in different systems. Worse, the knowledge that actually matters — the reasoning, the precedent, the hard-won judgment — tends to live in formats machines cannot read: slide decks, PDFs, email threads, and the heads of senior people who are about to retire. You can connect the cleanest model in the world to that, and it will faithfully reflect the chaos back to you.&lt;/p&gt;
&lt;p&gt;The most instructive proof of this comes, again, from a consultancy. When McKinsey built its internal AI platform, the firm discovered that the tool could not initially parse PowerPoint — which was a problem, because PowerPoint is where most of McKinsey's institutional knowledge actually lived. Sit with that for a moment. One of the most knowledge-intensive organizations on earth, a firm whose entire product is structured thinking, found that its crown-jewel intellectual property was effectively illegible to a machine until it did real work to fix the ingestion. If McKinsey's knowledge was trapped in slides, it is worth asking, honestly, what shape yours is in.&lt;/p&gt;
&lt;p&gt;The failure rarely announces itself as missing data. It hides in data that is present but means subtly different things in different places. Ask an agent a question as ordinary as how many active customers the business has, and it will find a dozen tables with a dozen definitions of "active" — a login within the last thirty days in one system, a non-zero balance in another, an uncancelled contract in a third. A human analyst resolves that ambiguity with context and a quick message to a colleague. An agent, lacking both, picks one definition silently and reports a confident number that is wrong in a way nobody can see. Multiply that across every entity and every metric a company cares about, and you have the real texture of the problem: not an empty warehouse, but a full one with no shared language.&lt;/p&gt;
&lt;h3&gt;You cannot retrieve your way out of a data swamp&lt;/h3&gt;
&lt;p&gt;The popular hope is that retrieval-augmented generation — pointing an agent at your documents and letting it fetch what it needs — will paper over the mess. It will not. An agent retrieving from a swamp returns swamp, dressed up in fluent prose that makes the swamp harder to detect. And the instinct to fix this by building a bigger data lake usually just produces a bigger swamp with better storage economics. Volume was never the problem. Meaning was.&lt;/p&gt;
&lt;p&gt;What actually closes the gap is a layer most enterprises have never built: a semantic, machine-readable map of what the data &lt;em&gt;means&lt;/em&gt;. In practice this goes by several names that point at the same idea — a semantic layer, an ontology, a knowledge graph, a governed data catalog. The common thread is that core business concepts get defined once, consistently, in a form an agent can consume: what a customer is, what counts as revenue, how entities relate, which rules and constraints apply. The catalog becomes the control plane of truth, and the semantic layer becomes the thing that lets a model answer in terms of your business rather than in terms of raw, ambiguous tables.&lt;/p&gt;
&lt;p&gt;The organizing principle behind all of this is treating data as a product rather than as exhaust. Exhaust is whatever a system happens to emit, owned by no one, documented nowhere. A product has an owner, a contract, documentation, versioning, and a consumer whose needs shape it. The research bears out how much this matters: organizations that treat data as a product — with curated models and shared vocabularies — are dramatically more likely to scale generative AI successfully than those that do not. When the foundation is built this way, retrieval techniques like &lt;a href="https://theharshdeepsingh.com/blog/building-an-llm-project-from-scratch-in-2026" rel="noopener noreferrer"&gt;graph-based RAG&lt;/a&gt; can ground every answer in verified, connected data, enforce access controls at the moment of the query, and trace each response back to the exact source it came from. That is the difference between an agent that confidently invents a court case and one that shows its work.&lt;/p&gt;
&lt;p&gt;A knowledge graph earns its keep precisely here. Instead of flat, disconnected tables, it stores the business as a web of entities and the relationships between them: this customer belongs to this account, which is covered by this contract, governed by this policy, owned by this team. An agent reasoning over that structure can follow the connections the way a knowledgeable employee would, and every answer it produces carries its lineage — which source, which definition, which version. That is also what makes governance enforceable at the level of &lt;em&gt;meaning&lt;/em&gt; rather than the level of the raw row, because the graph knows what a thing is and who is permitted to see it.&lt;/p&gt;
&lt;h3&gt;The strategic point hiding in the plumbing&lt;/h3&gt;
&lt;p&gt;Strip away the vocabulary and the strategic truth is simple: AI readiness is mostly data maturity wearing a more exciting outfit. You cannot purchase your way past it with a model subscription, because the thing you are missing is not compute or intelligence — it is the slow, structural work of making your own knowledge legible. That work is unglamorous, expensive, and invisible in a board deck, which is exactly why most organizations skip it and exactly why most organizations end up in the ninety-five percent. The semantic foundation is not overhead on the way to the real prize. It &lt;em&gt;is&lt;/em&gt; the prize. It is the part competitors cannot copy by signing the same vendor contract you did.&lt;/p&gt;
&lt;h2&gt;Gap two: governance that was never written down&lt;/h2&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1745015446589-7ee6f702d8c1%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26h%3D720" alt="The blue glass facade of a modern office building, its windows forming a strict geometric grid." width="1600" height="720"&gt;&lt;p&gt;&lt;em&gt;Order, structure, repetition — the corporate grid. Governance is what gives an agent the same scaffolding a new employee takes for granted. Photo: Fabian Kleiser / Unsplash.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you ask most enterprises where their AI governance lives, the honest answer is a PDF on a shared drive — a well-intentioned document of principles that almost no one has read and that no system enforces. A PDF nobody reads is not a policy an agent can obey. It is a statement of hope. And hope does not survive contact with an autonomous system acting at machine speed across systems it was never explicitly cleared to touch.&lt;/p&gt;
&lt;p&gt;Governance for AI, and especially for agents, has to be machine-actionable to mean anything. The "SOP" intuition is the right one, but it resolves into two concrete questions that a slide of principles never answers: what is this agent actually allowed to touch and do, and what is the operating rhythm that keeps it honest over time? Get specific about both, and governance stops being a compliance ornament and starts being the thing that lets you deploy without holding your breath.&lt;/p&gt;
&lt;h3&gt;An agent is a new employee with root access and no onboarding&lt;/h3&gt;
&lt;p&gt;It helps to think of an agent as exactly what it is becoming: a digital worker. The trouble is that we wrap human workers in decades of accumulated controls and give agents almost none of them. A new human employee receives an identity, a defined role, least-privilege access to only the systems their job requires, a manager who reviews their work, and an audit trail that records what they did. An agent, in too many deployments, gets a single shared API key with broad standing credentials, the ability to call tools far outside its actual task, and no logging worth the name. It is the most over-permissioned new hire in the building, and no one interviewed it.&lt;/p&gt;
&lt;p&gt;The discipline that fixes this is well understood, even if it is rarely applied. Security researchers call the core idea least-agency, or least-privilege: an agent should receive the minimum autonomy required for its specific task and nothing more. A customer-support agent does not need write access to the billing database. A research agent does not need the ability to send external email. From there it cascades into concrete controls: whitelisting the specific tools an agent may use, issuing short-lived credentials instead of permanent keys, sandboxing execution, restricting where an agent can send data, and — critically — keeping a human in the loop for actions that are irreversible or sensitive. A mature deployment will refuse to let an agent move money without clearing a confidence threshold or obtaining a second approval, and will strip personally identifiable information before it ever reaches a model, restoring it only on the way back. None of that lives in a principles document. All of it lives in enforced policy. That, and not the PDF, is the real standard operating procedure: rules expressed as controls a system cannot route around.&lt;/p&gt;
&lt;p&gt;The danger is not hypothetical, and it does not require malice. Picture an agent handed broad database credentials so it could "be helpful," then asked to tidy up some duplicate records. With no constraint on its scope and no human checkpoint, a single ambiguous instruction becomes a destructive write across production data in seconds — faster than any person could intervene, and recorded nowhere anyone thought to look. The same autonomy that makes agents useful is what makes their mistakes fast and quiet. Standing credentials, missing audit trails, and unrestricted tool access are not exotic edge cases; they are the default state of most early agent deployments, and they are exactly how a promising program turns into a board-level incident.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A policy an agent cannot read is decoration. Governance that scales is policy an agent is structurally unable to disobey.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3&gt;You do not have to invent the rulebook&lt;/h3&gt;
&lt;p&gt;The encouraging part is that the scaffolding for all of this already exists, written by people who have thought about it harder than any individual team has time to. The U.S. National Institute of Standards and Technology publishes the AI Risk Management Framework, along with a dedicated profile for generative AI, and its emphasis maps almost directly onto agent controls: role-based access, continuous monitoring, adversarial testing, and lifecycle logging for traceability. The international ISO/IEC 42001 standard formalizes the idea of an AI management system, with oversight and continual improvement built in. The OWASP GenAI Security Project maintains a Top 10 for large-language-model applications and a newer Top 10 for agentic applications, cataloguing the exact failure classes teams keep rediscovering the hard way: prompt injection, tool misuse, memory leakage. And the external pressure is rising fast, from the EU AI Act to a wave of national AI laws, which means governance is shifting from a nice-to-have to a condition of doing business.&lt;/p&gt;
&lt;p&gt;Underneath the frameworks sits a single overlooked capability that decides whether any of them are real: observability. If you cannot see what an agent did — which data it touched, which tools it called, which decision it made and why — then you cannot govern it, debug it, or defend it to a regulator, and you certainly cannot trust it with anything that matters. Audit logging and traceability are not paperwork. They are the line between an agent you can put into production and a black box you can only hope behaves. Trust, in the end, is not extended to systems because they are clever. It is extended to systems because they are accountable.&lt;/p&gt;
&lt;p&gt;The reframe that matters most here is that governance is not a brake on AI. It is the enabler. The organizations that actually reach production are, consistently, the ones that invested in governance frameworks &lt;em&gt;before&lt;/em&gt; they scaled agent capabilities — not after a breach forced their hand. The absence of guardrails does not make you faster; it produces exactly the brittle, untrustworthy, occasionally catastrophic behavior that makes leadership pull the plug and sends a promising program back to purgatory. Guardrails are what let you move quickly without flinching.&lt;/p&gt;
&lt;h2&gt;The consultants' mirror&lt;/h2&gt;
&lt;p&gt;Return now to where we started, because the consulting firms are not just a cautionary anecdote. They are the most public, best-funded live experiment in internal AI adoption that exists, and watching them is the closest thing the rest of us have to a controlled trial. They are simultaneously the largest sellers of AI transformation and a room full of organizations trying to transform themselves — and they have been unusually candid about how hard it is. BCG, in its own cross-industry research, found that roughly three-quarters of companies struggle to achieve and scale value from their AI initiatives. The firms are not describing a problem they have solved from the outside. They are describing one they are living from the inside.&lt;/p&gt;
&lt;p&gt;The stakes are visible in their own pyramids. Internal tools like McKinsey's assistant and BCG's slide-polishing system can already perform a large share of the research-and-formatting work that used to define a junior analyst's first years, and entry-level hiring across the industry has tightened as a result. That is what it looks like when this technology genuinely lands inside an organization — and it is a useful reminder that getting it right is not a productivity nicety. It restructures the firm. The flip side is that getting it wrong, in public, with a client's name on the document, restructures the firm's reputation just as quickly.&lt;/p&gt;
&lt;p&gt;And so every major firm now has its own platform: McKinsey with its knowledge assistant and a fleet of internal agents numbering in the tens of thousands, BCG with its build unit and internal tools, Deloitte with its assistant and an agentic platform, PwC with an agent operating system spanning tens of thousands of deployed agents, EY with a platform giving tens of thousands of staff access to a growing roster of agents and a multi-year plan to scale into the hundreds of thousands, and KPMG with its own agentic workbench. Billions of dollars, real engineering, genuine ambition. But the tools are the visible ten percent. The invisible ninety percent — the part that determines whether any of it works — is the data and governance plumbing underneath. There is a useful rule of thumb circulating in this world that only a small fraction of AI value comes from the algorithms and the technology, with the overwhelming majority coming from people, process, and the organizational change required to make the technology stick. The firms that are winning internally are the ones that took that ratio seriously.&lt;/p&gt;
&lt;h2&gt;The five percent: what McKinsey's Lilli actually proves&lt;/h2&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1524995997946-a1c2e315a42f%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26h%3D640" alt="The curved, balconied interior of a grand library, its shelves densely and neatly filled with books." width="1600" height="640"&gt;&lt;p&gt;&lt;em&gt;Organized, machine-legible knowledge was the real moat — not the model. Every rival had the same models. Photo: Susan Q Yin / Unsplash.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If the failure rate has a counterexample worth studying, it is McKinsey's internal platform, Lilli. It is the case study everyone cites, and almost everyone draws the wrong lesson from it. The wrong lesson is that McKinsey succeeded because it had access to powerful models. That cannot be the explanation, because every competitor had access to the same models. The right lesson is far less flattering to the technology and far more useful to anyone trying to replicate the result: McKinsey succeeded because it did the boring work that everyone else was skipping.&lt;/p&gt;
&lt;p&gt;Look at what the boring work actually was. The platform draws on more than forty knowledge sources and over a hundred thousand documents and interview transcripts — but the unlock was not aggregation, it was curation and tagging, the patient labor of making a century of accumulated knowledge consistent and machine-legible. The team built what is better described as an orchestration layer than a simple retrieval bot, designed to synthesize and contextualize rather than just fetch. They confronted the unglamorous reality that their best material was trapped in slides and fixed the ingestion so the machine could read it. Only then did the human side of adoption begin: a phased rollout, training that cured what the firm called "prompt anxiety" in roughly an hour, internal evangelists, and senior leaders modeling the behavior they wanted to see.&lt;/p&gt;
&lt;p&gt;The results are the part people quote, and they are genuinely impressive: more than three-quarters of the firm's tens of thousands of employees now use the tool, heavy users return to it more than a dozen times a week, and the firm reports its people save close to a third of their research time. But the number to internalize is not the adoption rate. It is what produced it. The moat was never the model. The moat was a hundred years of knowledge made legible to machines, wrapped in the governance and the change management required to make people actually trust it and use it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;McKinsey's edge was not a smarter model — every rival had the same models. The edge was a century of knowledge made legible to machines, and the discipline to govern it.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The pattern repeats across the rest of the industry, even if less dramatically. The firms making real internal progress — across the spectrum of platforms and agents now deployed — are consistently the ones that invested in their data foundations and their governance before they tried to scale. And the lesson generalizes cleanly to any enterprise, in any sector, that wants off the wrong side of the divide. The winners are not the organizations with the best model. Everyone has the same models. The winners are the organizations that did the unglamorous data-and-governance work that everyone else found a reason to defer.&lt;/p&gt;
&lt;p&gt;It is worth saying plainly what this does and does not mean for everyone else, because the lesson is easy to mislearn. You cannot buy McKinsey's result by buying McKinsey's tool, any more than you could acquire a rival's culture by licensing their software. What travels is not the platform; it is the method — the willingness to treat your own knowledge as an asset worth making machine-legible, and your own governance as an engineering problem worth solving before the agents arrive. That method is available to any organization in any industry. It is simply not for sale, and it cannot be rushed.&lt;/p&gt;
&lt;h2&gt;What the winners actually do&lt;/h2&gt;
&lt;p&gt;None of this resolves into a checklist, and anyone selling you one is selling the wrong thing. But the organizations on the right side of the divide do share a small number of strategic commitments, and they are worth stating plainly — not as steps to execute in order, but as the shape of a serious posture toward AI.&lt;/p&gt;
&lt;h3&gt;They sequence data before models&lt;/h3&gt;
&lt;p&gt;The single most counterintuitive move the winners make is to stop running hero pilots and fix the foundation first. That does not mean a multi-year data project before any value is delivered; it means choosing initial use cases precisely where the data is already clean and compatible, shipping those to generate real and defensible returns, and then using that credibility and momentum to fund the remediation of the messier domains. The failure mode is the opposite: picking use cases based on strategic ambition and executive enthusiasm, discovering the data underneath cannot support them, and producing an over-budget pilot that demonstrates the limits of the data rather than the capability of the AI. Match the ambition to the data maturity, not to the org chart's excitement.&lt;/p&gt;
&lt;h3&gt;They treat data as a product, not exhaust&lt;/h3&gt;
&lt;p&gt;The winners give their data owners, contracts, documentation, and a semantic layer that defines the business vocabulary once and reuses it everywhere. The catalog becomes the control plane of truth; the ontology and knowledge graph become the connective tissue that lets agents reason over entities and relationships rather than guess at ambiguous tables. This is the work that does not show up in a launch announcement and entirely determines whether the launch was real. It is also, not coincidentally, the part of the strategy a competitor cannot acquire by signing the same contracts you did.&lt;/p&gt;
&lt;h3&gt;They make governance machine-actionable&lt;/h3&gt;
&lt;p&gt;The winners do not confuse a principles document with a control. They express policy as something a system enforces: identity for every agent, least-agency access, tool whitelisting, audit trails, and a human in the loop for anything irreversible or sensitive. They adopt an established standard rather than inventing their own — the NIST framework, the ISO management-system standard, the OWASP failure catalogues — and they wrap it in an operating rhythm: regular reviews, red-teaming, and genuine change management for agents, treating a new agent with the seriousness one would treat a new hire with broad system access. Governance, done this way, is not the thing that slows the program down. It is the thing that lets the program move without fear.&lt;/p&gt;
&lt;h3&gt;They build the context layer agents inherit&lt;/h3&gt;
&lt;p&gt;Rather than re-explaining the business inside every prompt, the winners push meaning and rules down into the data itself, so that any agent connecting to it inherits both. It is worth watching the emerging plumbing here — &lt;a href="https://theharshdeepsingh.com/blog/the-best-claude-setup-that-works-on-any-ai-tool" rel="noopener noreferrer"&gt;open protocols that standardize how agents connect&lt;/a&gt; to tools and to one another, sometimes described as an "HTTP for agents," are quickly becoming the connective standard for this world. But a word of caution that the protocol enthusiasm tends to skip: plumbing that lets agents reach your data faster does nothing good if the house behind the tap is a mess. Standard connectivity over a swamp just distributes the swamp at higher throughput. The connectivity is necessary; the clean, governed foundation is what makes it worth having.&lt;/p&gt;
&lt;h3&gt;They treat adoption as a change program, not a software rollout&lt;/h3&gt;
&lt;p&gt;Finally, the winners understand that buying licenses is not the same as achieving adoption. The most replicable lesson from the internal success stories is almost embarrassingly human: an hour of training to dissolve the anxiety of a blank prompt, visible evangelists, and leaders who actually use the tools they are asking their people to use. If the overwhelming majority of AI value comes from people and process rather than the algorithm, then the overwhelming majority of the effort has to go there too. Technology adoption has always been a human problem wearing a technical mask, and AI has not changed that. It has only raised the stakes.&lt;/p&gt;
&lt;h3&gt;They measure value, not motion&lt;/h3&gt;
&lt;p&gt;The organizations stuck in the ninety-five percent tend to measure activity — pilots launched, seats provisioned, models evaluated — and mistake it for progress. The winners measure outcomes, and they are ruthless about it: a use case either moves a number a finance team recognizes, or it is killed quickly, before it hardens into a permanent science project. That discipline is exactly what frees the budget and the attention to pour into foundations, where the compounding returns actually live. Counting pilots is how a program feels busy while going nowhere. Counting value is how it escapes the lab.&lt;/p&gt;
&lt;h2&gt;The real divide&lt;/h2&gt;
&lt;p&gt;It is worth being precise about what the so-called GenAI Divide actually divides. It is not a line between companies with good models and companies with bad ones. Frontier models are a commodity now; the same handful are available to everyone with a credit card. The divide is between the organizations that did the foundational work and the organizations that did not — and underneath the AI costume, that is simply a gap in data maturity and governance discipline that has existed for years and that AI has suddenly made expensive to ignore.&lt;/p&gt;
&lt;p&gt;And it compounds. The organizations on the right side of the divide get faster every quarter, because their agents inherit ever-cleaner data and ever-tighter rules, and each success funds the next. The organizations on the wrong side re-run pilots that fail for the same reason they failed last time, mistaking a foundation problem for a model problem and waiting for a model that was never going to save them. The gap between the two groups does not stay constant. It widens.&lt;/p&gt;
&lt;p&gt;The deepest irony of the whole story is the one we began with. The cure for the failing enterprise AI program was never a smarter model. It was the boring, expensive, unglamorous discipline that the consultants themselves had to learn the hard way, in public, with a refunded invoice as tuition: organize the data so a machine can reason over it, write the rules down in a form a machine is forced to obey, and only then let the agents loose. The companies that internalize that will not merely adopt AI. They will compound on it — quietly, structurally, and largely out of view — while everyone else is still abandoning pilots and blaming the model.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1689085383734-32c6d88221a7%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26h%3D720" alt="An aerial view of a road winding in tight switchbacks up a green mountain pass." width="1600" height="720"&gt;&lt;p&gt;&lt;em&gt;The divide compounds. The foundation you lay now decides how fast you can move later. Photo: Robert Bye / Unsplash.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Sources and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;MIT NANDA initiative, &lt;em&gt;The GenAI Divide: State of AI in Business 2025&lt;/em&gt; — the source of the widely cited finding that roughly 95% of enterprise generative-AI pilots show no measurable business impact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://www.gartner.com/en/newsroom/press-releases/2024-07-29-gartner-predicts-30-percent-of-generative-ai-projects-will-be-abandoned-after-proof-of-concept-by-end-of-2025"&gt;Gartner&lt;/a&gt; — predictions on proof-of-concept abandonment, AI-ready data, and agentic-project cancellation rates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://venturebeat.com/ai/consulting-giant-mckinsey-unveils-its-own-generative-ai-tool-for-employees-lilli"&gt;VentureBeat&lt;/a&gt; — background on McKinsey's internal Lilli platform and how it was built.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://www.deloitte.com/ce/en/issues/generative-ai/state-of-ai-in-enterprise.html"&gt;Deloitte&lt;/a&gt;, &lt;em&gt;State of AI in the Enterprise&lt;/em&gt; — recurring survey data on enterprise AI spend, scaling, and ROI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://www.nist.gov/itl/ai-risk-management-framework"&gt;NIST AI Risk Management Framework&lt;/a&gt; and its Generative AI Profile — role-based access, monitoring, adversarial testing, and lifecycle logging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://genai.owasp.org/"&gt;OWASP GenAI Security Project&lt;/a&gt; — the Top 10 for LLM applications and the Top 10 for agentic applications, covering prompt injection, tool misuse, and related failure classes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ISO/IEC 42001 — the international standard for an AI management system, covering oversight and continual improvement.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


</description>
      <category>enterpriseai</category>
      <category>aistrategy</category>
      <category>datagovernance</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>Building an LLM Project From Scratch in 2026</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Tue, 09 Jun 2026 17:33:40 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/building-an-llm-project-from-scratch-in-2026-20i8</link>
      <guid>https://dev.to/harshdeepsingh13/building-an-llm-project-from-scratch-in-2026-20i8</guid>
      <description>&lt;p&gt;Here’s the uncomfortable truth about “AI projects” a few years ago: the hard part was never the model. It was the plumbing. Standing up a vector database, wiring an embeddings pipeline, fighting with streaming responses, gluing five libraries together — by the time it worked, you’d forgotten what you set out to build.&lt;/p&gt;
&lt;p&gt;In 2026 that plumbing has largely collapsed into a weekend’s worth of work. Model prices have fallen roughly &lt;strong&gt;80% year over year&lt;/strong&gt;, free tiers are genuinely usable, your database now does vector search natively, and one SDK handles streaming and tool calling across every provider. The skill that’s actually in demand — retrieval-augmented generation with agents — is now reachable by a developer who has never touched machine learning.&lt;/p&gt;
&lt;p&gt;So this guide does something specific. We’re going to build &lt;strong&gt;one real project&lt;/strong&gt;, end to end, that you can put on your portfolio and let strangers use: an app where someone uploads their documents and &lt;em&gt;chats with them&lt;/em&gt; — asking questions and getting answers grounded in their own files, with citations, streamed token by token. It’s the canonical 2026 LLM project, and it teaches almost everything else by osmosis.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;In plain English.&lt;/strong&gt; “RAG” means the AI doesn’t answer from memory — it &lt;em&gt;looks things up&lt;/em&gt; first. You give it a pile of documents; when you ask a question, it finds the most relevant passages and answers using only those. That’s why it can talk about &lt;em&gt;your&lt;/em&gt; files without ever having been trained on them.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;This guide is written for three readers at once: newcomers, working software engineers, and AI engineers. The main text stays approachable; the “In plain English” notes add no-jargon explanations, and the “Under the hood” notes add depth for engineers.&lt;/p&gt;
&lt;h3&gt;The roadmap — what we’ll actually do&lt;/h3&gt;
&lt;p&gt;Eight steps. Each one produces something that works before we add the next layer.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build the mental model&lt;/strong&gt; — how RAG (and &lt;em&gt;agentic&lt;/em&gt; RAG) really works, in one diagram.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose your models&lt;/strong&gt; — a cost comparison of hosted and self-hosted LLMs, and which embedding model to use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up the MERN stack&lt;/strong&gt; — project skeleton plus a MongoDB Atlas vector index.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ingest documents&lt;/strong&gt; — upload, parse a PDF, and split it into chunks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embed &amp;amp; store&lt;/strong&gt; — turn chunks into vectors and save them in MongoDB.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieve&lt;/strong&gt; — find the right passages with a single &lt;code&gt;$vectorSearch&lt;/code&gt; query.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make it agentic&lt;/strong&gt; — let the model call retrieval as a tool, on its own terms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stream &amp;amp; deploy&lt;/strong&gt; — render tokens live in React, then ship it to its own URL for free.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s start with the one idea that makes the other seven make sense.&lt;/p&gt;

&lt;h2&gt;Step 1 · The mental model: how RAG actually works&lt;/h2&gt;
&lt;p&gt;An LLM is a brilliant improviser with no access to your private data and a tendency to confidently make things up. &lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt; fixes both problems with one move: before the model answers, you fetch relevant facts and hand them over as context. The model then answers from &lt;em&gt;evidence&lt;/em&gt; rather than from vibes.&lt;/p&gt;
&lt;p&gt;There are two phases. The first happens once, ahead of time (&lt;strong&gt;ingestion&lt;/strong&gt;); the second happens on every question (&lt;strong&gt;retrieval + generation&lt;/strong&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INGESTION (run once, when a document is added)
  document --&amp;gt; split into chunks --&amp;gt; embed each chunk --&amp;gt; store vectors in MongoDB

RETRIEVAL + GENERATION (run on every question)
  question --&amp;gt; embed --&amp;gt; vector search in MongoDB --&amp;gt; top-k chunks
                                                         |
                             +---------------------------+
                             v
        [ question + retrieved chunks ] --&amp;gt; LLM --&amp;gt; grounded answer + citations&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The magic ingredient is the &lt;strong&gt;embedding&lt;/strong&gt;: a list of numbers (a vector) that captures the &lt;em&gt;meaning&lt;/em&gt; of a piece of text. Two passages about “canceling a subscription” land near each other in this number-space even if one says “refund” and the other says “cancel my plan.” Searching by meaning instead of keywords is what makes RAG feel intelligent.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;In plain English.&lt;/strong&gt; Imagine every sentence gets pinned onto a giant map, where similar meanings sit close together. To answer your question, the app drops a pin for &lt;em&gt;your question&lt;/em&gt; and grabs whatever text is pinned nearby. Those nearby notes become the AI’s cheat sheet.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3&gt;What makes it “agentic” — the 2026 upgrade&lt;/h3&gt;
&lt;p&gt;Classic RAG retrieves &lt;em&gt;once&lt;/em&gt; and hopes the first search was good enough. That breaks on real questions: “Compare the refund policy in the 2024 contract with the 2025 one” needs two different searches and a comparison. &lt;strong&gt;Agentic RAG&lt;/strong&gt; hands the model the steering wheel. Retrieval becomes a &lt;em&gt;tool&lt;/em&gt; the model can call — repeatedly — deciding what to search for, judging whether the results are sufficient, and searching again before it commits to an answer.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Under the hood.&lt;/strong&gt; A 2025 survey (“Agentic Retrieval-Augmented Generation,” arXiv:2501.09136) frames these systems around four patterns: &lt;strong&gt;reflection, planning, tool use, and multi-agent collaboration&lt;/strong&gt;. In practice you’ll implement query rewriting/decomposition, multi-hop “retrieve → reason → retrieve” loops, and self-critique (“do these passages actually answer the question?”). The cost: 3–10× the tokens and 2–5× the latency of vanilla RAG. So &lt;strong&gt;gate it&lt;/strong&gt; — a trivial FAQ should never enter the loop; a cross-document question can’t be answered without it. Cap the loop at ~5 iterations so a confused agent can’t spend your budget in a runaway.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;We’ll build the simple pipeline first (so you can see every piece), then promote retrieval to an agentic tool in Step 7. That progression &lt;em&gt;is&lt;/em&gt; the lesson.&lt;/p&gt;
&lt;h2&gt;Step 2 · Choosing your LLMs — cheapest viable first&lt;/h2&gt;
&lt;p&gt;You’ll use two kinds of model: an &lt;strong&gt;embedding model&lt;/strong&gt; (turns text into vectors) and a &lt;strong&gt;generation model&lt;/strong&gt; (writes the answer). They’re priced and chosen separately. Let’s start with generation, since that’s where the “which LLM?” anxiety lives.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Read this first.&lt;/strong&gt; Every price below is a &lt;strong&gt;2026 snapshot&lt;/strong&gt; and model names change almost monthly. Treat this table as a shape, not gospel — &lt;strong&gt;confirm the current number on the provider’s pricing page&lt;/strong&gt; before you commit. The &lt;em&gt;strategy&lt;/em&gt; (route cheap, escalate rarely) outlives any specific figure.&lt;/p&gt;&lt;/blockquote&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Provider / model&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Input ($/1M)&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Output ($/1M)&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Free tier?&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Best for&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Google Gemini Flash-Lite&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.10–0.25&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.40–1.50&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes — generous&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Learning &amp;amp; high volume; the default starter&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Groq · Llama 3.1 8B&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.05&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.08&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Blazing-fast responses; cheapest tokens&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;DeepSeek&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.14&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.28&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;No (cheap)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Cheapest frontier-class; OpenAI-compatible API&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;OpenAI · GPT mini-tier&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.15–0.75&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.60–4.50&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Credits&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Strong all-rounder; great tool calling&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Anthropic · Claude Haiku&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$1.00&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$5.00&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;No&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Cheapest Claude; reliable instruction-following&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Frontier (GPT / Claude / Gemini Pro)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$2.50–5.00&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$15–30&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;No&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Final answer only, when quality truly matters&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The pattern jumps out: the cheapest models are &lt;strong&gt;50–100× cheaper&lt;/strong&gt; than the flagships. For a RAG app, most of your token spend is feeding retrieved context into the model — so a cheap, capable model handling that bulk is the entire cost game. Use a frontier model only for the final synthesis, and only if you can measure that it’s actually better for your task.&lt;/p&gt;
&lt;h3&gt;Self-hosted: running models on your own machine&lt;/h3&gt;
&lt;p&gt;You can skip API bills entirely with &lt;strong&gt;Ollama&lt;/strong&gt;, which runs open models locally and exposes an OpenAI-compatible endpoint at &lt;code&gt;http://localhost:11434/v1&lt;/code&gt; — meaning your code barely changes. One command pulls and runs a model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# install from ollama.com, then:
ollama run llama3.1:8b          # chat model, ~6-8 GB VRAM
ollama pull nomic-embed-text    # local embedding model, free&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Under the hood.&lt;/strong&gt; Rough VRAM at Q4 quantization: 7–8B models ≈ 6–8 GB, 14B ≈ 10–12 GB, 32B ≈ 20–22 GB, 70B ≈ 43–48 GB (Apple Silicon unified memory counts fully). &lt;strong&gt;Break-even vs hosted APIs is roughly 500K tokens/day of sustained traffic&lt;/strong&gt; — below that, hosted is cheaper &lt;em&gt;and&lt;/em&gt; you skip the ops. Trade-offs: full privacy and $0/token, but weaker reasoning than frontier models and you own the uptime.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3&gt;The embedding model — quieter, but it matters&lt;/h3&gt;
&lt;p&gt;Embeddings are dramatically cheaper than generation, so this is an easy call. For most projects, &lt;strong&gt;OpenAI’s &lt;/strong&gt;&lt;code&gt;text-embedding-3-small&lt;/code&gt; is the sweet spot.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Model&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Dimensions&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Price ($/1M)&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Notes&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;OpenAI text-embedding-3-small&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;1536&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.02&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Best balance; our pick for the build&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Google gemini-embedding&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;768&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Free tier / ~$0.025&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Free-tier friendly&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Voyage (voyage-3.5-lite)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;512–1024 (reducible)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;~$0.02&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Now MongoDB-owned; long context&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;nomic-embed-text / BGE-M3 (open)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;768 / 1024&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Free (self-host)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Run free in Ollama; great quality&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha that bites everyone.&lt;/strong&gt; Vectors from different embedding models are &lt;strong&gt;not compatible&lt;/strong&gt;. If you switch embedding models later, you must &lt;em&gt;re-embed your entire corpus&lt;/em&gt;. Pick one and commit. (Embedding 10M chunks with &lt;code&gt;text-embedding-3-small&lt;/code&gt; costs only ~$100, so this is about consistency, not cost.)&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Our choices for this build:&lt;/strong&gt; embeddings via &lt;code&gt;text-embedding-3-small&lt;/code&gt;; generation via a cheap, fast model (Gemini Flash or a GPT mini-tier model) while learning — swappable in one line thanks to the SDK we’re about to set up.&lt;/p&gt;
&lt;h2&gt;Step 3 · Setting up the MERN stack&lt;/h2&gt;
&lt;p&gt;MERN is a natural fit for RAG in 2026 for one reason that didn’t used to be true: &lt;strong&gt;MongoDB does vector search natively&lt;/strong&gt;. Your embeddings live in the same documents as your data, queried with a normal aggregation pipeline. No separate vector database to run, sync, or pay for.&lt;/p&gt;
&lt;p&gt;Here’s the shape of the app — a standard MERN split, with the LLM logic living safely on the server:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MongoDB Atlas&lt;/strong&gt; — stores documents, chunks, embeddings, and chat history. Free &lt;code&gt;M0&lt;/code&gt; tier includes vector search.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Express + Node.js&lt;/strong&gt; — the API: handles uploads, embedding, retrieval, and talking to the LLM. &lt;em&gt;All API keys live here, never in the browser.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;React&lt;/strong&gt; — the chat UI, rendering streamed tokens as they arrive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The glue: the Vercel AI SDK&lt;/strong&gt; — one library for streaming, provider-switching, and tool calling, on both server and client.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The one piece of setup that’s new: the vector index&lt;/h3&gt;
&lt;p&gt;After creating a free cluster on Atlas, you define a &lt;strong&gt;vector search index&lt;/strong&gt; on the collection that will hold your chunks. This tells MongoDB how to search the embedding field. In the Atlas UI (Atlas Search → Create Index → JSON editor), or via code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  "fields": [
    {
      "type": "vector",
      "path": "embedding",
      "numDimensions": 1536,
      "similarity": "cosine"
    },
    { "type": "filter", "path": "userId" }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Under the hood.&lt;/strong&gt; &lt;code&gt;numDimensions&lt;/code&gt; must &lt;em&gt;exactly&lt;/em&gt; match your embedding model’s output (1536 for &lt;code&gt;text-embedding-3-small&lt;/code&gt;). &lt;code&gt;similarity&lt;/code&gt; can be &lt;code&gt;cosine&lt;/code&gt;, &lt;code&gt;euclidean&lt;/code&gt;, or &lt;code&gt;dotProduct&lt;/code&gt; — cosine is the safe default. The &lt;code&gt;filter&lt;/code&gt; field on &lt;code&gt;userId&lt;/code&gt; is what lets you scope searches per-user, so visitors only ever retrieve their own documents — essential the moment your demo is public. Atlas uses HNSW for approximate nearest-neighbor search under the hood.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;That’s the only “AI-specific infrastructure” in the whole project. Everything else is ordinary Express and React.&lt;/p&gt;
&lt;h2&gt;Step 4 · Ingesting documents: upload, parse, chunk&lt;/h2&gt;
&lt;p&gt;When a user uploads a file, three things happen on the server: we accept the upload, extract its text, and split that text into bite-sized &lt;strong&gt;chunks&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Accepting uploads is standard Express (use &lt;code&gt;multer&lt;/code&gt;). Extracting text from a PDF is a one-liner with &lt;code&gt;pdf-parse&lt;/code&gt; (v2 fork, TypeScript-native — see note in code):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { PDFParse } from "pdf-parse"; // requires the v2 fork, not the default pdf-parse@1

// `buffer` is the uploaded file from multer
const parser = new PDFParse({ data: buffer });
const { text } = await parser.getText();&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why we chunk — and how big&lt;/h3&gt;
&lt;p&gt;You can’t embed an entire 50-page PDF as one vector; the meaning gets blurred into mush, and you’d feed the model far more than it needs. So we slice the text into passages. Each chunk becomes one searchable unit.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ~1 token = ~4 characters, so 2000 chars = ~500 tokens.
// We overlap chunks so a sentence split across a boundary
// still appears whole in at least one chunk.
function chunkText(text, size = 2000, overlap = 200) {
  const chunks = [];
  for (let i = 0; i &amp;lt; text.length; i += size - overlap) {
    chunks.push(text.slice(i, i + size).trim());
  }
  return chunks.filter(Boolean);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;In plain English.&lt;/strong&gt; Think of chunking like cutting a long article into index cards. Too big and each card covers too many topics to be useful; too small and you lose context. A few hundred words per card, with a little overlap so sentences don’t get sliced in half, is the reliable starting point.&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Under the hood.&lt;/strong&gt; Start with recursive character splitting at &lt;strong&gt;~400–512 tokens with 10–20% overlap&lt;/strong&gt; — the pragmatic default (~85–90% retrieval recall). &lt;em&gt;Semantic chunking&lt;/em&gt; can add ~2–3% recall but costs roughly 14× more to index, so only graduate to it when your evaluation metrics demand it. One caveat: at least one 2026 analysis found overlap added no measurable benefit in its setup while raising indexing cost — so treat the overlap figure as a starting point to validate against your own data, not a law.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;Step 5 · Embedding &amp;amp; storing vectors in MongoDB&lt;/h2&gt;
&lt;p&gt;Now we turn each chunk into a vector and save it. The AI SDK’s &lt;code&gt;embedMany&lt;/code&gt; batches the whole array efficiently, then we write one MongoDB document per chunk — text and vector together, tagged with the owner and source.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { embedMany } from "ai";
import { openai } from "@ai-sdk/openai";

const chunks = chunkText(text);                 // from Step 4

const { embeddings } = await embedMany({
  model: openai.embedding("text-embedding-3-small"),
  values: chunks,                               // array of strings
});

await db.collection("chunks").insertMany(
  chunks.map((chunk, i) =&amp;gt; ({
    userId,                                     // who owns it
    source: filename,                           // where it came from
    text: chunk,                                // the passage itself
    embedding: embeddings[i],                   // the 1536-dim vector
    chunkIndex: i,
    createdAt: new Date(),
  }))
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s ingestion done. The vector is just an array of floats stored on a normal document — no special database, no migration. Upload a 30-page PDF and you’ve got a few dozen searchable, meaning-aware chunks sitting in MongoDB.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Under the hood.&lt;/strong&gt; &lt;code&gt;embedMany&lt;/code&gt; auto-batches large arrays, so you can hand it hundreds of chunks without managing request limits yourself. Store rich metadata (page number, section heading, document ID) alongside each chunk now — you’ll want it later for citations, filtering, and “parent-document” retrieval. This is the step you run once per upload, not once per question.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;Step 6 · Retrieval: finding the right passages&lt;/h2&gt;
&lt;p&gt;Here’s the payoff for all that setup. To answer a question, we embed the question with the &lt;em&gt;same&lt;/em&gt; model, then run a single &lt;code&gt;$vectorSearch&lt;/code&gt; aggregation to pull the closest chunks. This is the whole of “search by meaning,” in one query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { embed } from "ai";
import { openai } from "@ai-sdk/openai";

const { embedding } = await embed({
  model: openai.embedding("text-embedding-3-small"),
  value: userQuestion,
});

const passages = await db.collection("chunks").aggregate([
  {
    $vectorSearch: {
      index: "vector_index",
      path: "embedding",
      queryVector: embedding,
      numCandidates: 150,        // over-fetch, then narrow
      limit: 5,                  // keep the best 5
      filter: { userId: { $eq: currentUserId } }
    }
  },
  {
    $project: {
      _id: 0,
      text: 1,
      source: 1,
      score: { $meta: "vectorSearchScore" }
    }
  }
]).toArray();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You now have the five most relevant passages, each with a similarity &lt;code&gt;score&lt;/code&gt;. Feed those into the model as context and you have working RAG. But before we generate, two notes that separate a toy from something good:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Under the hood.&lt;/strong&gt; &lt;code&gt;$vectorSearch&lt;/code&gt; must be the &lt;strong&gt;first&lt;/strong&gt; stage in the pipeline. &lt;code&gt;numCandidates&lt;/code&gt; is the approximate-search breadth — it must be ≥ &lt;code&gt;limit&lt;/code&gt;, and a common heuristic is 10–20× your limit (here 150 for a limit of 5). The &lt;code&gt;filter&lt;/code&gt; on &lt;code&gt;userId&lt;/code&gt; uses the field we declared in the index, enforcing per-user isolation efficiently. Use the &lt;code&gt;score&lt;/code&gt; as a relevance gate: if the top result scores below ~0.75, it’s often better to answer “I don’t have enough information” than to let the model hallucinate from weak matches.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The single biggest quality upgrade you’ll make later isn’t a bigger model — it’s &lt;strong&gt;hybrid search + reranking&lt;/strong&gt; (we cover it in “Where to go next”). For now, vector search alone is plenty to ship.&lt;/p&gt;
&lt;h2&gt;Step 7 · Making retrieval agentic&lt;/h2&gt;
&lt;p&gt;So far the server retrieves &lt;em&gt;before&lt;/em&gt; calling the model — a fixed pipeline. To make it agentic, we flip the control: we describe retrieval as a &lt;strong&gt;tool&lt;/strong&gt;, hand it to the model, and let the model decide when (and how often) to call it. This is the “hot” part of 2026 — and with the AI SDK it’s remarkably little code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { streamText, tool, embed, stepCountIs } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

// 1) Retrieval, described as a tool the model can call
const searchDocuments = tool({
  description: "Search the user's uploaded documents for passages " +
               "relevant to a question. Call this whenever you need facts.",
  inputSchema: z.object({
    query: z.string().describe("a focused search query"),
  }),
  execute: async ({ query }) =&amp;gt; {
    const { embedding } = await embed({
      model: openai.embedding("text-embedding-3-small"),
      value: query,
    });
    return db.collection("chunks").aggregate([
      { $vectorSearch: {
          index: "vector_index", path: "embedding",
          queryVector: embedding, numCandidates: 150, limit: 5,
          filter: { userId: { $eq: currentUserId } } } },
      { $project: { _id: 0, text: 1, source: 1,
          score: { $meta: "vectorSearchScore" } } },
    ]).toArray();
  },
});

// 2) Let the model run the loop: think -&amp;gt; search -&amp;gt; (search again) -&amp;gt; answer
const result = streamText({
  model: openai("gpt-4o-mini"),     // swap to any provider in one line
  system: "Answer ONLY using passages returned by searchDocuments. " +
          "Cite the source. If the passages don't contain the answer, " +
          "say you don't know - do not guess.",
  messages,               // from req.body — full chat history from the client
  tools: { searchDocuments },
  stopWhen: stepCountIs(5),         // hard cap on the agentic loop
});

return result.toUIMessageStreamResponse();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read what that does, because it’s genuinely different from classic RAG: the model receives the question, decides on its own to call &lt;code&gt;searchDocuments&lt;/code&gt; with a query &lt;em&gt;it&lt;/em&gt; wrote, reads the results, and may call it again with a refined query before answering. For “compare the 2024 and 2025 refund policies,” it can naturally run two searches and synthesize. You didn’t orchestrate that — the model did.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Important 2026 change.&lt;/strong&gt; If you learned the AI SDK before v5: &lt;code&gt;maxSteps&lt;/code&gt; was removed from the client. Multi-step tool loops are now controlled &lt;strong&gt;server-side&lt;/strong&gt; with &lt;code&gt;stopWhen&lt;/code&gt; (e.g. &lt;code&gt;stepCountIs(5)&lt;/code&gt;). This cap is also your cost safety rail — without it, a confused agent could loop and run up your bill.&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Under the hood.&lt;/strong&gt; The &lt;code&gt;system&lt;/code&gt; prompt is doing heavy lifting for safety and grounding: “answer only from retrieved passages” plus “say you don’t know” is your first and cheapest defense against hallucination. The Zod &lt;code&gt;inputSchema&lt;/code&gt; gives the model a typed contract for the tool’s arguments. &lt;code&gt;toUIMessageStreamResponse()&lt;/code&gt; emits a standard SSE stream the React client consumes natively. Want it reusable across other AI clients (Claude Desktop, Cursor, etc.)? Expose this same retrieval as an &lt;strong&gt;MCP&lt;/strong&gt; server — overkill for a single app, but the natural next step if your tools should be shared.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;Step 8 · Streaming to React, then deploying for free&lt;/h2&gt;
&lt;p&gt;The backend streams tokens; the frontend renders them as they land. The AI SDK’s &lt;code&gt;useChat&lt;/code&gt; hook handles the entire streaming lifecycle, so your component stays tiny:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;"use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";

export default function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({ api: "/api/chat" }),
  });

  return (
    &amp;lt;div&amp;gt;
      {messages.map((m) =&amp;gt; (
        &amp;lt;div key={m.id}&amp;gt;
          &amp;lt;strong&amp;gt;{m.role}: &amp;lt;/strong&amp;gt;
          {m.parts.map((p, i) =&amp;gt;
            p.type === "text" ? &amp;lt;span key={i}&amp;gt;{p.text}&amp;lt;/span&amp;gt; : null
          )}
        &amp;lt;/div&amp;gt;
      ))}

      &amp;lt;input value={input} onChange={(e) =&amp;gt; setInput(e.target.value)} /&amp;gt;
      &amp;lt;button
        disabled={status !== "ready"}
        onClick={() =&amp;gt; { sendMessage({ text: input }); setInput(""); }}
      &amp;gt;
        {status === "streaming" ? "Thinking..." : "Ask"}
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;status&lt;/code&gt; field (&lt;code&gt;ready&lt;/code&gt; / &lt;code&gt;submitted&lt;/code&gt; / &lt;code&gt;streaming&lt;/code&gt; / &lt;code&gt;error&lt;/code&gt;) gives you loading and disabled states for free. Tokens appear live as the model writes them — the experience people now expect from any AI app.&lt;/p&gt;
&lt;h3&gt;Putting it on its own website — for free&lt;/h3&gt;
&lt;p&gt;This is the part that turns a tutorial into a portfolio piece. Three boxes, three free tiers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vercel&lt;/strong&gt; — React frontend, free Hobby tier, global CDN, custom domain, auto-deploy from GitHub.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Render&lt;/strong&gt; — Node/Express backend, free web service (sleeps when idle), where streaming and keys live.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MongoDB Atlas M0&lt;/strong&gt; — database + vector search, permanently free, 512 MB, limited Vector Search index capacity (verify current limits in Atlas docs).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Total cost for a low-traffic demo: &lt;strong&gt;$0–$5/month&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Under the hood.&lt;/strong&gt; Run the streaming endpoint on the long-lived backend (Render/Railway), not a short serverless function that can time out mid-stream. Know the free-tier edges: Render free services &lt;strong&gt;spin down after ~15 min idle&lt;/strong&gt; (a ~30–60s cold start on the next request — fine for a portfolio), and Atlas M0 caps at &lt;strong&gt;512 MB and limited Vector Search index capacity&lt;/strong&gt;. When you outgrow them, a dedicated Atlas tier and an always-on backend plan are the upgrade path.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h4&gt;Cost controls so you never get a surprise bill&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Set a &lt;strong&gt;hard spend cap&lt;/strong&gt; in your LLM provider dashboard. Non-negotiable for a public demo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Default to a cheap model; cap &lt;code&gt;max_output_tokens&lt;/code&gt;; keep the agentic &lt;code&gt;stepCountIs&lt;/code&gt; low.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add &lt;strong&gt;per-user / per-IP rate limiting&lt;/strong&gt; and an auth wall so bots can’t drain your quota.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Log token usage per request (the SDK’s &lt;code&gt;onFinish&lt;/code&gt; callback) so you can see costs before they surprise you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep &lt;strong&gt;every API key on the server&lt;/strong&gt;. A key shipped to the browser is a key that will be stolen.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Common pitfalls (and the fixes)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The semantic gap.&lt;/strong&gt; Your question and the document use different words and vector search misses the match. &lt;em&gt;Fix:&lt;/em&gt; add hybrid search (keyword + vector).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context dilution.&lt;/strong&gt; You retrieve 10 chunks when only 2 are relevant, and the noise degrades the answer. &lt;em&gt;Fix:&lt;/em&gt; rerank, then keep a tighter top-k.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chunk-boundary amnesia.&lt;/strong&gt; The answer is split across two chunks and neither is retrieved whole. &lt;em&gt;Fix:&lt;/em&gt; overlap, or parent-document retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confident nonsense.&lt;/strong&gt; The model answers from weak matches as if certain. &lt;em&gt;Fix:&lt;/em&gt; a similarity-score threshold plus a system prompt that permits “I don’t know.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reaching for the agent loop too early.&lt;/strong&gt; Simple lookups don’t need multi-hop reasoning — they need one fast search. &lt;em&gt;Fix:&lt;/em&gt; gate the agentic path to genuinely complex questions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where to go next&lt;/h2&gt;
&lt;p&gt;You’ve shipped a working agentic RAG app. Three upgrades, in priority order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hybrid search + reranking&lt;/strong&gt; — the highest-ROI quality jump. Run keyword (Atlas full-text via &lt;code&gt;$search&lt;/code&gt;) &lt;em&gt;and&lt;/em&gt; vector search, fuse them with Reciprocal Rank Fusion (RRF), then rerank the top 20–50 candidates with a cross-encoder (Cohere, Voyage, or self-hosted BGE) and keep the best handful. Benchmarks routinely show reranking as the single biggest accuracy gain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better ingestion&lt;/strong&gt; — messy PDFs with tables and multi-column layouts need a real parser (LlamaIndex’s LiteParse, Unstructured, or Docling) rather than plain text extraction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make it shareable via MCP&lt;/strong&gt; — expose your retrieval as a Model Context Protocol server so other AI clients can use the same tool. Worth it once your tools outlive this one app.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s the whole arc: from “an LLM can’t see my data” to a public, agentic, document-grounded chat app that costs about nothing to run. The plumbing finally got out of the way — what you build on top of it is the interesting part. Now go put something on that empty portfolio URL.&lt;/p&gt;
&lt;h2&gt;Frequently asked questions&lt;/h2&gt;
&lt;h3&gt;What is agentic RAG, exactly?&lt;/h3&gt;
&lt;p&gt;Agentic RAG turns retrieval into a tool the model calls on demand. Instead of one fixed “retrieve then answer” pass, the model plans, searches, judges whether the results are sufficient, and searches again until it has enough evidence — then answers. It’s slower and costs more tokens, but it handles complex, multi-step questions that one-shot RAG can’t.&lt;/p&gt;
&lt;h3&gt;Do I need a separate vector database?&lt;/h3&gt;
&lt;p&gt;No. On the MERN stack you store embeddings inside your normal MongoDB documents and query them with the &lt;code&gt;$vectorSearch&lt;/code&gt; aggregation stage in MongoDB Atlas. For the vast majority of projects, that removes the need for a dedicated vector database entirely.&lt;/p&gt;
&lt;h3&gt;What’s the cheapest LLM for a RAG app in 2026?&lt;/h3&gt;
&lt;p&gt;For learning, the most generous free tiers are Google Gemini Flash/Flash-Lite and Groq. For the cheapest paid frontier-class model, DeepSeek is usually lowest. Prices change monthly — confirm on the provider’s pricing page. The durable strategy is to route bulk work to a cheap model and reserve a frontier model for the final answer only.&lt;/p&gt;
&lt;h3&gt;How much does it cost to run?&lt;/h3&gt;
&lt;p&gt;A portfolio-grade demo runs at roughly $0–$5/month: MongoDB Atlas free M0, a free LLM tier, embeddings at ~$0.02 per million tokens, and free deploy tiers on Vercel and Render. Set a provider spend cap and rate limits so a public demo can’t surprise you.&lt;/p&gt;
&lt;h3&gt;LangChain, LlamaIndex, or the Vercel AI SDK?&lt;/h3&gt;
&lt;p&gt;For a MERN streaming chat app, the Vercel AI SDK plus direct MongoDB vector queries is the lighter, recommended path in 2026. Reach for LlamaIndex.TS if your main challenge is heavy document ingestion, or LangChain.js/LangGraph for complex multi-agent orchestration. For ~90% of RAG web apps, the AI SDK is the right call.&lt;/p&gt;
&lt;h3&gt;Can I run this fully offline with a local model?&lt;/h3&gt;
&lt;p&gt;Yes. Ollama runs open models locally and exposes an OpenAI-compatible endpoint, so your code barely changes. Use a local embedding model like &lt;code&gt;nomic-embed-text&lt;/code&gt; too. It’s ideal for development and privacy-sensitive data; for a low-traffic public demo, hosted free tiers are usually simpler and cheaper.&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A note on accuracy.&lt;/strong&gt; LLM pricing, model names, and SDK APIs change fast. Every figure here is a 2026 snapshot — verify current prices on each provider’s pricing page and current method names in the Vercel AI SDK docs before shipping to production. Code samples are illustrative walkthroughs, not drop-in files. Images: “Visualising AI” by Google DeepMind on Unsplash, free under the Unsplash License.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You can build and ship a production-shaped, &lt;strong&gt;agentic RAG “chat with your documents”&lt;/strong&gt; app entirely on &lt;strong&gt;MERN&lt;/strong&gt; for about &lt;strong&gt;$0/month&lt;/strong&gt;: MongoDB Atlas’ free tier (with built-in vector search), a free LLM tier (Gemini Flash or Groq), embeddings at &lt;strong&gt;$0.02 per million tokens&lt;/strong&gt;, and free deploys on Vercel + Render.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The 2026 stack is leaner than you think: &lt;strong&gt;MongoDB Atlas Vector Search&lt;/strong&gt; (no separate vector database), the &lt;strong&gt;Vercel AI SDK&lt;/strong&gt; for streaming + tool calling, and &lt;strong&gt;“agentic retrieval”&lt;/strong&gt; — where the model itself decides when and what to search — instead of the old retrieve-once-then-answer pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick models by job, not by brand.&lt;/strong&gt; Route cheap, high-volume work to Gemini Flash-Lite, DeepSeek, or Groq-hosted Llama; reserve a frontier model only for the final answer when quality matters. Self-hosting with Ollama only beats hosted APIs above heavy, sustained traffic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;By the end you’ll understand embeddings, chunking, vector retrieval, tool calling, token streaming in React, and how to put the whole thing on its own public website — with cost controls so you never get a surprise bill.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


</description>
      <category>rag</category>
      <category>agenticai</category>
      <category>llm</category>
      <category>mongodbatlas</category>
    </item>
    <item>
      <title>The Best Claude Setup (That Works on Any AI Tool)</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Thu, 04 Jun 2026 23:16:05 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/the-best-claude-setup-that-works-on-any-ai-tool-5h7i</link>
      <guid>https://dev.to/harshdeepsingh13/the-best-claude-setup-that-works-on-any-ai-tool-5h7i</guid>
      <description>&lt;p&gt;Here is a confession that might sound odd in a guide about setting up Claude Code: the goal is not to marry Claude Code. The goal is to build a setup so portable that if something better ships next month — OpenAI's Codex, Cursor, Windsurf, whatever wins the week — you could pack up everything you have built and move in an afternoon. Your instructions, your custom tools, your reusable workflows: all of it should come with you.&lt;/p&gt;
&lt;p&gt;That sounds like a strange thing to optimize for. Most "best setup" posts try to lock you deeper into one tool. But the AI coding world is moving too fast for that bet to be safe, and — happily — a small set of open standards now make portability the default instead of a fantasy. If you are a working engineer, this is how you stop re-plumbing your environment every time you switch editors. And if you are a &lt;em&gt;vibe coder&lt;/em&gt; — someone who builds mostly by describing what you want in plain English and steering the AI, a style Andrej Karpathy named in early 2025 — this is how you get a professional-grade setup without months of fiddling.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The short version: keep your AI's instructions, tools, and skills in plain, open formats you own — not buried in one vendor's settings. Three standards make this work: MCP for tools, AGENTS.md for instructions, and Agent Skills for reusable know-how. Learn those, and the question "which coding agent should I use?" stops being scary.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Three standards do the heavy lifting here, and we will spend most of our time on them: &lt;span&gt;MCP&lt;/span&gt; &lt;span&gt;AGENTS.md&lt;/span&gt; &lt;span&gt;Agent Skills&lt;/span&gt;. Let's build up to a setup you would actually be happy to leave.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1587654780291-39c9404d746b%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="Yellow, red, blue, and green LEGO bricks scattered on a surface, representing modular, interchangeable building blocks" width="1600" height="1067"&gt;&lt;p&gt;&lt;em&gt;Think of your setup as interchangeable bricks, not a sculpture glued to one base. Photo by &lt;/em&gt;&lt;a rel="noopener noreferrer nofollow" href="https://unsplash.com/@xavi_cabrera"&gt;&lt;em&gt;Xavi Cabrera&lt;/em&gt;&lt;/a&gt;&lt;em&gt; on &lt;/em&gt;&lt;a rel="noopener noreferrer nofollow" href="https://unsplash.com/"&gt;&lt;em&gt;Unsplash&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Why portability is the whole game now&lt;/h2&gt;
&lt;p&gt;In the last 18 months, more than a dozen serious AI coding agents have shipped: Claude Code, OpenAI Codex, Cursor, Windsurf, Zed's agent, Aider, Cline, Continue, Gemini CLI, and more. Each one claims to be the fastest or the smartest. Some genuinely leapfrog the others — for a few weeks, until the next release.&lt;/p&gt;
&lt;p&gt;Here is the trap. If you pour weeks into one tool — memorizing its config files, hand-tuning its rules, wiring up its integrations — you have quietly built a switching cost. The day a clearly better tool arrives, you do the math on re-learning everything and you stay put, not because your tool is best, but because leaving hurts. That is the real lock-in. It is not a contract; it is your own sunk effort.&lt;/p&gt;
&lt;p&gt;The fix is to treat the agent as a replaceable part and invest in the layer underneath it. The engineer Geoffrey Huntley has made this point sharply: the agents themselves are becoming commodities, and the durable advantage is the standards layer you build around them. Put your effort there, and any agent becomes a front-end you can swap.&lt;/p&gt;
&lt;p&gt;An analogy: remember when every phone had its own charger, and switching brands meant a drawer full of dead cables? USB-C fixed that by agreeing on one shape. You buy a charger once; it works with the next phone, and the one after. The standards below are USB-C for your AI setup. Learn them once, and your tools become things you plug into — not things you are wired into.&lt;/p&gt;
&lt;h2&gt;The three open standards that set you free&lt;/h2&gt;
&lt;p&gt;You do not need to memorize specs. You need to understand what each standard &lt;em&gt;is for&lt;/em&gt;, because once you see the shape of the problem each one solves, the portability falls out naturally. They split cleanly: one is for tools, one is for instructions, one is for skills.&lt;/p&gt;
&lt;h3&gt;MCP — one way to plug in tools&lt;/h3&gt;
&lt;p&gt;The Model Context Protocol (MCP) is an open standard, created and open-sourced by Anthropic in November 2024, for connecting an AI to outside tools and data — your database, your GitHub, your Notion, your company's internal services.&lt;/p&gt;
&lt;p&gt;If you have used a code editor, you have already benefited from this idea. Editors once needed custom code to support each programming language, until the Language Server Protocol let any editor talk to any language through one shared interface. MCP does the same trick for AI and tools. Instead of every AI app writing a custom integration for every service — an N-times-M explosion — you write one MCP &lt;strong&gt;server&lt;/strong&gt; for a service, and every MCP-compatible &lt;strong&gt;host&lt;/strong&gt; can use it. The math collapses from N times M down to N plus M.&lt;/p&gt;
&lt;p&gt;Mechanically, a host (Claude Code, Cursor, ChatGPT, and others) runs a client that talks to your server over one of two transports: &lt;code&gt;stdio&lt;/code&gt; for a local tool running as a subprocess, or streamable HTTP for a remote service. The server exposes tools, read-only resources, and prompt templates; the host stays in charge of what the model is actually allowed to touch. Crucially, the configuration looks almost identical across tools — a small block naming each server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
    },
    "github": {
      "url": "https://api.githubcopilot.com/mcp/",
      "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Move from Claude Code to Cursor or Codex and you copy that block over, rename one key if needed, and your tools come with you. One safety note worth tattooing on your brain: an MCP server can run code and reach real systems, so only install servers you trust, keep secrets in environment variables like the &lt;code&gt;${GITHUB_TOKEN}&lt;/code&gt; above rather than hard-coding them, and review what each tool is permitted to do.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1545987796-200677ee1011%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="Low-angle photograph of an interconnected lattice of nodes and edges, representing a network of clients and servers" width="1600" height="1067"&gt;&lt;p&gt;&lt;em&gt;MCP is the wiring: many tools, one shared protocol. Photo by &lt;/em&gt;&lt;a rel="noopener noreferrer nofollow" href="https://unsplash.com/@alinnnaaaa"&gt;&lt;em&gt;Alina Grubnyak&lt;/em&gt;&lt;/a&gt;&lt;em&gt; on &lt;/em&gt;&lt;a rel="noopener noreferrer nofollow" href="https://unsplash.com/"&gt;&lt;em&gt;Unsplash&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;AGENTS.md — a README for your AI&lt;/h3&gt;
&lt;p&gt;Every project has unspoken rules: how to install it, how to run the tests, which folders are off-limits, what "good code" means here. A human teammate learns these over weeks. An AI agent starts fresh every session and will happily reinvent your conventions unless you write them down.&lt;/p&gt;
&lt;p&gt;That is what &lt;code&gt;AGENTS.md&lt;/code&gt; is — a plain Markdown file at the root of your repo that tells any coding agent how to work on your project. No special syntax, no required fields. Think of it as a README written for your AI instead of for a human. It was introduced alongside OpenAI's Codex and is now supported by Codex, Cursor, Aider, Cline, Windsurf, and others — with Gemini CLI requiring a GEMINI.md symlink as described below. A good one leads with commands, because the agent refers back to them constantly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Project: Acme API

## Commands
- Install: `pnpm install`
- Dev server: `pnpm dev`
- Test (run before every PR): `pnpm test`
- Lint &amp;amp; typecheck: `pnpm lint &amp;amp;&amp;amp; pnpm typecheck`

## Conventions
- TypeScript strict mode. Never use `any`.
- Do not edit files in `/generated` — they are built from schemas.
- Write a test for every new endpoint.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the one wrinkle on the Claude side. Anthropic's tool uses its own file, &lt;code&gt;CLAUDE.md&lt;/code&gt;, and as of mid-2026 Claude Code does not read &lt;code&gt;AGENTS.md&lt;/code&gt; natively — a gap the community has been loudly requesting for months. The portable move is to keep &lt;strong&gt;one&lt;/strong&gt; source of truth in &lt;code&gt;AGENTS.md&lt;/code&gt; and point the others at it with a symlink, so you never maintain the same rules twice:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# One canonical file, linked everywhere your tools look
ln -s AGENTS.md CLAUDE.md
ln -s AGENTS.md GEMINI.md&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One file, every tool, zero duplication. One caution worth knowing: do not blindly auto-generate this file and walk away. Research has consistently shown that bloated, auto-generated instruction files tend to hurt more than they help — the important rules get buried in noise and the agent learns to half-ignore them. Keep it short, hand-curated, and honest about what is genuinely non-obvious.&lt;/p&gt;
&lt;h3&gt;Agent Skills — teach once, reuse everywhere&lt;/h3&gt;
&lt;p&gt;The third standard is the one that feels like magic the first time it clicks. An Agent Skill is a folder containing a &lt;code&gt;SKILL.md&lt;/code&gt; file — Markdown instructions for a specific task — plus any optional scripts or reference docs that task needs. Anthropic's own framing is the clearest: building a skill is like writing an onboarding guide for a new hire. You capture how to do something once, and the agent follows it forever after.&lt;/p&gt;
&lt;p&gt;The clever part is &lt;em&gt;progressive disclosure&lt;/em&gt;. At startup the agent only reads each skill's name and one-line description — a few dozen words — so a hundred skills cost almost nothing. Only when a task actually matches does it open the full instructions, and only then does it reach for the bundled scripts. Your context window stays clean until the moment a skill is relevant.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pdf-form-filler/
├── SKILL.md          # name + description + instructions
├── scripts/          # code the agent runs (stays out of context)
│   └── fill.py
├── references/       # long docs, loaded only when needed
└── assets/           # templates, fonts, icons&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because a skill is just Markdown and files in a folder, it is inherently portable — you can point Codex or Gemini CLI at a skills folder, tell it to read the &lt;code&gt;SKILL.md&lt;/code&gt;, and it simply works. Agent Skills are now supported across a growing list of tools: Claude Code, Codex, Cursor, Gemini CLI, and more. Write your "how we deploy" or "how we write migrations" skill once, and it travels.&lt;/p&gt;
&lt;p&gt;So when do you reach for which? They are not competitors; they answer different questions. Here is the cheat sheet:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Mechanism&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;What it gives the agent&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Reach for it when&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Agent Skill&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;A repeatable procedure — the &lt;em&gt;how&lt;/em&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;You keep re-explaining the same multi-step workflow&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;MCP server&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;A connection to an outside system — the &lt;em&gt;reach&lt;/em&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;The agent needs live data or a third-party API&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Subagent&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;A fresh worker with its own clean context&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;A job is large, or you want an independent reviewer&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;AGENTS.md&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Always-on project facts and rules&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Conventions every session should already know&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Hook&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Enforcement that always runs, no exceptions&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Something &lt;em&gt;must&lt;/em&gt; happen — formatting, blocking secrets&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;Does "switch tomorrow" actually hold?&lt;/h2&gt;
&lt;p&gt;It is a fair thing to be skeptical about — a portability promise is only as good as the tools honoring it. So here is an honest snapshot of where the major agents stood in mid-2026 on all three standards. It is not perfect across the board, but it is good enough that moving your setup is a copy-and-tweak job, not a rebuild.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Agent&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;MCP&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;AGENTS.md&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Agent Skills&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Claude Code&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Via CLAUDE.md / symlink&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes, native&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;OpenAI Codex&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes (its home format)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Cursor&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Windsurf&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Zed&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Via hosted agents&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Aider&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Limited&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Via conversion&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Cline&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Gemini CLI&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes (GEMINI.md)&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Yes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The weak spots are real and worth naming — Aider's MCP support is limited, and a few tools only run skills through their cloud agents — but the spine holds. Your tools, instructions, and skills are written in formats more than one vendor understands.&lt;/p&gt;
&lt;h2&gt;Your portable setup, layer by layer&lt;/h2&gt;
&lt;p&gt;Now let's assemble it. The trick is two tiers: one that lives &lt;em&gt;with each project&lt;/em&gt; and travels in its Git repo, and one that is &lt;em&gt;personal to you&lt;/em&gt; and follows you across every machine and every project.&lt;/p&gt;
&lt;h4&gt;Tier 1 — the project repo (commit this)&lt;/h4&gt;
&lt;p&gt;At the root of each project, keep an &lt;code&gt;AGENTS.md&lt;/code&gt; as the single source of truth, with &lt;code&gt;CLAUDE.md&lt;/code&gt; and friends symlinked to it. Add an &lt;code&gt;.mcp.json&lt;/code&gt; for the tools that project needs (databases, issue trackers), with secrets pulled from environment variables. Put team-shared skills in a folder like &lt;code&gt;.agents/skills/&lt;/code&gt;. Commit all of it. The payoff: a teammate — or you, six months later — clones the repo and the AI is instantly productive, with the same rules and the same tools, no setup call required.&lt;/p&gt;
&lt;h4&gt;Tier 2 — your personal dotfiles&lt;/h4&gt;
&lt;p&gt;Your personal taste — how you like commit messages written, your favorite skills, your global tool configs — belongs in a dotfiles repo you carry everywhere. The reliable pattern is to keep one canonical copy of each file and symlink it into the locations each tool expects, using a tiny helper so a fresh machine is set up in seconds:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;safe_symlink() {
  local src=$1 dst=$2
  [ -L "$dst" ] &amp;amp;&amp;amp; rm "$dst"
  [ -e "$dst" ] &amp;amp;&amp;amp; mv "$dst" "$dst.bak"
  mkdir -p "$(dirname "$dst")"
  ln -s "$src" "$dst"
}

safe_symlink "$DOTFILES/AGENTS.md" "$HOME/.claude/CLAUDE.md"
safe_symlink "$DOTFILES/AGENTS.md" "$HOME/.codex/AGENTS.md"
safe_symlink "$DOTFILES/skills"    "$HOME/.claude/skills"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tools like GNU Stow and chezmoi exist to manage exactly this, and chezmoi is the kinder choice if you bounce between macOS, Linux, and Windows. One Windows gotcha to save you an hour: symlinks there need Developer Mode or admin rights and &lt;code&gt;core.symlinks=true&lt;/code&gt; in Git, and mixing WSL and native-Windows symlinks does not work — pick one world and stay in it.&lt;/p&gt;
&lt;h2&gt;Advanced Claude Code moves that survive the move&lt;/h2&gt;
&lt;p&gt;Everything above keeps you portable. But within Claude Code there are sharper techniques worth knowing — and because they are built on the same standards and plain files, they travel with you too. The single mental model that explains most of them: Claude's context window fills up fast, and quality drops as it fills. Almost every good habit is really about protecting that space.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subagents&lt;/strong&gt; are separate Claude instances with their own clean context and their own narrow tool permissions. Hand a big exploration or an independent code review to a subagent, and only its summary comes back — your main conversation stays uncluttered. A favorite pattern: after a long stretch of work, spin up a fresh reviewer subagent that sees only the final diff and the requirements, with no memory of the messy path that got there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Slash commands and hooks&lt;/strong&gt; are where you encode habits. A custom command is a saved prompt you trigger by name. A hook is different and more powerful: it is a script that fires automatically at a set moment — and this is the key distinction — your instruction files only &lt;em&gt;suggest&lt;/em&gt; behavior, while a hook &lt;em&gt;guarantees&lt;/em&gt; it. Want code formatted every single time a file is written, no exceptions?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "pnpm prettier --write \"$CLAUDE_FILE_PATHS\"" }]
      }
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Plan mode&lt;/strong&gt; is the antidote to an agent that charges off in the wrong direction. Toggle it and Claude reads, analyzes, and proposes a plan &lt;em&gt;without touching your files&lt;/em&gt;. The workflow Anthropic recommends is simple and worth internalizing: explore, plan, implement, commit — in that order, with your eyes on the plan before any code is written.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Worktrees and headless mode&lt;/strong&gt; are for scale. Git worktrees let you run two to four Claude sessions in parallel on separate branches without collisions. Headless mode — the much-missed &lt;code&gt;claude -p&lt;/code&gt; flag — turns Claude into a command-line citizen you can pipe into and wire into CI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Pipe an error log straight into a headless review
cat error.log | claude -p "Find the root cause and propose a fix"

# CI-safe: cap turns and restrict tools
claude -p "Run the test suite and fix failures" \
  --max-turns 12 \
  --allowedTools "Bash(pnpm test:*)" "Edit"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And one underused habit: ask Claude to explain its reasoning as it works — add "and briefly explain each decision" to your prompt. You absorb the why as you read the diff, not just the what. For a new codebase, this single habit is worth the extra few lines of output.&lt;/p&gt;
&lt;h2&gt;If you are a vibe coder, start here&lt;/h2&gt;
&lt;p&gt;If the section above felt like a lot, this part is for you. Vibe coding — building by describing and steering rather than hand-writing every line — is a real and legitimate way to ship. But there is an honest catch worth saying plainly: the impressive demo is the easy 80%. The boring 20% — real authentication, a real database, payments, deployment, security — is where projects quietly fall apart. Security researchers scanning AI-assisted apps throughout 2025 found troubling rates of exposed credentials — API keys hardcoded in frontends, admin passwords committed to public repos — almost all of it preventable with a single review step.&lt;/p&gt;
&lt;p&gt;So here is the short, high-leverage starter kit. None of it slows you down much, and all of it keeps you out of the ditch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Treat the AI like a sharp junior engineer with tools, not a magic box. Build features in layers — get auth working, &lt;em&gt;then&lt;/em&gt; the database, &lt;em&gt;then&lt;/em&gt; payments — rather than asking for everything in one breath.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Give it a role that changes its priorities. "Act as a senior engineer who has been burned by sloppy payment code" produces noticeably more careful, defensive work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Always run an adversarial reviewer at the end — a fresh subagent that sees only the diff and is asked to find what is wrong. This one habit catches most of those exposed-secret disasters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do not launch Claude Code from your home folder — it then has reach over your SSH keys and tokens. Work inside a dedicated project folder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn on the "Learning" output style and let the tool explain itself. You will absorb the why, not just collect the what.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1732120195121-dcbe6db79197%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26h%3D720%26q%3D80" alt="Hands typing on a laptop in a cozy, plant-filled home office in warm daylight" width="1600" height="720"&gt;&lt;p&gt;&lt;em&gt;You do not need a 10-monitor battlestation to build well — you need good defaults. Photo by &lt;/em&gt;&lt;a rel="noopener noreferrer nofollow" href="https://unsplash.com/@jakubzerdzicki"&gt;&lt;em&gt;Jakub Żerdzicki&lt;/em&gt;&lt;/a&gt;&lt;em&gt; on &lt;/em&gt;&lt;a rel="noopener noreferrer nofollow" href="https://unsplash.com/"&gt;&lt;em&gt;Unsplash&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Anti-patterns to skip&lt;/h2&gt;
&lt;p&gt;A few traps catch nearly everyone. Knowing them in advance saves real time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instruction-file bloat.&lt;/strong&gt; A giant &lt;code&gt;AGENTS.md&lt;/code&gt; backfires — the important rules get lost in the noise and the agent half-ignores them. For each line, ask whether removing it would cause a mistake. If not, cut it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MCP overload.&lt;/strong&gt; Connecting fifteen servers globally floods the agent with tool options and it starts picking wrong. Add tools per project, only where they earn their place.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No way to check the work.&lt;/strong&gt; This is the big one. If you do not give the agent a test, a build, or a linter it can run, it stops when the code merely &lt;em&gt;looks&lt;/em&gt; done — and you become the error-checker. Give it a check it can run, and have it show you the passing output rather than just claiming success.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rules where guarantees belong.&lt;/strong&gt; "Always format the code" written in an instruction file is a suggestion it may drop. If it must happen, make it a hook.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keeping it all to yourself.&lt;/strong&gt; If your &lt;code&gt;.mcp.json&lt;/code&gt; and shared skills are not committed, your teammates are flying blind. Portability includes your team.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The point&lt;/h2&gt;
&lt;p&gt;The best Claude Code setup is not a clever pile of configuration locked inside one app. It is a small, portable kit — instructions in &lt;code&gt;AGENTS.md&lt;/code&gt;, tools behind MCP, know-how in Skills — written in open formats you own and can carry anywhere. Build it that way and you are not betting on which agent wins the next release cycle. You are making that question stop mattering, because whatever you reach for tomorrow, your setup is already waiting for you there.&lt;/p&gt;
&lt;p&gt;You can start in the next ten minutes. Write a short, honest &lt;code&gt;AGENTS.md&lt;/code&gt; for your current project. Symlink &lt;code&gt;CLAUDE.md&lt;/code&gt; to it. Then take the one workflow you keep re-explaining to your AI, and move it out of your head and into a &lt;code&gt;SKILL.md&lt;/code&gt;. That is the whole foundation — and it already belongs to you.&lt;/p&gt;

&lt;h3&gt;Sources &amp;amp; further reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://modelcontextprotocol.io/"&gt;Model Context Protocol — official documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://agents.md/"&gt;AGENTS.md — the open agent-instructions format&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills"&gt;Anthropic — Equipping agents for the real world with Agent Skills&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://docs.anthropic.com/en/docs/claude-code/"&gt;Claude Code — best practices for agentic coding&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://simonwillison.net/tags/claude-code/"&gt;Simon Willison — writing on Claude Code and skills&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


</description>
      <category>claudecode</category>
      <category>mcp</category>
      <category>agentsmd</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>Photography &amp; AI: a faster, smarter 2026 workflow</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Thu, 04 Jun 2026 05:00:35 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/photography-ai-a-faster-smarter-2026-workflow-24f4</link>
      <guid>https://dev.to/harshdeepsingh13/photography-ai-a-faster-smarter-2026-workflow-24f4</guid>
      <description>&lt;p&gt;Ask a working photographer where their week actually goes, and almost none of them will say “behind the camera.” The shoot is the fun part. The grind is everything after it: culling thousands of frames, matching edits across an entire gallery, color-grading video, chasing invoices, and answering the same five client emails for the hundredth time.&lt;/p&gt;
&lt;p&gt;That grind is exactly what AI is good at. Industry surveys now put AI adoption among professional photographers in the low-90s percent — it has quietly gone from novelty to default. Used well, it doesn’t replace your eye; it removes the repetitive work standing between you and the next shoot. Here’s what a modern, AI-assisted pipeline looks like, from capture to paid invoice.&lt;/p&gt;
&lt;h2&gt;The AI Photo editing Pipeline&lt;/h2&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1551232865-e0a56728e881%3Fw%3D1400%26h%3D620%26auto%3Dformat%26fit%3Dcrop" alt="AI photo culling and editing workflow for professional photographers" width="1400" height="620"&gt;&lt;p&gt;It starts before you touch a single slider. AI culling tools like &lt;strong&gt;Aftershoot&lt;/strong&gt; and &lt;strong&gt;Imagen&lt;/strong&gt; sort a 3,000-frame wedding in minutes — flagging the sharp shots, catching closed eyes, and grouping near-duplicates so you choose from a shortlist instead of the whole card.&lt;/p&gt;
&lt;p&gt;Then comes the part that actually matters: editing &lt;em&gt;in your style&lt;/em&gt;. This is where 2026 tools pull ahead of presets. A preset applies the same math to every photo; feed a “bright and airy” preset a dark reception and you get mush. Imagen’s Personal AI Profile works differently — point it at a few thousand of your previously edited images and it studies how &lt;em&gt;you&lt;/em&gt; handle exposure, white balance, and color, then applies that judgment to a fresh set.&lt;/p&gt;
&lt;p&gt;For pixel-level rescue, &lt;strong&gt;Topaz Photo AI&lt;/strong&gt; handles denoise, sharpening, and Gigapixel upscaling; &lt;strong&gt;Lightroom&lt;/strong&gt;’s AI denoise and masking isolate skies and subjects in a click; and &lt;strong&gt;Photoshop&lt;/strong&gt;’s Firefly Generative Fill removes a stray tourist or extends a cramped background without a clone-stamp marathon. The pattern is consistent: AI does the first 80%, and you spend your time on the 20% that carries your signature.&lt;/p&gt;
&lt;h2&gt;AI in the video pipeline&lt;/h2&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1778052404716-b39eef1f17c6%3Fw%3D1400%26h%3D820%26q%3D80%26auto%3Dformat%26fit%3Dcrop" alt="AI-powered video editing timeline in DaVinci Resolve" width="1400" height="820"&gt;&lt;p&gt;If you shoot hybrid, video is where AI buys back the most time, because video post is where the most time disappears. The headline shift is &lt;strong&gt;text-based editing&lt;/strong&gt;: your footage is transcribed, and you cut the video by editing the transcript — delete a sentence, and the matching frames vanish. Rough cuts that used to eat an afternoon now take minutes.&lt;/p&gt;
&lt;p&gt;Color is the other big win. &lt;strong&gt;DaVinci Resolve&lt;/strong&gt;’s Magic Mask isolates a subject for targeted grading, and neural color matching lines up clips shot on different bodies — your A-cam, a second camera, and the drone — into one consistent look. Imagen’s video tool, launched at NAB 2026, brings the same learn-your-style grading photographers already enjoy straight to the timeline.&lt;/p&gt;
&lt;p&gt;The finishing touches are increasingly automatic too: smart reframing turns a 16:9 edit into vertical 9:16 and square 4:5 cuts for social, AI leveling and noise removal clean up audio without manual EQ, and upscaling pushes older footage to 4K. The result is same-day turnarounds on work that used to take a week.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;AI for the business: clients, CRM &amp;amp; delivery&lt;/strong&gt;&lt;/h2&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1769865563021-8792ccf279e4%3Fw%3D1400%26h%3D620%26auto%3Dformat%26fit%3Dcrop" alt="Photographer using CRM software to automate client management" width="1400" height="620"&gt;&lt;p&gt;Here’s the unglamorous truth no gear review mentions: the thing most likely to sink a photography business isn’t bad photos — it’s bad admin. Inquiries that go cold, contracts that sit unsigned, invoices that slip a month. A well-run CRM is the fix, and the payoff is real: photographers consistently report clawing back the better part of a working day every week once the back office runs itself.&lt;/p&gt;
&lt;p&gt;Platforms built for this — &lt;strong&gt;HoneyBook&lt;/strong&gt;, &lt;strong&gt;Dubsado&lt;/strong&gt;, &lt;strong&gt;Studio Ninja&lt;/strong&gt;, &lt;strong&gt;Táve&lt;/strong&gt;, &lt;strong&gt;Bloom&lt;/strong&gt;, &lt;strong&gt;Sprout Studio&lt;/strong&gt; — automate the entire client journey: an inquiry triggers an instant reply, a proposal, a contract, and a payment schedule, with reminders firing on their own. The AI layer goes further. Sprout Studio drafts your emails and questionnaires, HoneyBook plugs into post-production tools so editing and client management finally talk to each other, and gallery platforms now use face recognition so guests find and buy their own photos without you lifting a finger.&lt;/p&gt;
&lt;p&gt;Speed is the quiet advantage. The studio that answers an inquiry in five minutes books the client that the studio answering in five hours was still drafting a reply to. AI simply makes those five minutes happen while you’re on a shoot.&lt;/p&gt;
&lt;blockquote&gt;&lt;h2&gt;AI hand&lt;em&gt;les the first 80%. Your taste is the last 20% — an&lt;/em&gt;&lt;strong&gt;d that’s the only part a client is really paying for.&lt;/strong&gt;
&lt;/h2&gt;&lt;/blockquote&gt;
&lt;h3&gt;Tools mentioned&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;Aftershoot&lt;/span&gt;&lt;span&gt;Imagen&lt;/span&gt;&lt;span&gt;Topaz Photo AI&lt;/span&gt;&lt;span&gt;Lightroom&lt;/span&gt;&lt;span&gt;Photoshop&lt;/span&gt;&lt;span&gt;Firefly&lt;/span&gt;&lt;span&gt;DaVinci Resolve&lt;/span&gt;&lt;span&gt;CapCut&lt;/span&gt;&lt;span&gt;HoneyBook&lt;/span&gt;&lt;span&gt;Dubsado&lt;/span&gt;&lt;span&gt;Studio Ninja&lt;/span&gt;&lt;span&gt;Sprout Studio&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;Where the Human still wins&lt;/h2&gt;
&lt;p&gt;None of this is about handing your work to a machine. AI is leverage, not authorship. It can match your edit, but it can’t decide what’s worth photographing, read a nervous couple on a wedding morning, or build the trust that turns a one-off booking into a decade of referrals. That judgment is the moat — and it’s getting more valuable, not less.&lt;/p&gt;
&lt;p&gt;So don’t try to automate everything at once. Find your single biggest bottleneck — for most photographers it’s culling or the CRM — and hand that one to AI first. Win back those hours, then reinvest them where they compound: better shoots, sharper craft, and a client experience no software will ever replicate.&lt;/p&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The shoot was never the bottleneck.&lt;/strong&gt; AI’s real value is post-shoot — it clears the repetitive work, not your creative judgment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Photo editing:&lt;/strong&gt; AI culls thousands of frames and edits in &lt;em&gt;your&lt;/em&gt; learned style (Imagen, Aftershoot), while Topaz, Lightroom, and Firefly handle pixel-level fixes. You keep the final 20%.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Video:&lt;/strong&gt; text-based editing, neural color-matching across cameras, and auto reframe + audio cleanup turn week-long edits into same-day deliveries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Business:&lt;/strong&gt; a CRM plus AI automations (HoneyBook, Dubsado, Sprout Studio) save roughly a day a week — and a five-minute inquiry reply wins the booking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start small:&lt;/strong&gt; automate one bottleneck (culling or your CRM) first, then reinvest the hours into better work.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


</description>
      <category>aiphotoediting</category>
      <category>photographyworkflow</category>
      <category>aiforphotographers</category>
      <category>photocullingsoftware</category>
    </item>
    <item>
      <title>Full Stack Developer Portfolio Lessons: What I Learned Building 10+ Projects</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Tue, 02 Jun 2026 21:33:43 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/full-stack-developer-portfolio-lessons-what-i-learned-building-10-projects-2bdi</link>
      <guid>https://dev.to/harshdeepsingh13/full-stack-developer-portfolio-lessons-what-i-learned-building-10-projects-2bdi</guid>
      <description>&lt;p&gt;I applied for a role at a mid-sized SaaS company about two years into my career. Strong company, interesting problem, good pay. I sent my application, got a recruiter callback, and then nothing for two weeks. When the feedback finally came: "We went with candidates with a stronger portfolio presence."&lt;/p&gt;

&lt;p&gt;I had 23 GitHub repositories. I had a portfolio site. I had projects. What I didn't have — and what I didn't understand for another six months — was a portfolio that told a story. I had code. Not evidence of thinking, decision-making, or the ability to ship something real.&lt;/p&gt;

&lt;p&gt;I've since built, rebuilt, and advised on a lot of developer portfolios. I've seen what gets people calls and what gets them ghosted. This isn't a guide about which framework to use or how to pick colors. It's about what actually moves the needle — the things I wish someone had told me in year one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: Two Great Projects Beat Twenty Mediocre Ones
&lt;/h2&gt;

&lt;p&gt;The instinct is to fill the portfolio. More projects = more evidence of experience. This is wrong.&lt;/p&gt;

&lt;p&gt;A hiring manager or engineering lead looking at your portfolio has about three minutes. They're going to look at your two or three most prominent projects, click one or two live demo links, and form an opinion. If they see twenty repositories and most of them are "Todo App v2," "Weather App," "Netflix Clone," "Portfolio v1 through v6" — they've already categorized you as someone who builds tutorials, not someone who builds things.&lt;/p&gt;

&lt;p&gt;The better approach: three to five projects, each with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real problem it solves (not "I wanted to learn React")&lt;/li&gt;
&lt;li&gt;A live deployment that actually works&lt;/li&gt;
&lt;li&gt;A README that explains why you made the decisions you made&lt;/li&gt;
&lt;li&gt;Enough complexity to have generated at least one interesting engineering problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Projects that tend to work: tools you built because you were frustrated with an existing tool, apps solving problems you personally had, projects where you integrated with a real API or real data source, anything with a live user base (even 10 users counts).&lt;/p&gt;

&lt;p&gt;Projects that tend not to work: tutorial clones (unless heavily modified), apps that only run locally, projects that stop at the MVP and never got deployed, apps with the same name as thousands of other developer portfolios ("My Todo App," "My Weather App").&lt;/p&gt;

&lt;p&gt;If you have 20 repos, that's fine. Pin your three best to your GitHub profile. Don't make people wade through everything — curate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: Case Studies Beat Code Screenshots
&lt;/h2&gt;

&lt;p&gt;Here's the thing about showing a screenshot of your app: everyone can make an app look good in a screenshot. Filters, cropping, ideal state data. A screenshot shows what you built. It tells me nothing about how you think.&lt;/p&gt;

&lt;p&gt;A case study shows how you think. And how you think is what you're being hired for.&lt;/p&gt;

&lt;p&gt;A case study doesn't have to be a five-page document. Two or three paragraphs on each project covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem.&lt;/strong&gt; What did you set out to solve? Be specific. Not "I wanted to learn Next.js" — that's not a problem. "Resume submissions were getting lost in email threads, so I built a tool that…" — that's a problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your approach and the tradeoffs you considered.&lt;/strong&gt; What did you think about? What did you try first? What didn't work? This is where you demonstrate that you can make technical decisions, not just execute instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What you shipped.&lt;/strong&gt; Not every feature you imagined — what you actually built and deployed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What you'd do differently.&lt;/strong&gt; This one is disarming in the best way. It shows self-awareness, reflection, and the ability to evaluate your own work critically. Engineers who can't critique their own code can't grow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've seen portfolios with two projects and a well-written case study for each that outperformed portfolios with fifteen projects and no context. The case study gives an interviewer something to ask about. It shows you've thought deeply about the work. It makes the technical interview easier because you already answered half the questions in writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: If It's Not Deployed, It Doesn't Exist
&lt;/h2&gt;

&lt;p&gt;This is the blunt version. A project that runs on localhost is a project you're still working on. It is not a portfolio piece.&lt;/p&gt;

&lt;p&gt;I've reviewed portfolios where the "live demo" link was a localhost URL. I've seen GitHub repositories where the README says "deployment in progress" with a date from 18 months ago. I've seen apps in screenshots that couldn't actually run because they depended on a local database with no seed data.&lt;/p&gt;

&lt;p&gt;Deploying has never been easier or cheaper. There's no excuse for a portfolio project that isn't live.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Vercel (free), Netlify (free), Cloudflare Pages (free). Zero configuration for most frameworks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend / API:&lt;/strong&gt; Railway (free tier), Render (free tier), Fly.io (free tier). These all support Node.js, Python, Go, whatever you're running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; MongoDB Atlas free tier (512MB), Supabase free tier (PostgreSQL), PlanetScale free tier (MySQL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full stack:&lt;/strong&gt; Railway handles full-stack apps well. Render lets you deploy multiple services from one repo. Both have one-click GitHub deploys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total cost of a deployed side project: $0, with a free domain subdomain. Add a custom domain for $12/year and you have a genuinely professional-looking production deployment.&lt;/p&gt;

&lt;p&gt;There's a secondary benefit to deploying: it forces you to actually finish things. There's a long list of problems you don't know about until you deploy — environment variable management, CORS configuration, database connection pooling, static asset serving. Deploying is part of building. A portfolio project that's never been deployed has never been truly finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: The README Is Your First Interview
&lt;/h2&gt;

&lt;p&gt;When a hiring manager or senior engineer clicks the GitHub link from your portfolio, the first thing they see is the README. If it says "A project I made for learning" or has no description at all, they've already lost interest.&lt;/p&gt;

&lt;p&gt;The README is where you make the technical case for yourself before you're in the room. Here's what a good one contains:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First paragraph:&lt;/strong&gt; What does this thing do and why does it exist? Not "this is a web app" — tell me the specific problem it solves. One or two sentences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech stack and why:&lt;/strong&gt; Not just a logo grid. A sentence about why you chose what you chose. "Used PostgreSQL instead of MongoDB because the data has strong relational structure with lots of joins." "Chose Next.js App Router over CRA because we needed SSR for SEO and a built-in API layer." These sentences prove you made intentional decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshots or a GIF:&lt;/strong&gt; A 10-second screen recording of the app working is worth a thousand words. Not staged, not filtered — just the actual app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to run it:&lt;/strong&gt; Clear, complete instructions. If I clone it and follow your README and it doesn't work, that's a flag. If it works first try, that's a positive signal — it means you document carefully and you care about the developer experience of your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known limitations / what you'd do differently:&lt;/strong&gt; One paragraph. Shows maturity. "If I built this again, I'd use a message queue for the email sending instead of doing it synchronously in the request lifecycle — it caused timeouts under load."&lt;/p&gt;

&lt;p&gt;This README takes maybe 45 minutes to write. It dramatically changes how your project is perceived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 5: Get Your Own Domain
&lt;/h2&gt;

&lt;p&gt;This one is simple and often skipped. Your portfolio should live at &lt;code&gt;yourname.com&lt;/code&gt; or &lt;code&gt;yourname.dev&lt;/code&gt; — not &lt;code&gt;github.io/yourname/portfolio&lt;/code&gt; or &lt;code&gt;yourname.netlify.app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A custom domain does two things: it signals that you take yourself seriously as a professional, and it's a much better URL to put on a resume, LinkedIn, or business card. "theharshdeepsingh.com" looks intentional. "harshdeep-singh-13.github.io/portfolio-2024" looks like a homework assignment.&lt;/p&gt;

&lt;p&gt;Domains cost $10–15 per year. That is a rounding error in any budget. Buy yours today. Redirect your GitHub Pages / Vercel / Netlify deployment to it. It takes 30 minutes and it never needs to change — you own it.&lt;/p&gt;

&lt;p&gt;A note on choosing the domain: use your name. Not your "developer brand" or a clever handle. Names rank in Google. If someone searches for you, they should find your portfolio at the top. A personal domain with your name is one of the easiest SEO wins available to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 6: One AI Integration Changes Everything
&lt;/h2&gt;

&lt;p&gt;Here's the hiring landscape in 2025 from a practical perspective: companies want developers who can work with AI, build on top of AI APIs, and integrate AI capabilities into existing products. This is new enough that not everyone has done it. Old enough that "I'm planning to learn it" isn't a compelling answer.&lt;/p&gt;

&lt;p&gt;One project with a real AI integration moves you from the pile to the shortlist. Not because AI is a magic word — but because it demonstrates technical currency. You know what the OpenAI API looks like. You've dealt with token limits and streaming and prompt engineering. You've thought about cost and abuse prevention. These are all non-trivial.&lt;/p&gt;

&lt;p&gt;What counts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A feature in an existing project that uses GPT-4o, Claude, or Gemini for a specific, meaningful task (not "ask AI anything" — that's too vague to be impressive)&lt;/li&gt;
&lt;li&gt;A RAG (retrieval-augmented generation) pipeline — document upload, embedding, search, answer generation&lt;/li&gt;
&lt;li&gt;An agent that takes structured actions based on LLM output (web search, database queries, API calls)&lt;/li&gt;
&lt;li&gt;A classification or extraction feature that uses an LLM where a simpler approach wouldn't have worked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What doesn't count:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Used ChatGPT to help me write this code" (everyone does this)&lt;/li&gt;
&lt;li&gt;A UI wrapper around ChatGPT that just passes prompts through (no engineering decision was made)&lt;/li&gt;
&lt;li&gt;A project that uses AI for something that a regex would handle just as well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bar isn't high. Ship one genuinely useful AI feature, document the decisions you made (model selection, prompt design, cost management), and you're ahead of the majority of developers applying for the same roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch: Portfolio Reviews — What Actually Works
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Weak vs. Strong Portfolio Signals
&lt;/h2&gt;

&lt;p&gt;Signal&lt;br&gt;
Weak&lt;br&gt;
Strong&lt;/p&gt;

&lt;p&gt;Project count&lt;br&gt;
20+ repos, half are tutorial clones&lt;br&gt;
3–5 curated projects, each with a clear purpose&lt;/p&gt;

&lt;p&gt;Project quality&lt;br&gt;
Todo apps, weather apps, Netflix/Airbnb clones&lt;br&gt;
Tools solving real problems, deployed with real users&lt;/p&gt;

&lt;p&gt;Live demos&lt;br&gt;
No live link, "works on my machine," localhost screenshots&lt;br&gt;
Deployed URLs that load in under 3 seconds&lt;/p&gt;

&lt;p&gt;Documentation&lt;br&gt;
No README or "this is a project I made"&lt;br&gt;
Problem statement, tech choices explained, known limitations&lt;/p&gt;

&lt;p&gt;Tech recency&lt;br&gt;
Create React App, class components, outdated dependencies&lt;br&gt;
Current stack (Next.js 15, TypeScript, modern APIs)&lt;/p&gt;

&lt;p&gt;AI integration&lt;br&gt;
None, or "used AI to help me code"&lt;br&gt;
One genuine AI feature with documented engineering decisions&lt;/p&gt;

&lt;p&gt;Domain&lt;br&gt;
github.io/username or platform subdomain&lt;br&gt;
yourname.com — personal, memorable, professional&lt;/p&gt;

&lt;p&gt;Case studies&lt;br&gt;
Screenshots in a grid with a "View Project" button&lt;br&gt;
Problem → approach → tradeoffs → outcome — per project&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do on Day 1 If I Were Starting Over
&lt;/h2&gt;

&lt;p&gt;If I were a developer today with no portfolio and a job to find, here's the exact sequence I'd follow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 1:&lt;/strong&gt; Register &lt;code&gt;firstnamelastname.com&lt;/code&gt;. It costs $12. Do it before you build anything. Having the domain makes the whole thing feel real and gives you a deadline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1:&lt;/strong&gt; Identify one problem I genuinely have — something I do manually that should be automated, something I've searched for that doesn't exist, something at work that annoys me. Build the simplest version of the solution. Not a full product — a working tool. One feature, deployed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2:&lt;/strong&gt; Write the case study. What was the problem? What did I consider? What did I ship? What didn't make the cut? What would I do differently? Two or three paragraphs per question. This is more valuable than the code itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3:&lt;/strong&gt; Add an AI integration to the project — something that actually makes the tool better, not a bolt-on. Even a single endpoint that uses an LLM for classification or text generation counts, as long as it's doing something a simpler approach couldn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 4:&lt;/strong&gt; Point the domain at the project. Add the URL to LinkedIn's "Featured" section and your resume. Ask one person who is not a developer to try using the tool and tell you what they're confused by. Fix those things.&lt;/p&gt;

&lt;p&gt;That's it. One good project, one good case study, one AI integration, a custom domain, a LinkedIn presence. Four weeks. That's a portfolio that gets callbacks. Everything else is refinement.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Curate, don't accumulate.&lt;/strong&gt; Three deployed projects with case studies beat twenty unfinished repos. Pin your best work. Hide the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write case studies for every project.&lt;/strong&gt; Problem → approach → tradeoffs → outcome → what you'd do differently. This is what interviews are about anyway — you're just answering in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing without a live URL.&lt;/strong&gt; Free tiers on Vercel, Railway, and Render make deployment trivial. An undeployed project is an unfinished project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The README is your first impression from GitHub.&lt;/strong&gt; Spend 45 minutes on it. Explain the why, not just the what. Include how to run it. List the limitations. That's a professional developer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get the domain.&lt;/strong&gt; yourname.com, $12, 30 minutes. It signals intent and makes your portfolio findable in Google searches by your own name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build one thing with a real AI integration.&lt;/strong&gt; Not a ChatGPT wrapper — a feature that uses an LLM to solve a specific problem in your project, with documented decisions about model selection and prompt design.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>portfolio</category>
      <category>career</category>
      <category>fullstack</category>
      <category>jobsearch</category>
    </item>
    <item>
      <title>How to Integrate the OpenAI API into a Production Express App</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Tue, 02 Jun 2026 21:28:00 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/how-to-integrate-the-openai-api-into-a-production-express-app-2mff</link>
      <guid>https://dev.to/harshdeepsingh13/how-to-integrate-the-openai-api-into-a-production-express-app-2mff</guid>
      <description>&lt;p&gt;Last year I helped a startup integrate the OpenAI API into their product. It was a chat feature — users could ask questions about their data and get natural language answers. The integration took about a day. Three days after launch, the founder messaged me: "Hey, something's wrong. Our AWS bill just showed an unexpected charge."&lt;/p&gt;

&lt;p&gt;It was $340. For three days. They had 60 users.&lt;/p&gt;

&lt;p&gt;The issue wasn't a bug — it was that production API usage looks nothing like a tutorial. The tutorial shows you &lt;code&gt;openai.chat.completions.create()&lt;/code&gt; and returns a response. The tutorial doesn't show you what happens when users send 500-token messages, when they open 15 browser tabs each maintaining their own chat context, or when one user fires requests 30 times per minute because they think it's broken.&lt;/p&gt;

&lt;p&gt;This guide covers what the tutorials skip: rate limiting, token counting, cost guards, streaming, error handling with retries, and model selection. These aren't optional additions — they're what separates a demo from a production feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Production Is Different
&lt;/h2&gt;

&lt;p&gt;Here's the gap between tutorial code and production code, stated plainly:&lt;/p&gt;

&lt;p&gt;Concern&lt;br&gt;
Tutorial Code&lt;br&gt;
Production Code&lt;/p&gt;

&lt;p&gt;Cost control&lt;br&gt;
Not mentioned&lt;br&gt;
Token counting, spending limits, model selection by task&lt;/p&gt;

&lt;p&gt;Rate limiting&lt;br&gt;
Not mentioned&lt;br&gt;
Per-user and per-IP limits to prevent abuse&lt;/p&gt;

&lt;p&gt;Error handling&lt;br&gt;
try/catch that logs to console&lt;br&gt;
Typed errors, retries with backoff, user-facing messages&lt;/p&gt;

&lt;p&gt;Response delivery&lt;br&gt;
Wait for full completion, return at once&lt;br&gt;
Streaming via SSE — response appears as it generates&lt;/p&gt;

&lt;p&gt;Context management&lt;br&gt;
Each request is independent&lt;br&gt;
Conversation history managed, truncated at token limit&lt;/p&gt;

&lt;p&gt;Secrets management&lt;br&gt;
API key hardcoded or in &lt;code&gt;.env&lt;/code&gt; (no rotation)&lt;br&gt;
Rotation strategy, usage monitoring, per-feature keys&lt;/p&gt;

&lt;p&gt;Let's build a production-grade Express API that addresses all of this. We'll go layer by layer.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                  CLIENT (Browser / Mobile)               │&lt;br&gt;
│          POST /api/chat  { messages: [...] }             │&lt;br&gt;
│          GET  /api/chat/stream (SSE)                     │&lt;br&gt;
└──────────────────────┬──────────────────────────────────┘&lt;br&gt;
                       │&lt;br&gt;
                       ▼&lt;br&gt;
┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                   EXPRESS MIDDLEWARE STACK               │&lt;br&gt;
│                                                          │&lt;br&gt;
│  1. express-rate-limit  (10 req/min per IP)             │&lt;br&gt;
│  2. tokenGuard()        (reject if &amp;gt; 4,000 tokens)      │&lt;br&gt;
│  3. auth middleware     (verify user session)            │&lt;br&gt;
└──────────────────────┬──────────────────────────────────┘&lt;br&gt;
                       │&lt;br&gt;
                       ▼&lt;br&gt;
┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                   ROUTE HANDLER                          │&lt;br&gt;
│                                                          │&lt;br&gt;
│  Select model by task type                               │&lt;br&gt;
│  Build messages array from context                       │&lt;br&gt;
│  Call openai.chat.completions.create()                   │&lt;br&gt;
│  Stream or return response                               │&lt;br&gt;
└──────────────────────┬──────────────────────────────────┘&lt;br&gt;
                       │&lt;br&gt;
                       ▼&lt;br&gt;
┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                   OPENAI API                             │&lt;br&gt;
│  Model: gpt-4o-mini (default) / gpt-4o (complex tasks)  │&lt;br&gt;
└─────────────────────────────────────────────────────────┘&lt;/p&gt;
&lt;h2&gt;
  
  
  Project Setup
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;express-openai &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;express-openai
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express openai express-rate-limit tiktoken dotenv
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; nodemon

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

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-proj-your-key-here
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3001

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

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1: The OpenAI Client (Configured for Production)
&lt;/h2&gt;

&lt;p&gt;Don't instantiate the OpenAI client inside route handlers. Create it once, configure it for production, and export it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/openaiClient.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// retry on transient failures (rate limits, timeouts)&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// 30 second timeout — don't hang forever&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Model selection by task complexity&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// classification, simple Q&amp;amp;A, summarization&lt;/span&gt;
  &lt;span class="na"&gt;smart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// complex reasoning, code generation, analysis&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;maxRetries: 3&lt;/code&gt; and &lt;code&gt;timeout&lt;/code&gt; settings are critical. Without a timeout, a hung OpenAI request will keep your Express server's response object open indefinitely — and if you're running on a serverless function, you'll pay for that idle time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Token Counting and Cost Guard
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;tiktoken&lt;/code&gt; library is OpenAI's own tokenizer — it counts tokens the exact same way the API does. Use it to reject requests before they hit the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/tokenCounter.js&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;encoding_for_model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tiktoken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;countMessageTokens&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="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-mini&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encoding_for_model&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;of&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="nx"&gt;totalTokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// every message has ~4 tokens of overhead&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;totalTokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;totalTokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;totalTokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// reply primer&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;free&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// tiktoken requires explicit cleanup&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;totalTokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// overall reply overhead&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Express middleware — rejects requests over the token limit&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tokenGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxInputTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;_000&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messages must be an array&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;countMessageTokens&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxInputTokens&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Message too long: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tokens (limit: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maxInputTokens&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;). Shorten your message or clear the conversation.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;tokenCount&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;maxInputTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokenCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// pass downstream for logging&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;A note on the limit: GPT-4o-mini's context window is 128K tokens, so 4,000 is conservative. But conservative is good here — a user who sends 30,000 tokens in one request is either doing something unusual or has a bug in their client. Reject it, log it, and let them know to clear their context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Rate Limiting
&lt;/h2&gt;

&lt;p&gt;One user shouldn't be able to drain your API budget or trigger OpenAI rate limits for everyone else. Add rate limiting before the AI routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/middleware/rateLimiter.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rateLimit&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express-rate-limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiRateLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// 1-minute window&lt;/span&gt;
  &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// 15 requests per minute per IP&lt;/span&gt;
  &lt;span class="na"&gt;standardHeaders&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;// return RateLimit headers&lt;/span&gt;
  &lt;span class="na"&gt;legacyHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too many requests. Please wait a moment before trying again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;keyGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Use authenticated user ID if available, otherwise fall back to IP&lt;/span&gt;
    &lt;span class="k"&gt;return&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;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&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;// Stricter limit for expensive models&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;smartModelLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too many complex requests. Rate limited for 60 seconds.&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;
  
  
  Step 4: Error Handling with Typed OpenAI Errors
&lt;/h2&gt;

&lt;p&gt;The OpenAI Node SDK throws typed errors. Use them — don't just check &lt;code&gt;err.message&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/middleware/openaiErrorHandler.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleOpenAIError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OpenAI API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AI service is busy. Please try again in a moment.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry-after&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;400&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid request to AI service. Check your message format.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OpenAI authentication failed — check OPENAI_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AI service unavailable.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Not an OpenAI error — pass to your generic error handler&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: The Chat Endpoint (Non-Streaming)
&lt;/h2&gt;

&lt;p&gt;Let's wire everything together for a standard, non-streaming response first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/routes/chat.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../openaiClient.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tokenGuard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../tokenCounter.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;aiRateLimiter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../middleware/rateLimiter.js&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;aiRateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;tokenGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;_000&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="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="nx"&gt;useSmartModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSmartModel&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smart&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;completion&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// cap output tokens to control cost&lt;/span&gt;
        &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&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;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&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;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;inputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completion_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;totalTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;estimatedCostUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;estimateCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usage&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="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimateCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usage&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Prices per million tokens (as of mid-2025)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pricing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-mini&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;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o&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;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;5.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.00&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pricing&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="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-mini&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;inputCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&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;outputCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completion_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;inputCost&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;outputCost&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;max_tokens: 1_000&lt;/code&gt;. Without this, GPT-4o can produce 4,096 output tokens per request. If a user asks it to "write me a book," it will try. The &lt;code&gt;max_tokens&lt;/code&gt; cap is your backstop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Streaming Responses with Server-Sent Events
&lt;/h2&gt;

&lt;p&gt;Streaming makes AI features feel responsive. Instead of a blank screen for 3–8 seconds, the user sees text appear word by word. It's the difference between "this feels AI-powered" and "this is broken."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/routes/chat-stream.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../openaiClient.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tokenGuard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../tokenCounter.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;aiRateLimiter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../middleware/rateLimiter.js&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;aiRateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;tokenGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;_000&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Establish SSE connection&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Connection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flushHeaders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// send headers immediately&lt;/span&gt;

    &lt;span class="k"&gt;try&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;stream&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fast&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="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalOutputTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&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;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;totalOutputTokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// approximate; tiktoken is more accurate&lt;/span&gt;
          &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&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;// Check for stop reason&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;finish_reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;length&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Response truncated at token limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;

`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;

`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Send error over SSE before closing&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Generation failed. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;

`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="c1"&gt;// Also pass to error handler for logging&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Streaming error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Watch: OpenAI API with Node.js + Express
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Streaming vs. Non-Streaming — When to Use Which
&lt;/h2&gt;

&lt;p&gt;Factor&lt;br&gt;
Non-Streaming&lt;br&gt;
Streaming (SSE)&lt;/p&gt;

&lt;p&gt;User experience&lt;br&gt;
Blank screen until done (3–8s)&lt;br&gt;
Text appears word by word — feels instant&lt;/p&gt;

&lt;p&gt;Complexity&lt;br&gt;
Standard REST response&lt;br&gt;
SSE connection, chunked parsing on frontend&lt;/p&gt;

&lt;p&gt;Usage logging&lt;br&gt;
Easy — &lt;code&gt;completion.usage&lt;/code&gt; has exact token counts&lt;br&gt;
Harder — token counts only available via the final chunk&lt;/p&gt;

&lt;p&gt;Caching&lt;br&gt;
Can cache the full response&lt;br&gt;
Can't cache a stream&lt;/p&gt;

&lt;p&gt;Best for&lt;br&gt;
API-to-API calls, short responses, classification tasks&lt;br&gt;
User-facing chat, long completions, code generation&lt;/p&gt;

&lt;p&gt;Serverless functions&lt;br&gt;
Works everywhere&lt;br&gt;
Needs long-running connection — use Vercel Edge Functions or a real server&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your OpenAI Integration
&lt;/h2&gt;

&lt;p&gt;Mocking the OpenAI API in tests is a trap. The mock will pass but the real integration will fail in ways you didn't anticipate — different error formats, unexpected token usage, streaming chunk structure variations.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit test everything except the API call.&lt;/strong&gt; Test your token counting, your error handler, your response formatter — all without touching OpenAI. These functions should be pure and deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a cheap model for integration tests.&lt;/strong&gt; &lt;code&gt;gpt-4o-mini&lt;/code&gt; is $0.15 per million input tokens. Your integration test suite probably costs fractions of a cent to run. Run it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record and replay for expensive tests.&lt;/strong&gt; Libraries like &lt;code&gt;nock&lt;/code&gt; or VCR-style recording let you record real API responses and replay them in future test runs without hitting the API.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: testing the token guard middleware in isolation&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;tokenGuard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/tokenCounter.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMockMiddlewareContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./helpers.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tokenGuard rejects messages over the limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="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;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokenGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// tiny limit for test&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMockMiddlewareContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;guard&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;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;too long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&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;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Initialize the OpenAI client once&lt;/strong&gt; with &lt;code&gt;maxRetries&lt;/code&gt; and &lt;code&gt;timeout&lt;/code&gt; set. Don't instantiate it in route handlers or you'll get a new client per request with no retry or timeout configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count tokens before you call the API.&lt;/strong&gt; Use &lt;code&gt;tiktoken&lt;/code&gt; to measure input size and reject oversized requests before they cost you money. Set a &lt;code&gt;max_tokens&lt;/code&gt; cap on output for the same reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit by user ID, not just IP.&lt;/strong&gt; Authenticated users with the same IP (corporate NAT, mobile networks) would all share a single IP limit — use their user ID as the rate limit key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use typed error handling&lt;/strong&gt; — &lt;code&gt;instanceof OpenAI.APIError&lt;/code&gt; gives you the status code, request ID, and message. Turn 429s into user-friendly retry prompts, not 500 errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream for user-facing features, skip it for internal calls.&lt;/strong&gt; SSE streaming transforms the UX for chat interfaces. For batch processing or API-to-API calls, non-streaming is simpler to implement and log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test everything except the API call.&lt;/strong&gt; Token counting, error handling, and response formatting are all pure functions you can test cheaply. For integration tests, use &lt;code&gt;gpt-4o-mini&lt;/code&gt; — it's cheap enough to run in CI.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>openai</category>
      <category>express</category>
      <category>node</category>
      <category>ai</category>
    </item>
    <item>
      <title>Deploying a Next.js App to AWS with CI/CD Pipelines (Step-by-Step)</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Tue, 02 Jun 2026 21:27:52 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/deploying-a-nextjs-app-to-aws-with-cicd-pipelines-step-by-step-2j76</link>
      <guid>https://dev.to/harshdeepsingh13/deploying-a-nextjs-app-to-aws-with-cicd-pipelines-step-by-step-2j76</guid>
      <description>&lt;p&gt;The first time I deployed a Next.js app to production, it took me three days. Not because the app was complicated — it was a straightforward portfolio site. It took three days because I had no idea what I was doing with AWS, I'd never written a GitHub Actions workflow, and every tutorial I found either skipped the hard parts or assumed I already knew them.&lt;/p&gt;

&lt;p&gt;By the time I was done, I had a deployment pipeline I was genuinely proud of: push to main, GitHub Actions runs the build, tests pass, the app deploys to an EC2 instance behind CloudFront. Zero manual steps. Zero downtime deploys. Total cost: about $5/month.&lt;/p&gt;

&lt;p&gt;This guide is the one I wish had existed. We're going to deploy a Next.js app to AWS from scratch — EC2 for compute, CloudFront for CDN, GitHub Actions for CI/CD — with every step explained so you understand what you're building, not just copying commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AWS Instead of Vercel?
&lt;/h2&gt;

&lt;p&gt;This is a fair question. Vercel is genuinely excellent for Next.js, and for most projects it's the right call. You push, it deploys. Done.&lt;/p&gt;

&lt;p&gt;AWS makes sense when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to control the infrastructure (compliance, data residency, custom VPC configuration)&lt;/li&gt;
&lt;li&gt;You're running other services (databases, queues, lambdas) in AWS and want everything in the same network&lt;/li&gt;
&lt;li&gt;You want to learn infrastructure skills that transfer to enterprise environments&lt;/li&gt;
&lt;li&gt;Your app has specific performance requirements that benefit from custom CloudFront configuration&lt;/li&gt;
&lt;li&gt;You're a freelancer or consultant who wants to bill separately for infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If none of those apply to you, use Vercel. This guide is for when they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Here's what we're building:&lt;/p&gt;

&lt;p&gt;┌────────────────────────────────────────────────────────┐&lt;br&gt;
│                   GITHUB ACTIONS CI/CD                  │&lt;br&gt;
│                                                          │&lt;br&gt;
│  Push to main → Build → Test → Deploy to EC2           │&lt;br&gt;
└──────────────────────┬─────────────────────────────────┘&lt;br&gt;
                       │ SSH deploy&lt;br&gt;
                       ▼&lt;br&gt;
┌────────────────────────────────────────────────────────┐&lt;br&gt;
│                    AWS EC2 INSTANCE                      │&lt;br&gt;
│                                                          │&lt;br&gt;
│  Ubuntu 22.04 LTS                                        │&lt;br&gt;
│  Node.js 20 + PM2 (process manager)                     │&lt;br&gt;
│  Next.js app running on port 3000                        │&lt;br&gt;
│  Nginx reverse proxy (port 80/443 → 3000)               │&lt;br&gt;
└──────────────────────┬─────────────────────────────────┘&lt;br&gt;
                       │ Origin&lt;br&gt;
                       ▼&lt;br&gt;
┌────────────────────────────────────────────────────────┐&lt;br&gt;
│                  CLOUDFRONT CDN                          │&lt;br&gt;
│                                                          │&lt;br&gt;
│  Static assets cached at edge (/_next/static/*)         │&lt;br&gt;
│  TTL: 1 year for static, 0 for HTML                    │&lt;br&gt;
│  SSL termination via ACM certificate                     │&lt;br&gt;
│  Custom domain: yourapp.com                             │&lt;br&gt;
└────────────────────────────────────────────────────────┘&lt;/p&gt;

&lt;p&gt;This isn't the only way to run Next.js on AWS. You could use Elastic Beanstalk, App Runner, ECS, or deploy static exports to S3 + CloudFront. The EC2 + CloudFront approach gives you the most control and transfers the most skills to enterprise environments.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account (free tier works for learning; a t3.micro is enough for small apps)&lt;/li&gt;
&lt;li&gt;A domain name (optional but recommended — we'll set up SSL)&lt;/li&gt;
&lt;li&gt;A GitHub repository with your Next.js app&lt;/li&gt;
&lt;li&gt;Basic familiarity with the AWS console&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The total setup takes about 90 minutes the first time. After that, every deployment is automatic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Set Up the EC2 Instance
&lt;/h2&gt;

&lt;p&gt;In the AWS Console, navigate to EC2 and launch a new instance. The settings that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AMI:&lt;/strong&gt; Ubuntu Server 22.04 LTS (free tier eligible)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instance type:&lt;/strong&gt; t3.micro (1 vCPU, 1GB RAM) for small apps; t3.small for medium traffic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key pair:&lt;/strong&gt; Create a new one, download it — you'll need this for SSH and GitHub Actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security group:&lt;/strong&gt; Allow inbound traffic on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). Add your IP as the only source for port 22 (don't expose SSH to 0.0.0.0/0).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the instance is running, SSH in and set up the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Connect to your instance&lt;/span&gt;
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; your-key.pem ubuntu@YOUR_EC2_PUBLIC_IP

&lt;span class="c"&gt;# Update system packages&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# Install Node.js 20 via NodeSource&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_20.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs

&lt;span class="c"&gt;# Install PM2 globally (process manager for Node.js)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pm2

&lt;span class="c"&gt;# Install Nginx&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx

&lt;span class="c"&gt;# Verify everything installed correctly&lt;/span&gt;
node &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# v20.x.x&lt;/span&gt;
npm &lt;span class="nt"&gt;--version&lt;/span&gt;    &lt;span class="c"&gt;# 10.x.x&lt;/span&gt;
pm2 &lt;span class="nt"&gt;--version&lt;/span&gt;    &lt;span class="c"&gt;# 5.x.x&lt;/span&gt;
nginx &lt;span class="nt"&gt;-v&lt;/span&gt;         &lt;span class="c"&gt;# nginx/1.24.x&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Configure Nginx as a Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;Nginx will listen on port 80 and forward requests to your Next.js app on port 3000. This is the standard setup for Node.js apps on Linux servers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/nextjs-app

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

&lt;/div&gt;



&lt;p&gt;Paste this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;server &lt;span class="o"&gt;{&lt;/span&gt;
    listen 80&lt;span class="p"&gt;;&lt;/span&gt;
    server_name YOUR_DOMAIN.com www.YOUR_DOMAIN.com&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c"&gt;# Proxy requests to Next.js&lt;/span&gt;
    location / &lt;span class="o"&gt;{&lt;/span&gt;
        proxy_pass http://localhost:3000&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_http_version 1.1&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header Upgrade &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header Connection &lt;span class="s1"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header Host &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header X-Real-IP &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header X-Forwarded-For &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header X-Forwarded-Proto &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_cache_bypass &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;# Cache Next.js static assets at Nginx level too&lt;/span&gt;
    location /_next/static/ &lt;span class="o"&gt;{&lt;/span&gt;
        proxy_pass http://localhost:3000&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_cache_valid 200 1y&lt;span class="p"&gt;;&lt;/span&gt;
        add_header Cache-Control &lt;span class="s2"&gt;"public, max-age=31536000, immutable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable the site and test the config&lt;/span&gt;
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/nextjs-app /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;            &lt;span class="c"&gt;# should say "test is successful"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: The GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;This is where the CI/CD magic happens. The workflow does four things: checks out code, runs your build, SSHs into the server, and restarts the app. Create this file in your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .github/workflows

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

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to AWS EC2&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;# also allow manual triggers&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run linter&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Next.js app&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Pass any build-time env vars here&lt;/span&gt;
          &lt;span class="na"&gt;NEXT_PUBLIC_GA_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NEXT_PUBLIC_GA_ID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to EC2&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EC2_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EC2_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;cd /var/www/nextjs-app&lt;/span&gt;

            &lt;span class="s"&gt;# Pull latest code&lt;/span&gt;
            &lt;span class="s"&gt;git pull origin main&lt;/span&gt;

            &lt;span class="s"&gt;# Install dependencies (production only)&lt;/span&gt;
            &lt;span class="s"&gt;npm ci --omit=dev&lt;/span&gt;

            &lt;span class="s"&gt;# Build the app on the server&lt;/span&gt;
            &lt;span class="s"&gt;npm run build&lt;/span&gt;

            &lt;span class="s"&gt;# Restart with PM2 (zero-downtime reload)&lt;/span&gt;
            &lt;span class="s"&gt;pm2 reload nextjs-app --update-env&lt;/span&gt;

            &lt;span class="s"&gt;echo "Deploy complete at $(date)"&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Set Up GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;In your GitHub repository, go to Settings → Secrets and variables → Actions, and add these three secrets:&lt;/p&gt;

&lt;p&gt;Secret Name&lt;br&gt;
Value&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EC2_HOST&lt;/code&gt;&lt;br&gt;
Your EC2 instance's public IP address or domain&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EC2_PRIVATE_KEY&lt;/code&gt;&lt;br&gt;
The full contents of your &lt;code&gt;.pem&lt;/code&gt; file (including the BEGIN/END lines)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NEXT_PUBLIC_GA_ID&lt;/code&gt;&lt;br&gt;
Your Google Analytics measurement ID (or any other public env vars)&lt;/p&gt;

&lt;p&gt;For the private key, open the .pem file in a text editor, copy everything including &lt;code&gt;-----BEGIN RSA PRIVATE KEY-----&lt;/code&gt; and &lt;code&gt;-----END RSA PRIVATE KEY-----&lt;/code&gt;, and paste it as the secret value.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5: First Deploy and PM2 Setup
&lt;/h2&gt;

&lt;p&gt;Before the GitHub Action can work, you need to get the app running on the server for the first time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone your repo to the server&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/nextjs-app
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;ubuntu:ubuntu /var/www/nextjs-app
&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/nextjs-app
git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Create .env file on the server&lt;/span&gt;
nano .env
&lt;span class="c"&gt;# Add your production environment variables here&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies and build&lt;/span&gt;
npm ci
npm run build

&lt;span class="c"&gt;# Start with PM2&lt;/span&gt;
pm2 start npm &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"nextjs-app"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; start
pm2 save                  &lt;span class="c"&gt;# persist across server restarts&lt;/span&gt;
pm2 startup               &lt;span class="c"&gt;# generate startup script&lt;/span&gt;
&lt;span class="c"&gt;# PM2 will output a command to run — run it&lt;/span&gt;

&lt;span class="c"&gt;# Verify the app is running&lt;/span&gt;
pm2 status
curl http://localhost:3000  &lt;span class="c"&gt;# should return HTML&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: CloudFront CDN (Optional but Recommended)
&lt;/h2&gt;

&lt;p&gt;CloudFront puts your app behind a global CDN, which means static assets load from an edge location near your users instead of your EC2 server. For most apps, this makes a meaningful difference in load times outside your server's region.&lt;/p&gt;

&lt;p&gt;In the AWS Console, go to CloudFront and create a new distribution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Origin domain:&lt;/strong&gt; Your EC2 public IP or domain (not localhost)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin protocol policy:&lt;/strong&gt; HTTP only (Nginx handles the connection to EC2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Viewer protocol policy:&lt;/strong&gt; Redirect HTTP to HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache policy for &lt;code&gt;/_next/static/*&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;CachingOptimized&lt;/code&gt; — these files are content-addressed, so they can be cached for years&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache policy for &lt;code&gt;/*&lt;/code&gt; (HTML pages):&lt;/strong&gt; &lt;code&gt;CachingDisabled&lt;/code&gt; — Next.js handles its own cache headers; CloudFront should pass them through&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a domain, attach it to the CloudFront distribution and request an ACM (AWS Certificate Manager) certificate for free SSL. DNS validation takes about 15 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch: Next.js CI/CD to AWS EC2 with GitHub Actions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Building on the server vs. building in CI
&lt;/h3&gt;

&lt;p&gt;The workflow above builds in the GitHub Action AND on the server. That's redundant — you only need to do it in one place. For small apps, building on the server is fine (simpler). For larger teams, build in CI, upload the artifact, and skip the build step on the server. The tradeoff: artifacts can be large (100MB+), so you need S3 or similar to store them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Forgetting to set NODE_ENV=production
&lt;/h3&gt;

&lt;p&gt;When you run &lt;code&gt;npm start&lt;/code&gt; (which runs &lt;code&gt;next start&lt;/code&gt;), Next.js automatically sets &lt;code&gt;NODE_ENV=production&lt;/code&gt;. But PM2 doesn't always inherit this. Be explicit in your PM2 config or startup command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start npm &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"nextjs-app"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; start &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Not configuring PM2 to restart on crash
&lt;/h3&gt;

&lt;p&gt;By default PM2 restarts crashed processes, but you want to limit restarts to prevent crash loops. Add &lt;code&gt;--max-restarts 10&lt;/code&gt; and &lt;code&gt;--min-uptime 5000&lt;/code&gt; to your pm2 start command. Five seconds of uptime before a restart counts is usually enough to catch truly broken deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. SSH key permissions
&lt;/h3&gt;

&lt;p&gt;The most common SSH error you'll hit is &lt;code&gt;UNPROTECTED PRIVATE KEY FILE&lt;/code&gt;. GitHub Actions handles this correctly when you use &lt;code&gt;appleboy/ssh-action&lt;/code&gt;, but if you're doing raw SSH commands, your .pem file needs &lt;code&gt;chmod 400 your-key.pem&lt;/code&gt; — readable only by the owner, nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  EC2 vs. Vercel vs. AWS Amplify — Which Should You Choose?
&lt;/h2&gt;

&lt;p&gt;Factor&lt;br&gt;
EC2 + CloudFront&lt;br&gt;
Vercel&lt;br&gt;
AWS Amplify&lt;/p&gt;

&lt;p&gt;Setup time&lt;br&gt;
90 min (first time)&lt;br&gt;
5 min&lt;br&gt;
20 min&lt;/p&gt;

&lt;p&gt;Next.js feature support&lt;br&gt;
Full (you control the runtime)&lt;br&gt;
Full (built for Next.js)&lt;br&gt;
Most features, some lag&lt;/p&gt;

&lt;p&gt;Cost at low traffic&lt;br&gt;
~$5/month (t3.micro)&lt;br&gt;
Free tier, then $20+/month&lt;br&gt;
Pay per build + hosting&lt;/p&gt;

&lt;p&gt;Cost at high traffic&lt;br&gt;
Predictable (fixed instance)&lt;br&gt;
Can get expensive fast&lt;br&gt;
Moderate&lt;/p&gt;

&lt;p&gt;Infrastructure control&lt;br&gt;
Full — you own everything&lt;br&gt;
None — Vercel manages it&lt;br&gt;
Partial&lt;/p&gt;

&lt;p&gt;Learning value&lt;br&gt;
High — enterprise-transferable&lt;br&gt;
Low (it just works)&lt;br&gt;
Medium&lt;/p&gt;

&lt;p&gt;Best for&lt;br&gt;
Learning, compliance, cost control&lt;br&gt;
Speed, simplicity, teams&lt;br&gt;
Existing AWS customers&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The stack:&lt;/strong&gt; EC2 (compute) + Nginx (reverse proxy) + PM2 (process manager) + CloudFront (CDN) + GitHub Actions (CI/CD). Each layer has one job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions workflow:&lt;/strong&gt; trigger on push to main → install → lint → build → SSH into EC2 → &lt;code&gt;git pull&lt;/code&gt; → rebuild → &lt;code&gt;pm2 reload&lt;/code&gt;. About 25 lines of YAML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store secrets properly:&lt;/strong&gt; EC2 host, private key, and env vars go in GitHub repository secrets — never hardcoded in workflow files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PM2 is essential&lt;/strong&gt; for production Node.js — it keeps the process alive, restarts on crash, and enables zero-downtime reloads. Run &lt;code&gt;pm2 startup&lt;/code&gt; to make it persist across server reboots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront is optional but worth it&lt;/strong&gt; — static assets cached at the edge make a real difference for users outside your server's region, and the free ACM SSL certificate saves you the hassle of Certbot configuration.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>aws</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>React + TypeScript Best Practices in 2025: What Actually Matters</title>
      <dc:creator>Harshdeep Singh</dc:creator>
      <pubDate>Tue, 02 Jun 2026 21:21:57 +0000</pubDate>
      <link>https://dev.to/harshdeepsingh13/react-typescript-best-practices-in-2025-what-actually-matters-22dn</link>
      <guid>https://dev.to/harshdeepsingh13/react-typescript-best-practices-in-2025-what-actually-matters-22dn</guid>
      <description>&lt;p&gt;You open a new React project, add TypeScript, and immediately hit Stack Overflow for how to type your first prop. The first answer says use &lt;code&gt;interface&lt;/code&gt;. The second says &lt;code&gt;type&lt;/code&gt;. The third is a six-paragraph thread about why one is semantically superior to the other. You close the tab and just write &lt;code&gt;any&lt;/code&gt; to get on with your life.&lt;/p&gt;

&lt;p&gt;Sound familiar? TypeScript in React has a reputation problem. Not because it's hard — it's genuinely great once it clicks — but because the community has generated a staggering volume of contradictory, context-free advice. Every dev tool tutorial starts with TypeScript. Every linting config bans &lt;code&gt;any&lt;/code&gt;. Every PR reviewer has a hot take on generics.&lt;/p&gt;

&lt;p&gt;This guide cuts through that. I'm not going to cover every TypeScript feature or every React pattern. What I'm going to do is share the specific conventions I use in production React + TypeScript apps in 2025 — the things that have made codebases genuinely easier to work in, not just theoretically safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this guide is NOT
&lt;/h2&gt;

&lt;p&gt;Before we get into it, let me set expectations clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a TypeScript basics tutorial.&lt;/strong&gt; I'm assuming you know what a type is, what an interface is, and that &lt;code&gt;string !== String&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not exhaustive.&lt;/strong&gt; TypeScript has dozens of utility types, conditional types, template literal types, and more. I'm not covering all of them — just the ones I reach for constantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not framework-neutral.&lt;/strong&gt; This is specifically about TypeScript in React apps. Some of these patterns won't apply to a Node.js CLI or a library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not about configuration.&lt;/strong&gt; Strict mode settings, tsconfig targets, module resolution — another article for another day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this guide IS is opinionated. I'm going to tell you what I think the right call is in most situations, and why. You'll disagree with some of it. That's fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typing Props the Right Way
&lt;/h2&gt;

&lt;p&gt;Let's start here because it's where every React + TypeScript journey begins, and where a lot of the confusion lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interface vs. Type Alias
&lt;/h3&gt;

&lt;p&gt;Here's my rule: &lt;strong&gt;use &lt;code&gt;interface&lt;/code&gt; for component props, &lt;code&gt;type&lt;/code&gt; for everything else&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why? Interfaces have declaration merging, which can occasionally bite you in unexpected ways, but they also produce cleaner error messages and feel more natural for describing object shapes. They're also what the React community defaults to, so your code will look familiar to anyone joining your team.&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;// Good — interface for component props&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;label&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="nl"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secondary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ghost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Good — type alias for unions and computed shapes&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ButtonVariant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secondary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ghost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You'll see guides that say "always use &lt;code&gt;type&lt;/code&gt;" or "always use &lt;code&gt;interface&lt;/code&gt;." Honestly? Consistency matters more than which one you pick. Pick a rule and stick to it across your codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Required vs. Optional Props
&lt;/h3&gt;

&lt;p&gt;Default to required. Make something optional only when it genuinely has a sensible default or when it's truly not needed in many use cases.&lt;/p&gt;

&lt;p&gt;This is the inverse of what a lot of developers do. They add &lt;code&gt;?&lt;/code&gt; to everything to make TypeScript stop complaining, and then their components have fifteen optional props where most of them are actually always passed. That erases the value of having types at all.&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;// Bad — over-optionalized&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CardProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&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="nl"&gt;description&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="nl"&gt;imageUrl&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="nl"&gt;href&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;// Good — be explicit about what's truly optional&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CardProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&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="nl"&gt;description&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="nl"&gt;imageUrl&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="c1"&gt;// genuinely optional — card can work without an image&lt;/span&gt;
  &lt;span class="nl"&gt;href&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="c1"&gt;// optional — sometimes cards aren't clickable&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Extending HTML Element Props
&lt;/h3&gt;

&lt;p&gt;This is one of the most useful patterns in React + TypeScript, and it's underused. When you're building a component that wraps an HTML element, extend that element's props so your component accepts all the native attributes automatically.&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;// Without this pattern — you have to manually add every HTML attribute&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;label&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="nl"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// what about type="submit"? aria-label? data-testid? className?&lt;/span&gt;
  &lt;span class="c1"&gt;// you'll spend forever adding these one by one&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// With this pattern — extend React's built-in types&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ButtonHTMLAttributes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;label&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="nl"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secondary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...and you automatically get onClick, type, aria-*, data-*, className, etc.&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;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary&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="nx"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;...rest&lt;/code&gt; spread pattern combined with extended HTML props is one of those things that once you start using, you can't go back. Your components become instantly more composable and you stop maintaining a manual list of passthrough props.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Hooks with TypeScript
&lt;/h2&gt;

&lt;p&gt;Custom hooks are where TypeScript really earns its keep, because hooks often manage complex state and return multiple values. If your hook's return type is just inferred as &lt;code&gt;any[]&lt;/code&gt;, you've lost all the benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typing Return Values Explicitly
&lt;/h3&gt;

&lt;p&gt;Always define the return type of custom hooks explicitly. Don't rely on inference here — it breaks down the moment your hook has multiple return paths or conditional logic.&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;// Bad — inferred return type is unreliable&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useUser&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// typed as null&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;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ...fetch logic&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// TypeScript infers this poorly&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Good — define the return interface explicitly&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&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="nl"&gt;name&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="nl"&gt;email&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UseUserResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useUser&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;UseUserResult&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;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ...fetch logic&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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;Now when you destructure this hook in a component, every field is typed correctly, and you get autocomplete without having to remember what the hook returns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generics for Reusable Hooks
&lt;/h3&gt;

&lt;p&gt;Okay so here's where hooks get really powerful. If you're building a reusable data-fetching hook, generics let you make it work with any shape of data without losing type safety.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;FetchResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FetchResult&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;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;url&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage — TypeScript knows exactly what data is&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;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/user/123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// data is typed as User | null — not unknown, not any&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;T&lt;/code&gt; propagates through the entire hook. That's the magic. You call &lt;code&gt;useFetch&amp;lt;User&amp;gt;&lt;/code&gt; once at the call site, and TypeScript figures out the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generic Components
&lt;/h2&gt;

&lt;p&gt;Generics in components are the thing that trips up most mid-level React developers. They look intimidating. They have funny angle-bracket syntax. But once you understand when to reach for them, they save you from maintaining three slightly-different versions of the same component.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Use Generic Components
&lt;/h3&gt;

&lt;p&gt;Reach for generics when your component works with data of a variable shape, but still needs to be type-safe. A list component, a select dropdown, a data table — these are classic candidates.&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;// Without generics — you end up with separate UserList, ProjectList, etc.&lt;/span&gt;
&lt;span class="c1"&gt;// or you use any[] and lose type safety&lt;/span&gt;

&lt;span class="c1"&gt;// With generics — one component that works for any data shape&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ListProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;keyExtractor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;emptyMessage&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;List&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyExtractor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;emptyMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ListProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emptyMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;


      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
      &lt;span class="p"&gt;))}&lt;/span&gt;


  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage — TypeScript infers T from the items array&lt;/span&gt;
 &lt;span class="nx"&gt;u&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="c1"&gt;// TypeScript knows u is a User&lt;/span&gt;
  &lt;span class="nx"&gt;renderItem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;u&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that you don't even need to write &lt;code&gt;&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;&lt;/code&gt; at the call site — TypeScript infers &lt;code&gt;T = User&lt;/code&gt; from the &lt;code&gt;items&lt;/code&gt; prop. That's inference doing its job.&lt;/p&gt;

&lt;p&gt;One thing to watch: in &lt;code&gt;.tsx&lt;/code&gt; files, the compiler can confuse &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; with a JSX tag. If you get a parse error, either add a constraint (&lt;code&gt;&amp;lt;T extends object&amp;gt;&lt;/code&gt;) or use a comma (&lt;code&gt;&amp;lt;T,&amp;gt;&lt;/code&gt;) to disambiguate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discriminated Unions for State
&lt;/h2&gt;

&lt;p&gt;Here's the thing that's changed how I think about React state more than anything else: replacing boolean flags with discriminated union types. This single pattern eliminates entire categories of bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Boolean Flag Problem
&lt;/h3&gt;

&lt;p&gt;You've seen this component. You've written this component.&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;// The boolean flag trap&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;errorMessage&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="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;SubmitResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Nothing stops you from setting isLoading: true AND isSuccess: true simultaneously&lt;/span&gt;
&lt;span class="c1"&gt;// That's an impossible state — but TypeScript can't catch it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;isSuccess&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;// ← this should be impossible&lt;/span&gt;
  &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;When you have three booleans representing what should be a single sequential state, you have 2³ = 8 possible combinations, but only 4 of them are actually valid. TypeScript can't protect you from the invalid ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Discriminated Union Fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Model the actual states that can exist&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;errorMessage&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="c1"&gt;// Now the impossible states are literally unrepresentable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// And in your component, TypeScript narrows types automatically&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;FormFeedback&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&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="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// TypeScript knows state.errorMessage exists here&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;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&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;// TypeScript knows state.data exists here&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;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// idle&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The key is the &lt;code&gt;status&lt;/code&gt; discriminant property. When you narrow on &lt;code&gt;state.status === "error"&lt;/code&gt;, TypeScript automatically knows which variant of the union you're in, and which other fields are available.&lt;/p&gt;

&lt;p&gt;This pattern is especially powerful in data-fetching scenarios, form submission flows, and anywhere you have a multi-step process. Start reaching for it instead of &lt;code&gt;isLoading / isError / isSuccess&lt;/code&gt; and your state management will become dramatically cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taming any
&lt;/h2&gt;

&lt;p&gt;Let me be direct: &lt;code&gt;any&lt;/code&gt; is a code smell, but it's not always your fault. Sometimes you're working with a library that has poor types, an API that returns unpredictable shapes, or legacy code you don't own. The goal isn't to never use &lt;code&gt;any&lt;/code&gt; — it's to reach for better tools first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use unknown Instead of any for External Data
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;unknown&lt;/code&gt; is the type-safe cousin of &lt;code&gt;any&lt;/code&gt;. It says "I don't know what this is yet" instead of "pretend this is whatever I need it to be." You can't do anything with an &lt;code&gt;unknown&lt;/code&gt; value without first narrowing it with a type guard.&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;// Bad — any lets you do anything, including wrong things&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;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="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;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/user&lt;/span&gt;&lt;span class="dl"&gt;"&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;doesNotExist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;boom&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// TypeScript is fine with this. Your app is not.&lt;/span&gt;

&lt;span class="c1"&gt;// Good — unknown forces you to validate before using&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;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;is&lt;/span&gt; &lt;span class="nx"&gt;User&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof &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;as&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&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="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;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isUser&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// TypeScript now knows data is User&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Type Guards Are Your Friends
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;value is User&lt;/code&gt; return type in the example above is a &lt;strong&gt;type guard&lt;/strong&gt;. It's a function that tells TypeScript "if this returns true, the value is of type T in the branches that follow." This is how you move from &lt;code&gt;unknown&lt;/code&gt; territory into properly typed territory without resorting to &lt;code&gt;any&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reusable type guard pattern&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isNonNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;is&lt;/span&gt; &lt;span class="nx"&gt;T&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;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isNonNull&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// typed as User[], not (User | null)[]&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  When never Is the Right Answer
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;never&lt;/code&gt; is the type that can't exist. It's useful for exhaustiveness checks — making sure you've handled every case in a union.&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;circle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;square&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;triangle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;circle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;square&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;triangle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// If you add "hexagon" to the Shape union and forget to handle it here,&lt;/span&gt;
      &lt;span class="c1"&gt;// TypeScript will throw a compile error on this line&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;_exhaustiveCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unhandled shape: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;_exhaustiveCheck&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This pattern scales beautifully with discriminated unions. Add a new status to your state type, and every switch statement that wasn't updated will fail at compile time. That's exactly the kind of safety net TypeScript is supposed to provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before vs. After: TypeScript Patterns
&lt;/h2&gt;

&lt;p&gt;Here's a quick-reference table of the common before/after shifts. These are the six patterns I most often see in code review that a bit of TypeScript discipline cleans up immediately.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Pattern&lt;br&gt;
  Without TypeScript Discipline&lt;br&gt;
  With TypeScript Discipline

&lt;p&gt;&lt;strong&gt;Prop typing&lt;/strong&gt;&lt;br&gt;
  &lt;code&gt;props: any&lt;/code&gt; or no types at all&lt;br&gt;
  &lt;code&gt;interface ButtonProps extends React.ButtonHTMLAttributes&amp;amp;lt;HTMLButtonElement&amp;amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional props&lt;/strong&gt;&lt;br&gt;
  Everything is &lt;code&gt;?&lt;/code&gt; to avoid errors&lt;br&gt;
  Only truly optional fields are optional; defaults handled by destructuring&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loading/error state&lt;/strong&gt;&lt;br&gt;
  &lt;code&gt;isLoading: boolean, isError: boolean, isSuccess: boolean&lt;/code&gt;&lt;br&gt;
  Discriminated union: &lt;code&gt;{ status: "idle" | "loading" | "success" | "error" }&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External API data&lt;/strong&gt;&lt;br&gt;
  &lt;code&gt;const data: any = await fetch(...).then(r =&amp;amp;gt; r.json())&lt;/code&gt;&lt;br&gt;
  &lt;code&gt;const data: unknown&lt;/code&gt; + type guard before use&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reusable list&lt;/strong&gt;&lt;br&gt;
  Separate &lt;code&gt;UserList&lt;/code&gt;, &lt;code&gt;ProjectList&lt;/code&gt; components with duplicated logic&lt;br&gt;
  One generic &lt;code&gt;List&amp;amp;lt;T&amp;amp;gt;&lt;/code&gt; component with typed &lt;code&gt;renderItem&lt;/code&gt; and &lt;code&gt;keyExtractor&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hook return type&lt;/strong&gt;&lt;br&gt;
  Inferred — breaks on multiple return paths, confusing autocomplete&lt;br&gt;
  Explicit &lt;code&gt;interface UseXxxResult { ... }&lt;/code&gt; as the return type annotation&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Module Structure for a TypeScript React Project&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;Okay, one more thing worth getting right from the start: where files live. A consistent folder structure does more for long-term maintainability than almost any TypeScript pattern. Here's the structure I use and recommend for mid-to-large React + TypeScript apps in 2025.&lt;/p&gt;

&lt;p&gt;src/&lt;br&gt;
├── app/                    # Next.js App Router pages (or pages/ for older setup)&lt;br&gt;
│   ├── layout.tsx&lt;br&gt;
│   ├── page.tsx&lt;br&gt;
│   └── blog/&lt;br&gt;
│       └── [slug]/&lt;br&gt;
│           └── page.tsx&lt;br&gt;
│&lt;br&gt;
├── components/             # Shared UI components&lt;br&gt;
│   ├── Button/&lt;br&gt;
│   │   ├── Button.tsx      # Component&lt;br&gt;
│   │   ├── Button.types.ts # Interface / type exports&lt;br&gt;
│   │   ├── Button.styles.ts# Styled components or CSS module&lt;br&gt;
│   │   └── index.ts        # Re-export for clean imports&lt;br&gt;
│   └── List/&lt;br&gt;
│       └── ...&lt;br&gt;
│&lt;br&gt;
├── hooks/                  # Custom hooks&lt;br&gt;
│   ├── useFetch.ts&lt;br&gt;
│   ├── useLocalStorage.ts&lt;br&gt;
│   └── useDebounce.ts&lt;br&gt;
│&lt;br&gt;
├── lib/                    # Utilities, helpers, non-UI logic&lt;br&gt;
│   ├── api.ts              # Fetch wrappers&lt;br&gt;
│   ├── formatters.ts       # Date, currency, string helpers&lt;br&gt;
│   └── validators.ts       # Type guards and runtime validation&lt;br&gt;
│&lt;br&gt;
├── types/                  # Shared TypeScript types&lt;br&gt;
│   ├── api.ts              # API response shapes&lt;br&gt;
│   ├── models.ts           # Domain model interfaces (User, Post, etc.)&lt;br&gt;
│   └── index.ts            # Re-exports&lt;br&gt;
│&lt;br&gt;
├── context/                # React context providers&lt;br&gt;
│   └── ThemeContext.tsx&lt;br&gt;
│&lt;br&gt;
└── styles/                 # Global styles, theme tokens&lt;br&gt;
    └── globals.css&lt;/p&gt;

&lt;p&gt;A few rules I enforce in this structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Co-locate component types.&lt;/strong&gt; Each component folder has its own &lt;code&gt;.types.ts&lt;/code&gt; file. Don't dump all types into a single global &lt;code&gt;types.ts&lt;/code&gt; — that file becomes a graveyard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;types/&lt;/code&gt; directory is for shared domain types only.&lt;/strong&gt; API response shapes, database models, shared interfaces that more than one component needs. Not component-specific props.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Barrel files are useful but dangerous.&lt;/strong&gt; An &lt;code&gt;index.ts&lt;/code&gt; in each component folder is fine. A single barrel for your entire &lt;code&gt;components/&lt;/code&gt; directory will cause circular dependency nightmares in larger apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks go in &lt;code&gt;hooks/&lt;/code&gt;, not co-located with components.&lt;/strong&gt; This is a deliberate choice against the "co-locate everything" philosophy. In my experience, hooks get reused across features, and burying them inside a component folder makes them harder to find and share.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;interface&lt;/code&gt; for component props, &lt;code&gt;type&lt;/code&gt; for everything else&lt;/strong&gt; — pick a rule and apply it consistently. Default to required props; only mark things optional when they genuinely are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extend HTML element props&lt;/strong&gt; using &lt;code&gt;React.ButtonHTMLAttributes&amp;lt;HTMLButtonElement&amp;gt;&lt;/code&gt; and friends — this gets you all native attributes for free and makes components composable with &lt;code&gt;...rest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always annotate custom hook return types explicitly&lt;/strong&gt; — define a &lt;code&gt;UseXxxResult&lt;/code&gt; interface and return it. Don't trust inference when there's conditional logic involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use discriminated unions instead of boolean flags&lt;/strong&gt; for anything with multiple states — &lt;code&gt;{ status: "idle" | "loading" | "success" | "error" }&lt;/code&gt; is safer, clearer, and catches impossible states at compile time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace &lt;code&gt;any&lt;/code&gt; with &lt;code&gt;unknown&lt;/code&gt; at API boundaries&lt;/strong&gt; — then validate with type guards before use. Save &lt;code&gt;never&lt;/code&gt; for exhaustiveness checks in switch statements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure your modules intentionally&lt;/strong&gt; — co-locate component types, put shared domain types in &lt;code&gt;types/&lt;/code&gt;, hooks in &lt;code&gt;hooks/&lt;/code&gt;, and use barrel files per component folder (not globally).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>frontend</category>
      <category>bestpractices</category>
    </item>
  </channel>
</rss>
