AI Without a Backend: The Browser's Built-in AI APIs for Web Developers Chrome is shipping built-in AI APIs that run Google's Gemini Nano language model inside the browser, enabling web developers to perform tasks like summarization, translation, and structured data generation without a backend or API key. The APIs include task-specific interfaces (Summarizer, Translator, etc.) and a general-purpose Prompt API, all following a three-step pattern of feature detection, availability check, and creation. While currently desktop-only and requiring a ~4 GB model download, they offer a privacy-preserving, offline-capable alternative for progressive enhancement. For years, "adding AI to your web app" meant the same thing: sign up for an API key, wire up a backend proxy so you don't leak that key to the browser, meter your token usage, handle rate limits, and pray your bill doesn't explode when a feature goes viral. Every summary, every classification, every bit of generated text was a round-trip to someone else's data center. Chrome is quietly changing that. A family of built-in AI APIs now ships a language model β€” Google's Gemini Nano β€” inside the browser . Your JavaScript can summarize an article, translate a comment, or generate structured data from an image without a single network call to an inference server. No API key. No backend. No per-token bill. And because the model runs on-device, the user's data never leaves their machine. In this article, I will walk you through what these APIs are, how to use them safely, and two hands-on examples: summarizing user reviews, and turning a product photo into structured JSON with the Prompt API. ⚠️ Reality check before you get too excited sorry πŸ˜… :these APIs are desktop-only for now no mobile , require a fairly capable machine, and the model is a ~4 GB download on first use. They're production-viable asprogressive enhancements, not as your only code path. We'll cover graceful degradation throughout. Chrome's built-in AI comes in two flavors. Task-specific APIs do one job well, with a clean, purpose-built interface: | API | What it does | |---|---| Translator | Translates text between languages on-device. | Language Detector | Detects the language of a piece of text. | Summarizer | Condenses long text into key points, a TL;DR, a teaser, or a headline. | Writer | Generates new text from a prompt e.g. a product description . | Rewriter | Restructures or improves existing text shorter, more formal, etc. . | The Prompt API also called the LanguageModel API is the general-purpose escape hatch. When no task API fits, you talk to Gemini Nano directly with your own prompts β€” and it's multimodal , accepting text, images, and audio as input. The mental model: reach for a task API when your need matches one translation, summarization, rewriting β€” it's simpler and more predictable. Reach for the Prompt API when you need something custom, structured, or multimodal. All of these share the same lifecycle, so once you learn one, you know them all. Every API follows the same three-step pattern: feature-detect β†’ check availability β†’ create and use. The interfaces live as globals Summarizer , LanguageModel , Translator , … . Check before you touch: if 'Summarizer' in self { // The Summarizer API exists in this browser. } Here's the part that trips people up. The API being present doesn't mean the model is ready . Gemini Nano is downloaded separately the first time an origin uses it β€” and it's a multi-gigabyte download. availability tells you where you stand: js const status = await Summarizer.availability ; // "unavailable" β†’ not supported on this device/config // "downloadable" β†’ supported, but the model must download first // "downloading" β†’ download in progress // "available" β†’ ready to go right now create instantiates the API. If the model still needs downloading, create triggers it β€” so always give the user feedback , because "downloading 4 GB" is not something to do silently: js const summarizer = await Summarizer.create { monitor m { m.addEventListener 'downloadprogress', e = { console.log Model download: ${Math.round e.loaded 100 }% ; } ; }, } ; Two things worth knowing: create navigator.userActivation.isActive if you want to be strict.Picture a product page with dozens of customer reviews. Nobody reads all of them. A classic use case: a "Summarize reviews" button that distills them into a few key points β€” computed entirely in the browser. Summarizer.create takes options that shape the output: type β€” "key-points" default , "tldr" , "teaser" , or "headline" . format β€” "markdown" default or "plain-text" . length β€” "short" default , "medium" , or "long" . What that means depends on the type: a short key-points summary is 3 bullets; a short tldr is one sentence. sharedContext β€” background info that helps the model do a better job. async function summarizeReviews reviewsText { // 1. Feature detection β€” degrade gracefully if unsupported. if 'Summarizer' in self { return { supported: false }; } // 2. Availability check. const availability = await Summarizer.availability ; if availability === 'unavailable' { return { supported: false }; } // 3. Create the summarizer, tuned for product reviews. const summarizer = await Summarizer.create { sharedContext: 'These are customer product reviews from an e-commerce store. ' + 'Summarize the overall sentiment and the points customers mention most.', type: 'key-points', format: 'markdown', length: 'medium', monitor m { m.addEventListener 'downloadprogress', e = { updateProgressUI e.loaded ; // your UI hook } ; }, } ; // 4. Summarize. const summary = await summarizer.summarize reviewsText, { context: 'Focus on product quality, value for money, and shipping.', } ; return { supported: true, summary }; } Wiring it to a button: js summarizeBtn.addEventListener 'click', async = { // innerText, not innerHTML β€” feed the model clean text, not markup. const reviewsText = document.querySelector ' reviews' .innerText; summarizeBtn.disabled = true; const { supported, summary } = await summarizeReviews reviewsText ; if supported { // Progressive enhancement: fall back to your server, or just hide the feature. summaryEl.textContent = 'Review summaries aren’t available on this device.'; } else { summaryEl.textContent = summary; } summarizeBtn.disabled = false; } ; πŸ’‘ Feed it clean text.Use innerText , not innerHTML . The model summarizes better when it isn't wading through