DEV Community

Mohamed Idris
Mohamed Idris

Posted on

Learning the Web Platform APIs As If You Built Them Yourself

If you have ever reached for a library to do something the browser already does, you have met the gap this post is about. Need a debounce? You wrote a useEffect. Need to detect when a card scrolls into view? You added a 300 line library. Need offline support? You shrugged and gave up.

The browser has changed. Modern browsers ship a generous, capable platform. Most of the libraries we still install in 2026 were written when the platform was missing the feature. They are not missing anymore.

A senior frontend engineer knows what the browser already gives them, and reaches for it before reaching for npm.

That is the gap this post fills.

What is the web platform, really

Think of the browser as a small operating system that runs in a tab. It has storage, a network stack, threads, scheduling, sensors, sometimes even a file system and Bluetooth. Each capability has an API. Many of them are excellent. Many of them are five lines of code instead of a 12KB dependency.

Two ideas drive the whole thing:

  • The platform is doing more than you think. Capabilities ship every six weeks across all the major browsers.
  • Promises and observers are the shape. Most modern APIs are either awaitable or "watch this thing and call me back".

That is the whole vibe.

Let's pretend we are building one

We are not building the platform. We are doing a senior level tour of it. For the running example, we will sprinkle a tiny read later notes app with native APIs as we go. Save notes offline, fetch articles, observe the page, schedule work, broadcast across tabs.

API 1: fetch (and AbortController), the network everyone forgets has features

Every frontend engineer knows fetch. Half of them only use 20% of it.

const res = await fetch("/api/notes", {
  method:      "POST",
  headers:     { "Content-Type": "application/json", Accept: "application/json" },
  body:        JSON.stringify({ title: "Read later" }),
  credentials: "include",        // send cookies on cross origin requests
  cache:       "no-cache",       // override default
  mode:        "cors",           // also "same-origin", "no-cors"
  signal:      ctrl.signal,      // AbortController
});

if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
Enter fullscreen mode Exit fullscreen mode

The features senior engineers actually use:

AbortController for cancelling

const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 5000);   // 5s timeout

try {
  const res = await fetch(url, { signal: ctrl.signal });
} finally {
  clearTimeout(timer);
}
Enter fullscreen mode Exit fullscreen mode

The same AbortController works with addEventListener, with most modern Promise APIs, and with React Query's queries. Make it your default mental model: any long lived async work should be cancellable.

The shortcut for "fetch with timeout":

fetch(url, { signal: AbortSignal.timeout(5000) });
Enter fullscreen mode Exit fullscreen mode

Streaming responses

The body is a ReadableStream. You can consume it as it arrives, perfect for AI streaming or large downloads:

const res = await fetch("/api/stream");
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  console.log("chunk:", value);
}
Enter fullscreen mode Exit fullscreen mode

Response and Request are real classes

You can build them, store them, clone them. Service Workers rely on this. The same Response API you receive from fetch is the one you return from a Service Worker.

API 2: Storage, three flavors

The browser has three storage mechanisms. Pick on purpose.

localStorage and sessionStorage

Tiny key value store. Synchronous. Strings only. Good for: small UI state (theme, last opened tab), tiny preferences. Bad for: anything large, anything secret, anything that should sync.

localStorage.setItem("theme", "dark");
localStorage.getItem("theme");
localStorage.removeItem("theme");
Enter fullscreen mode Exit fullscreen mode

sessionStorage is the same, scoped to the tab. Cleared on close.

A senior level rule: never store auth tokens in localStorage. JavaScript can read it, which means every third party script can too.

Cookies

Sent automatically with every same origin request. The browser respects HttpOnly, Secure, SameSite. We covered them in the auth post. Use them for sessions and CSRF tokens.

IndexedDB, the actual database

A real client side database. Asynchronous. Stores anything structurable, including blobs. Indexed for fast queries. Quotas in the megabytes to gigabytes range.

The native API is famously verbose. Use a tiny wrapper like idb-keyval for simple cases or Dexie.js for richer querying.

import { get, set, del } from "idb-keyval";

await set("note:42", { title: "Read later", body: "..." });
const note = await get("note:42");
await del("note:42");
Enter fullscreen mode Exit fullscreen mode

