cd /news/artificial-intelligence/ai-without-a-backend-the-browser-s-b… Β· home β€Ί topics β€Ί artificial-intelligence β€Ί article
[ARTICLE Β· art-51237] src=dev.to β†— pub= topic=artificial-intelligence verified=true sentiment=↑ positive

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.

read10 min views1 publishedJul 8, 2026

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:

const status = await Summarizer.availability();
// "unavailable"  β†’ not supported on this device/config
// "downloadable" β†’ supported, but the model must download first
// "down"  β†’ download in progress
// "available"    β†’ ready to go right now

create()

instantiates the API. If the model still needs down, create()

triggers it β€” so always give the user feedback, because "down 4 GB" is not something to do silently:

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:

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.UseinnerText

, notinnerHTML

. The model summarizes better when it isn't wading through<div>

soup, 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.

For a big block of text, waiting for the whole summary feels sluggish. Stream it instead and render tokens as they arrive:

const stream = summarizer.summarizeStreaming(reviewsText, {
  context: 'Focus on product quality and value for money.',
});

summaryEl.textContent = '';
for await (const chunk of stream) {
  summaryEl.append(chunk); // each chunk is an incremental piece of text
}

The Summarizer currently supports en

, ja

, es

, de

, and fr

. Declaring the languages up front lets the browser reject an unsupported combination early instead of failing mid-request:

const summarizer = await Summarizer.create({
  type: 'key-points',
  expectedInputLanguages: ['en', 'fr'],
  outputLanguage: 'en',
});

The 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."

That'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).

Because we're sending an image, we declare image

(and text

) as expected inputs when creating the session:

const session = await LanguageModel.create({
  expectedInputs: [
    { type: 'text', languages: ['en'] },
    { type: 'image' },
  ],
  expectedOutputs: [{ type: 'text', languages: ['en'] }],
  monitor(m) {
    m.addEventListener('downloadprogress', (e) => {
      updateProgressUI(e.loaded);
    });
  },
});

Prompting "reply in JSON" and hoping for the best is a recipe for parse errors. Instead, pass a JSON Schema via responseConstraint

, and the model is constrained to produce output matching it:

const reviewSchema = {
  title:       { type: 'string' },
  description: { type: 'string' },
  rating:      { type: 'number', minimum: 1, maximum: 5 },
  pros:        { type: 'array', items: { type: 'string' } },
  cons:        { type: 'array', items: { type: 'string' } },
};

The content

of a message is an array of typed parts. We combine a text instruction with the image, and pass the schema as a constraint:

async function generateReviewFromPhoto(imageElement) {
  const result = await session.prompt(
    [
      {
        role: 'user',
        content: [
          {
            type: 'text',
            value:
              'You are given a photo of a product. Write a short, honest ' +
              'customer-style review of it. Return the result as JSON.',
          },
          { type: 'image', value: imageElement },
        ],
      },
    ],
    { responseConstraint: reviewSchema },
  );

  // Guaranteed to match the schema β€” safe to parse.
  return JSON.parse(result);
}

Calling it against an image on the page:

const photo = document.querySelector('#product-photo'); // an <img> element
const review = await generateReviewFromPhoto(photo);

console.log(review);
// {
//   title: "Sleek and sturdy everyday mug",
//   description: "A clean matte-white ceramic mug that looks premium...",
//   rating: 4,
//   pros: ["Solid build", "Comfortable handle"],
//   cons: ["Slightly heavier than expected"]
// }

Because 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

around a hopeful JSON.parse

. You get an object shaped exactly like your database row.

πŸ’‘

Why the schema matters.WithoutresponseConstraint

, 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!

A LanguageModel

session 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:

session.destroy();

The 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.

A robust feature does this:

async function getSummarizer() {
  if (!('Summarizer' in self)) return null;
  const availability = await Summarizer.availability();
  if (availability === 'unavailable') return null;
  return Summarizer.create(/* … */);
}

const summarizer = await getSummarizer();
if (summarizer) {
  // On-device path: instant, private, free.
} else {
  // Fallback: call your server-side summarizer, or hide the feature entirely.
}

Keep these in mind:

Progressive 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.

Think of it as a spectrum rather than a rivalry:

The 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()

pattern with a fallback to Amazon Bedrock, which exposes many foundation models behind one API:

// Client side β€” same seam as before, now with a real fallback.
const summarizer = await getSummarizer();

let summary;
if (summarizer) {
  // On-device path: instant, private, free.
  summary = await summarizer.summarize(reviewsText);
} else {
  // Cloud path: works on any device, at the cost of a network round-trip.
  const res = await fetch('/api/summarize', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: reviewsText }),
  });
  ({ summary } = await res.json());
}
js
// Server side (Node/Lambda) β€” a thin proxy to Amazon Bedrock.
import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime';

const client = new BedrockRuntimeClient({ region: 'us-east-1' });

export async function handler(req, res) {
  const { text } = req.body;

  const response = await client.send(new ConverseCommand({
    modelId: 'anthropic.claude-3-5-haiku-20241022-v1:0',
    messages: [{
      role: 'user',
      content: [{ text: `Summarize these product reviews as key points:\n\n${text}` }],
    }],
  }));

  const summary = response.output.message.content[0].text;
  res.json({ summary });
}

Same 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.

πŸ’‘

Disclosure: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 sameelse

branch.

Built-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.

It 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.

@types/dom-chromium-ai

npm package for TypeScript typings.

── more in #artificial-intelligence 4 stories Β· sorted by recency
── more on @chrome 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/ai-without-a-backend…] indexed:0 read:10min 2026-07-08 Β· β€”