{"slug": "ai-without-a-backend-the-browser-s-built-in-ai-apis-for-web-developers", "title": "AI Without a Backend: The Browser's Built-in AI APIs for Web Developers", "summary": "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.", "body_md": "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.\n\nChrome 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.\n\nIn 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.\n\n⚠️\n\nReality 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.\n\nChrome's built-in AI comes in two flavors.\n\n**Task-specific APIs** do one job well, with a clean, purpose-built interface:\n\n| API | What it does |\n|---|---|\nTranslator |\nTranslates text between languages on-device. |\nLanguage Detector |\nDetects the language of a piece of text. |\nSummarizer |\nCondenses long text into key points, a TL;DR, a teaser, or a headline. |\nWriter |\nGenerates new text from a prompt (e.g. a product description). |\nRewriter |\nRestructures or improves existing text (shorter, more formal, etc.). |\n\n**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.\n\nThe 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.\n\nAll of these share the same lifecycle, so once you learn one, you know them all.\n\nEvery API follows the same three-step pattern: **feature-detect → check availability → create and use.**\n\nThe interfaces live as globals (`Summarizer`\n\n, `LanguageModel`\n\n, `Translator`\n\n, …). Check before you touch:\n\n```\nif ('Summarizer' in self) {\n  // The Summarizer API exists in this browser.\n}\n```\n\nHere'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.\n\n`availability()`\n\ntells you where you stand:\n\n``` js\nconst status = await Summarizer.availability();\n// \"unavailable\"  → not supported on this device/config\n// \"downloadable\" → supported, but the model must download first\n// \"downloading\"  → download in progress\n// \"available\"    → ready to go right now\n```\n\n`create()`\n\ninstantiates the API. If the model still needs downloading, `create()`\n\ntriggers it — so **always give the user feedback**, because \"downloading 4 GB\" is not something to do silently:\n\n``` js\nconst summarizer = await Summarizer.create({\n  monitor(m) {\n    m.addEventListener('downloadprogress', (e) => {\n      console.log(`Model download: ${Math.round(e.loaded * 100)}%`);\n    });\n  },\n});\n```\n\nTwo things worth knowing:\n\n`create()`\n\n`navigator.userActivation.isActive`\n\nif 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.\n\n`Summarizer.create()`\n\ntakes options that shape the output:\n\n`type`\n\n— `\"key-points\"`\n\n(default), `\"tldr\"`\n\n, `\"teaser\"`\n\n, or `\"headline\"`\n\n.`format`\n\n— `\"markdown\"`\n\n(default) or `\"plain-text\"`\n\n.`length`\n\n— `\"short\"`\n\n(default), `\"medium\"`\n\n, or `\"long\"`\n\n. What that means depends on the type: a short `key-points`\n\nsummary is 3 bullets; a short `tldr`\n\nis one sentence.`sharedContext`\n\n— background info that helps the model do a better job.\n\n```\nasync function summarizeReviews(reviewsText) {\n  // 1. Feature detection — degrade gracefully if unsupported.\n  if (!('Summarizer' in self)) {\n    return { supported: false };\n  }\n\n  // 2. Availability check.\n  const availability = await Summarizer.availability();\n  if (availability === 'unavailable') {\n    return { supported: false };\n  }\n\n  // 3. Create the summarizer, tuned for product reviews.\n  const summarizer = await Summarizer.create({\n    sharedContext:\n      'These are customer product reviews from an e-commerce store. ' +\n      'Summarize the overall sentiment and the points customers mention most.',\n    type: 'key-points',\n    format: 'markdown',\n    length: 'medium',\n    monitor(m) {\n      m.addEventListener('downloadprogress', (e) => {\n        updateProgressUI(e.loaded); // your UI hook\n      });\n    },\n  });\n\n  // 4. Summarize.\n  const summary = await summarizer.summarize(reviewsText, {\n    context: 'Focus on product quality, value for money, and shipping.',\n  });\n\n  return { supported: true, summary };\n}\n```\n\nWiring it to a button:\n\n``` js\nsummarizeBtn.addEventListener('click', async () => {\n  // innerText, not innerHTML — feed the model clean text, not markup.\n  const reviewsText = document.querySelector('#reviews').innerText;\n\n  summarizeBtn.disabled = true;\n  const { supported, summary } = await summarizeReviews(reviewsText);\n\n  if (!supported) {\n    // Progressive enhancement: fall back to your server, or just hide the feature.\n    summaryEl.textContent = 'Review summaries aren’t available on this device.';\n  } else {\n    summaryEl.textContent = summary;\n  }\n  summarizeBtn.disabled = false;\n});\n```\n\n💡\n\nFeed it clean text.Use`innerText`\n\n, not`innerHTML`\n\n. The model summarizes better when it isn't wading through`<div>`\n\nsoup, and you don't waste the context window on markup. If you retrieve the feedback from an API, you can also give it the json directly.\n\nFor a big block of text, waiting for the whole summary feels sluggish. Stream it instead and render tokens as they arrive:\n\n``` js\nconst stream = summarizer.summarizeStreaming(reviewsText, {\n  context: 'Focus on product quality and value for money.',\n});\n\nsummaryEl.textContent = '';\nfor await (const chunk of stream) {\n  summaryEl.append(chunk); // each chunk is an incremental piece of text\n}\n```\n\nThe Summarizer currently supports `en`\n\n, `ja`\n\n, `es`\n\n, `de`\n\n, and `fr`\n\n. Declaring the languages up front lets the browser reject an unsupported combination early instead of failing mid-request:\n\n``` js\nconst summarizer = await Summarizer.create({\n  type: 'key-points',\n  expectedInputLanguages: ['en', 'fr'],\n  outputLanguage: 'en',\n});\n```\n\nThe task APIs are great when your need fits one of them. But what about something bespoke — like: *\"Here's a photo of a product. Generate a draft review and auto-fill the review form.\"*\n\nThat's a job for the **Prompt API**, and it shows off two of its best features: **multimodal input** (an image) and **structured output** (a JSON schema).\n\nBecause we're sending an image, we declare `image`\n\n(and `text`\n\n) as expected inputs when creating the session:\n\n``` js\nconst session = await LanguageModel.create({\n  expectedInputs: [\n    { type: 'text', languages: ['en'] },\n    { type: 'image' },\n  ],\n  expectedOutputs: [{ type: 'text', languages: ['en'] }],\n  monitor(m) {\n    m.addEventListener('downloadprogress', (e) => {\n      updateProgressUI(e.loaded);\n    });\n  },\n});\n```\n\nPrompting *\"reply in JSON\"* and hoping for the best is a recipe for parse errors. Instead, pass a **JSON Schema** via `responseConstraint`\n\n, and the model is *constrained* to produce output matching it:\n\n``` js\nconst reviewSchema = {\n  title:       { type: 'string' },\n  description: { type: 'string' },\n  rating:      { type: 'number', minimum: 1, maximum: 5 },\n  pros:        { type: 'array', items: { type: 'string' } },\n  cons:        { type: 'array', items: { type: 'string' } },\n};\n```\n\nThe `content`\n\nof a message is an array of typed parts. We combine a text instruction with the image, and pass the schema as a constraint:\n\n``` js\nasync function generateReviewFromPhoto(imageElement) {\n  const result = await session.prompt(\n    [\n      {\n        role: 'user',\n        content: [\n          {\n            type: 'text',\n            value:\n              'You are given a photo of a product. Write a short, honest ' +\n              'customer-style review of it. Return the result as JSON.',\n          },\n          { type: 'image', value: imageElement },\n        ],\n      },\n    ],\n    { responseConstraint: reviewSchema },\n  );\n\n  // Guaranteed to match the schema — safe to parse.\n  return JSON.parse(result);\n}\n```\n\nCalling it against an image on the page:\n\n``` js\nconst photo = document.querySelector('#product-photo'); // an <img> element\nconst review = await generateReviewFromPhoto(photo);\n\nconsole.log(review);\n// {\n//   title: \"Sleek and sturdy everyday mug\",\n//   description: \"A clean matte-white ceramic mug that looks premium...\",\n//   rating: 4,\n//   pros: [\"Solid build\", \"Comfortable handle\"],\n//   cons: [\"Slightly heavier than expected\"]\n// }\n```\n\nBecause the output is schema-constrained, you skip the usual defensive gymnastics — no regex-scraping a JSON blob out of a chatty response, no `try/catch`\n\naround a hopeful `JSON.parse`\n\n. You get an object shaped exactly like your database row.\n\n💡\n\nWhy the schema matters.Without`responseConstraint`\n\n, the modelmighthonor a \"reply in JSON\" instruction — most of the time. With it, the output is structurally guaranteed. In production, \"most of the time\" is where the 2 a.m. pages come from. Use the schema!\n\nA `LanguageModel`\n\nsession keeps context across prompts, so you can hold a conversation or run several images through the same instance. When you're done, free the resources:\n\n```\nsession.destroy();\n```\n\nThe single most important design principle for built-in AI: **treat it as an enhancement, never a hard dependency.** A big chunk of your users won't have it — wrong OS, mobile device, insufficient hardware, or the model simply hasn't downloaded yet.\n\nA robust feature does this:\n\n```\nasync function getSummarizer() {\n  if (!('Summarizer' in self)) return null;\n  const availability = await Summarizer.availability();\n  if (availability === 'unavailable') return null;\n  return Summarizer.create(/* … */);\n}\n\nconst summarizer = await getSummarizer();\nif (summarizer) {\n  // On-device path: instant, private, free.\n} else {\n  // Fallback: call your server-side summarizer, or hide the feature entirely.\n}\n```\n\nKeep these in mind:\n\nProgressive enhancement is only half a strategy if the \"else\" branch just hides the feature. For a lot of products you want the capability to work *everywhere* — on the mobile users, the older laptops, the browsers that haven't shipped these APIs yet — while still giving the on-device path to everyone who can use it.\n\nThink of it as a spectrum rather than a rivalry:\n\nThe nice part: your feature-detection branch is already the seam between them. Where the on-device path falls through, call your cloud model instead. Here's the same `getSummarizer()`\n\npattern with a fallback to [Amazon Bedrock](https://aws.amazon.com/bedrock/), which exposes many foundation models behind one API:\n\n``` js\n// Client side — same seam as before, now with a real fallback.\nconst summarizer = await getSummarizer();\n\nlet summary;\nif (summarizer) {\n  // On-device path: instant, private, free.\n  summary = await summarizer.summarize(reviewsText);\n} else {\n  // Cloud path: works on any device, at the cost of a network round-trip.\n  const res = await fetch('/api/summarize', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ text: reviewsText }),\n  });\n  ({ summary } = await res.json());\n}\njs\n// Server side (Node/Lambda) — a thin proxy to Amazon Bedrock.\nimport { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime';\n\nconst client = new BedrockRuntimeClient({ region: 'us-east-1' });\n\nexport async function handler(req, res) {\n  const { text } = req.body;\n\n  const response = await client.send(new ConverseCommand({\n    modelId: 'anthropic.claude-3-5-haiku-20241022-v1:0',\n    messages: [{\n      role: 'user',\n      content: [{ text: `Summarize these product reviews as key points:\\n\\n${text}` }],\n    }],\n  }));\n\n  const summary = response.output.message.content[0].text;\n  res.json({ summary });\n}\n```\n\nSame feature, two engines: the browser serves everyone it can for free, and the cloud quietly covers the rest. Keeping the API key server-side (never in the browser) is exactly the backend concern the on-device path lets you *skip* for the majority of your users — so you pay for cloud inference only on the requests that genuinely need it.\n\n💡\n\nDisclosure:I'm a Developer Advocate at AWS, so Bedrock is the cloud fallback I reach for. The pattern is vendor-neutral — any hosted model API slots into the same`else`\n\nbranch.\n\nBuilt-in AI shifts a whole class of features from \"backend project with ongoing costs\" to \"a few dozen lines of client-side JavaScript.\" No key to rotate, no proxy to secure, no bill that scales with usage, and no user data leaving the device.\n\nIt won't replace big cloud models for heavy reasoning, and it isn't everywhere yet. But for the everyday tasks that make up so much of real web work — summarize this, classify that, translate the other thing, extract structured data from a mess — the model is right there in the browser, waiting for a function call.\n\n`@types/dom-chromium-ai`\n\nnpm package for TypeScript typings.", "url": "https://wpnews.pro/news/ai-without-a-backend-the-browser-s-built-in-ai-apis-for-web-developers", "canonical_source": "https://dev.to/olivierleplus/ai-without-a-backend-the-browsers-built-in-ai-apis-for-web-developers-2745", "published_at": "2026-07-08 14:12:29+00:00", "updated_at": "2026-07-08 14:41:51.829337+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools", "ai-products", "ai-infrastructure"], "entities": ["Chrome", "Google", "Gemini Nano", "Summarizer API", "Translator API", "Prompt API", "LanguageModel API"], "alternates": {"html": "https://wpnews.pro/news/ai-without-a-backend-the-browser-s-built-in-ai-apis-for-web-developers", "markdown": "https://wpnews.pro/news/ai-without-a-backend-the-browser-s-built-in-ai-apis-for-web-developers.md", "text": "https://wpnews.pro/news/ai-without-a-backend-the-browser-s-built-in-ai-apis-for-web-developers.txt", "jsonld": "https://wpnews.pro/news/ai-without-a-backend-the-browser-s-built-in-ai-apis-for-web-developers.jsonld"}}