Use IndexedDB for: offline caches, drafts, full text search indexes, large user data, anything you need to survive a refresh and a flaky network. It is the storage that makes serious offline apps possible.

In 2026 there is also an exciting trend: SQLite in the browser, via wasm libraries like sql.js and wa-sqlite, often backed by IndexedDB or the Origin Private File System (OPFS). Real SQL queries on the client. Worth knowing about for offline first apps.

API 3: Service Workers, the engine of offline

A Service Worker is a script that runs in the background, separate from any page, and can intercept network requests for a whole origin. It is how websites become installable apps that work offline.

// app/main.ts
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/sw.js");
}

// public/sw.js
self.addEventListener("install", (e) => {
  e.waitUntil(caches.open("v1").then((c) => c.addAll(["/", "/styles.css", "/app.js"])));
});

self.addEventListener("fetch", (e) => {
  e.respondWith(
    caches.match(e.request).then((cached) => cached || fetch(e.request))
  );
});
Enter fullscreen mode Exit fullscreen mode

What that gives you: the page loads from cache instantly, even offline.

The senior level patterns:

  • Cache first for static assets that change rarely.
  • Network first, cache fallback for HTML.
  • Stale while revalidate for API responses.
  • Use a tool, do not hand roll. Workbox (Google) and Vite PWA Plugin scaffold a sane Service Worker in a few lines. They handle caching strategies, precaching, runtime caching, navigation fallback, and updates.

The full PWA story (offline + installable + push notifications) is a real thing in 2026. Browsers across Mac, Windows, Linux, Android, and even iOS support it. For an app that runs daily, "Add to Home Screen" is real distribution.

API 4: Web Workers, threads for compute

JavaScript runs on a single thread. Long work blocks the page. A Web Worker runs a script on a separate thread, communicating by message passing.

// worker.ts
self.onmessage = (e) => {
  const result = expensiveCompute(e.data);
  self.postMessage(result);
};

// main.ts
const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
worker.postMessage(input);
worker.onmessage = (e) => setResult(e.data);
Enter fullscreen mode Exit fullscreen mode

The friendlier modern API: Comlink turns the worker into a proxy you can await:

// worker.ts
import { expose } from "comlink";

expose({
  parseMarkdown(md: string) { return slowParser(md); },
});

// main.ts
import { wrap } from "comlink";
const api = wrap<{ parseMarkdown(md: string): string }>(
  new Worker(new URL("./worker.ts", import.meta.url), { type: "module" })
);
const html = await api.parseMarkdown(text);
Enter fullscreen mode Exit fullscreen mode

Use Web Workers for: heavy parsing (Markdown, syntax highlighting, JSON), image processing, data transforms, anything more than 50ms of compute. The page stays smooth, INP stays under 200ms.

A close cousin: requestIdleCallback runs a function when the browser is idle. Great for non urgent work like analytics flushing.

requestIdleCallback(() => sendQueuedAnalytics(), { timeout: 2000 });
Enter fullscreen mode Exit fullscreen mode

API 5: IntersectionObserver, "tell me when this is on screen"

Before this API, "is the element visible" was solved by listening to scroll and reading layout in a tight loop. It was awful.

Now:

const obs = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log("visible:", entry.target);
    }
  }
}, { rootMargin: "200px" });

document.querySelectorAll(".lazy").forEach((el) => obs.observe(el));
Enter fullscreen mode Exit fullscreen mode

Uses for senior frontends:

  • Lazy load images and components before they are needed.
  • Infinite scroll: observe a sentinel at the bottom of the list.
  • Trigger animations when an element scrolls into view.
  • Track impressions for analytics.

rootMargin lets you start the work early, so the user never sees the loading state.

A close cousin: ResizeObserver fires when an element changes size. Great for charts, tables, and components that need to re-render on layout changes:

new ResizeObserver(([entry]) => {
  drawChart(entry.contentRect.width, entry.contentRect.height);
}).observe(chartEl);
Enter fullscreen mode Exit fullscreen mode

And MutationObserver for "tell me when the DOM changed". Niche but lifesaving for browser extensions and integrations with markup you do not control.

API 6: BroadcastChannel, talking across tabs

If a user has your app open in three tabs and logs out in one, the others should know. BroadcastChannel posts messages across same origin tabs:

const ch = new BroadcastChannel("auth");
ch.postMessage({ type: "logout" });
ch.onmessage = (e) => { if (e.data.type === "logout") goToLogin(); };
Enter fullscreen mode Exit fullscreen mode

Five lines, no library. Use it for: logout sync, cache invalidation across tabs, "this item just changed" notifications.

For more general cross window coordination, the Web Locks API ensures only one tab does a piece of work at a time:

await navigator.locks.request("sync-notes", async () => {
  await syncNotesWithServer();
});
Enter fullscreen mode Exit fullscreen mode

Other tabs that try to acquire the lock will queue. Brilliant for "only one tab should be syncing".

API 7: Clipboard, Share, File access, the human integration layer

These are the APIs that make a web app feel native.

Clipboard

await navigator.clipboard.writeText("hello");
const text = await navigator.clipboard.readText();
Enter fullscreen mode Exit fullscreen mode

Modern, async, permission gated. The old document.execCommand("copy") is deprecated. For images and other rich data, use navigator.clipboard.write with ClipboardItem.

Web Share API

if (navigator.share) {
  await navigator.share({
    title: "Mochi's Blog",
    text:  "A great post",
    url:   "https://example.com/post",
  });
}
Enter fullscreen mode Exit fullscreen mode

On mobile, this opens the system share sheet. On desktop, it falls back gracefully. One line replaces a custom share modal.

File System Access

const handle = await window.showSaveFilePicker({
  suggestedName: "notes.json",
  types: [{ description: "JSON", accept: { "application/json": [".json"] } }],
});
const writable = await handle.createWritable();
await writable.write(JSON.stringify(data));
await writable.close();
Enter fullscreen mode Exit fullscreen mode

Real "save as" dialog, real file. Limited to Chromium browsers in 2026 but excellent for productivity apps.

The simpler alternative for downloads is the venerable <a download>:

const url = URL.createObjectURL(new Blob([data], { type: "application/json" }));
const a = Object.assign(document.createElement("a"), { href: url, download: "notes.json" });
a.click();
URL.revokeObjectURL(url);
Enter fullscreen mode Exit fullscreen mode

API 8: View Transitions, navigation feels smooth

A genuinely magical API. Animate between any two DOM states with one line.

document.startViewTransition(() => {
  // any DOM mutation here
  setTheme("dark");
});
Enter fullscreen mode Exit fullscreen mode

The browser captures a snapshot before, applies your changes, captures after, and animates between them. With CSS view-transition-name on shared elements, you get FLIP-style transitions for free.

In 2026, the cross document version (@view-transition) lets multi page apps animate between full page navigations as smoothly as SPAs. This is one of the reasons SPAs are no longer the only path to good UX.

API 9: Scheduler, Idle Detection, and modern timing

The scheduler family of APIs, increasingly supported, gives you fine grained control over priority:

// new in 2024+ browsers
await scheduler.yield();        // give the browser a frame
scheduler.postTask(work, { priority: "background" });
Enter fullscreen mode Exit fullscreen mode

If scheduler.yield() is not available, the polyfill is a setTimeout(0) plus a microtask check.

The classic timing tools still matter:

  • requestAnimationFrame for any visual update tied to the next frame. Always use it for animations driven by JS.
  • performance.now() for high resolution timing.
  • PerformanceObserver to subscribe to LCP, INP, CLS, long tasks, navigation timing, resource timing. This is what web-vitals is built on.
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) console.log(entry);
}).observe({ entryTypes: ["longtask", "layout-shift", "largest-contentful-paint"] });
Enter fullscreen mode Exit fullscreen mode

Drop into devtools when you want to see what is actually slow.

API 10: Notifications, Push, Background Sync

For "this app feels real" features:

  • Notifications: new Notification("Hi") with permission. Used as the visual layer for Push.
  • Push: a Service Worker can receive pushes from your server even when the page is closed. Requires the user to grant permission and your server to send via the Web Push protocol.
  • Background Sync: the browser will retry a queued task when the user comes back online. Perfect for "send this comment whenever there is connectivity".

These are senior level features. Only enable them where they pay off. Most users have notification fatigue, so ask politely or not at all.

API 11: The "knows about the user" APIs (use sparingly)

The platform has a long tail of "tell me about the device" APIs. Senior engineers know they exist and use them with care.

  • matchMedia("(prefers-color-scheme: dark)").matches for theme detection.
  • matchMedia("(prefers-reduced-motion: reduce)").matches to respect motion preferences.
  • navigator.connection (Network Information API) for adaptive loading. Slow 3G? Send fewer images.
  • document.visibilityState to pause work when the tab is hidden.
  • navigator.locks (already mentioned).
  • Battery, Bluetooth, USB, Serial, Geolocation: each is a permissioned API, useful for very specific apps.

The principle: respect the user, ask before you peek. Permission prompts kill conversion if used carelessly.

API 12: Modern selection, drag, paste, undo

A handful of small APIs that delete entire libraries:

  • getSelection() for the current text selection.
  • document.execCommand is deprecated. For rich text, use contenteditable plus a library like TipTap or Lexical.
  • InputEvent with inputType ("insertText", "deleteContentBackward", etc.) for fine grained input handling.
  • HTMLDialogElement.showModal() for native modals (we covered this in the HTML post).
  • <details> and <summary> for native disclosures, no JS needed.
  • <input type="search">, type="date", type="time", type="color" for native pickers on mobile.

A surprising amount of "I need a library for this" turns into "the browser already does it" once you check first.

API 13: WebRTC, WebSockets, SSE, beyond request/response

Three transports for real time:

  • WebSockets for bidirectional persistent connections. We mentioned this in the HTTP post.
  • Server-Sent Events (EventSource) for one way streaming from server to client. Simpler than WebSockets, plays nicely with HTTP infrastructure.
  • WebRTC for peer to peer audio, video, and data. The transport behind every browser based video call.

Most apps need SSE for live updates and never touch the others. Reach for them when you have a specific need (real time collaboration, video, gaming).

A peek under the hood

What really happens when you call a platform API:

  1. JavaScript calls into the browser's C++ implementation.
  2. The browser does the work (often on another thread or in another process).
  3. The result comes back as a Promise resolution, an event, or a callback.
  4. Your code runs in the JS event loop.

Two consequences for senior engineers:

  • Native APIs are usually faster than userland equivalents because the heavy lifting happens off the main thread.
  • Browser support varies. Use caniuse.com before committing. For features you must have everywhere, polyfills exist. For features that gracefully degrade, feature detect with if ("foo" in window) and ship the better experience to capable browsers.

Tiny tips that will save you later

  • Read MDN. It is the single best web platform documentation, and it is free.
  • Use caniuse.com before adopting any platform API. Browser share matters.
  • Feature detect, do not user agent sniff. if ("share" in navigator) is the right check.
  • Cancel everything. AbortController works with fetch, with addEventListener, with most modern APIs.
  • Use IntersectionObserver instead of scroll listeners.
  • Use ResizeObserver instead of window resize listeners when you only care about an element.
  • Use BroadcastChannel for cross tab communication.
  • Use idb-keyval or Dexie for IndexedDB unless you really enjoy callbacks.
  • Use Workbox or Vite PWA for Service Workers.
  • Use view-transition for navigation animations. Free polish.
  • Respect prefers-reduced-motion and prefers-color-scheme. They are one query away.
  • Keep platform APIs small. Wrap each in a tiny module so the call sites do not get noisy.

Wrapping up

So that is the whole story. The web platform stopped being a dumb document viewer years ago. Modern browsers ship a small operating system: a network stack with cancellation and streaming, three storage tiers, real threads, real schedulers, observers for everything that used to need a polling loop, broadcast channels for cross tab life, file system access, share sheets, push notifications, and beautiful view transitions.

A senior frontend engineer treats these as the first toolkit, not the last resort. The libraries on npm exist because the platform was missing the feature. Many features have shipped. Many libraries can come out of your package.json if you check what the browser already does.

Once that map is in your head, you write less code, ship smaller bundles, and build apps that feel like the device they run on.

Happy platforming, and may your dependency list stay short.

Top comments (1)

Collapse
 
imann_12 profile image
Iman

This was a good read. Some of these APIs (like IndexedDB native, or Service Worker hand-rolling) still have sharp edges where a thin library is the responsible choice. But the core point stands: reach for the platform first, then reach for npm when the platform's ergonomics truly fail you.