<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Apps on Smashing Magazine — For Web Designers And Developers</title><link>https://www.smashingmagazine.com/category/apps/index.xml</link><description>Recent content in Apps on Smashing Magazine — For Web Designers And Developers</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 12 May 2026 16:11:33 +0000</lastBuildDate><item><author>Durgesh Pawar</author><title>The Architecture Of Local-First Web Development</title><link>https://www.smashingmagazine.com/2026/05/architecture-local-first-web-development/</link><pubDate>Wed, 06 May 2026 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/05/architecture-local-first-web-development/</guid><description>What does it really take to build local-first web apps in 2026? A grounded, experience-driven perspective for developers who&amp;rsquo;ve been doing this long enough to be skeptical of silver bullets.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/05/architecture-local-first-web-development/" />
              <title>The Architecture Of Local-First Web Development</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>The Architecture Of Local-First Web Development</h1>
                  
                    
                    <address>Durgesh Pawar</address>
                  
                  <time datetime="2026-05-06T10:00:00&#43;00:00" class="op-published">2026-05-06T10:00:00+00:00</time>
                  <time datetime="2026-05-06T10:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Last October, I was sitting in a hotel room in Lisbon, the night before I was supposed to demo a project management tool my team had spent four months building. The hotel Wi-Fi was doing that thing where it <em>connects</em> but nothing actually loads. And I watched our app, this thing I was genuinely proud of, render a blank screen with a spinner. Then a timeout error. Then nothing.</p>

<p>I pulled out my phone, tethered to cellular, and got a shaky connection. The app loaded, but every click was a two-second wait. Create a task? Spinner. Move a task between columns? Spinner. I sat there thinking: we built a front end in React, a back end in Node, a Postgres database, a Redis cache, a GraphQL API with six resolvers just for the task board. All that infrastructure, and the damn thing can’t show me my own data without a round-trip to a server 3,000 miles away.</p>

<p>That was the night I started seriously looking at <strong>local-first architecture</strong>. Not because I read a blog post or saw a tweet. Because I was <em>embarrassed</em>.</p>

<p>I want to be upfront about something: I spent the first year or so dismissing local-first as academic. I read the <a href="https://www.inkandswitch.com/local-first-software/">Ink &amp; Switch “Local-First Software” paper</a> when it came out in 2019 and thought, <em>“Cool research, not practical for real apps.”</em> I was wrong. The tooling in 2019 genuinely wasn’t ready. But I was also being lazy, defaulting to the architecture I already knew. The paper laid out seven ideals for software: <strong>fast, multi-device, offline, collaboration, longevity, privacy, user ownership</strong>. And I remember thinking those sounded like a wish list, not engineering requirements.</p>

<p>Seven years later, I’ve shipped three production apps using local-first patterns. I’ve also ripped local-first out of two projects where it was the wrong call. I have opinions. Some of them are probably wrong. But they’re earned.</p>

<p>So here’s what I actually think about building local-first web apps in 2026, written for developers who’ve been doing this long enough to be skeptical of silver bullets.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="what-local-first-actually-means-and-the-confusion-that-won-t-die">What “Local-First” Actually Means (And The Confusion That Won’t Die)</h2>

<p>I need to clear something up because I keep having this conversation at meetups. <strong>Local-first is not offline-first.</strong> It’s not “add a service worker and call it a day.” It’s not a synonym for PWA. I’ve seen all of these conflated in conference talks, and it drives me a little crazy.</p>

<p>Offline-first means your app handles network loss gracefully, but <a href="https://www.smashingmagazine.com/2019/04/cloudflare-workers-serverless/">the server is still the source of truth</a>. When the network comes back, the server wins. Cache-first (service workers caching responses) is a performance optimization. You’re serving stale data faster, which is great, but you haven’t changed who <em>owns</em> the data. PWAs are a delivery mechanism: installable, cached, push notifications. None of these is a data architecture.</p>

<p><strong>Local-first is a data architecture.</strong> Your user’s device holds the primary copy of their data. The app reads and writes to a local database. Renders instantly. Syncs with servers or other devices in the background. The server, when it exists, is a sync peer with some special authority (authentication, backup, access control). But it’s not the gatekeeper.</p>

<p>The Ink &amp; Switch paper defined seven ideals, and I think they still hold up. But the one that matters most in practice, the one that changes how you build everything, is this:</p>

<blockquote>The client is not a thin view requesting permission to show data. The client is a <strong>node</strong> in a distributed system with its own database.</blockquote>

<p>That distinction sounds subtle. It isn’t. It changes your entire stack.</p>

<h2 id="be-honest-early-when-you-should-not-do-this">Be Honest Early: When You Should Not Do This</h2>

<p>I’m putting this near the top because I’ve watched too many developers (including myself, once) get excited about a new architecture and shoehorn it into projects where it doesn’t belong. I wasted about six weeks trying to make a local-first approach work for an internal analytics dashboard at a previous job. My colleague Sarah finally pulled me aside and said, <em>“The data is generated on the server. There’s nothing to replicate to the client. What are you doing?”</em> She was right.</p>

<p>Local-first is a bad fit when your data is primarily server-generated. Analytics dashboards, social media feeds, search results: the server <em>produces</em> this data, so the client consuming it via API requests is completely fine.</p>

<p>It’s wrong for systems that need strong transactional consistency. Banking, payment processing, and inventory management. If two people try to buy the last item in stock, you need a single authoritative database making that decision with <a href="https://en.wikipedia.org/wiki/ACID">ACID</a> guarantees. Eventual consistency will lose you money, or worse.</p>

<p>It’s overkill for simple CRUD apps with no offline or collaboration needs. If you’re building an internal admin panel used by five people in an office with good internet, adding a sync engine is over-engineering. And it’s physically impractical for massive datasets that won’t fit on client devices.</p>

<p>But here’s where it shines: note-taking, document editing, collaborative design tools, project management, field apps with unreliable connectivity, basically anything where <strong>data privacy is a selling point</strong>, as well as anything with <strong>real-time collaboration</strong>. In other words, it’s great for <strong>user-generated data</strong> that benefits from instant interaction and should survive the server going down.</p>

<p>One more thing I wish someone had told me earlier: you don’t have to go all-in. I’ve had the best results using local-first for <em>specific features</em> within otherwise traditional apps. Offline drafts in a blog editor. Real-time collaborative notes inside a project management tool that’s otherwise standard REST.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20%e2%80%9cspectrum%20of%20local-first%e2%80%9d%20is%20a%20real%20thing,%20and%20starting%20with%20one%20feature%20is%20how%20I%e2%80%99d%20recommend%20anyone%20begin.%0a&url=https://smashingmagazine.com%2f2026%2f05%2farchitecture-local-first-web-development%2f">
      
The “spectrum of local-first” is a real thing, and starting with one feature is how I’d recommend anyone begin.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<h2 id="replicas-not-requests">Replicas, Not Requests</h2>

<p>If you’ve used Git, you already understand the mental model.</p>

<p>SVN (remember SVN?) was centralized. One server. You check out files, make changes, and commit to the server. Server down? Can’t commit. Can’t even see history.</p>

<p>Git gave every developer a full clone. You commit locally, branch locally, and merge locally. Push and pull when you’re ready. The remote repository is important, but it’s not the only copy of the truth.</p>

<p><strong>Local-first web development is Git for application data.</strong> Every client device holds a replica (full or partial) of the relevant data. Writes happen locally. Sync is push/pull in the background. Conflicts get resolved through defined merge strategies.</p>

<p>I remember the first time this clicked for me in practice. I was prototyping a task board, and I wrote a function to add a task. In our old architecture, it would be:</p>

<ol>
<li>POST to API.</li>
<li>Wait for the response.</li>
<li>If success, update the local state.</li>
<li>If failure, show error toast and maybe roll back optimistic update.</li>
</ol>

<p>In the local-first version, it was: write to local SQLite, done. The UI updated instantly because it was reading from the same local database. Sync happened whenever. No loading state, no error handling for the write itself, no optimistic update logic (because there’s nothing to be “optimistic” about; the local write <em>is</em> the state).</p>

<p>The implications ripple through everything. You don’t need React Query or SWR for data fetching, because you’re not fetching. You don’t need Redux or Zustand for server-derived state, because the local database <em>is</em> your state. Your routing doesn’t trigger API calls. Authentication works differently because the server isn’t checking permissions on every read.</p>

<p>Here’s a visual comparison that might help if you’re the kind of person (like me) who thinks spatially:</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/architecture-local-first-web-development/local-first-vs-traditional-architecture.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="800"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/architecture-local-first-web-development/local-first-vs-traditional-architecture.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/architecture-local-first-web-development/local-first-vs-traditional-architecture.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/architecture-local-first-web-development/local-first-vs-traditional-architecture.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/architecture-local-first-web-development/local-first-vs-traditional-architecture.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/architecture-local-first-web-development/local-first-vs-traditional-architecture.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/architecture-local-first-web-development/local-first-vs-traditional-architecture.png"
			
			sizes="100vw"
			alt="Traditional request/response architecture vs. local-first architecture"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Traditional request/response architecture vs. local-first architecture. (<a href='https://files.smashing.media/articles/architecture-local-first-web-development/local-first-vs-traditional-architecture.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>On the left, every user interaction is a round-trip. Click, wait, render. On the right, reads and writes hit the local database directly. The sync server is still there, but it’s doing its work in the background. The user never waits for it. That’s the fundamental shift.</p>

<p>But I’m getting ahead of myself. Before we can talk about sync and conflicts, we need to talk about where the data actually lives on the client.</p>

<h2 id="where-data-lives-on-the-client">Where Data Lives on the Client</h2>

<p>Forget <code>localStorage</code>. It’s synchronous (blocks the main thread), caps at 5-10 MB, and only stores strings. It’s fine for a theme preference. It’s not a database.</p>

<p>IndexedDB is the workhorse that nobody loves. It’s in every browser, it’s asynchronous, it can handle hundreds of megabytes, and its API is absolutely miserable to work with. I’ve used it directly a grand total of once. Now I use it through abstractions or, more often, I don’t use it at all.</p>

<p>Because the real story in 2026 is SQLite running in the browser via WebAssembly.</p>

<p>I know that sounds like a party trick, but it’s not. SQLite compiled to WASM, persisted to the Origin Private File System (OPFS), gives you a <em>real relational database</em> in the browser. Full SQL queries. Transactions. Indexes. The works.</p>

<p>OPFS is the newer API that makes this practical. It gives web apps a sandboxed file system with high-performance synchronous access (in Web Workers), which is exactly what SQLite needs. Before OPFS, you could run SQLite in memory and manually persist to IndexedDB, which worked but was slow and fragile.</p>

<p>Here’s roughly what initialization looks like in a real project (I’m using <a href="https://github.com/rhashimoto/wa-sqlite"><code>wa-sqlite</code></a> here, which is the library I’ve had the best luck with):</p>

<div class="break-out">
<pre><code class="language-typescript">import { SQLiteAPI } from 'wa-sqlite';
import { OPFSCoopSyncVFS } from 'wa-sqlite/src/examples/OPFSCoopSyncVFS.js';

async function initDatabase() {
  const module = await SQLiteAPI.initialize();
  const vfs = new OPFSCoopSyncVFS('pm-tool-db');
  await vfs.initialize(module);

  const db = await module.open&#95;v2('workspace.db');

  // HACK: wa-sqlite doesn't handle concurrent writes well on Safari,
  // so we serialize through a queue. See vlcn-io/wa-sqlite&#35;247
  await module.exec(db, `PRAGMA journal&#95;mode=WAL`);

  await module.exec(db, `
    CREATE TABLE IF NOT EXISTS tasks (
      id TEXT PRIMARY KEY,
      title TEXT NOT NULL,
      status TEXT DEFAULT 'backlog',
      assignee&#95;id TEXT,
      project&#95;id TEXT NOT NULL,
      position REAL DEFAULT 0,
      created&#95;at TEXT DEFAULT (datetime('now')),
      updated&#95;at TEXT DEFAULT (datetime('now'))
    )
  `);

  return db;
}
</code></pre>
</div>

<p>In production, I wrap all database access in a write queue that serializes mutations. I also log every failed write to Sentry with the full SQL statement (scrubbed of PII, obviously) because debugging database issues in a user’s browser is hell without that telemetry.</p>

<p>A gotcha I wasted almost two days on: Safari’s OPFS implementation behaves differently from Chrome’s in subtle ways. Specifically, I hit a bug where <code>createSyncAccessHandle()</code> would silently fail in certain iframe contexts on Safari 18. There’s no error, no exception. It just doesn’t work. I ended up falling back to IndexedDB-backed persistence on Safari, which was slower but at least functioned. (I’m told <a href="https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes">Safari <sup>19</sup>&frasl;<sub>26</sub></a> fixes this, but I haven’t verified it yet.)</p>

<p>Quick comparison of the options I’ve actually used:</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Storage</th>
            <th>Good For</th>
      <th>Watch Out For</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>IndexedDB</td>
            <td>Broad compatibility, moderate data</td>
      <td>Terrible DX, no SQL, verbose</td>
        </tr>
        <tr>
            <td>OPFS + SQLite WASM</td>
            <td>Relational data, complex queries, serious apps</td>
      <td>Safari quirks, ~400KB bundle addition</td>
        </tr>
        <tr>
            <td>PGlite (Postgres in WASM)</td>
            <td>Full Postgres compatibility on client</td>
      <td>Newer, larger bundle, still maturing</td>
        </tr>
    </tbody>
</table>

<p>I’ve also tried <a href="https://github.com/vlcn-io/cr-sqlite"><code>cr-sqlite</code></a>, which adds CRDT column support directly to SQLite tables. Clever idea, but I found it too early-stage for production use when I evaluated it in late 2025. The merge semantics were sometimes surprising, and debugging CRDT state inside SQLite was painful. I’d revisit it later this year.</p>

<div class="partners__lead-place"></div>

<h2 id="the-part-that-s-actually-hard">The Part That’s Actually Hard</h2>

<p>Storing data locally is a solved problem. Syncing it reliably across devices and users is where you earn your gray hairs.</p>

<p>When multiple replicas can independently read and write, you need a mechanism to reconcile changes. There are basically four approaches, and I’ve used three of them.</p>

<p><strong>CRDTs (Conflict-Free Replicated Data Types)</strong> are data structures designed so that concurrent edits can always be merged without conflicts, mathematically guaranteed. Yjs is the most popular implementation in JavaScript, and it’s genuinely excellent for real-time collaborative text editing. I used it to build a collaborative document editor at my last company, and the experience was mostly good, though I’ll get into the pain points in the conflict resolution section.</p>

<p>Here’s what setting up a shared Yjs document looks like in practice:</p>

<pre><code class="language-typescript">import &#42; as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

const ydoc = new Y.Doc();

const provider = new WebsocketProvider(
  'wss://sync.our-app.dev',
  'workspace-a1b2c3d4',
  ydoc
);

const tasks = ydoc.getMap('tasks');

// Add a task
const task = new Y.Map();
task.set('title', 'Review Q3 roadmap draft');
task.set('completed', false);
task.set('assignee', 'maria');
// TODO: type this properly once; yjs exports better TS types
// for nested maps. For now, this works fine.
tasks.set('f47ac10b-58cc-4372-a567-0e02b2c3d479', task as any);

tasks.observeDeep(() =&gt; {
  // Re-render UI. In practice, I debounce this to ~16ms
  // because observeDeep fires a LOT during active collaboration
  renderTaskList(tasks.toJSON());
});
</code></pre>

<p><a href="https://github.com/automerge/automerge">Automerge</a> is the other major CRDT library, backed by Rust and with a document-oriented model. I’ve used it less, but I know teams who swear by it. <a href="https://github.com/loro-dev/loro">Loro</a> is newer, Rust-based, and claims better performance. I haven’t shipped anything with Loro yet.</p>

<p><strong>Database replication</strong> is the other big approach, and honestly, for most apps that don’t need Google Docs-style real-time text editing, I think it’s the better choice. The idea is straightforward: replicate rows between a server database (Postgres) and a client database (SQLite) with a sync engine managing the plumbing.</p>

<p><a href="https://www.powersync.com">PowerSync</a> does this well. It gives you one-way replication from Postgres to client SQLite with a write-back path for mutations. ElectricSQL is more ambitious, going for full active-active sync between Postgres and SQLite. I’ve used PowerSync in production and <a href="https://electric-sql.com/docs/intro">ElectricSQL</a> in prototypes. PowerSync felt more stable when I evaluated them both in early 2026, but ElectricSQL’s approach is more powerful if they nail the execution.</p>

<p><a href="https://github.com/aspen-cloud/triplit">Triplit</a> takes a different angle entirely: it’s a full-stack database with sync built in, so you don’t think about “client DB” and “server DB” separately. I haven’t tried it beyond a weekend prototype, but the developer experience was surprisingly nice.</p>

<p><strong>Event sourcing</strong> (syncing a log of mutations rather than the current state) is the approach <a href="https://livestore.dev">LiveStore</a> takes. I find it intellectually appealing and occasionally useful, but in practice, I’ve found that reconstructing state from an event log adds complexity that most apps don’t need. My controversial opinion: Event sourcing is over-recommended for application development. It’s great for audit logs and certain domains, but for a task board? Just sync the rows.</p>

<p>Not everyone will agree with that. I know event sourcing has passionate advocates, and I’ve been told I’m wrong about this at least twice at conferences. Maybe I just haven’t built the right app for it yet.</p>

<h2 id="conflicts-the-thing-everyone-s-afraid-of">Conflicts: The Thing Everyone’s Afraid Of</h2>

<p>I used to think conflict resolution was a terrifying, unsolvable problem. After building three apps that handle it, I’d revise that to: it’s a <em>manageable</em> problem that requires you to think carefully about your specific data model, and most developers overthink it.</p>

<p>Conflicts happen when two replicas modify the same data without seeing each other’s changes. User A edits a task title on their phone while offline. User B edits the same title on their laptop. Both come back online. Now what?</p>

<p>My first attempt at handling this was embarrassingly naive:</p>

<pre><code class="language-typescript">// My first try. Don't do this.
function resolveConflict(local: any, remote: any) {
  // just... take the remote one? sure?
  return remote;
}
</code></pre>

<p>The problem is obvious: local changes get silently dropped. User A edits a title, syncs, and their edit vanishes. They don’t even know it happened.</p>

<p>What actually works for most cases is <strong>last-write-wins (LWW)</strong> at the <em>field</em> level, not the record level. If User A changes the title and User B changes the due date, you keep both changes because they touched different fields. You only have a real conflict when both modified the same field, and then you pick the later timestamp.</p>

<div class="break-out">
<pre><code class="language-typescript">interface FieldValue {
  value: string | number | boolean;
  // ISO timestamp with enough precision to break most ties
  updatedAt: string;
  // Client ID as tiebreaker when timestamps match.
  // This happens more often than you'd think.
  clientId: string;
}

function pickWinner(a: FieldValue, b: FieldValue): FieldValue {
  const timeA = new Date(a.updatedAt).getTime();
  const timeB = new Date(b.updatedAt).getTime();
  if (timeA !== timeB) return timeA &gt; timeB ? a : b;
  // Deterministic tiebreaker when timestamps match
  return a.clientId &gt; b.clientId ? a : b;
}

// In practice, I apply this per-field across the whole record.
function mergeTask(local: Record&lt;string, FieldValue&gt;, remote: Record&lt;string, FieldValue&gt;) {
  const merged: Record&lt;string, FieldValue&gt; = {};
  const allKeys = new Set([...Object.keys(local), ...Object.keys(remote)]);
  for (const key of allKeys) {
    if (!local[key]) { merged[key] = remote[key]; continue; }
    if (!remote[key]) { merged[key] = local[key]; continue; }
    merged[key] = pickWinner(local[key], remote[key]);
  }
  return merged;
}
</code></pre>
</div>

<p>In our production app, this handles about 95% of conflicts without any user-visible issues. For the remaining cases (two people editing the same text field), LWW means one person’s edit silently wins. For a task title? Honestly, that’s usually fine. For a document body? No. That’s where CRDTs earn their keep.</p>

<p>But there’s a subtler problem I didn’t appreciate until I hit it: <strong>semantic conflicts</strong>. Data merges cleanly at the structural level, but the result is nonsensical. Two users, both offline, book the same 2 PM meeting slot with different meetings. Field-level merge accepts both writes because they’re writing to different records. No structural conflict. But you’ve got a double-booking, and your merge function has no idea that’s a problem.</p>

<p>Semantic conflicts require application-level validation, and that has to happen on the server during sync. Your sync engine merges the data structurally, but <em>your</em> server needs to check domain invariants before accepting the result. The approach I’ve landed on (after getting it wrong twice) is: validate on the server during the write-back phase, but <em>flag</em> violations rather than silently rejecting them.</p>

<p>Here’s what I mean. When the client pushes mutations to the server during sync, the server runs them through a constraint validation layer before applying them to Postgres:</p>

<div class="break-out">
<pre><code class="language-typescript">interface SyncViolation {
  type: 'scheduling&#95;conflict' | 'capacity&#95;exceeded' | 'stale&#95;assignment';
  recordId: string;
  description: string;
  // The conflicting records so the client can show context
  conflictingRecords: string[];
  // When was this violation detected
  detectedAt: string;
}

async function validateSyncBatch(
  mutations: SyncMutation[],
  serverDb: Database
): Promise&lt;{ accepted: SyncMutation[]; violations: SyncViolation[] }&gt; {
  const accepted: SyncMutation[] = [];
  const violations: SyncViolation[] = [];

  for (const mutation of mutations) {
    if (mutation.table === 'calendar&#95;events') {
      // Check for double-booking
      const overlapping = await serverDb.query(
        `SELECT id, title FROM calendar&#95;events
         WHERE room&#95;id = ? AND id != ?
         AND start&#95;time &lt; ? AND end&#95;time &gt; ?`,
        [mutation.data.room&#95;id, mutation.data.id,
         mutation.data.end&#95;time, mutation.data.start&#95;time]
      );

      if (overlapping.length &gt; 0) {
        violations.push({
          type: 'scheduling&#95;conflict',
          recordId: mutation.data.id,
          description: `Conflicts with "${overlapping[0].title}"`,
          conflictingRecords: overlapping.map(r =&gt; r.id),
          detectedAt: new Date().toISOString()
        });
        // Still accept the write, but flag it
        // The alternative is rejecting it, but then the user's
        // local state and server state diverge, and that's worse
        accepted.push(mutation);
        continue;
      }
    }
    accepted.push(mutation);
  }

  return { accepted, violations };
}
</code></pre>
</div>

<p>The key decision here &mdash; and I went back and forth on this &mdash; is that we <em>accept</em> the conflicting write and flag it, rather than rejecting it outright. If you reject it, the user’s local database has a record that the server refuses to acknowledge, and now you’re in a state divergence situation that’s genuinely hard to recover from. I tried the rejection approach first, and it led to ghost records on the client that users couldn’t delete because they didn’t exist on the server. Nightmare.</p>

<p>So instead, the server accepts the write, stores the violation, and syncs the violation back to the client. The client shows a non-blocking notification: <em>“Your meeting ‘Q3 Planning’ conflicts with ‘Design Review’ in Room B at 2 PM. Tap to resolve.”</em> The user taps, sees both meetings, and picks one to reschedule or cancel. The resolution is a normal write that syncs back.</p>

<p>Is this perfect? No. There’s a window between when the violation is created and when the user resolves it, where both conflicting records exist. For meeting rooms, that’s tolerable. For something like inventory management where two people “buy” the last item, that window is unacceptable, and that’s exactly why I said earlier that local-first is wrong for systems requiring strong transactional consistency.</p>

<p>I’m still iterating on this pattern. The violation table grows if users ignore notifications (we expire them after 72 hours, which feels arbitrary). And deciding <em>which</em> invariants to validate on the server requires you to essentially maintain a parallel set of business rules outside your client-side application logic. It’s not elegant. But it works, and it’s the best approach I’ve found for the class of apps I’m building. If you’ve built something cleaner, I genuinely want to hear about it.</p>

<p>For CRDTs like Yjs, conflict resolution at the character level (for text) works remarkably well. Two people typing in the same paragraph will see both sets of characters appear in a sensible order. But CRDT merging of structured data (maps, arrays, nested objects) can produce results that surprise you. I once watched a Yjs-backed task list duplicate items after a merge because two users had reordered the same list offline, and the CRDT’s list merge semantics interleaved their orderings. Technically correct. Practically confusing. We ended up adding a post-merge de-duplication step, which felt like a hack but solved the problem.</p>

<p>When should you surface conflicts to the user, Git-style? In my experience, almost never for typical app data. Users don’t want to resolve merge conflicts. They want the app to figure it out. The exception is high-stakes content: legal documents, medical records, anything where silently dropping an edit could cause real harm.</p>

<h2 id="the-tools-right-now">The Tools Right Now</h2>

<p>I’m going to give you my honest read on the tools available as of mid-2026, with the caveat that this space is moving fast enough that some of this might be outdated by the time you read it.</p>

<p><strong>Yjs</strong> is the most mature CRDT library. Production-ready, huge community, integrates with most collaborative editors (TipTap, BlockNote, Lexical). If you need real-time collaborative editing, start here.</p>

<p><strong>Automerge</strong> is solid, Rust-backed, and takes a more document-oriented approach than Yjs. I’ve seen it used well in apps where the data model fits a document metaphor. Fewer integrations than Yjs, but the core is well-engineered.</p>

<p><strong>PowerSync</strong> is what I’d recommend for teams that have an existing Postgres back-end and want to add offline support. It’s production-ready, the docs are good, and the mental model (Postgres syncs to client SQLite, client writes go through a defined upload path) is easy to reason about. In our app, initial sync for a workspace with around 5,000 tasks takes about 1.2 seconds on a decent connection and about 3.5 seconds on a throttled 3G simulation. That was acceptable for us.</p>

<p><strong>ElectricSQL</strong> is going for something more ambitious: true active-active replication between Postgres and SQLite, with “shapes” defining what data syncs to which client. I want this to succeed because the developer experience in prototypes was excellent. But when I evaluated it for production in February 2026, I hit enough rough edges (particularly around shape management and reconnection behavior) that I went with PowerSync instead. I plan to revisit it.</p>

<p><strong>Triplit</strong> impressed me in a weekend prototype. Full-stack database with sync built in, nice TypeScript API. I haven’t stress-tested it with real production load, and I’d want to before committing.</p>

<p><a href="https://zero.rocicorp.dev"><strong>Zero</strong></a> (from Rocicorp, the Replicache people) is interesting because it takes a query-based approach to sync, which is different from the row-replication model. Replicache was sunset in favor of Zero, which tells you something about how fast approaches are evolving in this space. Worth watching, but I wouldn’t build on it yet for a production app.</p>

<p><a href="https://tinybase.org/"><strong>TinyBase</strong></a> is a lightweight reactive store that’s great for smaller apps or prototyping. I used it for a personal side project (a reading tracker) and liked it a lot. Not sure I’d use it for a team-scale product.</p>

<p><a href="https://pglite.dev"><strong>PGlite</strong></a> (Postgres compiled to WASM) is wild. Same SQL dialect on client and server. Combined with ElectricSQL, you could theoretically run identical queries everywhere. I think this is where things are heading long-term, but PGlite’s bundle size and memory footprint are still concerns for mobile browsers.</p>

<p>One thing the Replicache sunset taught me: don’t bet your architecture on a single tool from a small company without a fallback plan. I keep my sync layer abstracted enough that I could swap engines in a few weeks, not months. I know that sounds like premature abstraction, but in a space this young, I think it’s just prudence.</p>

<div class="partners__lead-place"></div>

<h2 id="building-a-real-app-architecture-auth-and-migrations">Building A Real App: Architecture, Auth, And Migrations</h2>

<p>I want to walk through how I actually structure a local-first app in practice, because the layer diagrams you see in blog posts rarely match what the code looks like.</p>

<p>My current stack for a collaborative project management tool looks like this:</p>

<ul>
<li><strong>UI:</strong> React components that never call <code>fetch()</code> for data reads.</li>
<li><strong>Query layer:</strong> <code>useLiveQuery</code> hooks that subscribe to the local SQLite database and re-render automatically when data changes.</li>
<li><strong>Local database:</strong> SQLite via wa-sqlite, persisted to OPFS.</li>
<li><strong>Mutation layer:</strong> Plain <code>INSERT</code>/<code>UPDATE</code>/<code>DELETE</code> statements against local SQLite.</li>
<li><strong>Sync:</strong> PowerSync managing replication between local SQLite and our Postgres back-end.</li>
<li><strong>Server:</strong> Postgres, a Node.js auth service, and a small sync validation layer.</li>
</ul>

<p>The component code ends up looking almost absurdly simple compared to what I used to write:</p>

<div class="break-out">
<pre><code class="language-typescript">import { useLiveQuery } from '@powersync/react';
import { db } from '../lib/database';

function TaskBoard({ projectId }: { projectId: string }) {
  const tasks = useLiveQuery(
    `SELECT &#42; FROM tasks WHERE project&#95;id = ? AND archived = 0 ORDER BY position`,
    [projectId]
  );

  async function addTask(title: string) {
    await db.execute(
      `INSERT INTO tasks (id, title, project&#95;id, position, created&#95;at)
       VALUES (?, ?, ?, ?, datetime('now'))`,
      [crypto.randomUUID(), title, projectId, tasks.length]
    );
    // That's it. useLiveQuery picks up the change automatically.
    // No invalidation, no refetch, no loading state.
  }

  // No isLoading check. Data is local. It's always there after the first sync.
  return (
    &lt;div&gt;
      {tasks.map(task =&gt; &lt;TaskCard key={task.id} task={task} /&gt;)}
      &lt;NewTaskInput onSubmit={addTask} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
</div>

<p>Compare that to the React Query + REST equivalent, which would be at least twice the code and include loading states, error states, optimistic update logic with rollback, and cache invalidation. I don’t miss it.</p>

<h3 id="auth-in-a-local-first-world">Auth In A Local-First World</h3>

<p>Authentication works roughly the same as traditional apps: JWT tokens, OAuth flows, and session management. The token authenticates the sync connection rather than every individual request. Offline access works because the data is already local. The user was authenticated when the data was originally synced.</p>

<p>Authorization is trickier, and I think most local-first articles under-explain this. You cannot sync your entire database to every client and rely on client-side code to hide unauthorized data. Someone will open DevTools, find the local SQLite file, and see everything. The client is not a trust boundary.</p>

<p>You enforce authorization at the sync layer. PowerSync has “sync rules” that define which rows go to which clients. ElectricSQL has “shapes.” Either way, the server only sends data that the user is authorized to see. When the client sends writes back, the server validates them against authorization rules before applying them to Postgres. If a user tries to modify something they shouldn’t, the server rejects it during sync.</p>

<p>I also want to mention <strong>end-to-end encryption (E2EE)</strong>, because it pairs naturally with local-first. Since data lives on the client, you can encrypt it before sync. The server stores and relays encrypted blobs it can’t read. Apps like Anytype do this. We haven’t implemented E2EE in our current app, but it’s on the roadmap for when we handle more sensitive data.</p>

<h3 id="schema-migrations-on-a-thousand-devices">Schema Migrations On A Thousand Devices</h3>

<p>This one caught me off guard the first time. On the server, you run a migration against one database you control. On the client, every user has their own database that might be running any version of your schema, depending on when they last opened the app.</p>

<p>I use a simple migration runner that checks a version number at app startup:</p>

<div class="break-out">
<pre><code class="language-typescript">const MIGRATIONS = [
  {
    version: 1,
    sql: `
      CREATE TABLE IF NOT EXISTS tasks (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        status TEXT DEFAULT 'backlog',
        project&#95;id TEXT NOT NULL,
        created&#95;at TEXT DEFAULT (datetime('now'))
      );
    `
  },
  {
    version: 2,
    // Added priority and due&#95;date in sprint 4
    sql: `
      ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0;
      ALTER TABLE tasks ADD COLUMN due&#95;date TEXT;
    `
  },
  {
    version: 3,
    // Denormalized assignee name for offline display.
    // Yes, I know this is a trade-off. The JOIN was killing
    // performance on low-end Android devices.
    sql: `
      ALTER TABLE tasks ADD COLUMN assignee&#95;name TEXT DEFAULT '';
    `
  }
];

async function runMigrations(db: Database) {
  await db.execute(`
    CREATE TABLE IF NOT EXISTS &#95;schema&#95;version (version INTEGER)
  `);

  const rows = await db.execute('SELECT version FROM &#95;schema&#95;version');
  const currentVersion = rows.length &gt; 0 ? rows[0].version : 0;

  for (const migration of MIGRATIONS) {
    if (migration.version &gt; currentVersion) {
      console.log(`Migrating local DB to v${migration.version}`);
      await db.execute('BEGIN');
      try {
        await db.execute(migration.sql);
        await db.execute(
          'INSERT OR REPLACE INTO &#95;schema&#95;version (rowid, version) VALUES (1, ?)',
          [migration.version]
        );
        await db.execute('COMMIT');
      } catch (err) {
        await db.execute('ROLLBACK');
        // In production, this fires a Sentry alert with the
        // migration version and error details
        throw err;
      }
    }
  }
}
</code></pre>
</div>

<p><strong>Design your migrations to be additive.</strong> New columns with defaults. New tables. Don’t rename or drop columns unless you absolutely must, because users running old app versions will still be syncing data, and your server needs to handle the mismatch. I learned this the hard way when I dropped a column that an older client was still writing to, which caused silent sync failures for about 200 users over a weekend. Not fun.</p>

<h2 id="if-i-were-starting-a-new-project-today">If I Were Starting A New Project Today</h2>

<p>I get asked this a lot, so here’s my current answer. It changes every six months or so.</p>

<p>For a collaborative app with real-time features and offline support, I’d start with: <strong>React</strong> on the front end, <strong>PowerSync</strong> for sync, <strong>SQLite via wa-sqlite</strong> on the client (persisted to OPFS with IndexedDB fallback for Safari), and <strong>Supabase</strong> (which gives me Postgres, auth, and row-level security out of the box). I’d use <strong>Yjs</strong> only if I needed rich text collaboration, and I’d avoid it if I didn’t, because CRDTs add meaningful complexity to your data model.</p>

<p>For a simpler app where I mostly need offline support and instant reads but collaboration is secondary, I might skip the sync engine entirely and just use a local SQLite database with a custom sync layer that pushes/pulls from a REST API. I know that sounds like reinventing the wheel, but for simple cases, a custom sync that you fully understand is better than a general-purpose sync engine that adds concepts you don’t need.</p>

<p>I would not currently use ElectricSQL or Zero for production, not because they’re bad, but because I want another 6-12 months of maturity before I’d trust them for something I’m on-call for. I’ve been burned before by building on early-stage infrastructure (I was an early <a href="https://docs.meteor.com">Meteor</a> adopter, if that tells you anything) and I’m more cautious now about where I accept novelty risk.</p>

<h2 id="performance-what-s-actually-fast-and-what-hurts">Performance: What’s Actually Fast And What Hurts</h2>

<p>Reads are instant. That’s not marketing. Querying a local SQLite database for a list of 500 tasks takes under two milliseconds on my M2 MacBook and about eight milliseconds on a mid-range Android phone. No network. No spinner. No loading state.</p>

<p>Writes are instant, too. <code>INSERT INTO tasks</code> runs locally, the UI updates reactively, and sync happens whenever. Users perceive writes as instantaneous because they are.</p>

<p>Initial sync is where you pay the cost. Bootstrapping the local replica on first load (or on a new device) means downloading potentially megabytes of data. In our app, a workspace with 5,000 tasks, 200 projects, and 50 users takes about 1.2 seconds on broadband and four to five seconds on a slow mobile connection. We mitigate this with partial sync (only sync the user’s active projects) and by showing a one-time “Setting up your workspace” screen during the first sync. After that initial sync, incremental updates are tiny.</p>

<p>Bundle size is a real concern. SQLite compiled to WASM adds roughly 400KB gzipped to your JavaScript bundle. That’s not trivial, especially if you care about Time to Interactive on mobile. I lazy-load the database module with dynamic <code>import()</code> so it doesn’t block the initial render.</p>

<p>Memory is the other gotcha. SQLite WASM runs in memory, and on mobile browsers with aggressive memory limits, a large database can cause tab crashes. I haven’t found a great solution for this beyond keeping the synced dataset small through partial sync and being aggressive about pruning old data.</p>

<p><strong>Note</strong>: Speaking of memory issues, I’ve been reading <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/"><em>Designing Data-Intensive Applications</em></a> by Martin Kleppmann for the third time. Every re-read, I catch something new. If you haven’t read it and you’re thinking about distributed data, just stop and read it first.</p>

<h2 id="testing-this-stuff">Testing This Stuff</h2>

<p>I’ll keep this brief because the honest answer is that testing local-first apps is harder than testing traditional apps, and the tooling isn’t great yet.</p>

<p>What works for me: unit tests for merge logic (these are pure functions, easy to test), integration tests that spin up two client instances in memory and verify they converge after concurrent edits, and Playwright E2E tests that use <code>context.setOffline(true)</code> to simulate offline/online transitions.</p>

<p>What I haven’t figured out well: reproducing bugs that only happen during conflict resolution with specific timing. When a user reports that a task <em>“lost its description,”</em> I often can’t reproduce it because I don’t know exactly what sequence of offline edits and sync events led to the conflict. I’ve started logging sync events in more detail (what was sent, what was received, what conflicts were detected, how they were resolved) and shipping those logs to our observability stack. It helps, but it’s not as clean as I’d like.</p>

<p>Property-based testing with something like fast-check is genuinely useful for CRDT logic. Generate random operation sequences, apply them in random orders, and assert convergence. I wish I’d started doing this earlier.</p>

<h2 id="what-i-m-watching-what-worries-me">What I’m Watching, What Worries Me</h2>

<p>I’m excited about where this is going. PGlite (full Postgres in the browser) feels like a glimpse of a future where the client/server data layer distinction just dissolves. You write SQL, it runs everywhere, sync is a runtime concern rather than an architectural decision. We’re not there yet, but you can see it from here.</p>

<p>I’m also watching the convergence of local-first and AI. Running models locally, keeping data on-device, using cloud AI only with explicit consent, and encrypted data. The privacy implications are compelling, and I think <em>“your data never leaves your device”</em> will become a real product differentiator as AI eats more of the software experience.</p>

<p>What worries me is <strong>fragmentation</strong>. Every sync engine uses its own protocol. There’s no standard. If ElectricSQL shuts down (it won’t, probably, but <em>if</em>), migrating to PowerSync isn’t trivial. I abstract my sync layer partly for this reason, but it still makes me nervous.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20web%20has%20standards%20for%20nearly%20everything.%20We%20don%e2%80%99t%20have%20one%20for%20sync,%20and%20I%20don%e2%80%99t%20see%20one%20emerging%20soon.%0a&url=https://smashingmagazine.com%2f2026%2f05%2farchitecture-local-first-web-development%2f">
      
The web has standards for nearly everything. We don’t have one for sync, and I don’t see one emerging soon.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>I’m also worried about the <strong>complexity budget</strong>. Local-first adds real architectural complexity: sync engines, conflict resolution, client-side migrations, partial replication, and auth at the sync boundary. For a team of experienced developers building the right kind of app, that complexity pays for itself many times over. For a team that just needs a CRUD app, it’s a trap.</p>

<p>I keep coming back to something a developer named Kevin said to me at a local-first meetup in Berlin last year:</p>

<blockquote>“The best architecture is the one your team can debug at 2 AM.”</blockquote>

<p>He’s right. If local-first makes your app faster, more reliable, and better for users, and your team understands how the sync works, go for it. If you’re adding it because it sounds cool and you don’t fully understand the failure modes yet, build a prototype first. Learn where it breaks. Then decide.</p>

<p>I’m building my fourth local-first app right now: a collaborative planning tool for small teams, with offline support and optional E2E encryption. It’s the most ambitious thing I’ve attempted with this architecture. I’ll write about how it goes.</p>

<p>If you’re starting out, pick one feature in your current app that would benefit from instant local reads and offline writes. Add a local SQLite database. Wire up reactive queries. See how it feels. I think you’ll have the same reaction I did: oh, <em>this</em> is how it should have always worked.</p>

<h2 id="further-reading">Further Reading</h2>

<ul>
<li>“<a href="https://www.inkandswitch.com/local-first/">Local-First Software</a>” (Ink &amp; Switch): This is still the best starting point.</li>
<li>“<a href="https://www.youtube.com/watch?v=PMVBuMK_pJY">CRDTs: The hard parts”</a>” (Martin Kleppmann, video): Martin’s talks on CRDTs are excellent.</li>
<li>The <a href="https://localfirstweb.dev/">localfirstweb.dev</a> community site: A good directory of tools.</li>
<li><a href="https://docs.powersync.com/">PowerSync Documentation</a></li>
<li><a href="https://electric-sql.com/docs">ElectricSQL Documentation</a></li>
<li><a href="https://docs.yjs.dev/">Yjs Documentation</a></li>
<li><a href="https://automerge.org/docs/hello/">Automerge Documentation</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Declan Chidlow</author><title>Optimizing PWAs For Different Display Modes</title><link>https://www.smashingmagazine.com/2025/08/optimizing-pwas-different-display-modes/</link><pubDate>Tue, 26 Aug 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/08/optimizing-pwas-different-display-modes/</guid><description>Progressive Web Apps (PWAs) are a great way to make apps built for the web feel native, but in moving away from a browser environment, we can introduce usability issues. Declan covers how we can modify our app depending on what display mode is applied to mitigate these issues.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/08/optimizing-pwas-different-display-modes/" />
              <title>Optimizing PWAs For Different Display Modes</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Optimizing PWAs For Different Display Modes</h1>
                  
                    
                    <address>Declan Chidlow</address>
                  
                  <time datetime="2025-08-26T08:00:00&#43;00:00" class="op-published">2025-08-26T08:00:00+00:00</time>
                  <time datetime="2025-08-26T08:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p><a href="https://www.smashingmagazine.com/2020/12/progressive-web-apps/">Progressive web apps</a> (PWA) are a fantastic way to turn web applications into native-like, standalone experiences. They bridge the gap between websites and native apps, but this transformation can be prone to introducing design challenges that require thoughtful consideration.</p>

<p>We define our PWAs <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest">with a manifest file</a>. In our PWA’s manifest, we can select from a collection of display modes, each offering different levels of browser interface visibility:</p>

<ul>
<li><code>fullscreen</code>: Hides all browser UI, using the entire display.</li>
<li><code>standalone</code>: Looks like a native app, hiding browser controls but keeping system UI.</li>
<li><code>minimal-ui</code>: Shows minimal browser UI elements.</li>
<li><code>browser</code>: Standard web browser experience with full browser interface.</li>
</ul>

<p>Oftentimes, we want our PWAs to feel like apps rather than a website in a browser, so we set the display manifest member to one of the options that hides the browser’s interface, such as <code>fullscreen</code> or <code>standalone</code>. This is fantastic for helping make our applications feel more at home, but it can introduce some issues we wouldn’t usually consider when building for the web.</p>

<p>It’s easy to forget just how much functionality the browser provides to us. Things like forward/back buttons, the ability to refresh a page, search within pages, or even manipulate, share, or copy a page’s URL are all browser-provided features that users can lose access to when the browser’s UI is hidden. There is also the case of things that we display on websites that don’t necessarily translate to app experiences.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="407"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png"
			
			sizes="100vw"
			alt="The different PWA display modes as seen on an Android phone running Chrome 138."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The different PWA display modes as seen on an Android phone running Chrome 138. (<a href='https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Imagine a user deep into a form with no back button, trying to share a product page without the ability to copy a URL, or hitting a bug with no refresh button to bail them out!</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aMuch%20like%20how%20we%20make%20different%20considerations%20when%20designing%20for%20the%20web%20versus%20designing%20for%20print,%20we%20need%20to%20make%20considerations%20when%20designing%20for%20independent%20experiences%20rather%20than%20browser-based%20experiences%20by%20tailoring%20the%20content%20and%20user%20experience%20to%20the%20medium.%0a&url=https://smashingmagazine.com%2f2025%2f08%2foptimizing-pwas-different-display-modes%2f">
      
Much like how we make different considerations when designing for the web versus designing for print, we need to make considerations when designing for independent experiences rather than browser-based experiences by tailoring the content and user experience to the medium.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Thankfully, we’re provided with plenty of ways to customise the web.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/image-optimization/">Image Optimization</a></strong>, Addy Osmani’s new practical guide to optimizing and delivering <strong>high-quality images</strong> on the web. Everything in one single <strong>528-pages</strong> book.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/image-optimization/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/image-optimization/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c669cf1-c6ef-4c87-9901-018b04f7871f/image-optimization-shop-cover-opt.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/87fd0cfa-692e-459c-b2f3-15209a1f6aa7/image-optimization-shop-cover-opt.png"
    alt="Feature Panel"
    width="480"
    height="697"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="using-media-queries-to-target-display-modes">Using Media Queries To Target Display Modes</h2>

<p>We use media queries all the time when writing CSS. Whether it’s switching up styles for print or setting breakpoints for responsive design, they’re commonplace in the web developer’s toolkit. Each of the display modes discussed previously can be used as a media query to alter the appearance of documents depending.</p>

<p>Media queries such as <code>@media (min-width: 1000px)</code> tend to get the most use for setting breakpoints based on the viewport size, but they’re capable of so much more. They can handle <a href="https://www.smashingmagazine.com/2018/05/print-stylesheets-in-2018/">print styles</a>, device orientation, contrast preferences, and a whole ton more. In our case, we’re interested in the <code>display-mode</code> media feature.</p>

<p>Display mode media queries correspond to the current display mode.</p>

<p><strong>Note</strong>: <em>While we may set display modes in our manifest, the actual display mode may differ depending on browser support.</em></p>

<p>These media queries directly reference the current mode:</p>

<ul>
<li><code>@media (display-mode: standalone)</code> will only apply to pages set to standalone mode.</li>
<li><code>@media (display-mode: fullscreen)</code> applies to fullscreen mode. It is worth noting that this also applies when using the Fullscreen API.</li>
<li><code>@media (display-mode: minimal-ui)</code> applies to minimal UI mode.</li>
<li><code>@media (display-mode: browser)</code> applies to standard browser mode.</li>
</ul>

<p>It is also worth keeping an eye out for the <code>window-controls-overlay</code> and <code>tabbed</code> display modes. At the time of writing, these two display modes are experimental and can be used with <code>display_override</code>. <code>display-override</code> is a member of our PWA’s manifest, like <code>display</code>, but provides some extra options and power.</p>

<p><code>display</code> has a predetermined fallback chain (<code>fullscreen</code> -&gt; <code>standalone</code> -&gt; <code>minimal-ui</code> -&gt; <code>browser</code>) that we can’t change, but <code>display-override</code> allows setting a fallback order of our choosing, like the following:</p>

<pre><code class="language-json">"display_override": ["fullscreen", "minimal-ui"]
</code></pre>

<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window_Controls_Overlay_API"><code>window-controls-overlay</code></a> can only apply to PWAs running on a desktop operating system. It makes the PWA take up the entire window, with window control buttons appearing as an overlay. Meanwhile, <code>tabbed</code> is relevant when there are multiple applications within a single window.</p>

<p>In addition to these, there is also the <code>picture-in-picture</code> display mode that applies to (you guessed it) picture-in-picture modes.</p>

<p>We use these media queries exactly as we would any other media query. To show an element with the class <code>.pwa-only</code> when the display mode is standalone, we could do this:</p>

<pre><code class="language-css">.pwa-only {
    display: none;
}

@media (display-mode: standalone) {
    .pwa-only {
        display: block;
    }
}
</code></pre>

<p>If we wanted to show the element when the display mode is standalone <em>or</em> <code>minimal-ui</code>, we could do this:</p>

<div class="break-out">
<pre><code class="language-css">@media (display-mode: standalone), (display-mode: minimal-ui) {
    .pwa-only {
        display: block;
    }
}
</code></pre>
</div>

<p>As great as it is, sometimes CSS isn’t enough. In those cases, we can also reference the display mode and make necessary adjustments with JavaScript:</p>

<div class="break-out">
<pre><code class="language-javascript">const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
// Listen for display mode changes
window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) =&gt; {
  if (e.matches) {
    // App is now in standalone mode
    console.log("Running as PWA");
  }
});
</code></pre>
</div>

<div class="partners__lead-place"></div>

<h2 id="practical-applications">Practical Applications</h2>

<p>Now that we know how to make display modifications depending on whether users are using our web app as a PWA or in a browser, we can have a look at how we might put these newly learnt skills to use.</p>

<h3 id="tailoring-content-for-pwa-users">Tailoring Content For PWA Users</h3>

<p>Users who have an app installed as a PWA are already converted, so you can tweak your app to tone down the marketing speak and focus on the user experience. Since these users have demonstrated commitment by installing your app, they likely don’t need promotional content or installation prompts.</p>

<h3 id="display-more-options-and-features">Display More Options And Features</h3>

<p>You might need to directly expose more things in PWA mode, as people won’t be able to access the browser’s settings as easily when the browser UI is hidden. Features like changing font sizing, switching between light and dark mode, bookmarks, sharing, tabs, etc., might need an in-app alternative.</p>

<h3 id="platform-appropriate-features">Platform-Appropriate Features</h3>

<p>There are features you might not want on your web app because they feel out of place, but that you might want on your PWA. A good example is the bottom navigation bar, which is common in native mobile apps thanks to the easier reachability it provides, but uncommon on websites.</p>

<p>People sometimes print websites, but they very rarely print apps. Consider whether features like print buttons should be hidden in PWA mode.</p>

<h3 id="install-prompts">Install Prompts</h3>

<p>A common annoyance is a prompt to install a site as a PWA appearing when the user has already installed the site. Ideally, the browser will provide an install prompt of its own if our PWA is configured correctly, but not all browsers do, and it can be finicky. MDN has a fantastic guide on <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Trigger_install_prompt">creating a custom button to trigger the installation of a PWA</a>, but it might not fit our needs.</p>

<p>We can improve things by hiding install prompts with our media query or detecting the current display mode with JavaScript and forgoing triggering popups in the first place.</p>

<p>We could even set this up as a reusable utility class so that anything we don’t want to be displayed when the app is installed as a PWA can be hidden with ease.</p>

<pre><code class="language-css">/&#42; Utility class to hide elements in PWA mode &#42;/
.hide-in-pwa {
  display: block;
}

@media (display-mode: standalone), (display-mode: minimal-ui) {
  .hide-in-pwa {
    display: none !important;
  }
}
</code></pre>

<p>Then in your HTML:</p>

<pre><code class="language-html">&lt;div class="install-prompt hide-in-pwa"&gt;
  &lt;button&gt;Install Our App&lt;/button&gt;
&lt;/div&gt;

&lt;div class="browser-notice hide-in-pwa"&gt;
  &lt;p&gt;For the best experience, install this as an app!&lt;/p&gt;
&lt;/div&gt;
</code></pre>

<p>We could also do the opposite and create a utility class to make elements only show when in a PWA, as we discussed earlier.</p>

<h3 id="strategic-use-of-scope-and-start-url">Strategic Use Of Scope And Start URL</h3>

<p>Another way to hide content from your site is to set the <code>scope</code> and <code>start_url</code> properties. These aren’t using media queries as we’ve discussed, but should be considered as ways to present different content depending on whether a site is installed as a PWA.</p>

<p>Here is an example of a manifest using these properties:</p>

<pre><code class="language-css">{
    "name": "Example PWA",</code>
    <code style="font-weight: bold;">"scope": "/dashboard/",</code>
    <code style="font-weight: bold;">"start_url": "/dashboard/index.html",</code>
    <code class="language-css">"display": "standalone",
    "icons": [
        {
            "src": "icon.png",
            "sizes": "192x192",
            "type": "image/png"
        }
    ]
}
</code></pre>

<p><code>scope</code> here defines the top level of the PWA. When users leave the scope of your PWA, they’ll still have an app-like interface but gain access to browser UI elements. This can be useful if you’ve got certain parts of your app that you still want to be part of the PWA but which aren’t necessarily optimised or making the necessary considerations.</p>

<p><code>start_url</code> defines the URL a user will be presented with when they open the application. This is useful if, for example, your app has marketing content at <code>example.com</code> and a dashboard at <code>example.com/dashboard/index.html</code>. It is likely that people who have installed the app as a PWA don’t need the marketing content, so you can set the <code>start_url</code> to <code>/dashboard/index.html</code> so the app starts on that page when they open the PWA.</p>

<h3 id="enhanced-transitions">Enhanced Transitions</h3>

<p><a href="https://www.smashingmagazine.com/2023/12/view-transitions-api-ui-animations-part1/">View transitions</a> can feel unfamiliar, out of place, and a tad gaudy on the web, but are a common feature of native applications. We can set up PWA-only view transitions by wrapping the relevant CSS appropriately:</p>

<pre><code class="language-css">@media (display-mode: standalone) {
  @view-transition {
    navigation: auto;
  }
}
</code></pre>

<p>If you’re <em>really</em> ambitious, you could also tweak the design of a site entirely to fit more closely with native design systems when running as a PWA by pairing a check for the display mode with a check for the device and/or browser in use as needed.</p>

<div class="partners__lead-place"></div>

<h2 id="browser-support-and-testing">Browser Support And Testing</h2>

<p>Browser support for display mode media queries is <a href="https://caniuse.com/mdn-css_at-rules_media_display-mode">good and extensive</a>. However, it’s worth noting that <strong>Firefox doesn’t have PWA support</strong>, and Firefox for Android only displays PWAs in <code>browser</code> mode, so you should make the necessary considerations. Thankfully, <a href="https://www.smashingmagazine.com/2013/09/progressive-enhancement-is-faster/">progressive enhancement</a> is on our side. If we’re dealing with a browser lacking support for PWAs or these media queries, we’ll be treated to <strong>graceful degradation</strong>.</p>

<p>Testing PWAs can be challenging because every device and browser handles them differently. Each display mode behaves slightly differently in every browser and OS combination.</p>

<p>Unfortunately, I don’t have a silver bullet to offer you with regard to this. Browsers don’t have a convenient way to simulate display modes for testing, so you’ll have to test out your PWA on different devices, browsers, and operating systems to be sure everything works everywhere it should, as it should.</p>

<h2 id="recap">Recap</h2>

<p>Using a PWA is a fundamentally different experience from using a web app in the browser, so considerations should be made. <code>display-mode</code> media queries provide a powerful way to create truly adaptive Progressive Web Apps that respond intelligently to their installation and display context. By leveraging these queries, we can do the following:</p>

<ul>
<li><strong>Hide redundant installation prompts</strong> for users who have already installed the app,</li>
<li><strong>Provide appropriate navigation aids</strong> when making browser controls unavailable,</li>
<li><strong>Tailor content and functionality</strong> to match user expectations in different contexts,</li>
<li><strong>Create more native-feeling experiences</strong> that respect platform conventions, and</li>
<li><strong>Progressively enhance the experience</strong> for committed users.</li>
</ul>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20key%20is%20remembering%20that%20PWA%20users%20in%20standalone%20mode%20have%20different%20needs%20and%20expectations%20than%20standard%20website%20visitors.%20By%20detecting%20and%20responding%20to%20display%20modes,%20we%20can%20create%20experiences%20that%20feel%20more%20polished,%20purposeful,%20and%20genuinely%20app-like.%0a&url=https://smashingmagazine.com%2f2025%2f08%2foptimizing-pwas-different-display-modes%2f">
      
The key is remembering that PWA users in standalone mode have different needs and expectations than standard website visitors. By detecting and responding to display modes, we can create experiences that feel more polished, purposeful, and genuinely app-like.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>As PWAs continue to mature, thoughtful implementations and tailoring will become increasingly important for creating truly compelling app experiences on the web. If you’re itching for even more information and PWA tips and tricks, check out Ankita Masand’s “<a href="https://www.smashingmagazine.com/2018/11/guide-pwa-progressive-web-applications/">Extensive Guide To Progressive Web Applications</a>”.</p>

<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>

<ul>
<li>“<a href="https://www.smashingmagazine.com/2021/11/magento-pwa-customizing-themes-coding/">Creating A Magento PWA: Customizing Themes vs. Coding From Scratch</a>”, Alex Husar</li>
<li>“<a href="https://www.smashingmagazine.com/2020/12/progressive-web-apps/">How To Optimize Progressive Web Apps: Going Beyond The Basics</a>”, Gert Svaiko</li>
<li>“<a href="https://www.smashingmagazine.com/2020/01/mobile-pwa-sticky-bars-elements/">How To Decide Which PWA Elements Should Stick</a>”, Suzanne Scacca</li>
<li>“<a href="https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/">Uniting Web And Native Apps With 4 Unknown JavaScript APIs</a>”, Juan Diego Rodríguez</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Tom Barrasso</author><title>Tiny Screens, Big Impact: The Forgotten Art Of Developing Web Apps For Feature Phones</title><link>https://www.smashingmagazine.com/2025/07/tiny-screens-big-impact-developing-web-apps-feature-phones/</link><pubDate>Wed, 16 Jul 2025 15:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/07/tiny-screens-big-impact-developing-web-apps-feature-phones/</guid><description>Learn why flip phones still matter in 2025, and how you can build and launch web apps for these tiny devices.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/07/tiny-screens-big-impact-developing-web-apps-feature-phones/" />
              <title>Tiny Screens, Big Impact: The Forgotten Art Of Developing Web Apps For Feature Phones</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Tiny Screens, Big Impact: The Forgotten Art Of Developing Web Apps For Feature Phones</h1>
                  
                    
                    <address>Tom Barrasso</address>
                  
                  <time datetime="2025-07-16T15:00:00&#43;00:00" class="op-published">2025-07-16T15:00:00+00:00</time>
                  <time datetime="2025-07-16T15:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Flip phones aren’t dead. On the contrary, <a href="https://www.sellcell.com/how-many-mobile-phones-are-sold-each-year/#sources-and-media-contacts">200+ million non-smartphones</a> are sold annually. That’s roughly equivalent to the number of <a href="https://increv.co/academy/iphone-users/">iPhones sold in 2024</a>. Even in the United States, <a href="https://www.counterpointresearch.com/insights/us-feature-phone-market/">millions of flip phones</a> are sold each year. As network operators struggle to <a href="https://restofworld.org/2025/shutting-down-2g-networks-phones-obsolete/">shut down 2G service</a>, new incentives are offered to encourage device upgrades that further increase demand for budget-friendly flip phones. This is especially true across South Asia and Africa, where an iPhone is unaffordable for the vast majority of the population (it takes <a href="https://www.indiatoday.in/business/story/study-shows-people-us-need-to-work-5-days-to-buy-iphone-16-how-long-do-indians-need-2603531-2024-09-20">two months of work</a> on an average Indian salary to afford the cheapest iPhone).</p>

<p>Like their “smart” counterparts, flip phones (technically, this category is called “Feature Phones”) are becoming increasingly more capable. They now offer features you’d expect from a smartphone, like 4G, WiFi, Bluetooth, and the ability to run apps. If you are targeting users in South Asia and Africa, or niches in Europe and North America, there are flip phone app platforms like <a href="https://www.cloudphone.tech/">Cloud Phone</a> and <a href="https://www.kaiostech.com/">KaiOS</a>. Building for these platforms is similar to developing a Progressive Web App (PWA), with distribution managed across several app stores.</p>

<blockquote><strong>Jargon Busting</strong><br />Flip phones go by many names. Non-smartphones are jokingly called “dumb phones”. The technology industry calls this device category “feature phones”. Regionally, they are also known as button phones or basic mobiles in Europe, and keypad mobiles in India. They all share a few traits: they are budget phones with small screens and physical buttons.</blockquote>

<h2 id="why-build-apps-for-flip-phones">Why Build Apps For Flip Phones?</h2>

<p>It’s a common misconception that people who use flip phones do not want apps. In fact, many first-time internet users are eager to discover new content and services. While this market isn’t as lucrative as Apple’s App Store, there are a few reasons why you should build for flip phones.</p>

<ul>
<li><strong>Organic Growth</strong><br />
You do not need to pay to acquire flip phone users. Unlike Android or IOS, where the cost per install (CPI) averages around <a href="https://www.gogochart.com/insights/7-things-you-need-to-know-about-mobile-app-cost-per-install-values/">$2.5-3.3 per install</a> according to GoGoChart, flip phone apps generate substantial organic downloads.<br /></li>
<li><strong>Brand Introduction</strong><br />
When flip phone users eventually upgrade to smartphones, they will search for the apps they are already familiar with. This will, in turn, generate more installs on the Google Play Store and, to a lesser extent, the Apple App Store.</li>
<li><strong>Low Competition</strong><br />
There are <a href="https://kaios.app/">~1,700 KaiOS apps</a> and fewer Cloud Phone widgets. Meanwhile, Google Play has over <a href="https://www.appbrain.com/stats/number-of-android-apps">1.55 million Android apps</a> to choose from. It is much easier to stand out as one in a thousand than one in a million.</li>
</ul>

<h2 id="technical-foundations">Technical Foundations</h2>

<p>Flip phones could not always run apps. It wasn’t until the <a href="https://www.theguardian.com/technology/appsblog/2011/jun/07/nokia-ovi-store">Ovi Store</a> (later renamed to the “Nokia Store”) launched in 2009, a year after Apple’s flagship iPhone launched, that flip phones got installable, third-party applications. At the time, apps were written for the fragmented Java 2 Mobile Edition (J2ME) runtime, available only on select Nokia models, and often required integration with poorly-documented, proprietary packages like the <a href="https://nikita36078.github.io/J2ME_Docs/docs/nokiaapi2/">Nokia UI API</a>.</p>

<p>Today, flip phone platforms have <strong>rejected native runtimes in favor of standard web technologies</strong> in an effort to reduce barriers to entry and attract a wider pool of software developers. Apps running on modern flip phones are primarily written in languages many developers are familiar with &mdash; HTML, CSS, and JavaScript &mdash; and with them, a set of trade-offs and considerations.</p>

<h3 id="hardware">Hardware</h3>

<p>Flip phones are affordable because they use low-end, often outdated, hardware. On the bottom end are budget phones with a real-time operating system (RTOS) running on chips like the <a href="https://www.unisoc.com/en_us/home/TGNSJ-T107-1">Unisoc T107</a> with as little as 16MB of RAM. These phones typically support Opera Mini and Cloud Phone. At the upper end is the recently-released <a href="https://www.tcl.com/us/en/products/mobile/flip-series/flip-4-5g">TCL Flip 4</a> running KaiOS 4.0 on the Qualcomm Snapdragon 4s with 1GB of RAM.</p>

<p>While it is difficult to accurately compare such different hardware, Apple’s latest iPhone 16 Pro has 500x more memory (8GB RAM) and supports download speeds up to 1,000x faster than a low-end flip phone (4G LTE CAT-1).</p>

<h3 id="performance">Performance</h3>

<p>You might think that flip phone apps are easily limited by the scarce available resources of budget hardware. This is the case for KaiOS, since apps are executed on the device. Code needs to be minified, thumbnails downsized, and performance evaluated across a range of real devices. You cannot simply test on your desktop with a small viewport.</p>

<p>However, as <a href="https://tigercosmos.xyz/post/2018/09/puffin/">remote browsers</a>, both Cloud Phone and Opera Mini overcome hardware constraints by offloading computationally expensive rendering to servers. This means <strong>performance is generally comparable to modern desktops</strong>, but can lead to a few quirky and, at times, unintuitive characteristics.</p>

<p>For instance, if your app fetches a 1MB file to display a data table, this does not consume 1MB of the user’s mobile data. Only changes to the screen contents get streamed to the user, consuming bandwidth. On the other hand, data is consumed by complex animations and page transitions, because each frame is at least a partial screen refresh. Despite this quirk, Opera Mini estimates it saves up to <a href="https://blogs.opera.com/africa/2021/11/free-data-mtn-south-africa/">90% of data</a> compared to conventional browsers.</p>

<h3 id="security">Security</h3>

<p><a href="https://www.trevorlasn.com/blog/the-problem-with-local-storage">Do not store sensitive data</a> in browser storage. This holds true for flip phones, where the security concerns are similar to those of traditional web browsers. Although apps cannot generally access data from other apps, KaiOS does not encrypt client-side data. The implications are different for remote browsers.</p>

<p>Opera Mini <a href="https://help.opera.com/en/opera-mini-and-javascript/">does not support client-side storage</a> at all, while Cloud Phone <a href="https://developer.cloudfone.com/docs/reference/data-storage/#secure-cloud-storage">stores data encrypted</a> in its data centers and not on the user’s phone.</p>

<h2 id="design-for-modern-flip-phones">Design For Modern Flip Phones</h2>

<h3 id="simplify-don-t-shrink-to-fit">Simplify, Don’t Shrink-to-fit</h3>

<p>Despite their staying power, these devices go largely ignored by nearly every web development framework and library. Popular front-end web frameworks like <a href="https://getbootstrap.com/docs/5.0/layout/breakpoints/">Bootstrap v5</a> categorize all screens below 576px as extra small. Another popular choice, <a href="https://tailwindcss.com/docs/responsive-design">Tailwind</a>, sets the smallest CSS breakpoint &mdash; a specific width where the layout changes to accommodate an optimal viewing experience across different devices &mdash; even higher at 40em (640px). Design industry experts like <a href="https://www.nngroup.com/articles/breakpoints-in-responsive-design/">Norman Nielsen suggest the smallest breakpoint</a>, “is intended for mobile and generally is up to 500px.” Standards like these advocate for a one-size-fits-all approach on small screens, but some small design changes can make a big difference for new internet users.</p>

<p>Small screens vary considerably in size, resolution, contrast, and brightness.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aSmall%20screen%20usability%20requires%20distinct%20design%20considerations%20%e2%80%94%20not%20a%20shrink-to-fit%20model.%20While%20all%20of%20these%20devices%20have%20a%20screen%20width%20smaller%20than%20the%20smallest%20common%20breakpoints,%20treating%20them%20equally%20would%20be%20a%20mistake.%0a&url=https://smashingmagazine.com%2f2025%2f07%2ftiny-screens-big-impact-developing-web-apps-feature-phones%2f">
      
Small screen usability requires distinct design considerations — not a shrink-to-fit model. While all of these devices have a screen width smaller than the smallest common breakpoints, treating them equally would be a mistake.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/1-apps-shrink-poorly.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="316"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/1-apps-shrink-poorly.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/1-apps-shrink-poorly.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/1-apps-shrink-poorly.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/1-apps-shrink-poorly.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/1-apps-shrink-poorly.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/1-apps-shrink-poorly.png"
			
			sizes="100vw"
			alt="Screenshots of A List Apart, Chrome for Developers, and MDN Web Docs on Cloud Phone"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <strong>Shrinks poorly</strong>: Screenshots of A List Apart, Chrome for Developers, and MDN Web Docs on Cloud Phone. (<a href='https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/1-apps-shrink-poorly.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/2-apps-shrink-well.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="316"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/2-apps-shrink-well.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/2-apps-shrink-well.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/2-apps-shrink-well.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/2-apps-shrink-well.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/2-apps-shrink-well.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/2-apps-shrink-well.png"
			
			sizes="100vw"
			alt="Screenshots of Rest of World, BBC News, and TED Talks on Cloud Phone"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <strong>Shrinks well</strong>: Screenshots of Rest of World, BBC News, and TED Talks on Cloud Phone. (<a href='https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/2-apps-shrink-well.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Most <strong>websites render too large for flip phones</strong>. They use fonts that are too big, graphics that are too detailed, and sticky headers that occupy a quarter of the screen. To make matters worse, many websites <a href="https://www.htmlallthethings.com/blog-posts/how-to-disable-scrolling-in-css">disable horizontal scrolling</a> by hiding content that overflows horizontally. This allows for smooth scrolling on a touchscreen, but also makes it impossible to read text that extends beyond the viewport on flip phones.</p>

<p>The table below includes physical display size, resolution, and examples to better understand the diversity of small screens across flip phones and budget smartphones.</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Resolution</th>
            <th>Display Size</th>
            <th>Pixel Size</th>
            <th>Example</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>QQVGA</td>
            <td>1.8”</td>
            <td>128×160</td>
            <td><a href="https://vietteltelecom.vn/tmdt-device/sumo-4g-v1">Viettel Sumo 4G V1</a></td>
        </tr>
        <tr>
            <td>QVGA</td>
            <td>2.4”</td>
            <td>240×320</td>
            <td><a href="https://www.hmd.com/en_int/nokia-235-4g-2024?sku=1GF026GPG3L01">Nokia 235 4G</a></td>
        </tr>
        <tr>
            <td>QVGA (Square)</td>
            <td>2.4”</td>
            <td>240×240</td>
            <td><a href="https://www.reddit.com/r/dumbphones/comments/1j7gsxk/frog_pocket_2_mwc_barcelona_2025/?tl=it">Frog Pocket2</a></td>
        </tr>
        <tr>
            <td>HVGA (480p)</td>
            <td>2.8-3.5”</td>
            <td>320×480</td>
            <td><a href="https://www.gsmarena.com/blackberry_9720-5625.php">BlackBerry 9720</a></td>
        </tr>
        <tr>
            <td>VGA</td>
            <td>2.8-3.5”</td>
            <td>480×640</td>
            <td><a href="https://www.t-mobile.com/support/tutorials/device/cat/s22-flip/specifications">Cat S22</a></td>
        </tr>
        <tr>
            <td>WVGA</td>
            <td>2.8-3.5”</td>
            <td>480×800</td>
            <td><a href="https://www.gsmarena.com/hp_pre_3-3770.php">HP Pre 3</a></td>
        </tr>
        <tr>
            <td>FWVGA+</td>
            <td>5”</td>
            <td>480×960</td>
            <td><a href="https://www.alcatelmobile.com/product/smartphone/alcatel1/alcatel-1/">Alcatel 1</a></td>
        </tr>
    </tbody>
</table>

<p><strong>Note</strong>: <em>Flip phones have small screens typically between 1.8”&ndash;2.8” with a resolution of 240x320 (QVGA) or 128x160 (QQVGA). For comparison, an Apple Watch Series 10 has a 1.8” screen with a resolution of 416x496. By modern standards, flip phone displays are small with low resolution, pixel density, contrast, and brightness.</em></p>

<h2 id="develop-for-small-screens">Develop For Small Screens</h2>

<p>Add custom, named breakpoints to your framework’s defaults, rather than manually using media queries to override layout dimensions defined by classes.</p>

<h4 id="bootstrap-v5">Bootstrap v5</h4>

<p>Bootstrap defines a map, <code>$grid-breakpoints</code>, in the <strong>_variables.scss</strong> Sass file that contains the default breakpoints from SM (576px) to XXL (1400px). Use the <code>map-merge()</code> function to extend the default and add your own breakpoint.</p>

<pre><code class="language-scss">@import "node_modules/bootstrap/scss/functions";
$grid-breakpoints: map-merge($grid-breakpoints, ("xs": 320px));
</code></pre>

<h4 id="tailwind-v4">Tailwind v4</h4>

<p>Tailwind allows you to extend the default theme in the <strong>tailwind.config.js</strong> configuration file. Use the <code>extend</code> key to define new breakpoints.</p>

<pre><code class="language-javascript">const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
  theme: {
    extend: {
      screens: {
        "xs": "320px",
       ...defaultTheme.screens,
         },
    },
  },
};
</code></pre>

<h2 id="the-key-board-to-success">The Key(board) To Success</h2>

<p>Successful flip phone apps support keyboard navigation using the directional pad (D-pad). This is the same navigation pattern as TV remotes: four arrow keys (up, down, left, right) and the central button. To build a great flip phone-optimized app, provide a <strong>navigation scheme</strong> where the user can quickly learn how to navigate your app using these limited controls. Ensure users can navigate to all visible controls on the screen.</p>

<figure><a href="https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/3-navigating-podlp.gif"><img src="https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/3-navigating-podlp.gif" width="480" height="320" alt="" /></a><figcaption>Navigating PodLP using d-pad (left) and a virtual cursor (right).</figcaption></figure>

<p>Although some flip phone platforms support spatial navigation using an emulated cursor, it is not universally available and creates a worse user experience. Moreover, while apps that support keyboard navigation will work with an emulated cursor, this isn’t necessarily true the other way around. Opera Mini Native only offers a virtual cursor, Cloud Phone only offers spatial navigation, and KaiOS <a href="https://developer.kaiostech.com/docs/getting-started/main-concepts/emulated-cursor/">supports both</a>.</p>

<p>If you develop with <strong>keyboard accessibility</strong> in mind, supporting flip phone navigation is easy. As general guidelines, <a href="https://theadminbar.com/accessibility-weekly/focus-outlines/">never remove a focus outline</a>. Instead, override default styles and use <a href="https://dev.to/hybrid_alex/better-css-outlines-with-box-shadows-1k7j">box shadows</a> to match your app’s color scheme while fitting appropriately. Autofocus on the first item in a sequence &mdash; list or grid &mdash; but be careful to avoid <a href="https://www.boia.org/blog/avoid-keyboard-traps-to-make-your-site-more-accessible">keyboard traps</a>. Finally, make sure that the lists scroll the newly-focused item completely into view.</p>

<h3 id="don-t-make-users-type">Don’t Make Users Type</h3>

<p>If you have ever been frustrated typing a long message on your smartphone, only to have it accidentally erased, now imagine that frustration when you typed the message using <a href="https://en.wikipedia.org/wiki/T9_%28predictive_text%29">T9</a> on a flip phone. Despite advancements in predictive typing, it’s a chore to fill forms and compose even a single 180-character Tweet with just nine keys.</p>

<blockquote>Whatever you do, don’t make flip phone users type!</blockquote>

<p>Fortunately, it is easy to adapt designs to require less typing. <strong>Prefer numbers whenever possible.</strong> Allow users to register using their phone number (which is easy to type), send a PIN code or one-time password (OTPs) that contains only numbers, and look up address details from a postal code. Each of these saves tremendous time and avoids frustration that often leads to user attrition.</p>

<p>Alternatively, integrate with single-sign-on (SSO) providers to “Log in with Google,” so users do not have to retype passwords that security teams require to be at least eight characters long and contain a letter, number, and symbol. Just keep in mind that many new internet users won’t have an email address. They may not know how to access it, or their phone might not be able to access emails.</p>

<p>Finally, allow users to <strong>search by voice</strong> when it is available. As difficult as it is typing English using T9, it’s much harder typing a language like Tamil, which has over 90M speakers across South India and Sri Lanka. Despite decades of advancement, technologies like auto-complete and predictive typing are seldom available for such languages. While imperfect, there are AI models like <a href="https://huggingface.co/vasista22/whisper-tamil-medium">Whisper Tamil</a> that can perform speech-to-text, thanks to researchers at universities like the <a href="https://asr.iitm.ac.in/">Speech Lab at IIT Madras</a>.</p>

<h2 id="flip-phone-browsers-and-operating-systems">Flip Phone Browsers And Operating Systems</h2>

<p>Another challenge with developing web apps for flip phones is their <strong>fragmented ecosystem</strong>. Various companies have used different approaches to allow websites and apps to run on limited hardware. There are at least three major web-based platforms that all operate differently:</p>

<ol>
<li><a href="https://developer.cloudfone.com/">Cloud Phone</a> is the most recent solution, launched in December 2023, using a modern <a href="https://www.puffin.com/">Puffin</a> (Chromium) based remote browser that serves as an app store.</li>
<li><a href="https://www.kaiostech.com/">KaiOS</a>, launched in 2016 using <a href="https://en.wikipedia.org/wiki/Firefox_OS">Firefox OS</a> as its foundation, is a mobile operating system where the entire system is a web browser.</li>
<li><a href="https://www.opera.com/mobile/basic-phones">Opera Mini</a> Native is by far the oldest, launched in 2005 as an ad-supported remote browser that still uses the decade-old, discontinued <a href="https://en.wikipedia.org/wiki/Presto_(browser_engine)">Presto engine</a>.</li>
</ol>

<p>Although both platforms are remote browsers, there are significant differences between <a href="https://developer.cloudfone.com/blog/cloud-phone-vs.-opera-mini/">Cloud Phone and Opera Mini</a> that are not immediately apparent.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/4-flip-phones.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="533"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/4-flip-phones.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/4-flip-phones.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/4-flip-phones.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/4-flip-phones.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/4-flip-phones.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/4-flip-phones.jpg"
			
			sizes="100vw"
			alt="Nokia 6300 4G (KaiOS), Viettel Sumo 4G V1S (Cloud Phone), and Itel Neo R60&#43; (Opera Mini)"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Left to right: Nokia 6300 4G (KaiOS), Viettel Sumo 4G V1S (Cloud Phone), and Itel Neo R60+ (Opera Mini). (<a href='https://files.smashing.media/articles/tiny-screens-big-impact-developing-web-apps-feature-phones/4-flip-phones.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Platform</th>
            <th>Cons</th>
            <th>Pros</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><strong>Cloud Phone</strong></td>
            <td><ul><li>Missing features like WebPush</li><li>No offline support</li><li>Monetization not provided</li></ul></td>
            <td><ul><li>Modern Chromium v128+ engine</li><li>Rich multimedia support</li><li>No optimizations needed</li><li>Actively developed</li><li>100+ models launched in 2024</li></ul></td>
        </tr>
        <tr>
            <td><strong>KaiOS</strong></td>
            <td><ul><li>Outdated Gecko engine</li><li>Hardware constrained</li><li>Few models released in 2024</li><li><a href="https://kaiads.com/">KaiAds</a> integration required</li><li>Two app stores</li></ul></td>
            <td><ul><li>Full offline support</li><li>APIs for low-level integration</li><li>Apps can be <a href="https://developer.kaiostech.com/docs/development/packaged-or-hosted/">packaged or hosted</a></li></ul></td>
        </tr>
        <tr>
            <td><strong>Opera Mini Native</strong></td>
            <td><ul><li>Discontinued Presto engine</li><li>~2.5s async execution limit</li><li>Limited ES5 support</li><li>No multimedia support</li><li>No app store
Last updated in 2020</li></ul></td>
            <td><ul><li>Preinstalled on hundreds of millions of phones</li><li>Partial offline support</li><li>Stable, cross-platform</li></ul></td>
        </tr>
    </tbody>
</table>

<p>Flip phones have come a long way, but each platform supports different capabilities. You may need to remove or scale back features based on what is supported. It is best to target <strong>the lowest common denominator</strong> that is feasible for your application.</p>

<p>For information-heavy news websites, wikis, or blogs, Opera Mini’s outdated technology works well enough. For video streaming services, both Cloud Phone and KaiOS work well. Conversely, remote browsers like Opera Mini and Cloud Phone cannot handle high frame rates, so only KaiOS is suitable for real-time interactive games. Just like with design, there is no one-size-fits-all approach to flip phone development. Even though all platforms are web-based, they require different tradeoffs.</p>

<h2 id="tiny-screens-big-impact">Tiny Screens, Big Impact</h2>

<p>The flip phone market is growing, particularly for 4G-enabled models. Reliance’s JioPhone is among the most successful models, selling more than <a href="https://kalingatv.com/business/reliance-jio-has-now-sold-135-million-units-of-jiophone-devices/">135 million units</a> of its flagship KaiOS-enabled phone. The company plans to increase 4G flip phone rollout steadily as it migrates India’s 250 million 2G users to 4G and 5G.</p>

<p>Similar campaigns are underway across emerging markets, like <a href="https://www.telecoms.com/public-cloud/vodacom-south-africa-launches-14-cloud-based-smartphone">Vodacom’s $14 Mobicel S4</a>, a Cloud phone-enabled device in South Africa, and <a href="https://e.vnexpress.net/news/business/companies/viettel-to-gift-4g-phones-to-700-000-2g-subscribers-4795412.html">Viettel’s gifting 700,000 4G flip phones</a> to current 2G subscribers to upgrade users in remote and rural areas.</p>

<p>Estimates of the total active flip phone market size are difficult to come by, and harder still to find a breakdown by platform. KaiOS claims to enable “<a href="https://www.kaiostech.com/developers/">over 160 million phones worldwide</a>,” while “<a href="https://www.opera.com/mobile/basic-phones">over 300 million people use Opera Mini</a> to stay connected.” Just a year after launch, Cloud Phone states that, “<strong><a href="https://www.theregister.com/2025/01/02/cloudmosa_cloudphone_4g_feature_phone/">one million Cloud Phone users</a></strong> already access the service from 90 countries.” By most estimates, there are already hundreds of millions of web-enabled flip phone users eager to discover new products and services.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Hundreds of millions still rely on flip phones to stay connected. Yet, these users go largely ignored even by products that target emerging markets. <strong>Modern software development often prioritizes the latest and greatest</strong> over finding ways to affordably serve more than <a href="https://www.itu.int/en/mediacentre/Pages/PR-2023-09-12-universal-and-meaningful-connectivity-by-2030.aspx">2.6 billion unconnected people</a>. If you are not designing for small screens using <a href="https://www.smashingmagazine.com/2025/04/what-mean-site-be-keyboard-navigable/">keyboard navigation</a>, you’re shutting out an entire population from accessing your service.</p>

<p><a href="https://www.htmlallthethings.com/blog-posts/why-feature-phones-are-important-in-2025">Flip phones still matter in 2025</a>. With ongoing network transitions, millions will upgrade, and millions more will connect for the first time using 4G flip phones. This creates an opportunity to put your app into the hands of the newly connected. And thanks to modern remote browser technology, it is now easier than ever to build and launch your app on flip phones without costly and time-consuming optimizations to function on low-end hardware.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Eric Burel</author><title>How OWASP Helps You Secure Your Full-Stack Web Applications</title><link>https://www.smashingmagazine.com/2025/02/how-owasp-helps-secure-full-stack-web-applications/</link><pubDate>Tue, 18 Feb 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/02/how-owasp-helps-secure-full-stack-web-applications/</guid><description>The OWASP vulnerabilities list is the perfect starting point for web developers looking to strengthen their security expertise. Let’s discover how these vulnerabilities materialize in full-stack web applications and how to prevent them.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/02/how-owasp-helps-secure-full-stack-web-applications/" />
              <title>How OWASP Helps You Secure Your Full-Stack Web Applications</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How OWASP Helps You Secure Your Full-Stack Web Applications</h1>
                  
                    
                    <address>Eric Burel</address>
                  
                  <time datetime="2025-02-18T08:00:00&#43;00:00" class="op-published">2025-02-18T08:00:00+00:00</time>
                  <time datetime="2025-02-18T08:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Security can be an intimidating topic for web developers. The vocabulary is rich and full of acronyms. Trends evolve quickly as hackers and analysts play a perpetual cat-and-mouse game. Vulnerabilities stem from little details we cannot afford to spend too much time on during our day-to-day operations.</p>

<p>JavaScript developers already have a lot to take with the emergence of a new wave of innovative architectures, such as React Server Components, Next.js App Router, or Astro islands.</p>

<p>So, let’s have a focused approach. What we need is to be able to <strong>detect</strong> and <strong>palliate the most common security issues</strong>. A top ten of the most common vulnerabilities would be ideal.</p>

<h2 id="meet-the-owasp-top-10">Meet The OWASP Top 10</h2>

<p>Guess what: there happens to be such a top ten of the most common vulnerabilities, curated by experts in the field!</p>

<p>It is provided by the <strong>OWASP Foundation</strong>, and it’s an extremely valuable resource for getting started with security.</p>

<p>OWASP stands for “Open Worldwide Application Security Project.” It’s a nonprofit foundation whose goal is to make software more secure globally. It supports many open-source projects and produces high-quality education resources, including the OWASP top 10 vulnerabilities list.</p>

<p>We will dive through each item of the OWASP top 10 to understand <em>how</em> to recognize these vulnerabilities in a full-stack application.</p>

<p><strong>Note</strong>: <em>I will use Next.js as an example, but this knowledge applies to any similar full-stack architecture, even outside of the JavaScript ecosystem.</em></p>

<p>Let’s start our countdown towards a safer web!</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/typescript-in-50-lessons/">“TypeScript in 50 Lessons”</a></strong>, our shiny new guide to TypeScript. With detailed <strong>code walkthroughs</strong>, hands-on examples and common gotchas. For developers who know enough <strong>JavaScript</strong> to be dangerous.</p>
<a data-instant href="/printed-books/typescript-in-50-lessons/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="/printed-books/typescript-in-50-lessons/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2732dfe9-e1ee-41c3-871a-6252aeda741c/typescript-panel.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c2f2c6d6-4e85-449a-99f5-58bd053bc846/typescript-shop-cover-opt.png"
    alt="Feature Panel"
    width="481"
    height="698"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="number-10-server-side-request-forgery-ssrf">Number 10: Server-Side Request Forgery (SSRF)</h2>

<p>You may have heard about Server-Side Rendering, aka SSR. Well, you can consider SSRF to be its evil twin acronym.</p>

<p>Server-Side Request Forgery can be summed up as <strong>letting an attacker fire requests using your backend server</strong>. Besides hosting costs that may rise up, the main problem is that the attacker will benefit from your server’s level of accreditation. In a complex architecture, this means being able to target your internal private services using your own corrupted server.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/ssr-good-ssrf-bad.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/ssr-good-ssrf-bad.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/ssr-good-ssrf-bad.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/ssr-good-ssrf-bad.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/ssr-good-ssrf-bad.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/ssr-good-ssrf-bad.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/ssr-good-ssrf-bad.jpg"
			
			sizes="100vw"
			alt="SSR is good vs SSRF is bad"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      SSR is good, but SSRF is bad! (<a href='https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/ssr-good-ssrf-bad.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Here is an example. Our app lets a user input a URL and summarizes the content of the target page server-side using an AI SDK. A mischievous user passes <code>localhost:3000</code> as the URL instead of a website they’d like to summarize. Your server will fire a request against itself or any other service running on port 3000 in your backend infrastructure. This is a severe SSRF vulnerability!</p>

<p>You’ll want to be careful when firing requests based on user inputs, especially server-side.</p>

<h2 id="number-9-security-logging-and-monitoring-failures">Number 9: Security Logging And Monitoring Failures</h2>

<p>I wish we could establish a telepathic connection with our beloved Node.js server running in the backend. Instead, the best thing we have to see what happens in the cloud is a dreadful stream of unstructured pieces of text we name “logs.”</p>

<p>Yet we will have to deal with that, not only for debugging or performance optimization but also because <strong>logs are often the only information you’ll get to discover and remediate a security issue</strong>.</p>

<p>As a starter, you might want to focus on logging the most important transactions of your application exactly like you would prioritize writing end-to-end tests. In most applications, this means login, signup, payouts, mail sending, and so on. In a bigger company, a more complete telemetry solution is a must-have, such as Open Telemetry, Sentry, or Datadog.</p>

<p>If you are using React Server Components, you may need to set up a proper logging strategy anyway since it’s not possible to debug them directly from the browser as we used to do for Client components.</p>

<h2 id="number-8-software-and-data-integrity-failures">Number 8: Software And Data Integrity Failures</h2>

<p>The OWASP top 10 vulnerabilities tend to have various levels of granularity, and this one is really a big family. I’d like to focus on <strong>supply chain attacks</strong>, as they have gained a lot of popularity over the years.</p>

<p>You may have heard about the <strong>Log4J vulnerability</strong>. It was very publicized, very critical, and very exploited by hackers. It’s a massive supply chain attack.</p>

<p>In the JavaScript ecosystem, you most probably install your dependencies using NPM. Before picking dependencies, you might want to craft yourself a small list of health indicators.</p>

<ul>
<li>Is the library maintained and tested with proper code?</li>
<li>Does it play a critical role in my application?</li>
<li>Who is the main contributor?</li>
<li>Did I spell it right when installing?</li>
</ul>

<p>For more serious business, you might want to consider setting up a <strong>Supply Chain Analysis (SCA)</strong> solution; GitHub’s Dependabot is a free one, and Snyk and Datadog are other famous actors.</p>

<h2 id="number-7-identification-and-authentication-failures">Number 7: Identification And Authentication Failures</h2>

<p>Here is a stereotypical vulnerability belonging to this category: your admin password is leaked. A hacker finds it. Boom, game over.</p>

<p>Password management procedures are beyond the scope of this article, but in the context of full-stack web development, let’s dive deep into how we can prevent brute force attacks using Next.js edge middlewares.</p>

<p><strong>Middlewares</strong> are tiny proxies written in JavaScript. They process requests in a way that is supposed to be very, very fast, faster than a normal Node.js endpoint, for example. They are a good fit for handling <strong>low-level processing</strong>, like blocking malicious IPs or redirecting users towards the correct translation of a page.</p>

<p>One interesting use case is <strong>rate limiting</strong>. You can quickly improve the security of your applications by limiting people’s ability to spam your POST endpoints, especially login and signup.</p>

<p>You may go even further by setting up a <strong>Web Applications Firewall (WAF)</strong>. A WAF lets developers implement elaborate security rules. This is not something you would set up directly in your application but rather at the host level. For instance, Vercel has released its own WAF in 2024.</p>

<div class="partners__lead-place"></div>

<h2 id="number-6-vulnerable-and-outdated-components">Number 6: Vulnerable And Outdated Components</h2>

<p>We have discussed supply chain attacks earlier. Outdated components are a variation of this vulnerability, where you actually are the person to blame. Sorry about that.</p>

<p>Security vulnerabilities are often discovered ahead of time by diligent security analysts before a mean attacker can even start thinking about exploiting them. Thanks, analysts friends! When this happens, they fill out a <strong>Common Vulnerabilities and Exposure</strong> and store that in a public database.</p>

<p>The remedy is the same as for supply chain attacks: set up an SCA solution like Dependabot that will regularly check for the use of vulnerable packages in your application.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/vulnerable-components.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/vulnerable-components.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/vulnerable-components.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/vulnerable-components.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/vulnerable-components.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/vulnerable-components.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/vulnerable-components.jpg"
			
			sizes="100vw"
			alt="A visualization showing that an app depends on packages and some of them can be vulnerable"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Your app depends on many packages. Sadly, some of them are probably affected by vulnerabilities that can spread to your application. (<a href='https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/vulnerable-components.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="halfway-break">Halfway break</h2>

<p>I just want to mention at this point how much progress we have made since the beginning of this article. To sum it up:</p>

<ul>
<li>We know how to recognize an SSRF. This is a nasty vulnerability, and it is easy to accidentally introduce while crafting a super cool feature.</li>
<li>We have identified monitoring and dependency analysis solutions as important pieces of “support” software for securing applications.</li>
<li>We have figured out a good use case for Next.js edge middlewares: rate limiting our authentication endpoints to prevent brute force attacks.</li>
</ul>

<p>It’s a good time to go grab a tea or coffee. But after that, come back with us because we are going to discover the five most common vulnerabilities affecting web applications!</p>

<h2 id="number-5-security-misconfiguration">Number 5: Security Misconfiguration</h2>

<p>There are so many configurations that we can mismanage. But let’s focus on the most insightful ones for a web developer learning about security: <strong>HTTP headers</strong>.</p>

<p>You can use HTTP response headers to pass on a lot of information to the user’s browser about what’s possible or not on your website.</p>

<p>For example, by narrowing down the “Permissions-Policy” headers, you can claim that your website will never require access to the user’s camera. This is an extremely powerful protection mechanism in case of a <strong>script injection attack (XSS)</strong>. Even if the hacker manages to run a malicious script in the victim’s browser, the latter will not allow the script to access the camera.</p>

<p>I invite you to observe the security configuration of any template or boilerplate that you use to craft your own websites. <em>Do you understand them properly?</em> <em>Can you improve them?</em> Answering these questions will inevitably lead you to vastly increase the safety of your websites!</p>

<h2 id="number-4-insecure-design">Number 4: Insecure Design</h2>

<p>I find this one funny, although a bit insulting for us developers.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aBad%20code%20is%20literally%20the%20fourth%20most%20common%20cause%20of%20vulnerabilities%20in%20web%20applications!%20You%20can%e2%80%99t%20just%20blame%20your%20infrastructure%20team%20anymore.%0a&url=https://smashingmagazine.com%2f2025%2f02%2fhow-owasp-helps-secure-full-stack-web-applications%2f">
      
Bad code is literally the fourth most common cause of vulnerabilities in web applications! You can’t just blame your infrastructure team anymore.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Design is actually not just about code but about <strong>the way we use our programming tools</strong> to produce software artifacts.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/bad-design.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="500"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/bad-design.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/bad-design.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/bad-design.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/bad-design.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/bad-design.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/bad-design.jpg"
			
			sizes="100vw"
			alt="A visualization with bad design"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Bad design can create vulnerabilities that are very hard to detect. The cure is good design, and good design is a lot of learning. Keep reading curated learning resources, and everything will be ok! (<a href='https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/bad-design.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In the context of full-stack JavaScript frameworks, I would recommend learning how to use them <strong>idiomatically</strong>, the same way you’d want to learn a foreign language. It’s not just about translating what you already know word-by-word. You need to get a grasp of how a native speaker would phrase their thoughts.</p>

<p>Learning idiomatic Next.js is really, really hard. Trust me, I teach this framework to web developers. Next is all about client and server logic hybridization, and some patterns may not even transfer to competing frameworks with a different architecture like Astro.js or Remix.</p>

<p>Hopefully, the Next.js core team has produced many free learning resources, including articles and documentation specifically focusing on security.</p>

<p>I recommend reading Sebastian Markbåge’s famous article “<a href="https://nextjs.org/blog/security-nextjs-server-components-actions">How to Think About Security in Next.js</a>” as a starting point. If you use Next.js in a professional setting, consider organizing proper training sessions before you start working on high-stakes projects.</p>

<h2 id="number-3-injection">Number 3: Injection</h2>

<p>Injections are the epitome of vulnerabilities, the quintessence of breaches, and the paragon of security issues. SQL injections are typically very famous, but JavaScript injections are also quite common. Despite being well-known vulnerabilities, injections are still in the top 3 in the OWASP ranking!</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aInjections%20are%20the%20reason%20why%20forcing%20a%20React%20component%20to%20render%20HTML%20is%20done%20through%20an%20unwelcoming%20%60dangerouslySetInnerHTML%60%20function.%0a&url=https://smashingmagazine.com%2f2025%2f02%2fhow-owasp-helps-secure-full-stack-web-applications%2f">
      
Injections are the reason why forcing a React component to render HTML is done through an unwelcoming `dangerouslySetInnerHTML` function.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>React doesn’t want you to include user input that could contain a malicious script.</p>

<p>The screenshot below is a demonstration of an injection using images. It could target a message board, for instance. The attacker misused the image posting system. They passed a URL that points towards an API GET endpoint instead of an actual image. Whenever your website’s users see this post in their browser, an authenticated request is fired against your backend, triggering a payment!</p>

<p>As a bonus, having a GET endpoint that triggers side-effects such as payment also constitutes a risk of <strong>Cross-Site Request Forgery</strong> (CSRF, which happens to be SSRF client-side cousin).</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/csrf-hacked.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="375"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/csrf-hacked.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/csrf-hacked.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/csrf-hacked.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/csrf-hacked.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/csrf-hacked.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/csrf-hacked.png"
			
			sizes="100vw"
			alt="Cross-Site Request Forgery example"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      This image will trigger payments using the end user’s identity when displayed! The mistake lies in using a GET endpoint to trigger payments instead of a POST endpoint. (<a href='https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/csrf-hacked.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Even experienced developers can be caught off-guard. Are you aware that dynamic route parameters are user inputs? For instance, <code>[language]/page.jsx</code> in a Next.js or Astro app. I often see clumsy attack attempts when logging them, like “language” being replaced by a path traversal like <code>../../../../passwords.txt</code>.</p>

<p><strong>Zod</strong> is a very popular library for running server-side data validation of user inputs. You can add a transform step to sanitize inputs included in database queries, or that could land in places where they end up being executed as code.</p>

<div class="partners__lead-place"></div>

<h2 id="number-2-cryptographic-failures">Number 2: Cryptographic Failures</h2>

<p>A typical discussion between two developers that are in deep, deep trouble:</p>

<blockquote>&mdash; We have leaked our database and encryption key. What algorithm was used to encrypt the password again? AES-128 or SHA-512?<br />&mdash; I don’t know, aren’t they the same thing? They transform passwords into gibberish, right?<br />&mdash; Alright. We are in deep, deep trouble.</blockquote>

<p>This vulnerability mostly concerns backend developers who have to deal with <strong>sensitive personal identifiers (PII)</strong> or <strong>passwords</strong>.</p>

<p>To be honest, I don’t know much about these algorithms; I studied computer science way too long ago.</p>

<p>The only thing I remember is that you need <strong>non-reversible algorithms to encrypt passwords</strong>, aka hashing algorithms. The point is that if the encrypted passwords are leaked, and the encryption key is also leaked, it will still be super hard to hack an account (you can’t just reverse the encryption).</p>

<p>In the State of JavaScript survey, we use passwordless authentication with an email magic link and one-way hash emails, so even as admins, we cannot guess a user’s email in our database.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/encrypted-email.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="177"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/encrypted-email.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/encrypted-email.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/encrypted-email.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/encrypted-email.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/encrypted-email.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/encrypted-email.png"
			
			sizes="100vw"
			alt="A hashed email"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      A hashed email generated when a user creates an account: it can’t be reversed even when possessing the encryption key. (<a href='https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/encrypted-email.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="and-number-1-is">And number 1  is&hellip;</h2>

<p>Such suspense! We are about to discover that the top 1 vulnerability in the world of web development is&hellip;</p>

<p><strong>Broken Access Control!</strong> Tada.</p>

<p>Yeah, the name is not super insightful, so let me rephrase it. It’s about people being able to access other people’s accounts or people being able to access resources they are not allowed to. That’s more impressive when put this way.</p>

<p>A while ago, I wrote an article about the fact that <a href="https://www.ericburel.tech/blog/static-paid-content-app-router"><strong>checking authorization within a layout may leave page content unprotected in Next.js</strong></a>. It’s not a flaw in the framework’s design but a consequence of how React Server Components have a different model than their client counterparts, which then affects how the layout works in Next.</p>

<p>Here is a demo of how you can implement a paywall in Next.js that doesn’t protect anything.</p>

<pre><code class="language-javascript">// app/layout.jsx
// Using cookie-based authentication as usual
async function checkPaid() {
  const token = cookies.get("auth_token");
  return await db.hasPayments(token);
}
// Running the payment check in a layout to apply it to all pages
// Sadly, this is not how Next.js works!
export default async function Layout() {
  // ❌ this won't work as expected!!
  const hasPaid = await checkPaid();
  if (!hasPaid) redirect("/subscribe");
  // then render the underlying page
  return &lt;div&gt;{children}&lt;/div&gt;;
}
// ❌ this can be accessed directly
// by adding “RSC=1” to the request that fetches it!
export default function Page() {
  return &lt;div&gt;PAID CONTENT&lt;/div&gt;
}
</code></pre>

<h2 id="what-we-have-learned-from-the-top-5-vulnerabilities">What We Have Learned From The Top 5 Vulnerabilities</h2>

<p>Most common vulnerabilities are tightly related to application design issues:</p>

<ul>
<li>Copy-pasting configuration without really understanding it.</li>
<li>Having an improper understanding of the framework we use in inner working. Next.js is a complex beast and doesn’t make our life easier on this point!</li>
<li>Picking an algorithm that is not suited for a given task.</li>
</ul>

<p>These vulnerabilities are tough ones because they confront us to our own limits as web developers. Nobody is perfect, and the most experienced developers will inevitably write vulnerable code at some point in their lives without even noticing.</p>

<p>How to prevent that? By not staying alone! When in doubt, ask around fellow developers; there are great chances that someone has faced the same issues and can lead you to the right solutions.</p>

<h2 id="where-to-head-now">Where To Head Now?</h2>

<p>First, I must insist that you have already done a great job of improving the security of your applications by reading this article. Congratulations!</p>

<p>Most hackers rely on a volume strategy and are not particularly skilled, so they are really in pain when confronted with educated developers who can spot and fix the most common vulnerabilities.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/owasp.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="500"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/owasp.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/owasp.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/owasp.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/owasp.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/owasp.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/owasp.jpg"
			
			sizes="100vw"
			alt="OWASP top 10"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      By discovering how the OWASP top 10 can affect full-stack JavaScript applications, you’ve just made hackers’ lives much harder! (<a href='https://files.smashing.media/articles/how-owasp-helps-secure-full-stack-web-applications/owasp.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>From there, I can suggest a few directions to get even better at securing your web applications:</p>

<ul>
<li><strong>Try to apply the OWASP top 10 to an application you know well</strong>, either a personal project, your company’s codebase, or an open-source solution.</li>
<li><strong>Give a shot at some third-party security tools.</strong> They tend to overflow developers with too much information but keep in mind that most actors in the field of security are aware of this issue and work actively to provide more focused vulnerability alerts.</li>
<li>I’ve added my favorite <strong>security-related resources</strong> at the end of the article, so you’ll have plenty to read!</li>
</ul>

<p>Thanks for reading, and stay secure!</p>

<h3 id="resources-for-further-learning">Resources For Further Learning</h3>

<ul>
<li><a href="https://nextpatterns.dev/p/security/ssrf-server-action">An interactive demo of an SSRF in a Next.js app and how to fix it</a></li>
<li><a href="https://owasp.org/www-project-top-ten/">OWASP Top 10</a></li>
<li><a href="https://www.assetnote.io/resources/research/digging-for-ssrf-in-nextjs-apps">An SSRF vulnerability that affected Next.js image optimization system</a></li>
<li><a href="https://www.dash0.com/blog/how-to-inspect-react-server-component-activity-with-next-js">Observe React Server Components using Open Telemetry</a></li>
<li><a href="https://opentelemetry.io/">OpenTelemetry and open source Telemtry standard</a></li>
<li><a href="https://www.ibm.com/think/topics/log4j">Log4J vulnerability</a></li>
<li><a href="https://upstash.com/blog/edge-rate-limiting">Setting up rate limiting in a middleware using a Redis service</a></li>
<li><a href="https://vercel.com/blog/introducing-the-vercel-waf">Vercel WAF annoucement</a></li>
<li><a href="https://cve.mitre.org/">Mitre CVE database</a></li>
<li><a href="https://nextpatterns.dev/p/security/csrf-image">An interactive demo of a CSRF vulnerability in a Next.js app and how to fix it</a></li>
<li><a href="https://www.smashingmagazine.com/2023/01/authentication-websites-banking-analogy/">A super complete guide on authentication specifically targeting web apps</a></li>
<li><a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#server-side-form-validation">Server form validation with zod in Next.js</a> (Astro has it built-in)</li>
<li><a href="https://github.com/colinhacks/zod/discussions/1358">Sanitization with zod</a></li>
<li><a href="https://www.ericburel.tech/blog/static-paid-content-app-router">Secure statically rendered paid content in Next.js and how layouts are a bad place to run authentication checks</a></li>
<li><a href="https://www.smashingmagazine.com/search/?q=security">Smashing Magazine articles related to security</a> (almost 50 matches at the time of writing!)</li>
</ul>

<p><em>This article is inspired by my talk at React Advanced London 2024, “<a href="https://gitnation.com/contents/securing-server-rendered-applications-nextjs-case">Securing Server-Rendered Applications: Next.js case</a>,” which is available to watch as a replay online.</em></p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Edoardo Dusi</author><title>Integrations: From Simple Data Transfer To Modern Composable Architectures</title><link>https://www.smashingmagazine.com/2025/02/integrations-from-simple-data-transfer-to-composable-architectures/</link><pubDate>Tue, 04 Feb 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/02/integrations-from-simple-data-transfer-to-composable-architectures/</guid><description>In today’s web development landscape, the concept of a monolithic application has become increasingly rare. Modern applications are composed of multiple specialized services, each of which handles specific aspects of functionality. This shift didn’t happen overnight &amp;mdash; it’s the result of decades of evolution in how we think about and implement data transfer between systems. Let’s explore this journey and see how it shapes modern architectures, particularly in the context of headless CMS solutions.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/02/integrations-from-simple-data-transfer-to-composable-architectures/" />
              <title>Integrations: From Simple Data Transfer To Modern Composable Architectures</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Integrations: From Simple Data Transfer To Modern Composable Architectures</h1>
                  
                    
                    <address>Edoardo Dusi</address>
                  
                  <time datetime="2025-02-04T08:00:00&#43;00:00" class="op-published">2025-02-04T08:00:00+00:00</time>
                  <time datetime="2025-02-04T08:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>Storyblok</b></p>
                

<p>When computers first started talking to each other, the methods were remarkably simple. In the early days of the Internet, systems exchanged files via FTP or communicated via raw TCP/IP sockets. This direct approach worked well for simple use cases but quickly showed its limitations as applications grew more complex.</p>

<div class="break-out">
<pre><code class="language-python">&#35; Basic socket server example
import socket

server&#95;socket = socket.socket(socket.AF&#95;INET, socket.SOCK&#95;STREAM)
server&#95;socket.bind(('localhost', 12345))
server&#95;socket.listen(1)

while True:
    connection, address = server&#95;socket.accept()
    data = connection.recv(1024)
    &#35; Process data
    connection.send(response)
</code></pre>
</div>
  

<p>The real breakthrough in enabling complex communication between computers on a network came with the introduction of <strong>Remote Procedure Calls (RPC)</strong> in the 1980s. RPC allowed developers to call procedures on remote systems as if they were local functions, abstracting away the complexity of network communication. This pattern laid the foundation for many of the modern integration approaches we use today.</p>

<blockquote>At its core, RPC implements a client-server model where the client prepares and serializes a procedure call with parameters, sends the message to a remote server, the server deserializes and executes the procedure, and then sends the response back to the client.</blockquote>

<p>Here’s a simplified example using Python’s XML-RPC.</p>

<pre><code class="language-python">&#35; Server
from xmlrpc.server import SimpleXMLRPCServer

def calculate&#95;total(items):
    return sum(items)

server = SimpleXMLRPCServer(("localhost", 8000))
server.register&#95;function(calculate&#95;total)
server.serve&#95;forever()

&#35; Client
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy("http://localhost:8000/")
try:
    result = proxy.calculate&#95;total([1, 2, 3, 4, 5])
except ConnectionError:
    print("Network error occurred")
</code></pre>
  

<p>RPC can operate in both synchronous (blocking) and asynchronous modes.</p>

<p>Modern implementations such as gRPC support streaming and bi-directional communication. In the example below, we define a gRPC service called <code>Calculator</code> with two RPC methods, <code>Calculate</code>, which takes a <code>Numbers</code> message and returns a <code>Result</code> message, and <code>CalculateStream</code>, which sends a stream of <code>Result</code> messages in response.</p>

<pre><code class="language-python">// protobuf
service Calculator {
  rpc Calculate(Numbers) returns (Result);
  rpc CalculateStream(Numbers) returns (stream Result);
}
</code></pre>
  

<h2 id="modern-integrations-the-rise-of-web-services-and-soa">Modern Integrations: The Rise Of Web Services And SOA</h2>

<p>The late 1990s and early 2000s saw the emergence of <strong>Web Services</strong> and <strong>Service-Oriented Architecture (SOA)</strong>. SOAP (Simple Object Access Protocol) became the standard for enterprise integration, introducing a more structured approach to system communication.</p>

<div class="break-out">
<pre><code class="language-xml">&lt;?xml version="1.0"?&gt;
&lt;soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"&gt;
  &lt;soap:Header&gt;
  &lt;/soap:Header&gt;
  &lt;soap:Body&gt;
    &lt;m:GetStockPrice xmlns:m="http://www.example.org/stock"&gt;
      &lt;m:StockName&gt;IBM&lt;/m:StockName&gt;
    &lt;/m:GetStockPrice&gt;
  &lt;/soap:Body&gt;
&lt;/soap:Envelope&gt;
</code></pre>
</div>
  

<p>While SOAP provided robust enterprise features, its complexity, and verbosity led to the development of simpler alternatives, especially the REST APIs that dominate Web services communication today.</p>

<p>But REST is not alone. Let’s have a look at some modern integration patterns.</p>

<h3 id="restful-apis">RESTful APIs</h3>

<p><strong>REST (Representational State Transfer)</strong> has become the de facto standard for Web APIs, providing a simple, stateless approach to manipulating resources. Its simplicity and HTTP-based nature make it ideal for web applications.</p>

<p>First defined by Roy Fielding in 2000 as an architectural style on top of the Web’s standard protocols, its constraints align perfectly with the goals of the modern Web, such as <strong>performance</strong>, <strong>scalability</strong>, <strong>reliability</strong>, and <strong>visibility</strong>: client and server separated by an interface and loosely coupled, stateless communication, cacheable responses.</p>

<p>In modern applications, the most common implementations of the REST protocol are based on the JSON format, which is used to encode messages for requests and responses.</p>

<div class="break-out">
<pre><code class="language-javascript">// Request
async function fetchUserData() {
  const response = await fetch('https://api.example.com/users/123');
  const userData = await response.json();
  return userData;
}

// Response
{
  "id": "123",
  "name": "John Doe",
  "&#95;links": {
    "self": { "href": "/users/123" },
    "orders": { "href": "/users/123/orders" },
    "preferences": { "href": "/users/123/preferences" }
  }
}
</code></pre>
</div>
  

<h3 id="graphql">GraphQL</h3>

<p>GraphQL emerged from Facebook’s internal development needs in 2012 before being open-sourced in 2015. Born out of the challenges of building complex mobile applications, it addressed limitations in traditional REST APIs, particularly the issues of over-fetching and under-fetching data.</p>

<p>At its core, GraphQL is a query language and runtime that provides a type system and declarative data fetching, allowing the client to specify exactly what it wants to fetch from the server.</p>

<pre><code class="language-graphql">// graphql
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  publishDate: String!
}

query GetUserWithPosts {
  user(id: "123") {
    name
    posts(last: 3) {
      title
      publishDate
    }
  }
}
</code></pre>

<p>Often used to build complex UIs with nested data structures, mobile applications, or microservices architectures, it has proven effective at handling complex data requirements at scale and offers a growing ecosystem of tools.</p>

<h3 id="webhooks">Webhooks</h3>

<p>Modern applications often require real-time updates. For example, e-commerce apps need to update inventory levels when a purchase is made, or content management apps need to refresh cached content when a document is edited. Traditional request-response models can struggle to meet these demands because they rely on clients’ polling servers for updates, which is inefficient and resource-intensive.</p>

<p>Webhooks and event-driven architectures address these needs more effectively. Webhooks let servers send real-time notifications to clients or other systems when specific events happen. This reduces the need for continuous polling. Event-driven architectures go further by decoupling application components. Services can publish and subscribe to events asynchronously, and this makes the system more scalable, responsive, and simpler.</p>

<pre><code class="language-javascript">import fastify from 'fastify';

const server = fastify();
server.post('/webhook', async (request, reply) =&gt; {
  const event = request.body;
  
  if (event.type === 'content.published') {
    await refreshCache();
  }
  
  return reply.code(200).send();
});
</code></pre>
  

<p>This is a simple Node.js function that uses Fastify to set up a web server. It responds to the endpoint <code>/webhook</code>, checks the <code>type</code> field of the JSON request, and refreshes a cache if the event is of type <code>content.published</code>.</p>

<p>With all this background information and technical knowledge, it’s easier to picture the current state of web application development, where <strong>a single, monolithic app is no longer the answer to business needs</strong>, but a new paradigm has emerged: Composable Architecture.</p>

<h2 id="composable-architecture-and-headless-cmss">Composable Architecture And Headless CMSs</h2>

<p>This evolution has led us to the concept of composable architecture, where applications are built by <strong>combining specialized services</strong>. This is where headless CMS solutions have a clear advantage, serving as the perfect example of how modern integration patterns come together.</p>

<p>Headless CMS platforms separate content management from content presentation, allowing you to build specialized frontends relying on a fully-featured content backend. This decoupling facilitates <strong>content reuse</strong>, <strong>independent scaling</strong>, and the <strong>flexibility</strong> to use a dedicated technology or service for each part of the system.</p>

<p>Take <a href="https://www.storyblok.com/?utm_source=smashing&amp;utm_medium=sponsor&amp;utm_campaign=DGM_DEV_SMA_TRA&amp;utm_content=smashing-OSS">Storyblok</a> as an example. Storyblok is a headless CMS designed to help developers build flexible, scalable, and composable applications. Content is exposed via API, REST, or GraphQL; it offers a long list of events that can trigger a webhook. Editors are happy with a great Visual Editor, where they can see changes in real time, and many integrations are available out-of-the-box via a marketplace.</p>

<p>Imagine this <code>ContentDeliveryService</code> in your app, where you can interact with Storyblok’s REST API using the <a href="https://github.com/storyblok/storyblok-js-client">open source JS Client</a>:</p>

<div class="break-out">
<pre><code class="language-javascript">import StoryblokClient from "storyblok-js-client";

class ContentDeliveryService {
  constructor(private storyblok: StoryblokClient) {}

  async getPageContent(slug: string) {
    const { data } = await this.storyblok.get(`cdn/stories/${slug}`, {
      version: 'published',
      resolve&#95;relations: 'featured-products.products'
    });

    return data.story;
  }

  async getRelatedContent(tags: string[]) {
    const { data } = await this.storyblok.get('cdn/stories', {
      version: 'published',
      with&#95;tag: tags.join(',')
    });

    return data.stories;
  }
}
</code></pre>
</div>
  

<p>The last piece of the puzzle is a real example of integration.</p>

<p>Again, many are already available in the Storyblok marketplace, and you can easily control them from the dashboard. However, to fully leverage the Composable Architecture, we can use the most powerful tool in the developer’s hand: code.</p>

<p>Let’s imagine a modern e-commerce platform that uses Storyblok as its content hub, Shopify for inventory and orders, Algolia for product search, and Stripe for payments.</p>

<p>Once each account is set up and we have our access tokens, we could quickly build a front-end page for our store. This isn’t production-ready code, but just to get a quick idea, let’s use React to build the page for a single product that integrates our services.</p>

<p>First, we should initialize our clients:</p>

<pre><code class="language-javascript">import StoryblokClient from "storyblok-js-client";
import { algoliasearch } from "algoliasearch";
import Client from "shopify-buy";


const storyblok = new StoryblokClient({
  accessToken: "your&#95;storyblok&#95;token",
});
const algoliaClient = algoliasearch(
  "your&#95;algolia&#95;app&#95;id",
  "your&#95;algolia&#95;api&#95;key",
);
const shopifyClient = Client.buildClient({
  domain: "your-shopify-store.myshopify.com",
  storefrontAccessToken: "your&#95;storefront&#95;access&#95;token",
});
</code></pre>
  

<p>Given that we created a <code>blok</code> in Storyblok that holds product information such as the <code>product_id</code>, we could write a component that takes the <code>productSlug</code>, fetches the product content from Storyblok, the inventory data from Shopify, and some related products from the Algolia index:</p>

<div class="break-out">
<pre><code class="language-javascript">async function fetchProduct() {
  // get product from Storyblok
  const { data } = await storyblok.get(`cdn/stories/${productSlug}`);

  // fetch inventory from Shopify
  const shopifyInventory = await shopifyClient.product.fetch(
    data.story.content.product&#95;id
  );

  // fetch related products using Algolia
  const { hits } = await algoliaIndex.search("products", {
    filters: `category:${data.story.content.category}`,
  });
}
</code></pre>
</div>
  

<p>We could then set a simple component state:</p>

<pre><code class="language-javascript">const [productData, setProductData] = useState(null);
const [inventory, setInventory] = useState(null);
const [relatedProducts, setRelatedProducts] = useState([]);

useEffect(() =&gt;
  // ...
  // combine fetchProduct() with setState to update the state
  // ...

  fetchProduct();
}, [productSlug]);
</code></pre>
  

<p>And return a template with all our data:</p>

<pre><code class="language-javascript">&lt;h1&gt;{productData.content.title}&lt;/h1&gt;
&lt;p&gt;{productData.content.description}&lt;/p&gt;
&lt;h2&gt;Price: ${inventory.variants[0].price}&lt;/h2&gt;
&lt;h3&gt;Related Products&lt;/h3&gt;
&lt;ul&gt;
  {relatedProducts.map((product) =&gt; (
    &lt;li key={product.objectID}&gt;{product.name}&lt;/li&gt;
  ))}
&lt;/ul&gt;
</code></pre>
  

<p>We could then use an event-driven approach and create a server that listens to our shop events and processes the checkout with Stripe (credits to Manuel Spigolon for <a href="https://backend.cafe/integrate-stripe-with-fastify">this tutorial</a>):</p>

<div class="break-out">
<pre><code class="language-javascript">const stripe = require('stripe')

module.exports = async function plugin (app, opts) {
  const stripeClient = stripe(app.config.STRIPE&#95;PRIVATE&#95;KEY)

  server.post('/create-checkout-session', async (request, reply) =&gt; {
    const session = await stripeClient.checkout.sessions.create({
      line&#95;items: [...], // from request.body
      mode: 'payment',
      success&#95;url: "https://your-site.com/success",
      cancel&#95;url: "https://your-site.com/cancel",
    })

    return reply.redirect(303, session.url)
  })
// ...
</code></pre>
</div>

<p>And with this approach, each service is independent of the others, which helps us achieve our business goals (performance, scalability, flexibility) with a good developer experience and a smaller and simpler application that’s easier to maintain.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The integration between headless CMSs and modern web services represents the current and future state of high-performance web applications. By using specialized, decoupled services, developers can focus on business logic and user experience. A composable ecosystem is not only modular but also resilient to the evolving needs of the modern enterprise.</p>

<p>These integrations highlight the importance of mastering API-driven architectures and understanding how different tools can harmoniously fit into a larger tech stack.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aIn%20today%e2%80%99s%20digital%20landscape,%20success%20lies%20in%20choosing%20tools%20that%20offer%20flexibility%20and%20efficiency,%20adapt%20to%20evolving%20demands,%20and%20create%20applications%20that%20are%20future-proof%20against%20the%20challenges%20of%20tomorrow.%0a&url=https://smashingmagazine.com%2f2025%2f02%2fintegrations-from-simple-data-transfer-to-composable-architectures%2f">
      
In today’s digital landscape, success lies in choosing tools that offer flexibility and efficiency, adapt to evolving demands, and create applications that are future-proof against the challenges of tomorrow.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>If you want to dive deeper into the integrations you can build with Storyblok and other services, check out <a href="https://www.storyblok.com/ecosystem/?utm_source=smashing&amp;utm_medium=sponsor&amp;utm_campaign=DGM_DEV_SMA_TRA&amp;utm_content=smashing-OSS">Storyblok’s integrations page</a>. You can also take your projects further by creating your own plugins with <a href="https://www.storyblok.com/docs/plugins/field-plugins/introduction/?utm_source=smashing&amp;utm_medium=sponsor&amp;utm_campaign=DGM_DEV_SMA_TRA&amp;utm_content=smashing-OSS">Storyblok’s plugin development</a> resources.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Joas Pambou</author><title>On-Device AI: Building Smarter, Faster, And Private Applications</title><link>https://www.smashingmagazine.com/2025/01/on-device-ai-building-smarter-faster-private-applications/</link><pubDate>Thu, 16 Jan 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/01/on-device-ai-building-smarter-faster-private-applications/</guid><description>Shouldn’t there be a way to keep your apps or project data private and improve performance by reducing server latency? This is what on-device AI is designed to solve. It handles AI processing locally, right on your device, without connecting to the internet and sending data to the cloud. In this article, Joas Pambou explains what on-device AI is, why it’s important, the tools to build this type of technology, and how it can change the way we use technology every day.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/01/on-device-ai-building-smarter-faster-private-applications/" />
              <title>On-Device AI: Building Smarter, Faster, And Private Applications</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>On-Device AI: Building Smarter, Faster, And Private Applications</h1>
                  
                    
                    <address>Joas Pambou</address>
                  
                  <time datetime="2025-01-16T13:00:00&#43;00:00" class="op-published">2025-01-16T13:00:00+00:00</time>
                  <time datetime="2025-01-16T13:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>It’s not too far-fetched to say AI is a pretty handy tool that we all rely on for everyday tasks. It handles tasks like recognizing faces, understanding or cloning speech, analyzing large data, and creating personalized app experiences, such as music playlists based on your listening habits or workout plans matched to your progress.</p>

<p>But here’s the catch:</p>

<blockquote>Where AI tool actually lives and does its work matters a lot.</blockquote>

<p>Take self-driving cars, for example. These types of cars need AI to process data from cameras, sensors, and other inputs to make split-second decisions, such as detecting obstacles or adjusting speed for sharp turns. Now, if all that processing depends on the cloud, network latency connection issues could lead to delayed responses or system failures. That’s why the AI should operate directly within the car. This ensures the car responds instantly without needing direct access to the internet.</p>

<p>This is what we call <strong>On-Device AI (ODAI)</strong>. Simply put, ODAI means AI does its job right where you are &mdash; on your phone, your car, or your wearable device, and so on &mdash; without a real need to connect to the cloud or internet in some cases. More precisely, this kind of setup is categorized as <strong>Embedded AI (EMAI)</strong>, where the intelligence is embedded into the device itself.</p>

<p>Okay, I mentioned ODAI and then EMAI as a subset that falls under the umbrella of ODAI. However, EMAI is slightly different from other terms you might come across, such as Edge AI, Web AI, and Cloud AI. So, what’s the difference? Here’s a quick breakdown:</p>

<ul>
<li><strong>Edge AI</strong><br />
It refers to running AI models directly on devices instead of relying on remote servers or the cloud. A simple example of this is a security camera that can analyze footage right where it is. It processes everything locally and is close to where the data is collected.</li>
<li><strong>Embedded AI</strong><br />
In this case, AI algorithms are built inside the device or hardware itself, so it functions as if the device has its own mini AI brain. I mentioned self-driving cars earlier &mdash; another example is AI-powered drones, which can monitor areas or map terrains. One of the main differences between the two is that EMAI uses dedicated chips integrated with AI models and algorithms to perform intelligent tasks locally.</li>
<li><strong>Cloud AI</strong><br />
This is when the AI lives and relies on the cloud or remote servers. When you use a language translation app, the app sends the text you want to be translated to a cloud-based server, where the AI processes it and the translation back. The entire operation happens in the cloud, so it requires an internet connection to work.</li>
<li><strong>Web AI</strong><br />
These are tools or apps that run in your browser or are part of websites or online platforms. You might see product suggestions that match your preferences based on what you’ve looked at or purchased before. However, these tools often rely on AI models hosted in the cloud to analyze data and generate recommendations.</li>
</ul>

<p>The main difference? It’s about <em>where</em> the AI does the work: on your device, nearby, or somewhere far off in the cloud or web.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/image-optimization/">Image Optimization</a></strong>, Addy Osmani’s new practical guide to optimizing and delivering <strong>high-quality images</strong> on the web. Everything in one single <strong>528-pages</strong> book.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/image-optimization/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/image-optimization/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c669cf1-c6ef-4c87-9901-018b04f7871f/image-optimization-shop-cover-opt.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/87fd0cfa-692e-459c-b2f3-15209a1f6aa7/image-optimization-shop-cover-opt.png"
    alt="Feature Panel"
    width="480"
    height="697"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="what-makes-on-device-ai-useful">What Makes On-Device AI Useful</h2>

<p>On-device AI is, first and foremost, about privacy &mdash; keeping your data secure and under your control. It processes everything directly on your device, avoiding the need to send personal data to external servers (cloud). So, what exactly makes this technology worth using?</p>

<h3 id="real-time-processing">Real-Time Processing</h3>

<p>On-device AI processes data instantly because it doesn’t need to send anything to the cloud. For example, think of a smart doorbell &mdash; it recognizes a visitor’s face right away and notifies you. If it had to wait for cloud servers to analyze the image, there’d be a delay, which wouldn’t be practical for quick notifications.</p>

<h3 id="enhanced-privacy-and-security">Enhanced Privacy and Security</h3>

<p>Picture this: You are opening an app using voice commands or calling a friend and receiving a summary of the conversation afterward. Your phone processes the audio data locally, and the AI system handles everything directly on your device without the help of external servers. This way, your data stays private, secure, and under your control.</p>

<h3 id="offline-functionality">Offline Functionality</h3>

<p>A big win of ODAI is that it doesn’t need the internet to work, which means it can function even in areas with poor or no connectivity. You can take modern GPS navigation systems in a car as an example; they give you turn-by-turn directions with no signal, making sure you still get where you need to go.</p>

<h3 id="reduced-latency">Reduced Latency</h3>

<p>ODAI AI skips out the round trip of sending data to the cloud and waiting for a response. This means that when you make a change, like adjusting a setting, the device processes the input immediately, making your experience smoother and more responsive.</p>

<h2 id="the-technical-pieces-of-the-on-device-ai-puzzle">The Technical Pieces Of The On-Device AI Puzzle</h2>

<p>At its core, ODAI uses special hardware and efficient model designs to carry out tasks directly on devices like smartphones, smartwatches, and Internet of Things (IoT) gadgets. Thanks to the advances in hardware technology, AI can now work locally, especially for tasks requiring AI-specific computer processing, such as the following:</p>

<ul>
<li><strong>Neural Processing Units (NPUs)</strong><br />
These chips are specifically designed for AI and optimized for neural nets, deep learning, and machine learning applications. They can handle large-scale AI training efficiently while consuming minimal power.</li>
<li><strong>Graphics Processing Units (GPUs)</strong><br />
Known for processing multiple tasks simultaneously, GPUs excel in speeding up AI operations, particularly with massive datasets.</li>
</ul>

<p>Here’s a look at some innovative AI chips in the industry:</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Product</th>
            <th>Organization</th>
      <th>Key Features</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><a href="https://medium.com/@ansuman_72498/the-promise-of-a-new-dawn-with-neuromorphic-computing-indian-institute-of-sciences-656da6426c2c">Spiking Neural Network Chip</a></td>
            <td>Indian Institute of Technology</td>
      <td>Ultra-low power consumption</td>
        </tr>
        <tr>
            <td><a href="https://ceremorphic.com/ceremorphic-exits-stealth-mode-unveils-technology-plans-to-deliver-a-new-architecture-specifically-designed-for-reliable-performance-computing/">Hierarchical Learning Processor</a></td>
            <td>Ceromorphic</td>
      <td>Alternative transistor structure</td>
        </tr>
        <tr>
            <td><a href="https://www.graphcore.ai/products/ipu">Intelligent Processing Units (IPUs)</a></td>
            <td>Graphcore</td>
      <td>Multiple products targeting end devices and cloud</td>
        </tr>
    <tr>
            <td><a href="https://www.synaptics.com/company/news/synaptics-katana-evk-accelerate-development-ai-sensor-fusion">Katana Edge AI</a></td>
            <td>Synaptics</td>
      <td>Combines vision, motion, and sound detection</td>
        </tr>
    <tr>
            <td><a href="https://www.esperanto.ai">ET-SoC-1 Chip</a></td>
            <td>Esperanto Technology</td>
      <td>Built on RISC-V for AI and non-AI workloads</td>
        </tr>
    <tr>
            <td><a href="https://www.leti-cea.com/cea-tech/leti/english/Pages/Leti/Projects%20supported/NEURAM3.aspx">NeuRRAM</a></td>
            <td>CEA–Leti</td>
      <td>Biologically inspired neuromorphic processor based on resistive RAM (RRAM)</td>
        </tr>
    </tbody>
</table>

<p>These chips or AI accelerators show different ways to make devices more efficient, use less power, and run advanced AI tasks.</p>

<div class="partners__lead-place"></div>

<h2 id="techniques-for-optimizing-ai-models">Techniques For Optimizing AI Models</h2>

<p>Creating AI models that fit resource-constrained devices often requires combining clever hardware utilization with techniques to make models smaller and more efficient. I’d like to cover a few choice examples of how teams are optimizing AI for increased performance using less energy.</p>

<h3 id="meta-s-mobilellm">Meta’s MobileLLM</h3>

<p><a href="https://arxiv.org/abs/2402.14905">Meta’s approach</a> to ODAI introduced a model built specifically for smartphones. Instead of scaling traditional models, they designed MobileLLM from scratch to balance efficiency and performance. One key innovation was increasing the number of smaller layers rather than having fewer large ones. This design choice improved the model’s accuracy and speed while keeping it lightweight. You can try out the model either on Hugging Face or using vLLM, a library for LLM inference and serving.</p>

<h3 id="quantization">Quantization</h3>

<p>This simplifies a model’s internal calculations by using lower-precision numbers, such as 8-bit integers, instead of 32-bit floating-point numbers. Quantization significantly reduces memory requirements and computation costs, often with minimal impact on model accuracy.</p>

<h3 id="pruning">Pruning</h3>

<p>Neural networks contain many weights (connections between neurons), but not all are crucial. Pruning identifies and removes less important weights, resulting in a smaller, faster model without significant accuracy loss.</p>

<h3 id="matrix-decomposition">Matrix Decomposition</h3>

<p>Large matrices are a core component of AI models. Matrix decomposition splits these into smaller matrices, reducing computational complexity while approximating the original model’s behavior.</p>

<h3 id="knowledge-distillation">Knowledge Distillation</h3>

<p>This technique involves training a smaller model (the “student”) to mimic the outputs of a larger, pre-trained model (the “teacher”). The smaller model learns to replicate the teacher’s behavior, achieving similar accuracy while being more efficient. For instance, <a href="https://arxiv.org/abs/1910.01108"><strong>DistilBERT</strong> successfully reduced BERT’s size by 40% while retaining 97% of its performance</a>.</p>

<h2 id="technologies-used-for-on-device-ai">Technologies Used For On-Device AI</h2>

<p>Well, all the model compression techniques and specialized chips are cool because they’re what make ODAI possible. But what’s even more interesting for us as developers is actually putting these tools to work. This section covers some of the key technologies and frameworks that make ODAI accessible.</p>

<h3 id="mediapipe-solutions">MediaPipe Solutions</h3>

<p><a href="https://chuoling.github.io/mediapipe/">MediaPipe Solutions</a> is a developer toolkit for adding AI-powered features to apps and devices. It offers cross-platform, customizable tools that are optimized for running AI locally, from real-time video analysis to natural language processing.</p>

<p>At the heart of MediaPipe Solutions is <a href="https://ai.google.dev/edge/mediapipe/solutions/tasks">MediaPipe Tasks</a>, a core library that lets developers deploy ML solutions with minimal code. It’s designed for platforms like Android, Python, and Web/JavaScript, so you can easily integrate AI into a wide range of applications.</p>

<p>MediaPipe also provides various specialized tasks for different AI needs:</p>

<ul>
<li><a href="https://mediapipe-studio.webapps.google.com/demo/llm_inference"><strong>LLM Inference API</strong></a><br />
This API runs lightweight large language models (LLMs) entirely on-device for tasks like text generation and summarization. It supports several open models like Gemma and external options like Phi-2.</li>
<li><a href="https://mediapipe-studio.webapps.google.com/demo/object_detector"><strong>Object Detection</strong></a><br />
The tool helps you Identify and locate objects in images or videos, which is ideal for real-time applications like detecting animals, people, or objects right on the device.</li>
<li><a href="https://mediapipe-studio.webapps.google.com/demo/image_segmenter"><strong>Image Segmentation</strong></a><br />
MediaPipe can also segment images, such as isolating a person from the background in a video feed, allowing it to separate objects in both single images (like photos) and continuous video streams (like live video or recorded footage).</li>
</ul>

<h3 id="litert">LiteRT</h3>

<p><a href="https://github.com/google-ai-edge/litert">LiteRT or Lite Runtime</a> (previously called TensorFlow Lite) is a lightweight and high-performance runtime designed for ODAI. It supports running pre-trained models or converting TensorFlow, PyTorch, and JAX models to a LiteRT-compatible format using AI Edge tools.</p>

<h3 id="model-explorer">Model Explorer</h3>

<p><a href="https://github.com/google-ai-edge/model-explorer">Model Explorer</a> is a visualization tool that helps you analyze machine learning models and graphs. It simplifies the process of preparing these models for on-device AI deployment, letting you understand the structure of your models and fine-tune them for better performance.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/on-device-ai-building-smarter-faster-private-applications/1-model-explorer.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="554"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/on-device-ai-building-smarter-faster-private-applications/1-model-explorer.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/on-device-ai-building-smarter-faster-private-applications/1-model-explorer.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/on-device-ai-building-smarter-faster-private-applications/1-model-explorer.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/on-device-ai-building-smarter-faster-private-applications/1-model-explorer.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/on-device-ai-building-smarter-faster-private-applications/1-model-explorer.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/on-device-ai-building-smarter-faster-private-applications/1-model-explorer.png"
			
			sizes="100vw"
			alt="Screenshot of the Model Explorer tool"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Model-Explorer: Visualize ML models and graphs to prepare and optimize them for On-Device ai. (Image source: Google) (<a href='https://files.smashing.media/articles/on-device-ai-building-smarter-faster-private-applications/1-model-explorer.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You can use Model Explorer locally or in <a href="https://github.com/google-ai-edge/model-explorer/blob/main/example_colabs/quick_start.ipynb">Colab</a> for testing and experimenting.</p>

<h3 id="executorch">ExecuTorch</h3>

<p>If you’re familiar with PyTorch, ExecuTorch makes it easy to deploy models to mobile, wearables, and edge devices. It’s part of the <a href="https://pytorch.org/edge">PyTorch Edge ecosystem</a>, which supports building AI experiences for edge devices like embedded systems and microcontrollers.</p>

<div class="partners__lead-place"></div>

<h2 id="large-language-models-for-on-device-ai">Large Language Models For On-Device AI</h2>

<p>Gemini is a powerful AI model that doesn’t just excel in processing text or images. It can also handle multiple types of data seamlessly. The best part? It’s designed to work right on your devices.</p>

<p>For on-device use, there’s <a href="https://deepmind.google/technologies/gemini/nano/"><strong>Gemini Nano</strong></a>, a lightweight version of the model. It’s built to perform efficiently while keeping everything private.</p>

<p><strong>What can Gemini Nano do?</strong></p>

<ul>
<li><strong>Call Notes on Pixel devices</strong><br />
This feature creates private summaries and transcripts of conversations. It works entirely on-device, ensuring privacy for everyone involved.</li>
</ul>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1044286315"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<ul>
<li><strong>Pixel Recorder app</strong><br />
With the help of Gemini Nano and AICore, the app provides an on-device summarization feature, making it easy to extract key points from recordings.</li>
</ul>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="lmeQEa0MMps"
      
			
		></lite-youtube>
	</div>
	
</figure>

<ul>
<li><strong>TalkBack</strong><br />
Enhances the accessibility feature on Android phones by providing clear descriptions of images, thanks to Nano’s multimodal capabilities.<br /></li>
</ul>

<p><strong>Note</strong>: <em>It’s similar to an application we built using LLaVA in <a href="https://www.smashingmagazine.com/2024/10/using-multimodal-ai-models-applications-part3/">a previous article</a>.</em></p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="7cpwzZLLiQw"
      
			
		></lite-youtube>
	</div>
	
</figure>

<p>Gemini Nano is far from the only language model designed specifically for ODAI. I’ve collected a few others that are worth mentioning:</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Model</th>
            <th>Developer</th>
      <th>Research Paper</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><strong>Octopus v2</strong></td>
            <td>NexaAI</td>
      <td><a href="https://arxiv.org/pdf/2404.01744">On-device language model for super agent</a></td>
        </tr>
        <tr>
            <td><strong>OpenELM</strong></td>
            <td>Apple ML Research</td>
      <td><a href="https://arxiv.org/abs/2404.14619">A significant large language model integrated within iOS to enhance application functionalities</a></td>
        </tr>
        <tr>
            <td><strong>Ferret-v2</strong></td>
            <td>Apple</td>
      <td><a href="https://arxiv.org/abs/2404.07973">Ferret-v2 significantly improves upon its predecessor, introducing enhanced visual processing capabilities and an advanced training regimen</a></td>
        </tr>
    <tr>
            <td><strong>MiniCPM</strong></td>
            <td>Tsinghua University</td>
      <td><a href="https://huggingface.co/openbmb/MiniCPM-Llama3-V-2_5">A GPT-4V Level Multimodal LLM on Your Phone</a></td>
        </tr>
        <tr>
            <td><strong>Phi-3</strong></td>
            <td>Microsoft</td>
      <td><a href="https://arxiv.org/pdf/2404.14219">Phi-3 Technical Report: A Highly Capable Language Model Locally on Your Phone</a></td>
        </tr>
    </tbody>
</table>

<h2 id="the-trade-offs-of-using-on-device-ai">The Trade-Offs of Using On-Device AI</h2>

<p>Building AI into devices can be exciting and practical, but it’s not without its challenges. While you may get a lightweight, private solution for your app, there are a few compromises along the way. Here’s a look at some of them:</p>

<h3 id="limited-resources">Limited Resources</h3>

<p>Phones, wearables, and similar devices don’t have the same computing power as larger machines. This means AI models must fit within limited storage and memory while running efficiently. Additionally, running AI can drain the battery, so the models need to be optimized to balance power usage and performance.</p>

<h3 id="data-and-updates">Data and Updates</h3>

<p>AI in devices like drones, self-driving cars, and other similar devices process data quickly, using sensors or lidar to make decisions. However, these models or the system itself don’t usually get real-time updates or additional training unless they are connected to the cloud. Without these updates and regular model training, the system may struggle with new situations.</p>

<h3 id="biases">Biases</h3>

<p>Biases in training data are a common challenge in AI, and ODAI models are no exception. These biases can lead to unfair decisions or errors, like misidentifying people. For ODAI, keeping these models fair and reliable means not only addressing these biases during training but also ensuring the solutions work efficiently within the device’s constraints.</p>

<p>These aren&rsquo;t the only challenges of on-device AI. It&rsquo;s still a new and growing technology, and the small number of professionals in the field makes it harder to implement.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Choosing between on-device and cloud-based AI comes down to what your application needs most. Here’s a quick comparison to make things clear:</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Aspect</th>
            <th>On-Device AI</th>
      <th>Cloud-Based AI</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><strong>Privacy</strong></td>
            <td>Data stays on the device, ensuring privacy.</td>
      <td>Data is sent to the cloud, raising potential privacy concerns.</td>
        </tr>
        <tr>
            <td><strong>Latency</strong></td>
            <td>Processes instantly with no delay.</td>
      <td>Relies on internet speed, which can introduce delays.</td>
        </tr>
        <tr>
            <td><strong>Connectivity</strong></td>
            <td>Works offline, making it reliable in any setting.</td>
      <td>Requires a stable internet connection.</td>
        </tr>
    <tr>
            <td><strong>Processing Power</strong></td>
            <td>Limited by device hardware.</td>
      <td>Leverages the power of cloud servers for complex tasks.</td>
        </tr>
    <tr>
            <td><strong>Cost</strong></td>
            <td>No ongoing server expenses.</td>
      <td>Can incur continuous cloud infrastructure costs.</td>
        </tr>
    </tbody>
</table>

<p>For apps that need <strong>fast processing</strong> and <strong>strong privacy</strong>, ODAI is the way to go. On the other hand, cloud-based AI is better when you need <strong>more computing power</strong> and <strong>frequent updates</strong>. The choice depends on your project’s needs and what matters most to you.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Juan Diego Rodríguez</author><title>Uniting Web And Native Apps With 4 Unknown JavaScript APIs</title><link>https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/</link><pubDate>Thu, 20 Jun 2024 18:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/</guid><description>Have you heard of the Screen Orientation API? What about the Device Orientation API, Vibration API, or the Contact Picker API? Juan Diego Rodriguez is interested in these under-the-radar web features and discusses how they can be used to create more usable and robust progressive web apps if and when they gain broader support.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/" />
              <title>Uniting Web And Native Apps With 4 Unknown JavaScript APIs</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Uniting Web And Native Apps With 4 Unknown JavaScript APIs</h1>
                  
                    
                    <address>Juan Diego Rodríguez</address>
                  
                  <time datetime="2024-06-20T18:00:00&#43;00:00" class="op-published">2024-06-20T18:00:00+00:00</time>
                  <time datetime="2024-06-20T18:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>A couple of years ago, <a href="https://www.smashingmagazine.com/2022/09/javascript-api-guide/">four JavaScript APIs that landed at the bottom of awareness in the State of JavaScript survey</a>. I took an interest in those APIs because they have so much potential to be useful but don’t get the credit they deserve. Even after a quick search, I was amazed at how many new web APIs have been added to the ECMAScript specification that aren’t getting their dues and with a lack of awareness and browser support in browsers.</p>

<p>That situation can be a “catch-22”:</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aAn%20API%20is%20interesting%20but%20lacks%20awareness%20due%20to%20incomplete%20support,%20and%20there%20is%20no%20immediate%20need%20to%20support%20it%20due%20to%20low%20awareness.%0a&url=https://smashingmagazine.com%2f2024%2f06%2funiting-web-native-apps-unknown-javascript-apis%2f">
      
An API is interesting but lacks awareness due to incomplete support, and there is no immediate need to support it due to low awareness.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Most of these APIs are designed to power progressive web apps (PWA) and close the gap between web and native apps. Bear in mind that creating a PWA involves more than just adding a <a href="https://css-tricks.com/how-to-transition-to-manifest-v3-for-chrome-extensions/">manifest file</a>. Sure, it’s a PWA by definition, but it functions like a bookmark on your home screen in practice. In reality, we need several APIs to achieve a fully native app experience on the web. And the four APIs I’d like to shed light on are part of that PWA puzzle that brings to the web what we once thought was only possible in native apps.</p>

<p>You can see all these <a href="https://monknow.github.io/pwa-features-demo/">APIs in action in this demo</a> as we go along.</p>

<h2 id="1-screen-orientation-api">1. Screen Orientation API</h2>

<p>The <a href="https://www.w3.org/TR/screen-orientation/">Screen Orientation API</a> can be used to sniff out the device’s current orientation. Once we know whether a user is browsing in a portrait or landscape orientation, we can use it to <strong>enhance the UX for mobile devices</strong> by changing the UI accordingly. We can also use it to <strong>lock the screen in a certain position</strong>, which is useful for displaying videos and other full-screen elements that benefit from a wider viewport.</p>

<p>Using the global <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/screen"><code>screen</code></a> object, you can access various properties the screen uses to render a page, including the <code>screen.orientation</code> object. It has two properties:</p>

<ul>
<li><strong><code>type</code>:</strong> The current screen orientation. It can be: <code>&quot;portrait-primary&quot;</code>, <code>&quot;portrait-secondary&quot;</code>, <code>&quot;landscape-primary&quot;</code>, or <code>&quot;landscape-secondary&quot;</code>.</li>
<li><strong><code>angle</code>:</strong> The current screen orientation angle. It can be any number from 0 to 360 degrees, but it’s normally set in multiples of 90 degrees (e.g., <code>0</code>, <code>90</code>, <code>180</code>, or <code>270</code>).</li>
</ul>

<p>On mobile devices, if the <code>angle</code> is <code>0</code> degrees, the <code>type</code> is most often going to evaluate to <code>&quot;portrait&quot;</code> (vertical), but on desktop devices, it is typically <code>&quot;landscape&quot;</code> (horizontal). This makes the <code>type</code> property precise for knowing a device’s true position.</p>

<p>The <code>screen.orientation</code> object also has two methods:</p>

<ul>
<li><strong><code>.lock()</code>:</strong> This is an async method that takes a <code>type</code> value as an argument to lock the screen.</li>
<li><strong><code>.unlock()</code>:</strong> This method unlocks the screen to its default orientation.</li>
</ul>

<p>And lastly, <code>screen.orientation</code> counts with an <code>&quot;orientationchange&quot;</code> event to know when the orientation has changed.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/image-optimization/">Image Optimization</a></strong>, Addy Osmani’s new practical guide to optimizing and delivering <strong>high-quality images</strong> on the web. Everything in one single <strong>528-pages</strong> book.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/image-optimization/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/image-optimization/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2c669cf1-c6ef-4c87-9901-018b04f7871f/image-optimization-shop-cover-opt.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/87fd0cfa-692e-459c-b2f3-15209a1f6aa7/image-optimization-shop-cover-opt.png"
    alt="Feature Panel"
    width="480"
    height="697"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h3 id="browser-support">Browser Support</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://caniuse.com/screen-orientation">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="289"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg"
			
			sizes="100vw"
			alt="Browser Support on Screen Orientation API"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://caniuse.com/screen-orientation'>Caniuse</a>. (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="finding-and-locking-screen-orientation">Finding And Locking Screen Orientation</h3>

<p>Let’s code a short demo using the Screen Orientation API to know the device’s orientation and lock it in its current position.</p>

<p>This can be our HTML boilerplate:</p>

<div class="break-out">
<pre><code class="language-html">&lt;main&gt;
  &lt;p&gt;
    Orientation Type: &lt;span class="orientation-type"&gt;&lt;/span&gt;
    &lt;br /&gt;
    Orientation Angle: &lt;span class="orientation-angle"&gt;&lt;/span&gt;
  &lt;/p&gt;

  &lt;button type="button" class="lock-button"&gt;Lock Screen&lt;/button&gt;

  &lt;button type="button" class="unlock-button"&gt;Unlock Screen&lt;/button&gt;

  &lt;button type="button" class="fullscreen-button"&gt;Go Full Screen&lt;/button&gt;
&lt;/main&gt;
</code></pre>
</div>

<p>On the JavaScript side, we inject the screen orientation <code>type</code> and <code>angle</code> properties into our HTML.</p>

<div class="break-out">
<pre><code class="language-javascript">let currentOrientationType = document.querySelector(".orientation-type");
let currentOrientationAngle = document.querySelector(".orientation-angle");

currentOrientationType.textContent = screen.orientation.type;
currentOrientationAngle.textContent = screen.orientation.angle;
</code></pre>
</div>

<p>Now, we can see the device’s orientation and angle properties. On my laptop, they are <code>&quot;landscape-primary&quot;</code> and <code>0°</code>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="367"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png"
			
			sizes="100vw"
			alt="Screen Orientation type and angle being displayed"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If we listen to the window’s <code>orientationchange</code> event, we can see how the values are updated each time the screen rotates.</p>

<pre><code class="language-javascript">window.addEventListener("orientationchange", () =&gt; {
  currentOrientationType.textContent = screen.orientation.type;
  currentOrientationAngle.textContent = screen.orientation.angle;
});
</code></pre>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="679"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png"
			
			sizes="100vw"
			alt="Screen Orientation type and angle are displayed in portrait mode"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To lock the screen, we need to first be in full-screen mode, so we will use another extremely useful feature: the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API"><strong>Fullscreen API</strong></a>. Nobody wants a webpage to pop into full-screen mode without their consent, so we need <a href="https://developer.mozilla.org/en-US/docs/Web/Security/User_activation#transient_activation">transient activation</a> (i.e., a user click) from a DOM element to work.</p>

<p>The Fullscreen API has two methods:</p>

<ol>
<li><code>Document.exitFullscreen()</code> is used from the global document object,</li>
<li><code>Element.requestFullscreen()</code> makes the specified element and its descendants go full-screen.</li>
</ol>

<p>We want the entire page to be full-screen so we can invoke the method from the root element at the <code>document.documentElement</code> object:</p>

<div class="break-out">
<pre><code class="language-javascript">const fullscreenButton = document.querySelector(".fullscreen-button");

fullscreenButton.addEventListener("click", async () =&gt; {
  // If it is already in full-screen, exit to normal view
  if (document.fullscreenElement) {
    await document.exitFullscreen();
  } else {
    await document.documentElement.requestFullscreen();
  }
});
</code></pre>
</div>

<p>Next, we can lock the screen in its current orientation:</p>

<pre><code class="language-javascript">const lockButton = document.querySelector(".lock-button");

lockButton.addEventListener("click", async () =&gt; {
  try {
    await screen.orientation.lock(screen.orientation.type);
  } catch (error) {
    console.error(error);
  }
});
</code></pre>

<p>And do the opposite with the unlock button:</p>

<pre><code class="language-javascript">const unlockButton = document.querySelector(".unlock-button");

unlockButton.addEventListener("click", () =&gt; {
  screen.orientation.unlock();
});
</code></pre>

<h3 id="can-t-we-check-orientation-with-a-media-query">Can’t We Check Orientation With a Media Query?</h3>

<p>Yes! We can indeed check page orientation via the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/orientation"><code>orientation</code> media feature</a> in a CSS media query. However, media queries compute the current orientation by checking if the width is “bigger than the height” for landscape or “smaller” for portrait. By contrast,</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20Screen%20Orientation%20API%20checks%20for%20the%20screen%20rendering%20the%20page%20regardless%20of%20the%20viewport%20dimensions,%20making%20it%20resistant%20to%20inconsistencies%20that%20may%20crop%20up%20with%20page%20resizing.%0a&url=https://smashingmagazine.com%2f2024%2f06%2funiting-web-native-apps-unknown-javascript-apis%2f">
      
The Screen Orientation API checks for the screen rendering the page regardless of the viewport dimensions, making it resistant to inconsistencies that may crop up with page resizing.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>You may have noticed how PWAs like Instagram and X force the screen to be in portrait mode even when the native system orientation is unlocked. It is important to notice that this behavior isn’t achieved through the Screen Orientation API, but by setting the <code>orientation</code> property on the <code>manifest.json</code> file to the desired orientation type.</p>

<h2 id="2-device-orientation-api">2. Device Orientation API</h2>

<p>Another API I’d like to poke at is the Device Orientation API. It provides access to a device’s gyroscope sensors to read the device’s orientation in space; something used all the time in mobile apps, mainly games. The API makes this happen with a <code>deviceorientation</code> event that triggers each time the device moves. It has the following properties:</p>

<ul>
<li><strong><code>event.alpha</code>:</strong> Orientation along the Z-axis, ranging from 0 to 360 degrees.</li>
<li><strong><code>event.beta</code>:</strong> Orientation along the X-axis, ranging from -180 to 180 degrees.</li>
<li><strong><code>event.gamma</code>:</strong> Orientation along the Y-axis, ranging from -90 to 90 degrees.</li>
</ul>

<h3 id="browser-support-1">Browser Support</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://caniuse.com/deviceorientation">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="293"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg"
			
			sizes="100vw"
			alt="Browser Support on Device Orientation API"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://caniuse.com/deviceorientation'>Caniuse</a>. (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="moving-elements-with-your-device">Moving Elements With Your Device</h3>

<p>In this case, we will make a 3D cube with CSS that can be rotated with your device! The full instructions I used to make the initial CSS cube are credited to <a href="https://desandro.com">David DeSandro</a> and can be <a href="https://3dtransforms.desandro.com/cube">found in his introduction to 3D transforms</a>.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="vYwdMNJ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Rotate cube [forked]](https://codepen.io/smashingmag/pen/vYwdMNJ) by <a href="https://codepen.io/desandro">Dave DeSandro</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/vYwdMNJ">Rotate cube [forked]</a> by <a href="https://codepen.io/desandro">Dave DeSandro</a>.</figcaption>
</figure>

<p>You can see raw full HTML in the demo, but let’s print it here for posterity:</p>

<pre><code class="language-html">&lt;main&gt;
  &lt;div class="scene"&gt;
    &lt;div class="cube"&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--front"&gt;1&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--back"&gt;2&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--right"&gt;3&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--left"&gt;4&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--top"&gt;5&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--bottom"&gt;6&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;h1&gt;Device Orientation API&lt;/h1&gt;
  &lt;p&gt;
    Alpha: &lt;span class="currentAlpha"&gt;&lt;/span&gt;
    &lt;br /&gt;
    Beta: &lt;span class="currentBeta"&gt;&lt;/span&gt;
    &lt;br /&gt;
    Gamma: &lt;span class="currentGamma"&gt;&lt;/span&gt;
  &lt;/p&gt;
&lt;/main&gt;
</code></pre>

<p>To keep this brief, I won’t explain the CSS code here. Just keep in mind that it provides the necessary styles for the 3D cube, and it can be rotated through all axes using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate">CSS <code>rotate()</code> function</a>.</p>

<p>Now, with JavaScript, we listen to the window’s <code>deviceorientation</code> event and access the event orientation data:</p>

<pre><code class="language-javascript">const currentAlpha = document.querySelector(".currentAlpha");
const currentBeta = document.querySelector(".currentBeta");
const currentGamma = document.querySelector(".currentGamma");

window.addEventListener("deviceorientation", (event) =&gt; {
  currentAlpha.textContent = event.alpha;
  currentBeta.textContent = event.beta;
  currentGamma.textContent = event.gamma;
});
</code></pre>

<p>To see how the data changes on a desktop device, we can open Chrome’s DevTools and access the Sensors Panel to emulate a rotating device.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="601"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png"
			
			sizes="100vw"
			alt="Emulating a device rotating on the Chrome DevTools Sensor Panel"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To rotate the cube, we change its CSS <code>transform</code> properties according to the device orientation data:</p>

<div class="break-out">
<pre><code class="language-javascript">const currentAlpha = document.querySelector(".currentAlpha");
const currentBeta = document.querySelector(".currentBeta");
const currentGamma = document.querySelector(".currentGamma");

const cube = document.querySelector(".cube");

window.addEventListener("deviceorientation", (event) =&gt; {
  currentAlpha.textContent = event.alpha;
  currentBeta.textContent = event.beta;
  currentGamma.textContent = event.gamma;

  cube.style.transform = `rotateX(${event.beta}deg) rotateY(${event.gamma}deg) rotateZ(${event.alpha}deg)`;
});
</code></pre>
</div>

<p>This is the result:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="392"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg"
			
			sizes="100vw"
			alt="Cube rotated according to emulated device orientation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="3-vibration-api">3. Vibration API</h2>

<p>Let’s turn our attention to the Vibration API, which, unsurprisingly, allows access to a device’s vibrating mechanism. This comes in handy when we need to alert users with in-app notifications, like when a process is finished or a message is received. That said, we have to use it sparingly; no one wants their phone blowing up with notifications.</p>

<p>There’s just one method that the Vibration API gives us, and it’s all we need: <code>navigator.vibrate()</code>.</p>

<p><code>vibrate()</code> is available globally from the <code>navigator</code> object and takes an argument for how long a vibration lasts in milliseconds. It can be either a number or an array of numbers representing a patron of vibrations and pauses.</p>

<div class="break-out">
<pre><code class="language-javascript">navigator.vibrate(200); // vibrate 200ms
navigator.vibrate([200, 100, 200]); // vibrate 200ms, wait 100, and vibrate 200ms.
</code></pre>
</div>

<h3 id="browser-support-2">Browser Support</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://caniuse.com/vibration">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="290"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg"
			
			sizes="100vw"
			alt="Browser Support on Vibration API"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://caniuse.com/vibration'>Caniuse</a>. (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="vibration-api-demo">Vibration API Demo</h3>

<p>Let’s make a quick demo where the user inputs how many milliseconds they want their device to vibrate and buttons to start and stop the vibration, starting with the markup:</p>

<pre><code class="language-html">&lt;main&gt;
  &lt;form&gt;
    &lt;label for="milliseconds-input"&gt;Milliseconds:&lt;/label&gt;
    &lt;input type="number" id="milliseconds-input" value="0" /&gt;
  &lt;/form&gt;

  &lt;button class="vibrate-button"&gt;Vibrate&lt;/button&gt;
  &lt;button class="stop-vibrate-button"&gt;Stop&lt;/button&gt;
&lt;/main&gt;
</code></pre>

<p>We’ll add an event listener for a click and invoke the <code>vibrate()</code> method:</p>

<div class="break-out">
<pre><code class="language-javascript">const vibrateButton = document.querySelector(".vibrate-button");
const millisecondsInput = document.querySelector("#milliseconds-input");

vibrateButton.addEventListener("click", () =&gt; {
  navigator.vibrate(millisecondsInput.value);
});
</code></pre>
</div>

<p>To stop vibrating, we override the current vibration with a zero-millisecond vibration.</p>

<div class="break-out">
<pre><code class="language-javascript">const stopVibrateButton = document.querySelector(".stop-vibrate-button");

stopVibrateButton.addEventListener("click", () =&gt; {
  navigator.vibrate(0);
});
</code></pre>
</div>

<div class="partners__lead-place"></div>

<h2 id="4-contact-picker-api">4. Contact Picker API</h2>

<p>In the past, it used to be that only native apps could connect to a device’s “contacts”. But now we have the fourth and final API I want to look at: the <a href="https://w3c.github.io/contact-picker/"><strong>Contact Picker API</strong></a>.</p>

<p>The API grants web apps access to the device’s contact lists. Specifically, we get the <code>contacts.select()</code> async method available through the <code>navigator</code> object, which takes the following two arguments:</p>

<ul>
<li><strong><code>properties</code>:</strong> This is an array containing the information we want to fetch from a contact card, e.g., <code>&quot;name&quot;</code>, <code>&quot;address&quot;</code>, <code>&quot;email&quot;</code>, <code>&quot;tel&quot;</code>, and <code>&quot;icon&quot;</code>.</li>
<li><strong><code>options</code>:</strong> This is an object that can only contain the <code>multiple</code> boolean property to define whether or not the user can select one or multiple contacts at a time.</li>
</ul>

<h3 id="browser-support-3">Browser Support</h3>

<p>I’m afraid that browser support is next to zilch on this one, limited to Chrome Android, Samsung Internet, and Android’s native web browser at the time I’m writing this.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://caniuse.com/mdn-api_contactsmanager">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="301"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg"
			
			sizes="100vw"
			alt="Browser Support on Contacts Manager API"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://caniuse.com/mdn-api_contactsmanager'>Caniuse</a>. (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="selecting-user-s-contacts">Selecting User’s Contacts</h3>

<p>We will make another demo to select and display the user’s contacts on the page. Again, starting with the HTML:</p>

<pre><code class="language-html">&lt;main&gt;
  &lt;button class="get-contacts"&gt;Get Contacts&lt;/button&gt;
  &lt;p&gt;Contacts:&lt;/p&gt;
  &lt;ul class="contact-list"&gt;
    &lt;!-- We’ll inject a list of contacts --&gt;
  &lt;/ul&gt;
&lt;/main&gt;
</code></pre>

<p>Then, in JavaScript, we first construct our elements from the DOM and choose which properties we want to pick from the contacts.</p>

<div class="break-out">
<pre><code class="language-javascript">const getContactsButton = document.querySelector(".get-contacts");
const contactList = document.querySelector(".contact-list");

const props = ["name", "tel", "icon"];
const options = {multiple: true};
</code></pre>
</div>

<p>Now, we asynchronously pick the contacts when the user clicks the <code>getContactsButton</code>.</p>

<div class="break-out">
<pre><code class="language-javascript">
const getContacts = async () =&gt; {
  try {
    const contacts = await navigator.contacts.select(props, options);
  } catch (error) {
    console.error(error);
  }
};

getContactsButton.addEventListener("click", getContacts);
</code></pre>
</div>

<p>Using DOM manipulation, we can then append a list item to each contact and an icon to the <code>contactList</code> element.</p>

<div class="break-out">
<pre><code class="language-javascript">const appendContacts = (contacts) =&gt; {
  contacts.forEach(({name, tel, icon}) =&gt; {
    const contactElement = document.createElement("li");

    contactElement.innerText = `${name}: ${tel}`;
    contactList.appendChild(contactElement);
  });
};

const getContacts = async () =&gt; {
  try {
    const contacts = await navigator.contacts.select(props, options);
    appendContacts(contacts);
  } catch (error) {
    console.error(error);
  }
};

getContactsButton.addEventListener("click", getContacts);
</code></pre>
</div>

<p>Appending an image is a little tricky since we will need to convert it into a URL and append it for each item in the list.</p>

<div class="break-out">
<pre><code class="language-javascript">const getIcon = (icon) =&gt; {
  if (icon.length &gt; 0) {
    const imageUrl = URL.createObjectURL(icon[0]);
    const imageElement = document.createElement("img");
    imageElement.src = imageUrl;

    return imageElement;
  }
};

const appendContacts = (contacts) =&gt; {
  contacts.forEach(({name, tel, icon}) =&gt; {
    const contactElement = document.createElement("li");

    contactElement.innerText = `${name}: ${tel}`;
    contactList.appendChild(contactElement);

    const imageElement = getIcon(icon);
    contactElement.appendChild(imageElement);
  });
};

const getContacts = async () =&gt; {
  try {
    const contacts = await navigator.contacts.select(props, options);
    appendContacts(contacts);
  } catch (error) {
    console.error(error);
  }
};

getContactsButton.addEventListener("click", getContacts);
</code></pre>
</div>

<p>And here’s the outcome:</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="780"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png"
			
			sizes="100vw"
			alt="Contact Picker showing three mock contacts"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Note</strong>: <strong><em>The Contact Picker API will only work if the context is secure</em></strong>, <em>i.e., the page is served over <code>https://</code> or <code>wss://</code> URLs.</em></p>

<h2 id="conclusion">Conclusion</h2>

<p>There we go, four web APIs that I believe would empower us to <strong>build more useful and robust PWAs</strong> but have slipped under the radar for many of us. This is, of course, due to inconsistent browser support, so I hope this article can bring awareness to new APIs so we have a better chance to see them in future browser updates.</p>

<p>Aren’t they interesting? We saw how much control we have with the orientation of a device and its screen as well as the level of access we get to access a device’s hardware features, i.e. vibration, and information from other apps to use in our own UI.</p>

<p>But as I said much earlier, there’s a sort of infinite loop where <strong>a lack of awareness begets a lack of browser support</strong>. So, while the four APIs we covered are super interesting, your mileage will inevitably vary when it comes to using them in a production environment. Please tread cautiously and refer to <a href="https://caniuse.com">Caniuse</a> for the latest support information, or check for your own devices using <a href="https://webapicheck.com/">WebAPI Check</a>.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Mikołaj Dobrucki</author><title>Penpot’s CSS Grid Layout: Designing With Superpowers</title><link>https://www.smashingmagazine.com/2024/04/penpot-css-grid-layout-designing-superpowers/</link><pubDate>Thu, 11 Apr 2024 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/04/penpot-css-grid-layout-designing-superpowers/</guid><description>Penpot helps designers and developers work better together by offering a free, open-source design tool based on open web standards. Today, let&amp;rsquo;s explore Penpot’s latest feature, CSS Grid Layout. Penpot’s latest release is about efficiency and so much more. It gives designers superpowers and a better place at the table. Excited? Let’s take a look at it together.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/04/penpot-css-grid-layout-designing-superpowers/" />
              <title>Penpot’s CSS Grid Layout: Designing With Superpowers</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Penpot’s CSS Grid Layout: Designing With Superpowers</h1>
                  
                    
                    <address>Mikołaj Dobrucki</address>
                  
                  <time datetime="2024-04-11T08:00:00&#43;00:00" class="op-published">2024-04-11T08:00:00+00:00</time>
                  <time datetime="2024-04-11T08:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>Penpot</b></p>
                

<p>It was less than a year ago when <a href="https://www.smashingmagazine.com/2023/02/meet-penpot-open-source-design-platform-designers-developers/">I first had a chance to use Penpot</a> and instantly got excited about it. They managed to build something that designers haven’t yet seen before &mdash; a modern, open-source tool for everyone. In the world of technology, that might not sound groundbreaking. After all, open-source tools and software are being taken for granted as a cornerstone of modern web development. But for some reason, not for design &mdash; until now. Penpot’s approach to building design software comes with a lot of good arguments. And it gathered a strong community.</p>

<p>One of the reasons why Penpot is so exciting is that it <strong>allows creators to build user interfaces in a visual environment</strong>, but using the same standards and technologies as the end product. It makes a design workflow easier on many levels. Today, we are going to focus on just one of them, building layouts.</p>

<p>Design tools went a long way trying to make it easier to design complex, responsive layouts and flexible, customizable components. Some of them tried to mimic the mechanisms used in web technologies and others tried to mimic these imitations. But such an approach will take you only so far.</p>

<h2 id="short-history-of-web-layouts">Short History Of Web Layouts</h2>

<p>So how are the layouts for the web built in practice?</p>

<p>If you’ve been around the industry long enough, you might remember the times when you used frames, tables, and floats to build layouts. And if you haven’t, you didn’t miss much. Just to give you a taste of how bad it was: same as exporting tiny images of rounded corners from ever-crashing Photoshop, just to meticulously position them in every corner of a rectangle so you could make a dull, rounded button, it was just a pain. Far too often, it was a pleasure to craft yet another amazing design &mdash; but so much tears and sorrow to actually implement it.</p>

<p>Then Flexbox came in and changed everything. And soon after it, Grid. Two powerful yet amazingly simple engines to build layouts that changed web developers’ lives forever.</p>

<p>Ironically, design tools never caught up. Flexbox and Grid opened an ocean of possibilities, yet gated behind a barrier of knowing how to code. None of the design tools ever implemented them so a larger audience of designers could leverage them in their workflows. Not until now.</p>

<h2 id="creating-layouts-with-penpot">Creating Layouts With Penpot</h2>

<p><a href="https://penpot.app/?utm_source=Article&amp;utm_medium=SmashingMag&amp;utm_id=Penpot2.0">Penpot</a> is becoming the first design tool to support both Flexbox and Grid in their toolkit. And by support, I don’t mean a layout feature that tries to copy what Flexbox or Grid has to offer. We’re talking about an actual implementation of Flexbox and Grid inside the design tool.</p>

<p>Penpot’s Flexbox implementation went public earlier this year. If you’d like to give it a try, last year, <a href="https://www.smashingmagazine.com/2023/06/penpot-flex-layout-building-css-layouts-design-tool/">I wrote a separate article just about it</a>. Now, <strong>Penpot is fully implementing both Flexbox and Grid</strong>.</p>

<p>You might be wondering why we need both. Couldn’t you just use Flexbox for everything, same as you use the same simple layout features in a design tool? Technically, yes, you could. In fact, most people do. (At the time of writing, only a quarter of websites worldwide use CSS Grid, but its adoption is steadily increasing; <a href="https://chromestatus.com/metrics/feature/timeline/popularity/1693">source</a>)</p>

<p>So if you want to build simple, mostly linear layouts, Flexbox is probably all you’ll ever need. But if you want to gain some design superpowers? Learn Grid.</p>

<p>Penpot’s CSS Grid Layout is out now, so you can already <a href="https://penpot.app/penpot-2.0?utm_source=Article&amp;utm_medium=SmashingMag&amp;utm_id=Penpot2.0">give it a try</a>. It’s a part of their major 2.0 release, bringing a bunch of long-awaited features and improvements. I’d strongly encourage you to give it a go and see how it works for yourself. And if you need some inspiration, keep reading!</p>

<h2 id="css-grid-layout-in-practice">CSS Grid Layout In Practice</h2>

<p>As an example, let’s build a portfolio page that consists of a sidebar and a grid of pictures.</p>

<h3 id="creating-a-layout">Creating A Layout</h3>

<p>Our first step will be to create a simple two-dimensional grid. In this case using a Grid Layout makes more sense than Flex Layout as we want to have more granular control over how elements are laid out on multiple axes.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/933631600"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>You can notice that each row and column of the layout has a value of “1FR”. FR stands for fraction, which means that the available space will be distributed evenly across rows and columns.</p>

<p>FRs are extremely useful. For example, the same as other units, FRs can take different values. So you could create 3 columns, one taking 2FRs and two counting 1FR each. As a result, the first column would occupy half of the layout’s width and the other two would take a quarter of available space each.</p>

<h3 id="adding-elements-to-the-layout">Adding Elements To The Layout</h3>

<p>To add elements to CSS Grid Layout, you can simply drag and drop them onto the canvas.</p>

<p>For each element you can either assign it to a dedicated cell of the grid or allow it to find its place on its own and fill the first available blank.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/933631629"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h3 id="spacing-and-alignment">Spacing And Alignment</h3>

<p>CSS Grid Layout gives you full control over how the elements are aligned and how they adjust to available space. A plethora of highly granular options allows you to create very precise, responsive layouts that work seamlessly with elements of any shape or form.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/ 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/ 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/ 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/ 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/ 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/"
			
			sizes="100vw"
			alt=""
		/>
    

  
    <figcaption class="op-vertical-bottom">
      (Image credit: <a href=''></a>) (<a href=''>Large preview</a>)
    </figcaption>
  
</figure>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/933631648"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>You can make cells and rows adjust to the size of the elements but you can also make the elements adjust to the grid.  In this case, we are going to add a sidebar on the left side of the layout and place it in a column with a fixed width of 320px.</p>

<p>The sidebar itself has its own layout. But in this case, for the simple vertical alignment, the Flex Layout is all we need.</p>

<h3 id="creating-grid-areas">Creating Grid Areas</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/ 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/ 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/ 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/ 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/ 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/"
			
			sizes="100vw"
			alt=""
		/>
    

  
    <figcaption class="op-vertical-bottom">
      (Image credit: <a href=''></a>) (<a href=''>Large preview</a>)
    </figcaption>
  
</figure>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/933631684"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>To make the grid even more powerful, you can merge cells, group them into functional areas, and name them. Here, we are going to create a dedicated area for the sidebar.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/933631711"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>As you adjust the layout later, you can see that the sidebar always keeps the same width and full height of the design while other cells get adjusted to the available space.</p>

<h3 id="building-even-more-complex-grids">Building Even More Complex Grids</h3>

<p>That’s not all. Apart from merging cells of the grid, you can tell elements inside it to take multiple cells. On our portfolio page, we are going to use this to make the featured picture bigger than others and take four cells instead of one.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/933631747"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>As a result, we created a complex, responsive layout that would be a breeze to turn it into a functional website but at the same time would be completely impossible to build in any other design tool out there. And that’s just a fraction of what Grid Layout can do.</p>

<h2 id="next-steps">Next Steps</h2>

<p>I hope you liked this demo of Penpot&rsquo;s Grid Layout. If you’d like to play around with the examples used in this article, go ahead and <a href="https://github.com/penpot/penpot-files/raw/main/Grid%20layout%20playground.penpot">duplicate this Penpot file</a>. It’s a great template that explains all the ins and outs of using Grid in your designs!</p>

<p>In case you’re more of a video-learning type, there’s a great tutorial on Grid Layout you can <a href="https://www.youtube.com/watch?v=4wFwffbFb44">watch now on YouTube</a>. And if you need help at any point, the Penpot community will be more than happy to answer your questions.</p>

<h2 id="summary">Summary</h2>

<p>Flexbox and Grid in Penpot open up opportunities to craft layouts like never before. Today, anyone can combine the power of Flex Layout and Grid Layout to create complex, sophisticated structures that are flexible, responsive, and ready to deploy out-of-the-box—all without writing a single line of code.</p>

<p>Working with the right technologies not only makes things easier, but it also just feels right. That&rsquo;s something I&rsquo;ve always longed for in design tools. Adopting CSS as a standard for both designers and developers facilitates smoother collaboration and helps them both feel more at home in their workflows.</p>

<p>For designers, that’s also a chance to strengthen their skill set, which matters today more than ever. The design industry is a competitive space that keeps changing rapidly, and staying competitive is hard work. However, learning the less obvious aspects and gaining a better understanding of the technologies you work with might help you do that.</p>

<h2 id="try-css-grid-layout-and-share-your-thoughts">Try CSS Grid Layout And Share Your Thoughts!</h2>

<p>If you decide to <a href="https://penpot.app/penpot-2.0?utm_source=Article&amp;utm_medium=SmashingMag&amp;utm_id=Penpot2.0">give CSS Grid Layout a try</a>, don’t hesitate to share your experience! The team behind Penpot would love to hear your feedback. Being a completely <strong>free and open-source tool</strong>, Penpot’s development thrives thanks to its community and people like you.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Joas Pambou</author><title>A High-Level Overview Of Large Language Model Concepts, Use Cases, And Tools</title><link>https://www.smashingmagazine.com/2023/10/overview-large-language-model-concepts-use-cases-tools/</link><pubDate>Tue, 10 Oct 2023 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/10/overview-large-language-model-concepts-use-cases-tools/</guid><description>While AI remains a collective point of interest &amp;mdash; or doom, depending on your outlook &amp;mdash; it also remains a bit of a black box. What exactly is inside an AI application that makes it seem as though it can hold a conversation? Discuss the concept of large language models (LLMs) and how they are implemented with a set of data to develop an application. Joas compares a collection of no-code and low-code apps designed to help you get a feel for not only how the concept works but also to get a sense of what types of models are available to train AI on different skill sets.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/10/overview-large-language-model-concepts-use-cases-tools/" />
              <title>A High-Level Overview Of Large Language Model Concepts, Use Cases, And Tools</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>A High-Level Overview Of Large Language Model Concepts, Use Cases, And Tools</h1>
                  
                    
                    <address>Joas Pambou</address>
                  
                  <time datetime="2023-10-10T13:00:00&#43;00:00" class="op-published">2023-10-10T13:00:00+00:00</time>
                  <time datetime="2023-10-10T13:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Even though a simple online search turns up countless tutorials on using Artificial Intelligence (AI) for everything from generative art to making technical documentation easier to use, there’s still plenty of mystery around it. What goes inside an AI-powered tool like ChatGPT? How does Notion’s AI feature know how to summarize an article for me on the fly? Or how are a bunch of sites suddenly popping up that can aggregate news and auto-publish a slew of “new” articles from it?</p>

<p>It all can seem like a black box of mysterious, arcane technology that requires an advanced computer science degree to understand. What I want to show you, though, is how we can peek inside that box and see how everything is wired up.</p>

<p>Specifically, this article is about <strong>large language models (LLMs)</strong> and how they “imbue” AI-powered tools with intelligence for answering queries in diverse contexts. I have previously written tutorials on how to use an LLM to <a href="https://www.smashingmagazine.com/2023/09/generating-real-time-audio-sentiment-analysis-ai/">transcribe and evaluate the expressed sentiment of audio files</a>. But I want to take a step back and look at another way around it that better demonstrates &mdash; and visualizes &mdash; how data flows through an AI-powered tool.</p>

<p>We will discuss LLM use cases, look at several new tools that abstract the process of modeling AI with LLM with visual workflows, and get our hands on one of them to see how it all works.</p>

<h2 id="large-language-models-overview">Large Language Models Overview</h2>

<p>Forgoing technical terms, LLMs are vast sets of text data. When we integrate an LLM into an AI system, we enable the system to leverage the language knowledge and capabilities developed by the LLM through its own training. You might think of it as dumping a lifetime of knowledge into an empty brain, assigning that brain to a job, and putting it to work.</p>

<p>“Knowledge” is a convoluted term as it can be subjective and qualitative. We sometimes describe people as “book smart” or “street smart,” and they are both types of knowledge that are useful in different contexts. This is what artificial “intelligence” is created upon. AI is fed with data, and that is what it uses to frame its understanding of the world, whether it is text data for “speaking” back to us or visual data for generating “art” on demand.</p>

<h3 id="use-cases">Use Cases</h3>

<p>As you may imagine (or have already experienced), the use cases of LLMs in AI are many and along a wide spectrum. And we’re only in the early days of figuring out what to make with LLMs and how to use them in our work. A few of the most common use cases include the following.</p>

<ul>
<li><strong>Chatbot</strong><br />
<a href="https://www.wired.com/story/how-chatgpt-works-large-language-model/">LLMs play a crucial role in building chatbots</a> for customer support, troubleshooting, and interactions, thereby ensuring smooth communications with users and delivering valuable assistance. <a href="https://www.salesforce.com/products/customer-service-chatbot/">Salesforce is a good example</a> of a company offering this sort of service.</li>
<li><strong>Sentiment Analysis</strong><br />
<a href="https://www.smashingmagazine.com/2023/06/ai-detect-sentiment-audio-files/">LLMs can analyze text for emotions.</a> Organizations use this to collect data, summarize feedback, and quickly identify areas for improvement. <a href="https://www.grammarly.com/tone">Grammarly’s “tone detector” is one such example</a>, where AI is used to evaluate sentiment conveyed in content.</li>
<li><strong>Content Moderation</strong><br />
Content moderation is an important aspect of social media platforms, and LLMs come in handy. They can spot and remove offensive content, including hate speech, harassment, or inappropriate photos and videos, which is exactly what <a href="https://blog.hubspot.com/marketing/ai-content-moderation">Hubspot’s AI-powered content moderation feature</a> does.</li>
<li><strong>Translation</strong><br />
Thanks to impressive advancements in language models, translation has become highly accurate. One noteworthy example is Meta AI’s latest model, <a href="https://ai.meta.com/blog/seamless-m4t/?utm_source=twitter&amp;utm_medium=organic_social&amp;utm_campaign=seamless&amp;utm_content=video">SeamlessM4T</a>, which represents a big step forward in speech-to-speech and speech-to-text technology.</li>
<li><strong>Email Filters</strong><br />
LLMs can be used to automatically detect and block unwanted spam messages, keeping your inbox clean. When trained on large datasets of known spam emails, the models learn to identify suspicious links, phrases, and sender details. This allows them to distinguish legitimate messages from those trying to scam users or market illegal or fraudulent goods and services. Google has offered <a href="https://www.theverge.com/2019/2/6/18213453/gmail-tensorflow-machine-learning-spam-100-million">AI-based spam protection</a> since 2019.</li>
<li><strong>Writing Assistance</strong><br />
Grammarly is the ultimate example of an AI-powered service that uses LLM to “learn” how you write in order to make writing suggestions. But this extends to other services as well, including <a href="https://blog.google/products/gmail/save-time-with-smart-reply-in-gmail/">Gmail’s “Smart Reply” feature</a>. The same thing is true of Notion’s AI feature, which is capable of <a href="https://www.notion.so/help/guides/notion-ai-for-docs">summarizing a page of content or meeting notes</a>. Hemmingway’s app recently shipped a <a href="https://4.hemingwayapp.com/beta">beta AI integration that corrects writing on the spot</a>.</li>
<li><strong>Code and Development</strong><br />
This is the one that has many developers worried about AI coming after their jobs. It hit the commercial mainstream with <a href="https://github.com/features/copilot">GitHub Copilot</a>, a service that performs automatic code completion. Same with <a href="https://aws.amazon.com/codewhisperer/">Amazon’s CodeWhisperer</a>. Then again, AI can be used to help sharpen development skills, which is the case of <a href="https://developer.mozilla.org/en-US/blog/introducing-ai-help/">MDN’s AI Help feature</a>.</li>
</ul>

<p>Again, these are still the early days of LLM. We’re already beginning to see language models integrated into our lives, whether it’s in our writing, email, or customer service, among many other services that seem to pop up every week. This is an evolving space.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h2 id="types-of-models">Types Of Models</h2>

<p>There are all kinds of AI models tailored for different applications. You can scroll through <a href="https://sapling.ai/llm/index">Sapling’s large list</a> of the most prominent commercial and open-source LLMs to get an idea of all the diverse models that are available and what they are used for. Each model is the context in which AI views the world.</p>

<p>Let’s look at some real-world examples of how LLMs are used for different use cases.</p>

<p><strong>Natural Conversation</strong><br />
Chatbots need to master the art of conversation. Models like <a href="https://www.anthropic.com/index/claude-2">Anthropic’s Claude</a> are trained on massive collections of conversational data to chat naturally on any topic. As a developer, you can tap into Claude’s conversational skills through an API to create interactive assistants.</p>

<p><strong>Emotions</strong><br />
Developers can leverage powerful pre-trained models like <a href="https://falconllm.tii.ae/">Falcon</a> for sentiment analysis. By fine-tuning Falcon on datasets with emotional labels, it can learn to accurately detect the sentiment in any text provided.</p>

<p><strong>Translation</strong><br />
Meta AI released <a href="https://ai.meta.com/blog/seamless-m4t/?utm_source=twitter&amp;utm_medium=organic_social&amp;utm_campaign=seamless&amp;utm_content=video">SeamlessM4T</a>, an LLM trained on huge translated speech and text datasets. This multilingual model is groundbreaking because it translates speech from one language into another without an intermediary step between input and output. In other words, SeamlessM4T enables real-time voice conversations across languages.</p>

<p><strong>Content Moderation</strong><br />
As a developer, you can integrate powerful moderation capabilities using <a href="https://platform.openai.com/docs/guides/moderation/overview">OpenAI’s API</a>, which includes a LLM trained thoroughly on flagging toxic content for the purpose of community moderation.</p>

<p><strong>Spam Filtering</strong><br />
Some LLMs are <a href="https://huggingface.co/models?pipeline_tag=text-classification&amp;sort=trending">used to develop AI programs capable of text classification tasks</a>, such as spotting spam emails. As an email user, the simple act of flagging certain messages as spam further informs AI about what constitutes an unwanted email. After seeing plenty of examples, AI is capable of establishing patterns that allow it to block spam before it hits the inbox.</p>

<h2 id="not-all-language-models-are-large">Not All Language Models Are Large</h2>

<p>While we’re on the topic, it’s worth mentioning that not all language models are “large.” There are plenty of models with smaller sets of data that may not go as deep as ChatGPT 4 or 5 but are well-suited for personal or niche applications.</p>

<p>For example, check out <a href="https://ask.lukew.com/chat">the chat feature that Luke Wrobleski added to his site</a>. He’s using a smaller language model, so the app at least knows how to form sentences, but is primarily trained on <a href="https://www.lukew.com/ff/">Luke’s archive of blog posts</a>. Typing a prompt into the chat returns responses that read very much like Luke’s writings. Better yet, Luke’s virtual persona will admit when a topic is outside of the scope of its knowledge. An LLM would provide the assistant with too much general information and would likely try to answer any question, regardless of scope. Members from the University of Edinburgh and the Allen Institute for AI published a paper in January 2023 (<a href="https://arxiv.org/pdf/2301.12726.pdf">PDF</a>) that advocates the use of <em>specialized</em> language models for the purpose of more narrowly targeted tasks.</p>

<h2 id="low-code-tools-for-llm-development">Low-Code Tools For LLM Development</h2>

<p>So far, we’ve covered what an LLM is, common examples of how it can be used, and how different models influence the AI tools that integrate them. Let’s discuss that last bit about integration.</p>

<p>Many technologies require a steep learning curve. That’s especially true with emerging tools that might be introducing you to new technical concepts, as I would argue is the case with AI in general. While AI is not a new term and has been studied and developed over decades in various forms, its entrance to the mainstream is certainly new and sparks the recent buzz about it. There’s been plenty of recent buzz in the front-end development community, and many of us are scrambling to wrap our minds around it.</p>

<p>Thankfully, new resources can help abstract all of this for us. They can power an AI project you might be working on, but more importantly, they are useful for learning the concepts of LLM by removing advanced technical barriers. You might think of them as “low” and “no” code tools, like <a href="https://wordpress.com/go/website-building/the-new-wordpress-way-modern-and-no-code/">WordPress.com vs. self-hosted WordPress</a> or a visual <a href="https://www.smashingmagazine.com/2023/06/codux-react-visual-editor-improves-developer-experience/">React editor</a> that is integrated with your IDE.</p>

<p>Low-code platforms make it easier to leverage large language models without needing to handle all the coding and infrastructure yourself. Here are some top options:</p>

<h3 id="chainlit">Chainlit</h3>

<p><a href="https://docs.chainlit.io/overview">Chainlit</a> is an open-source Python package that is capable of building a ChatGPT-style interface using a visual editor.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/871034783"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Source: <a href='https://github.com/Chainlit/chainlit'>GitHub</a>.</figcaption>
	
</figure>

<p>Features:</p>

<ul>
<li><strong>Visualize logic</strong>: See the step-by-step reasoning behind outputs.</li>
<li><strong>Integrations</strong>: Chainlit supports other tools like LangChain, <a href="https://gpt-index.readthedocs.io/en/stable/">LlamaIndex</a>, and <a href="https://haystack.deepset.ai/">Haystack</a>.</li>
<li><strong>Cloud deployment</strong>: Push your app directly into a production environment.</li>
<li><strong>Collaborate with your team</strong>: Annotate dataset and run team experiments.</li>
</ul>

<p>And since it’s open source, Chainlit is freely available at no cost.</p>

<h3 id="llmstack">LLMStack</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-llm.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="610"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-llm.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-llm.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-llm.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-llm.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-llm.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-llm.png"
			
			sizes="100vw"
			alt="LLMStack visual editing interface"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://llmstack.ai/docs/getting-started/ui'>LLMStack</a>. (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-llm.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><a href="https://llmstack.ai/">LLMStack</a> is another low-code platform for building AI apps and chatbots by leveraging large language models. Multiple models can be chained together into “pipelines” for channeling data. LLMStack supports standalone app development but also provides hosting that can be used to <a href="https://llmstack.ai/docs/apps/sharing">integrate an app into sites and products via API</a> or connected to platforms like Slack or Discord.</p>

<p>LLMStack is also what powers <a href="https://trypromptly.com">Promptly</a>, a cloud version of the app with freemium subscription pricing that includes a free tier.</p>

<h3 id="flowiseai">FlowiseAI</h3>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/871039131"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Source: <a href='https://flowiseai.com/'>FlowiseAI</a></figcaption>
	
</figure>

<p>What makes <a href="https://flowiseai.com">Flowise</a><a href="https://flowiseai.com">AI</a> unique is its drag-and-drop interface. It’s a lot like working with a mind-mapping app or a flowchart that stitches apps together with LLM APIs for a truly no-code visual editing experience. Plus, Flowise is freely available as an <a href="https://github.com/FlowiseAI/Flowise">open-source project</a>. You can grab any of the <a href="https://huggingface.co/models">330K-plus LLMs in the Hugging Face community</a>.</p>

<p>Cloud hosting is a feature that is on the horizon, but for now, it is possible to self-host FlowiseAI apps or deploy them on other services such as <a href="https://railway.app/template/pn4G8S?referralCode=WVNPD9">Raleway</a>, <a href="https://docs.flowiseai.com/deployment/render">Render</a>, and <a href="https://docs.flowiseai.com/deployment/hugging-face">Hugging Face Spaces</a>.</p>

<h3 id="stack-ai">Stack AI</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-ai-screenshot.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="378"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-ai-screenshot.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-ai-screenshot.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-ai-screenshot.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-ai-screenshot.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-ai-screenshot.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-ai-screenshot.png"
			
			sizes="100vw"
			alt="Stack AI visual editing interface"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/stack-ai-screenshot.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><a href="https://www.stack-ai.com">Stack AI</a> is another no-code offering for developing AI apps integrated with LLMs. It is much like FlowiseAI, particularly the drag-and-drop interface that visualizes connections between apps and APIs. One thing I particularly like about Stack AI is how it incorporates “data loaders” to fetch data from other platforms, like Slack or a Notion database.</p>

<p>I also like that Stack AI provides a wider range of LLM offerings. That said, <a href="https://huggingface.co/models">it will cost you</a>. While Stack AI offers a free pricing tier, it is restricted to a single project with only 100 runs per month. Bumping up to the first paid tier will set you back $199 per month, which I suppose is used toward the costs of accessing a wider range of LLM sources. For example, Flowise AI works with any LLM in the Hugging Face community. So does Stack AI, but it also gives you access to commercial LLM offerings, like Anthropic’s <a href="https://www.anthropic.com/product">Claude models</a> and Google’s <a href="https://developers.generativeai.google">PaLM</a>, as well as additional open-source offerings from <a href="https://replicate.com">Replicate</a>.</p>

<h3 id="voiceflow">Voiceflow</h3>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/871041499"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Source: <a href='https://www.voiceflow.com/'>Voiceflow</a></figcaption>
	
</figure>

<p><a href="https://www.voiceflow.com">Voiceflow</a> is like Flowise and Stack AI in the sense that it is another no-code visual editor. The difference is that Voiceflow is a niche offering <strong>focused solely on developing voice assistant and chat applications</strong>. Whereas the other offerings could be used to, say, train your Gmail account for spam filtering, Voiceflow is squarely dedicated to developing voice flows.</p>

<p>There is a free <a href="https://www.voiceflow.com/pricing">sandbox</a> you can use to test Voiceflow’s features, but using Voiceflow for production-ready app development <a href="https://www.voiceflow.com/pricing">starts at $50 per month</a> for individual use and $185 per month for collaborative teamwork for up to three users.</p>

<h3 id="the-rest">“The Rest”</h3>

<p>The truth is that no-code and low-code visual editors for developing AI-powered apps with integrated LLMs are being released all the time, or so it seems. Profiling each and every one is outside the scope of this article, though it would certainly be useful perhaps in another article.</p>

<p>That said, I have compiled a list of seven other tools in the following table. Even though I have not taken the chance to demo each and every one of them, I am providing what information I know about them from their sites and documentation, so you have a wider set of tools to compare and evaluate for your own needs.</p>

<table class="tablesaw break-out--full small">
    <thead>
        <tr>
            <th>Name</th>
            <th>Description</th>
      <th>Example Uses</th>
      <th>Pricing</th>
      <th>Documentation</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><a href="https://dify.ai/">Dify</a></td>
            <td class="small">“Seamlessly build & manage AI-native apps based on GPT-4.”</td>
      <td class="small">Chatbots, natural language search, content generation, summarization, sentiment analysis.</td>
      <td>Free (open source)</td>
      <td><a href="https://docs.dify.ai/getting-started/readme">Documentation</a></td>
        </tr>
        <tr>
            <td><a href="https://retune.so/">re:tune</a></td>
      <td class="small">“Build chatbots for any use case, from customer support to sales and more.”  
        “Connect any data source to your chatbot, from your website to hyper-personalized customer data."</td>
      <td class="small">Customer service chatbots, sales assistants.</td>
      <td><a href="https://retune.so/pricing">$0-$399 per month</a> with lifetime access plans available.</td>
      <td><a href="https://retune.so/roadmap">Roadmap</a></td>
        </tr>
        <tr>
            <td><a href="https://botpress.com/">Botpress</a></td>
            <td class="small">“The first next-generation chatbot builder powered by OpenAI. Build ChatGPT-like bots for your project or business to get things done.”</td>
      <td class="small">Chatbots, natural language search, content generation, summarization, sentiment analysis.</td>
      <td><a href="https://botpress.com/pricing">Free for up to 1,000 runs per month</a> with monthly pricing for additional runs in $25 increments.</td>
      <td><a href="https://botpress.com/pricing">Documentation</a></td>
        </tr>
    <tr>
            <td><a href="https://www.respell.ai/">Respell</a></td>
            <td class="small">“Respell makes it easy to use AI in your work life. Our drag-and-drop workflow builder can automate a tedious process in minutes. Powered by the latest AI models.”</td>
      <td class="small">Chatbots, natural language search, content generation, summarization, sentiment analysis.</td>
      <td><a href="https://www.respell.ai/pricing">A free starter plan is available</a> with more features and integrations starting at $20 per month.</td>
      <td><a href="https://docs.respell.ai/overview">Documentation</a></td>
        </tr>
    <tr>
            <td><a href="https://www.superagent.sh/">Superagent</a></td>
            <td class="small">"Make your applications smarter and more capable with AI-driven agents. Build unique ChatGPT-like experiences with custom knowledge, brand identity, and external APIs.”</td>
      <td class="small">Chatbots, legal document analysis, educational content generation, code reviews.</td>
      <td>Free (open source)</td>
      <td><a href="https://docs.superagent.sh/">Documentation</a></td>
        </tr>
    <tr>
            <td><a href="https://www.shuttle.rs/ai?ref=theresanaiforthat">Shuttle</a></td>
            <td class="small">“ShuttleAI is comprised of multiple LLM agents working together to handle your request. Starting from the beginning itself, they expand upon the user’s prompt, reason about the project, and define a plan of action.”</td>
      <td class="small">Creating a social media or community platform; developing an e-commerce site/store; making a booking/reservation system; constructing a dashboard for data insights.</td>
      <td>Free with <a href="https://www.shuttle.rs/pricing">custom pricing options</a> while Shuttle Pro is in a beta trial.</td>
      <td><a href="https://docs.shuttle.rs/introduction/welcome">Documentation</a></td>
        </tr>
    <tr>
            <td><a href="https://www.passio.ai/?ref=theresanaiforthat">Passio</a></td>
            <td class="small">“Ready to use Mobile AI Modules and SDK for your brand. Our Mobile AI platform supports complete end-to-end development of AI-powered applications, enabling you to rapidly add computer vision and AI-powered experiences to your apps.”</td>
      <td class="small">Food nutrition analysis, paint color detection, object identification.</li></ul></td>
      <td>Free</td>
      <td><a href="https://www.passio.ai/blog">Blog</a></td>
        </tr>
    </tbody>
</table>

<div class="partners__lead-place"></div>

<h2 id="example-ai-career-assistant-with-flowiseai">Example: AI Career Assistant With FlowiseAI</h2>

<p>Let’s get a feel for developing AI applications with no-code tools. In this section, I will walk you through a demonstration that uses FlowiseAI to train an AI-powered career assistant app trained with LLMs. The idea is less about promoting no-code tools than it is an extremely convenient way to visualize how the components of an AI application are wired together and where LLMs fit in.</p>

<p>Why are we using FlowiseAI instead of any other no-code and low-code tools we discussed? I chose it primarily because I found it to be the easiest one to demo without additional pricing and configurations. FlowiseAI may very well be the right choice for your project, but please carefully evaluate and consider other options that may be more effective for your specific project or pricing constraints.</p>

<p>I also chose FlowiseAI because it leverages <a href="https://github.com/langchain-ai/langchainjs">LangChain</a>, an open-source framework for building applications using large language models. LangChain provides components like prompt templates, LLMs, and memory that can be chained together to develop use cases like chatbots and question-answering.</p>

<p>To see the possibilities of FlowiseAI first-hand, we’ll use it to develop an AI assistant that offers personalized career advice and guidance by exploring a user’s interests, skills, and career goals. It will take all of these inputs and return a list of cities that not only have a high concentration of jobs that fit the user’s criteria but that provide a good “quality of life” as well.</p>

<p>These are the components we will use to piece together the experience:</p>

<ul>
<li><a href="https://js.langchain.com/docs/modules/data_connection/retrievers/">Retrievers</a> (i.e., interfaces that return documents given an unstructured query);</li>
<li><a href="https://js.langchain.com/docs/modules/chains/">Chains</a> (i.e., the ability to compose components by linking them together visually);</li>
<li><a href="https://js.langchain.com/docs/modules/chains/foundational/llm_chain">Language models</a> (i.e., what “trains” the assistant);</li>
<li><a href="https://js.langchain.com/docs/modules/memory/">Memory</a> (i.e., storing previous sessions);</li>
<li><a href="https://js.langchain.com/docs/modules/agents/tools/">Tools</a> (i.e., functions);</li>
<li><a href="https://js.langchain.com/docs/modules/agents/agent_types/chat_conversation_agent">Conversational agent</a> (i.e., determine which tools to use based on the user’s input).</li>
</ul>

<p>These are the foundational elements that pave the way for creating an intelligent and efficient assistant. Here is a visual of the final configuration in Flowise:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-full-workflow.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="392"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-full-workflow.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-full-workflow.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-full-workflow.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-full-workflow.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-full-workflow.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-full-workflow.png"
			
			sizes="100vw"
			alt="A visual of the final configuration in Flowise, showing how the workflow is organized"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-full-workflow.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="install-flowiseai">Install FlowiseAI</h2>

<p>First things first, we need to get FlowiseAI up and running. FlowiseAI is an open-source application that can be installed from the command line.</p>

<p>You can install it with the following command:</p>

<pre><code class="language-bash">npm install -g flowise
</code></pre>

<p>Once installed, start up Flowise with this command:</p>

<pre><code class="language-bash">npx flowise start
</code></pre>

<p>From here, you can access FlowiseAI in your browser at <code>localhost:3000</code>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-start-screen.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="550"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-start-screen.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-start-screen.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-start-screen.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-start-screen.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-start-screen.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-start-screen.png"
			
			sizes="100vw"
			alt="FlowiseAI initial screen designed to display chat flows"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      This is the screen you should see after FlowwiseAI is successfully installed. (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-start-screen.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>It’s possible to serve FlowiseAI so that you can access it online and provide access to others, which is <a href="https://docs.flowiseai.com/deployment">well-covered in the documentation</a>.</p>

<h3 id="setting-up-retrievers">Setting Up Retrievers</h3>

<blockquote><strong>Retrievers</strong> are templates that the multi-prompt chain will query.</blockquote>

<p>Different retrievers provide different templates that query different things. In this case, we want to select the <a href="https://github.com/FlowiseAI/Flowise/tree/main/packages/components/nodes/retrievers"><strong>Prompt Retriever</strong></a> because it is designed to retrieve documents like PDF, TXT, and CSV files. Unlike other types of retrievers, the Prompt Retriever does not actually need to <em>store</em> those documents; it only needs to fetch them.</p>

<p>Let’s take the first step toward creating our career assistant by adding a Prompt Retriever to the FlowiseAI canvas. The “canvas” is the visual editing interface we’re using to cobble the app’s components together and see how everything connects.</p>

<p>Adding the Prompt Retriever requires us to first navigate to the <strong>Chatflow</strong> screen, which is actually the initial page when first accessing FlowiseAI following installation. Click the “Add New” button located in the top-right corner of the page. This opens up the canvas, which is initially empty.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-chatflow-canvas.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="599"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-chatflow-canvas.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-chatflow-canvas.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-chatflow-canvas.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-chatflow-canvas.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-chatflow-canvas.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-chatflow-canvas.png"
			
			sizes="100vw"
			alt="Empty canvas"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-chatflow-canvas.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The “Plus” (+) button is what we want to click to open up the library of items we can add to the canvas. Expand the <strong>Retrievers</strong> tab, then drag and drop the <strong>Prompt Retriever</strong> to the canvas.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="599"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever.png"
			
			sizes="100vw"
			alt="Retrievers tab"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The Prompt Retriever takes three inputs:</p>

<ol>
<li><strong>Name</strong>: The name of the stored prompt;</li>
<li><strong>Description</strong>: A brief description of the prompt (i.e., its purpose);</li>
<li><strong>Prompt system message</strong>: The initial prompt message that provides context and instructions to the system.</li>
</ol>

<p>Our career assistant will provide <strong>career suggestions</strong>, <strong>tool recommendations</strong>, <strong>salary information</strong>, and <strong>cities with matching jobs</strong>. We can start by configuring the Prompt Retriever for career suggestions. Here is placeholder content you can use if you are following along:</p>

<ul>
<li><strong>Name</strong>: Career Suggestion;</li>
<li><strong>Description</strong>: Suggests careers based on skills and experience;</li>
<li><strong>Prompt system message</strong>: You are a career advisor who helps users identify a career direction and upskilling opportunities. Be clear and concise in your recommendations.</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever-add.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="599"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever-add.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever-add.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever-add.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever-add.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever-add.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever-add.png"
			
			sizes="100vw"
			alt="Configuring the Prompt Retriever with inputs"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retriever-add.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Be sure to repeat this step three more times to create each of the following:</p>

<ul>
<li>Tool recommendations,</li>
<li>Salary information,</li>
<li>Locations.</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retrievers-all.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="536"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retrievers-all.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retrievers-all.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retrievers-all.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retrievers-all.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retrievers-all.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retrievers-all.png"
			
			sizes="100vw"
			alt="Four configured prompt retrievers on the canvas"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-prompt-retrievers-all.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="adding-a-multi-prompt-chain">Adding A Multi-Prompt Chain</h3>

<blockquote>A <strong>Multi-Prompt Chain</strong> is a class that consists of two or more prompts that are connected together to establish a conversation-like interaction between the user and the career assistant.</blockquote>

<p>The idea is that we combine the four prompts we’ve already added to the canvas and connect them to the proper tools (i.e., chat models) so that the career assistant can prompt the user for information and collect that information in order to process it and return the generated career advice. It’s sort of like a normal system prompt but with a conversational interaction.</p>

<p>The <strong>Multi-Prompt Chain</strong> node can be found in the “Chains” section of the same inserter we used to place the Prompt Retriever on the canvas.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="536"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt.png"
			
			sizes="100vw"
			alt="Inserting the multi-prompt chain to the canvas"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Once the Multi-Prompt Chain node is added to the canvas, connect it to the prompt retrievers. This enables the chain to receive user responses and employ the most appropriate language model to generate responses.</p>

<p>To connect, click the tiny dot next to the “Prompt Retriever” label on the Multi-Prompt Chain and drag it to the “Prompt Retriever” dot on each Prompt Retriever to draw a line between the chain and each prompt retriever.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt-add.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="539"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt-add.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt-add.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt-add.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt-add.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt-add.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt-add.png"
			
			sizes="100vw"
			alt="The chain connected to each prompt retreiver"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-multi-chain-prompt-add.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="integrating-chat-models">Integrating Chat Models</h3>

<p>This is where we start interacting with LLMs. In this case, we will integrate Anthropic’s <a href="https://www.anthropic.com/product">Claude chat model</a>. Claude is a powerful LLM designed for tasks related to complex reasoning, creativity, thoughtful dialogue, coding, and detailed content creation. You can get a feel for Claude by <a href="https://claude.ai/login">registering for access</a> to interact with it, similar to how you’ve played around with OpenAI’s ChatGPT.</p>

<p>From the inserter, open “Chat Models” and drag the <strong>ChatAnthropic</strong> option onto the canvas.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="539"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude.png"
			
			sizes="100vw"
			alt="Inserting the ChatAnthropic node to the canvas"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Once the ChatAnthropic chat model has been added to the canvas, connect its node to the Multi-Prompt Chain’s “Language Model” node to establish a connection.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-add.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="539"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-add.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-add.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-add.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-add.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-add.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-add.png"
			
			sizes="100vw"
			alt="Connecting the language model to the mutlti-chain prompt"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-add.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>It’s worth noting at this point that Claude requires an API key in order to access it. <a href="https://console.anthropic.com/login">Sign up for an API key on the Anthropic website</a> to create a new API key. Once you have an API key, provide it to the Mutli-Prompt Chain in the “Connect Credential” field.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-api.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="555"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-api.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-api.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-api.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-api.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-api.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-api.png"
			
			sizes="100vw"
			alt="Anthropic API field with the credential name and API key"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-anthropic-claude-api.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="adding-a-conversational-agent">Adding A Conversational Agent</h3>

<blockquote>The <strong>Agent</strong> component in FlowiseAI allows our assistant to do more tasks, like accessing the internet and sending emails.</blockquote>

<p>It connects external services and APIs, making the assistant more versatile. For this project, we will use a <strong>Conversational Agent</strong>, which can be found in the inserter under “Agent” components.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="555"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent.png"
			
			sizes="100vw"
			alt="Adding the Conversational Agent to the canvas"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Once the Conversational Agent has been added to the canvas, connect it to the Chat Model to “train” the model on how to respond to user queries.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent-add.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="631"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent-add.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent-add.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent-add.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent-add.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent-add.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent-add.png"
			
			sizes="100vw"
			alt="Conversational Agent connected to the Chat Model"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-conversational-agent-add.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="integrating-web-search-capabilities">Integrating Web Search Capabilities</h3>

<p>The Conversational Agent requires additional tools and memory. For example, we want to enable the assistant to perform Google searches to obtain information it can use to generate career advice. The <strong>Serp API</strong> node can do that for us and is located under “Tools” in the inserter.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="507"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi.png"
			
			sizes="100vw"
			alt="Adding the Serp API node to the canvas"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Like Claude, Serp API requires an API key to be added to the node. <a href="https://serpapi.com/">Register with the Serp API site</a> to create an API key. Once the API is configured, connect Serp API to the Conversational Agent’s “Allowed Tools” node.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi-add.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="631"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi-add.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi-add.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi-add.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi-add.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi-add.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi-add.png"
			
			sizes="100vw"
			alt="Connecting Serp API to the Conversational Agent"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-serpapi-add.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="building-in-memory">Building In Memory</h3>

<blockquote>The <strong>Memory</strong> component enables the career assistant to retain conversation information.</blockquote>

<p>This way, the app remembers the conversation and can reference it during the interaction or even to inform future interactions.</p>

<p>There are different types of memory, of course. Several of the options in FlowiseAI require additional configurations, so for the sake of simplicity, we are going to add the <strong>Buffer Memory</strong> node to the canvas. It is the most general type of memory provided by LangChain, taking the raw input of the past conversation and storing it in a history parameter for reference.</p>

<p>Buffer Memory connects to the Conversational Agent’s “Memory” node.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-buffer-memory.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="656"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-buffer-memory.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-buffer-memory.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-buffer-memory.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-buffer-memory.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-buffer-memory.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-buffer-memory.png"
			
			sizes="100vw"
			alt=" Connecting Buffer Memory to the Conversational Agent"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-buffer-memory.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div class="partners__lead-place"></div>

<h2 id="the-final-workflow">The Final Workflow</h2>

<p>At this point, our workflow looks something like this:</p>

<ul>
<li>Four <strong>prompt retrievers</strong> that provide the prompt templates for the app to converse with the user.</li>
<li>A <strong>multi-prompt chain</strong> connected to each of the four prompt retrievers that chooses the appropriate tools and language models based on the user interaction.</li>
<li>The <strong>Claude language model</strong> connected to the multi-chain prompt to “train” the app.</li>
<li>A <strong>conversational agent</strong> connected to the Claude language model to allow the app to perform additional tasks, such as Google web searches.</li>
<li><strong>Serp API</strong> connected to the conversational agent to perform bespoke web searches.</li>
<li><strong>Buffer memory</strong> connected to the conversational agent to store, i.e., “remember,” conversations.</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-final-workflow.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="704"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-final-workflow.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-final-workflow.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-final-workflow.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-final-workflow.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-final-workflow.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-final-workflow.png"
			
			sizes="100vw"
			alt="Showing the entire workfloe on the canvas"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/overview-large-language-model-concepts-use-cases-tools/flowiseai-final-workflow.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If you haven’t done so already, this is a great time to save the project and give it a name like “Career Assistant.”</p>

<h3 id="final-demo">Final Demo</h3>

<p>Watch the following video for a quick demonstration of the final workflow we created together in FlowiseAI. The prompts lag a little bit, but you should get the idea of how all of the components we connected are working together to provide responses.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/871072004"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h2 id="conclusion">Conclusion</h2>

<p>As we wrap up this article, I hope that you’re more familiar with the concepts, use cases, and tools of large language models. LLMs are a key component of AI because they are the “brains” of the application, <strong>providing the lens through which the app understands how to interact with and respond to human input</strong>.</p>

<p>We looked at a wide variety of use cases for LLMs in an AI context, from chatbots and language translations to writing assistance and summarizing large blocks of text. Then, we demonstrated how LLMs fit into an AI application by using FlowiseAI to create a visual workflow. That workflow not only provided a visual of how an LLM, like Claude, informs a conversation but also how it relies on additional tools, such as APIs, for performing tasks as well as memory for storing conversations.</p>

<p>The career assistant tool we developed together in FlowiseAI was a detailed visual look inside the black box of AI, providing us with a map of the components that feed the app and how they all work together.</p>

<p>Now that you know the role that LLMs play in AI, what sort of models would you use? Is there a particular app idea you have where a specific language model would be used to train it?</p>

<h3 id="references">References</h3>

<ul>
<li>“<a href="https://www.smashingmagazine.com/2023/09/generating-real-time-audio-sentiment-analysis-ai/">Generating Real-Time Audio Sentiment Analysis With AI</a>,” (Smashing Magazine)</li>
<li><a href="https://js.langchain.com/docs/get_started/introduction">LangChain documentation</a></li>
<li><a href="https://docs.flowiseai.com">FlowiseAI documentation</a></li>
<li><a href="https://huggingface.co/models">Hugging Face models directory</a></li>
<li><a href="https://www.anthropic.com/product">Anthropic’s Claude language model</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Joas Pambou</author><title>Using AI To Detect Sentiment In Audio Files</title><link>https://www.smashingmagazine.com/2023/06/ai-detect-sentiment-audio-files/</link><pubDate>Thu, 22 Jun 2023 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/06/ai-detect-sentiment-audio-files/</guid><description>Imagine being able to unlock the emotional essence of audio. Dive into an article where you will build an app that evaluates audio files for positive and negative sentiments. The idea is that you will create an interface for uploading an audio file, then transcribe the contents into text before analyzing the text and assigning it a positive or negative score for how the tone is perceived. There are a few moving pieces you need to cobble together, including machine learning, natural language processing, speech-to-text conversion, and a UI framework.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/06/ai-detect-sentiment-audio-files/" />
              <title>Using AI To Detect Sentiment In Audio Files</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Using AI To Detect Sentiment In Audio Files</h1>
                  
                    
                    <address>Joas Pambou</address>
                  
                  <time datetime="2023-06-22T08:00:00&#43;00:00" class="op-published">2023-06-22T08:00:00+00:00</time>
                  <time datetime="2023-06-22T08:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>I don’t know if you’ve ever used Grammarly’s service for writing and editing content. But if you have, then you no doubt have seen the feature that detects the tone of your writing.</p>

<p>It’s an extremely helpful tool! It can be hard to know how something you write might be perceived by others, and this can help affirm or correct you. Sure, it’s some algorithm doing the work, and we know <a href="https://www.theatlantic.com/technology/archive/2022/12/chatgpt-openai-artificial-intelligence-writing-ethics/672386/">that not all AI-driven stuff is perfectly accurate</a>. But as a gut check, it’s really useful.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-grammarly.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="413"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-grammarly.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-grammarly.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-grammarly.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-grammarly.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-grammarly.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-grammarly.png"
			
			sizes="100vw"
			alt="Grammarly tone detector"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-grammarly.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Now imagine being able to do the same thing with audio files. How neat would it be to understand the underlying sentiments captured in audio recordings? Podcasters especially could stand to benefit from a tool like that, not to mention customer service teams and many other fields.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aAn%20audio%20sentiment%20analysis%20has%20the%20potential%20to%20transform%20the%20way%20we%20interact%20with%20data.%0a&url=https://smashingmagazine.com%2f2023%2f06%2fai-detect-sentiment-audio-files%2f">
      
An audio sentiment analysis has the potential to transform the way we interact with data.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>That’s what we are going to accomplish in this article.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-ui.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="381"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-ui.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-ui.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-ui.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-ui.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-ui.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-ui.png"
			
			sizes="100vw"
			alt="A screenshot of the audio sentiment analyzer built in this tutorial"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      A screenshot of the audio sentiment analyzer we are building together in this tutorial. (<a href='https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-ui.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The idea is fairly straightforward:</p>

<ul>
<li>Upload an audio file.</li>
<li>Convert the content from speech to text.</li>
<li>Generate a score that indicates the type of sentiment it communicates.</li>
</ul>

<p>But how do we actually build an interface that does all that? I’m going to introduce you to three tools and show how they work together to create an audio sentiment analyzer.</p>

<h2 id="but-first-why-audio-sentiment-analysis">But First: Why Audio Sentiment Analysis?</h2>

<p>By harnessing the capabilities of an audio sentiment analysis tool, developers and data professionals can <strong>uncover valuable insights from audio recordings, revolutionizing the way we interpret emotions and sentiments</strong> in the digital age. Customer service, for example, is crucial for businesses aiming to deliver personable experiences. We can surpass the limitations of text-based analysis to get a better idea of the feelings communicated by verbal exchanges in a variety of settings, including:</p>

<ul>
<li><strong>Call centers</strong><br />
Call center agents can gain real-time insights into customer sentiment, enabling them to provide personalized and empathetic support.</li>
<li><strong>Voice assistants</strong><br />
Companies can improve their natural language processing algorithms to deliver more accurate responses to customer questions.</li>
<li><strong>Surveys</strong><br />
Organizations can gain valuable insights and understand customer satisfaction levels, identify areas of improvement, and make data-driven decisions to enhance overall customer experience.</li>
</ul>

<p>And that is just the tip of the iceberg for one industry. Audio sentiment analysis offers valuable insights across various industries. Consider healthcare as another example. Audio analysis could enhance patient care and improve doctor-patient interactions. Healthcare providers can gain a deeper understanding of patient feedback, identify areas for improvement, and optimize the overall patient experience.</p>

<p>Market research is another area that could benefit from audio analysis. Researchers can leverage sentiments to gain valuable insights into a target audience’s reactions that could be used in everything from competitor analyses to brand refreshes with the use of audio speech data from interviews, focus groups, or even social media interactions where audio is used.</p>

<p>I can also see audio analysis being used in the design process. Like, instead of asking stakeholders to write responses, how about asking them to record their verbal reactions and running those through an audio analysis tool? The possibilities are endless!</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Roll up your sleeves and <strong>boost your UX skills</strong>! Meet <strong><a data-instant href="https://smart-interface-design-patterns.com/">Smart Interface Design Patterns</a></strong>&nbsp;🍣, a 10h video library by Vitaly Friedman. <strong>100s of real-life examples</strong> and live UX training. <a href="https://www.youtube.com/watch?v=3mwZztmGgbE">Free preview</a>.</p>
<a data-instant href="https://smart-interface-design-patterns.com/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://smart-interface-design-patterns.com/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3155f571-450d-42f9-81b4-494aa9b52841/video-course-smart-interface-design-patterns-vitaly-friedman.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8c98e7f9-8e62-4c43-b833-fc6bf9fea0a9/video-course-smart-interface-design-patterns-vitaly-friedman.jpg"
    alt="Feature Panel"
    width="690"
    height="790"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="the-technical-foundations-of-audio-sentiment-analysis">The Technical Foundations Of Audio Sentiment Analysis</h2>

<p>Let’s explore the technical foundations that underpin audio sentiment analysis. We will delve into machine learning for <strong>natural language processing (NLP)</strong> tasks and look into Streamlit as a web application framework. These essential components lay the groundwork for the audio analyzer we’re making.</p>

<h3 id="natural-language-processing">Natural Language Processing</h3>

<p>In our project, we leverage the Hugging Face <a href="https://huggingface.co/docs/transformers/index">Transformers</a> library, a crucial component of our development toolkit. Developed by Hugging Face, the Transformers library equips developers with a vast collection of pre-trained models and advanced techniques, enabling them to extract valuable insights from audio data.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-transformers.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="574"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-transformers.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-transformers.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-transformers.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-transformers.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-transformers.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-transformers.png"
			
			sizes="100vw"
			alt="A screenshot of a pre-trained model from Hugging Face called Transformers"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/ai-detect-sentiment-audio-files/ai-audio-transformers.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>With Transformers, we can supply our audio analyzer with the ability to classify text, recognize named entities, answer questions, summarize text, translate, and generate text. Most notably, it also provides <strong>speech recognition</strong> and <strong>audio classification capabilities.</strong> Basically, we get an API that taps into pre-trained models so that our AI tool has a starting point rather than us having to train it ourselves.</p>

<h3 id="ui-framework-and-deployments">UI Framework And Deployments</h3>

<p><a href="https://streamlit.io/">Streamlit</a> is a web framework that simplifies the process of building interactive data applications. What I like about it is that it provides a <a href="https://streamlit.io/components">set of predefined components</a> that works well in the command line with the rest of the tools we’re using for the audio analyzer, not to mention we can deploy directly to their service to preview our work. It’s not required, as there may be other frameworks you are more familiar with.</p>

<h2 id="building-the-app">Building The App</h2>

<p>Now that we’ve established the two core components of our technical foundation, we will next explore implementation, such as</p>

<ol>
<li>Setting up the development environment,</li>
<li>Performing sentiment analysis,</li>
<li>Integrating speech recognition,</li>
<li>Building the user interface, and</li>
<li>Deploying the app.</li>
</ol>

<h3 id="initial-setup">Initial Setup</h3>

<p>We begin by importing the libraries we need:</p>

<pre><code class="language-javascript">import os
import traceback
import streamlit as st
import speech&#95;recognition as sr
from transformers import pipeline
</code></pre>

<p>We import <code>os</code> for system operations, <code>traceback</code> for error handling, <code>streamlit</code> (<code>st</code>) as our UI framework and for deployments, <code>speech_recognition</code> (<code>sr</code>) for audio transcription, and <code>pipeline</code> from Transformers to perform sentiment analysis using pre-trained models.</p>

<p>The project folder can be a pretty simple single directory with the following files:</p>

<ul>
<li><code>app.py</code>: The main script file for the Streamlit application.</li>
<li><code>requirements.txt</code>: File specifying project dependencies.</li>
<li><code>README.md</code>: Documentation file providing an overview of the project.</li>
</ul>

<h3 id="creating-the-user-interface">Creating The User Interface</h3>

<p>Next, we set up the layout, courtesy of Streamlit’s framework. We can create a spacious UI by <a href="https://docs.streamlit.io/library/get-started/main-concepts">calling a <code>wide</code> layout</a>:</p>

<pre><code class="language-javascript">st.set_page_config(layout="wide")
</code></pre>

<p>This ensures that the user interface provides ample space for displaying results and interacting with the tool.</p>

<p>Now let’s add some elements to the page using Streamlit’s functions. We can <a href="https://docs.streamlit.io/library/api-reference/text/st.title">add a title</a> and <a href="https://docs.streamlit.io/library/api-reference/write-magic/st.write">write some text</a>:</p>

<pre><code class="language-javascript">// app.py
st.title("🎧 Audio Analysis 📝")
st.write("[Joas](https://huggingface.co/Pontonkid)")
</code></pre>

<p>I’d like to add a sidebar to the layout that can hold a description of the app as well as the form control for uploading an audio file. We’ll use the main area of the layout to display the audio transcription and sentiment score.</p>

<p>Here’s how we <a href="https://docs.streamlit.io/library/api-reference/layout/st.sidebar">add a sidebar</a> with Streamlit:</p>

<div class="break-out">
<pre><code class="language-javascript">// app.py
st.sidebar.title("Audio Analysis")
st.sidebar.write("The Audio Analysis app is a powerful tool that allows you to analyze audio files and gain valuable insights from them. It combines speech recognition and sentiment analysis techniques to transcribe the audio and determine the sentiment expressed within it.")
</code></pre>
</div>

<p>And here’s how we <a href="https://docs.streamlit.io/library/api-reference/widgets/st.file_uploader">add the form control for uploading an audio file</a>:</p>

<pre><code class="language-javascript">// app.py
st.sidebar.header("Upload Audio")
audio&#95;file = st.sidebar.file&#95;uploader("Browse", type=["wav"])
upload&#95;button = st.sidebar.button("Upload")
</code></pre>

<p>Notice that I’ve set up the <code>file_uploader()</code> so it only accepts WAV audio files. That’s just a preference, and you can specify the exact types of files you want to support. Also, notice how I <a href="https://docs.streamlit.io/library/api-reference/widgets/st.button">added an Upload button</a> to initiate the upload process.</p>

<div class="partners__lead-place"></div>

<h3 id="analyzing-audio-files">Analyzing Audio Files</h3>

<p>Here’s the fun part, where we get to extract text from an audio file, analyze it, and calculate a score that measures the sentiment level of what is said in the audio.</p>

<p>The plan is the following:</p>

<ol>
<li>Configure the tool to utilize a pre-trained NLP model fetched from the Hugging Face models hub.</li>
<li>Integrate Transformers’ <code>pipeline</code> to perform sentiment analysis on the transcribed text.</li>
<li>Print the transcribed text.</li>
<li>Return a score based on the analysis of the text.</li>
</ol>

<p>In the first step, we configure the tool to leverage a pre-trained model:</p>

<pre><code class="language-javascript">// app.py
def perform&#95;sentiment&#95;analysis(text):
  model&#95;name = "distilbert-base-uncased-finetuned-sst-2-english"
</code></pre>

<p>This points to a model in the hub called <a href="https://huggingface.co/docs/transformers/v4.29.1/en/model_doc/distilbert#overview">DistilBERT</a>. I like it because it’s focused on text classification and is pretty lightweight compared to some other models, making it ideal for a tutorial like this. But there are <a href="https://huggingface.co/models?sort=downloads">plenty of other models available in Transformers</a> out there to consider.</p>

<p>Now we integrate <a href="https://huggingface.co/docs/transformers/main/en/quicktour#pipeline">the <code>pipeline()</code> function</a> that does the sentiment analysis:</p>

<div class="break-out">
<pre><code class="language-javascript">// app.py
def perform&#95;sentiment&#95;analysis(text):
  model&#95;name = "distilbert-base-uncased-finetuned-sst-2-english"
  sentiment&#95;analysis = pipeline("sentiment-analysis", model=model&#95;name)
</code></pre>
</div>

<p>We’ve set that up to perform a sentiment analysis based on the DistilBERT model we’re using.</p>

<p>Next up, define a variable for the text that we get back from the analysis:</p>

<div class="break-out">
<pre><code class="language-javascript">// app.py
def perform&#95;sentiment&#95;analysis(text):
  model&#95;name = "distilbert-base-uncased-finetuned-sst-2-english"
  sentiment&#95;analysis = pipeline("sentiment-analysis", model=model&#95;name)
  results = sentiment&#95;analysis(text)
</code></pre>
</div>

<p>From there, we’ll assign variables for the score label and the score itself before returning it for use:</p>

<div class="break-out">
<pre><code class="language-javascript">// app.py
def perform&#95;sentiment&#95;analysis(text):
  model&#95;name = "distilbert-base-uncased-finetuned-sst-2-english"
  sentiment&#95;analysis = pipeline("sentiment-analysis", model=model&#95;name)
  results = sentiment&#95;analysis(text)
  sentiment&#95;label = results[0]['label']
  sentiment&#95;score = results[0]['score']
  return sentiment&#95;label, sentiment&#95;score
</code></pre>
</div>

<p>That’s our complete <code>perform_sentiment_analysis()</code> function!</p>

<h3 id="transcribing-audio-files">Transcribing Audio Files</h3>

<p>Next, we’re going to transcribe the content in the audio file into plain text. We’ll do that by defining a <code>transcribe_audio()</code> function that uses the <a href="https://pypi.org/project/SpeechRecognition/"><code>speech_recognition</code></a> library to transcribe the uploaded audio file:</p>

<pre><code class="language-javascript">// app.py
def transcribe&#95;audio(audio&#95;file):
  r = sr.Recognizer()
  with sr.AudioFile(audio&#95;file) as source:
    audio = r.record(source)
    transcribed&#95;text = r.recognize&#95;google(audio)
  return transcribed&#95;text
</code></pre>

<p>We initialize a recognizer object (<code>r</code>) from the <code>speech_recognition</code> library and open the uploaded audio file using the <a href="https://github.com/Uberi/speech_recognition/blob/master/examples/audio_transcribe.py"><code>AudioFile</code> function</a>. We then record the audio using <code>r.record(source)</code>. Finally, we use the <a href="https://cloud.google.com/speech-to-text/">Google Speech Recognition API</a> through <code>r.recognize_google(audio)</code> to transcribe the audio and obtain the transcribed text.</p>

<p>In a <code>main()</code> function, we first check if an audio file is uploaded and the upload button is clicked. If both conditions are met, we proceed with audio transcription and sentiment analysis.</p>

<pre><code class="language-javascript">// app.py
def main():
  if audio&#95;file and upload&#95;button:
    try:
      transcribed&#95;text = transcribe&#95;audio(audio&#95;file)
      sentiment&#95;label, sentiment&#95;score = perform&#95;sentiment&#95;analysis(transcribed&#95;text)
</code></pre>

<div class="partners__lead-place"></div>

<h3 id="integrating-data-with-the-ui">Integrating Data With The UI</h3>

<p>We have everything we need to display a sentiment analysis for an audio file in our app’s interface. We have the file uploader, a language model to train the app, a function for transcribing the audio into text, and a way to return a score. All we need to do now is hook it up to the app!</p>

<p>What I’m going to do is set up two headers and a text area from Streamlit, as well as variables for icons that represent the sentiment score results:</p>

<pre><code class="language-javascript">// app.py
st.header("Transcribed Text")
st.text&#95;area("Transcribed Text", transcribed&#95;text, height=200)
st.header("Sentiment Analysis")
negative&#95;icon = "👎"
neutral&#95;icon = "😐"
positive&#95;icon = "👍"
</code></pre>

<p>Let’s use conditional statements to display the sentiment score based on which label corresponds to the returned result. If a sentiment label is empty, we use <a href="https://docs.streamlit.io/library/api-reference/layout/st.empty"><code>st.empty()</code></a> to leave the section blank.</p>

<div class="break-out">
<pre><code class="language-javascript">// app.py
if sentiment&#95;label == "NEGATIVE":
  st.write(f"{negative&#95;icon} Negative (Score: {sentiment&#95;score})", unsafe&#95;allow&#95;html=True)
else:
  st.empty()

if sentiment&#95;label == "NEUTRAL":
  st.write(f"{neutral&#95;icon} Neutral (Score: {sentiment&#95;score})", unsafe&#95;allow&#95;html=True)
else:
  st.empty()

if sentiment&#95;label == "POSITIVE":
  st.write(f"{positive&#95;icon} Positive (Score: {sentiment&#95;score})", unsafe&#95;allow&#95;html=True)
else:
  st.empty()
</code></pre>
</div>

<p>Streamlit has a handy <a href="https://docs.streamlit.io/library/api-reference/status/st.info"><code>st.info()</code> element</a> for displaying informational messages and statuses. Let’s tap into that to display an explanation of the sentiment score results:</p>

<div class="break-out">
<pre><code class="language-javascript">// app.py
st.info(
  "The sentiment score measures how strongly positive, negative, or neutral the feelings or opinions are."
  "A higher score indicates a positive sentiment, while a lower score indicates a negative sentiment."
)
</code></pre>
</div>

<p>We should account for error handling, right? If any exceptions occur during the audio transcription and sentiment analysis processes, they are caught in an <code>except</code> block. We display an error message using Streamlit’s <a href="https://docs.streamlit.io/library/api-reference/status/st.error"><code>st.error()</code> function</a> to inform users about the issue, and we also print the exception traceback using <code>traceback.print_exc()</code>:</p>

<div class="break-out">
<pre><code class="language-javascript">// app.py
except Exception as ex:
  st.error("Error occurred during audio transcription and sentiment analysis.")
  st.error(str(ex))
  traceback.print&#95;exc()
</code></pre>
</div>

<p>This code block ensures that the app’s <code>main()</code> function is executed when the script is run as the main program:</p>

<pre><code class="language-javascript">// app.py
if &#95;&#95;name&#95;&#95; == "&#95;&#95;main&#95;&#95;": main()
</code></pre>

<p>It’s common practice to wrap the execution of the main logic within this condition to prevent it from being executed when the script is imported as a module.</p>

<h3 id="deployments-and-hosting">Deployments And Hosting</h3>

<p>Now that we have successfully built our audio sentiment analysis tool, it’s time to deploy it and publish it live. For convenience, I am using the <a href="https://docs.streamlit.io/streamlit-community-cloud">Streamlit Community Cloud</a> for deployments since I’m already using Streamlit as a UI framework. That said, I do think it is a fantastic platform because it’s free and allows you to share your apps pretty easily.</p>

<p>But before we proceed, there are a few prerequisites:</p>

<ul>
<li><strong>GitHub account</strong><br />
If you don’t already have one, <a href="https://github.com/">create a GitHub account</a>. GitHub will serve as our code repository that connects to the Streamlit Community Cloud. This is where Streamlit gets the app files to serve.</li>
<li><strong>Streamlit Community Cloud account</strong><br />
<a href="https://streamlit.io/cloud">Sign up for a Streamlit Cloud</a> so you can deploy to the cloud.</li>
</ul>

<p>Once you have your accounts set up, it’s time to dive into the deployment process:</p>

<ol>
<li><strong>Create a GitHub repository.</strong><br />
Create a new repository on GitHub. This repository will serve as a central hub for managing and collaborating on the codebase.</li>
<li><strong>Create the Streamlit application.</strong><br />
Log into Streamlit Community Cloud and create a new application project, providing details like the name and pointing the app to the GitHub repository with the app files.</li>
<li><strong>Configure deployment settings.</strong><br />
Customize the deployment environment by specifying a Python version and defining environment variables.</li>
</ol>

<p>That’s it! From here, Streamlit will automatically build and deploy our application when new changes are pushed to the main branch of the GitHub repository. You can see a working example of the audio analyzer I created: <a href="https://coding-audio-sentiment.streamlit.app">Live Demo</a>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>There you have it! You have successfully built and deployed an app that recognizes speech in audio files, transcribes that speech into text, analyzes the text, and assigns a score that indicates whether the overall sentiment of the speech is positive or negative.</p>

<p>We used a tech stack that only consists of a language model (Transformers) and a UI framework (Streamlit) that has integrated deployment and hosting capabilities. That’s really all we needed to pull everything together!</p>

<p>So, what’s next? Imagine capturing sentiments in real time. That could open up new avenues for instant insights and dynamic applications. It’s an exciting opportunity to push the boundaries and take this audio sentiment analysis experiment to the next level.</p>

<h3 id="further-reading-on-smashing-magazine">Further Reading on Smashing Magazine</h3>

<ul>
<li>“<a href="https://www.smashingmagazine.com/2023/05/ai-tools-skyrocket-programming-productivity/">How To Use AI Tools To Skyrocket Your Programming Productivity</a>,” Shane Duggan</li>
<li>“<a href="https://www.smashingmagazine.com/2022/03/audio-visualization-javascript-gsap-part1/">A Guide To Audio Visualization With JavaScript And GSAP</a>,” Jhey Tompkins</li>
<li>“<a href="https://www.smashingmagazine.com/2021/06/web-design-done-well-audio/">Web Design Done Well: Making Use Of Audio</a>,” Frederick O’Brien</li>
<li>“<a href="https://www.smashingmagazine.com/2018/04/audio-video-recording-react-native-expo/">How To Create An Audio/Video Recording App With React Native: An In-Depth Tutorial</a>,” Oleh Mryhlod</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Sriram Thiagarajan</author><title>How To Build Server-Side Rendered (SSR) Svelte Apps With SvelteKit</title><link>https://www.smashingmagazine.com/2023/06/build-server-side-rendered-svelte-apps-sveltekit/</link><pubDate>Wed, 14 Jun 2023 11:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/06/build-server-side-rendered-svelte-apps-sveltekit/</guid><description>SvelteKit is a framework for building apps using Svelte. You can use SvelteKit to build a variety of apps that can vary from being a Single page application (SPA) or Server Side Rendered (SSR) application and more. In this article, Sriram shows you how to build a server-side rendered SvelteKit application and deploy it to Netlify by following this step-by-step guide.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/06/build-server-side-rendered-svelte-apps-sveltekit/" />
              <title>How To Build Server-Side Rendered (SSR) Svelte Apps With SvelteKit</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How To Build Server-Side Rendered (SSR) Svelte Apps With SvelteKit</h1>
                  
                    
                    <address>Sriram Thiagarajan</address>
                  
                  <time datetime="2023-06-14T11:00:00&#43;00:00" class="op-published">2023-06-14T11:00:00+00:00</time>
                  <time datetime="2023-06-14T11:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>I’m not interested in starting a turf war between server-side rendering and client-side rendering. The fact is that SvelteKit supports both, which is one of the many perks it offers right out of the box. The server-side rendering paradigm is not a new concept. It means that the client (i.e., the user’s browser) sends a request to the server, and the server responds with the data and markup for that particular page, which is then rendered in the user’s browser.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/11-what-is-ssr.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="537"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/11-what-is-ssr.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/11-what-is-ssr.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/11-what-is-ssr.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/11-what-is-ssr.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/11-what-is-ssr.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/11-what-is-ssr.png"
			
			sizes="100vw"
			alt="Server-side rendering graph"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/11-what-is-ssr.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To build an SSR app using the primary <a href="https://svelte.dev">Svelte</a> framework, you would need to maintain two codebases, one with the server running in Node, along with with some templating engine, like <a href="https://handlebarsjs.com">Handlebars</a> or <a href="http://mustache.github.io">Mustache</a>. The other application is a client-side Svelte app that fetches data from the server.</p>

<p>The approach we’re looking at in the above paragraph isn’t without disadvantages. Two that immediately come to mind that I’m sure you thought of after reading that last paragraph:</p>

<ol>
<li>The application is more complex because we’re effectively maintaining two systems.</li>
<li>Sharing logic and data between the client and server code is more difficult than fetching data from an API on the client side.</li>
</ol>

<h2 id="sveltekit-simplifies-the-process">SvelteKit Simplifies The Process</h2>

<p>SvelteKit streamlines things by handling of complexity of the server and client on its own, allowing you to focus squarely on developing the app. There’s no need to maintain two applications or do a tightrope walk sharing data between the two.</p>

<p>Here’s how:</p>

<ul>
<li>Each route can have a <code>page.server.ts</code> file that’s used to run code in the server and return data seamlessly to your client code.</li>
<li>If you use TypeScript, SvelteKit auto-generates types that are shared between the client and server.</li>
<li>SvelteKit provides an option to select your rendering approach based on the route. You can choose SSR for some routes and CSR for others, like maybe your admin page routes.</li>
<li>SvelteKit also supports routing based on a file system, making it much easier to define new routes than having to hand-roll them yourself.</li>
</ul>

<h2 id="sveltekit-in-action-job-board">SvelteKit In Action: Job Board</h2>

<p>I want to show you how streamlined the SvelteKit approach is to the traditional way we have been dancing between the SSR and CSR worlds, and I think there’s no better way to do that than using a real-world example. So, what we’re going to do is build a job board &mdash; basically a list of job items &mdash; while detailing SvelteKit’s role in the application.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/1-job-listing-home-page.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="502"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/1-job-listing-home-page.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/1-job-listing-home-page.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/1-job-listing-home-page.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/1-job-listing-home-page.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/1-job-listing-home-page.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/1-job-listing-home-page.png"
			
			sizes="100vw"
			alt="Job listing home page"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/1-job-listing-home-page.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>When we’re done, what we’ll have is an app where SvelteKit fetches the data from a JSON file and renders it on the server side. We’ll go step by step.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/typescript-in-50-lessons/">“TypeScript in 50 Lessons”</a></strong>, our shiny new guide to TypeScript. With detailed <strong>code walkthroughs</strong>, hands-on examples and common gotchas. For developers who know enough <strong>JavaScript</strong> to be dangerous.</p>
<a data-instant href="/printed-books/typescript-in-50-lessons/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="/printed-books/typescript-in-50-lessons/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/2732dfe9-e1ee-41c3-871a-6252aeda741c/typescript-panel.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/c2f2c6d6-4e85-449a-99f5-58bd053bc846/typescript-shop-cover-opt.png"
    alt="Feature Panel"
    width="481"
    height="698"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="first-initialize-the-sveltekit-project">First, Initialize The SvelteKit Project</h2>

<p>The official SvelteKit docs already do a great job of explaining how to set up a new project. But, in general, we start any SvelteKit project in the command line with this command:</p>

<pre><code class="language-bash">npm create svelte@latest job-list-ssr-sveltekit
</code></pre>

<p>This command creates a new project folder called <code>job-list-ssr-sveltekit</code> on your machine and initializes Svelte and SvelteKit for us to use. But we don’t stop there &mdash; we get prompted with a few options to configure the project:</p>

<ol>
<li>First, we select a SvelteKit template. We are going to stick to using the basic Skeleton Project template.</li>
<li>Next, we can enable type-checking if you’re into that. Type-checking provides assistance when writing code by watching for bugs in the app’s data types. I’m going to use the “TypeScript syntax” option, but you aren’t required to use it and can choose the “None” option instead.</li>
</ol>

<p>There are additional options from there that are more a matter of personal preference:</p>

<ul>
<li><a href="https://eslint.org">ESLint</a> to enforce code consistency,</li>
<li><a href="https://prettier.io">Prettier</a> to clean up code formatting,</li>
<li><a href="https://playwright.dev">Playwright</a> for browser testing locally,</li>
<li><a href="https://vitest.dev">Vitest</a> for unit testing.</li>
</ul>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/2-project-init.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="232"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/2-project-init.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/2-project-init.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/2-project-init.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/2-project-init.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/2-project-init.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/2-project-init.png"
			
			sizes="100vw"
			alt="Project Initiation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/2-project-init.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If you are familiar with any of these, you can add them to the project. We are going to keep it simple and not select anything from the list since what I really want to show off is the app architecture and how everything works together to get data rendered by the app.</p>

<p>Now that we have the template for our project ready for us let’s do the last bit of setup by installing the dependencies for Svelte and SvelteKit to do their thing:</p>

<pre><code class="language-bash">cd job-listing-ssr-sveltekit
npm install
</code></pre>

<p>There’s something interesting going on under the hood that I think is worth calling out:</p>

<h3 id="is-sveltekit-a-dependency">Is SvelteKit A Dependency?</h3>

<p>If you are new to Svelte or SvelteKit, you may be pleasantly surprised when you open the project’s <code>package.json</code> file. Notice that the SvelteKit is listed in the <code>devDependencies</code> section. The reason for that is Svelte (and, in turn, SvelteKit) acts like a compiler that takes all your <code>.js</code> and <code>.svelte</code> files and converts them into optimized JavaScript code that is rendered in the browser.</p>

<p>This means the Svelte package is actually unnecessary when we deploy it to the server. That’s why it is not listed as a dependency in the package file. The final bundle of our job board app is going to contain just the app’s code, which means the <strong>size of the bundle is way smaller and loads faster than the regular Svelte-based architecture</strong>.</p>

<p>Look at how tiny and readable the <code>package-json</code> file is!</p>

<div class="break-out">
<pre><code class="language-javascript">{
    "name": "job-listing-ssr-sveltekit",
    "version": "0.0.1",
    "private": true,
    "scripts": {
        "dev": "vite dev",
        "build": "vite build",
        "preview": "vite preview",
        "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
        "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
    },
    "devDependencies": {
        "@sveltejs/adapter-auto": "^2.0.0",
        "@sveltejs/kit": "^1.5.0",
        "svelte": "^3.54.0",
        "svelte-check": "^3.0.1",
        "tslib": "^2.4.1",
        "typescript": "^4.9.3",
        "vite": "^4.0.0"
    },
    "type": "module"
}
</code></pre>
</div>

<p>I really find this refreshing, and I hope you do, too. Seeing a big list of packages tends to make me nervous because all those moving pieces make the entirety of the app architecture feel brittle and vulnerable. The concise SvelteKit output, by contrast, gives me much more confidence.</p>

<h2 id="creating-the-data">Creating The Data</h2>

<p>We need data coming from somewhere that can inform the app on what needs to be rendered. I mentioned earlier that we would be placing data in and pulling it from a JSON file. That’s still the plan.</p>

<p>As far as the structured data goes, what we need to define are properties for a job board item. Depending on your exact needs, there could be a lot of fields or just a few. I’m going to proceed with the following:</p>

<ul>
<li>Job title,</li>
<li>Job description,</li>
<li>Company Name,</li>
<li>Compensation.</li>
</ul>

<p>Here’s how that looks in JSON:</p>

<pre><code class="language-javascript">[{
    "job&#95;title": "Job 1",
    "job&#95;description": "Very good job",
    "company&#95;name": "ABC Software Company",
    "compensation&#95;per&#95;year": "$40000 per year"
}, {
    "job&#95;title": "Job 2",
    "job&#95;description": "Better job",
    "company&#95;name": "XYZ Software Company",
    "compensation&#95;per&#95;year": "$60000 per year"
}]
</code></pre>

<p>Now that we’ve defined some data let’s open up the main project folder. There’s a sub-directory in there called <code>src</code>. We can open that and create a new folder called <code>data</code> and add the JSON file we just made to it. We will come back to the JSON file when we work on fetching the data for the job board.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/3-json-file.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="416"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/3-json-file.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/3-json-file.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/3-json-file.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/3-json-file.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/3-json-file.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/3-json-file.png"
			
			sizes="100vw"
			alt="Json file"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/3-json-file.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div class="partners__lead-place"></div>

<h2 id="adding-typescript-model">Adding TypeScript Model</h2>

<p>Again, TypeScript is completely optional. But since it’s so widely used, I figure it’s worth showing how to set it up in a SvelteKit framework.</p>

<p>We start by creating a new <code>models.ts</code> file in the project’s <code>src</code> folder. This is the file where we define all of the data types that can be imported and used by other components and pages, and TypeScript will check them for us.</p>

<p>Here’s the code for the <code>models.ts</code> file:</p>

<pre><code class="language-javascript">export type JobsList = JobItem[]

export interface JobItem {
  job&#95;title: string
  job&#95;description: string
  company&#95;name: string
  compensation&#95;per&#95;year: string
}
</code></pre>

<p>There are two data types defined in the code:</p>

<ol>
<li><code>JobList</code> contains the array of job items.</li>
<li><code>JobItem</code> contains the job details (or properties) that we defined earlier.</li>
</ol>

<h2 id="the-main-job-board-page">The Main Job Board Page</h2>

<p>We’ll start by developing the code for the main job board page that renders a list of available job items. Open the <code>src/routes/+page.svelte</code> file, which is the main job board. Notice how it exists in the <code>/src/routes</code> folder? That’s the <strong>file-based routing system</strong> I referred to earlier when talking about the benefits of SvelteKit. The name of the file is automatically generated into a route. That’s a real DX gem, as it saves us time from having to code the routes ourselves and maintaining more code.</p>

<p>While <code>+page.svelte</code> is indeed the main page of the app, it’s also the template for any generic page in the app. But we can create a separation of concerns by adding more structure in the <code>/scr/routes</code> directory with more folders and sub-folders that result in different paths. SvelteKit’s docs have all the information you need for <a href="https://kit.svelte.dev/docs/routing#page">routing and routing conventions</a>.</p>

<p>This is the markup and styles we’ll use for the main job board:</p>

<pre><code class="language-html">&lt;div class="home-page"&gt;
  &lt;h1&gt;Job Listing Home page&lt;/h1&gt;
&lt;/div&gt;

&lt;style&gt;
  .home-page {
    padding: 2rem 4rem;
    display: flex;
    align-items: center;
    flex-direction: column;
    justify-content: center;
  }
&lt;/style&gt;
</code></pre>

<p>Yep, this is super simple. All we’re adding to the page is an <code>&lt;h1&gt;</code> tag for the page title and some light CSS styling to make sure the content is centered and has some nice padding for legibility. I don’t want to muddy the waters of this example with a bunch of opinionated markup and styles that would otherwise be a distraction from the app architecture.</p>

<h2 id="run-the-app">Run The App</h2>

<p>We’re at a point now where we can run the app using the following in the command line:</p>

<pre><code class="language-bash">npm run dev -- --open
</code></pre>

<p>The <code>-- --open</code> argument automatically opens the job board page in the browser. That’s just a small but nice convenience. You can also navigate to the URL that the command line outputs.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/4-initial-project-run.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="193"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/4-initial-project-run.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/4-initial-project-run.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/4-initial-project-run.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/4-initial-project-run.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/4-initial-project-run.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/4-initial-project-run.png"
			
			sizes="100vw"
			alt="Initial project run"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/4-initial-project-run.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="the-job-item-component">The Job Item Component</h2>

<p>OK, so we have a main job board page that will be used to list job items from the data fetched by the app. What we need is a new component specifically for the jobs themselves. Otherwise, all we have is a bunch of data with no instructions for how it is rendered.</p>

<p>Let’s take of that by opening the <code>src</code> folder in the project and creating a new sub-folder called <code>components</code>. And in that new <code>/src/components</code> folder, let’s add a new Svelte file called <code>JobDisplay.svelte</code>.</p>

<p>We can use this for the component’s markup and styles:</p>

<pre><code class="language-javascript">&lt;script lang="ts"&gt;
  import type { JobItem } from "../models";
  export let job: JobItem;
&lt;/script&gt;

&lt;div class="job-item"&gt;
  &lt;p&gt;Job Title: &lt;b&gt;{job.job&#95;title}&lt;/b&gt;&lt;/p&gt;
  &lt;p&gt;Description: &lt;b&gt;{job.job&#95;description}&lt;/b&gt;&lt;/p&gt;
  &lt;div class="job-details"&gt;
    &lt;span&gt;Company Name : &lt;b&gt;{job.company&#95;name}&lt;/b&gt;&lt;/span&gt;
    &lt;span&gt;Compensation per year: &lt;b&gt;{job.compensation&#95;per&#95;year}&lt;/b&gt;&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;style&gt;
  .job-item {
    border: 1px solid grey;
    padding: 2rem;
    width: 50%;
    margin: 1rem;
    border-radius: 10px;
  }

  .job-details {
    display: flex;
    justify-content: space-between;
  }
&lt;/style&gt;
</code></pre>

<p>Let’s break that down so we know what’s happening:</p>

<ol>
<li>At the top, we import the TypeScript <code>JobItem</code> model.</li>
<li>Then, we define a <code>job</code> prop with a type of <code>JobItem</code>. This prop is responsible for getting the data from its parent component so that we can pass that data to this component for rendering.</li>
<li>Next, the HTML provides this component’s markup.</li>
<li>Last is the CSS for some light styling. Again, I’m keeping this super simple with nothing but a little padding and minor details for structure and legibility. For example, <code>justify-content: space-between</code> adds a little visual separation between job items.</li>
</ol>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/5-job-display-component.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="165"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/5-job-display-component.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/5-job-display-component.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/5-job-display-component.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/5-job-display-component.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/5-job-display-component.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/5-job-display-component.png"
			
			sizes="100vw"
			alt="Job display component"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/5-job-display-component.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="fetching-job-data">Fetching Job Data</h2>

<p>Now that we have the <code>JobDisplay</code> component all done, we’re ready to pass it data to fill in all those fields to be displayed in each <code>JobDisplay</code> rendered on the main job board.</p>

<p>Since this is an SSR application, the data needs to be fetched on the server side. SvelteKit makes this easy by having a separate <code>load</code> function that can be used to fetch data and used as a hook for other actions on the server when the page loads.</p>

<p>To fetch, let’s create yet another new file TypeScript file &mdash; this time called <code>+page.server.ts</code> &mdash; in the project’s <code>routes</code> directory. Like the <code>+page.svelte</code> file, this also has a special meaning which will make this file run in the server when the route is loaded. Since we want this on the main job board page, we will create this file in the <code>routes</code> directory and include this code in it:</p>

<pre><code class="language-javascript">import jobs from ’../data/job-listing.json’
import type { JobsList } from ’../models’;

const job&#95;list: JobsList = jobs;

export const load = (() =&gt; {
  return {
    job&#95;list
  };
})
</code></pre>

<p>Here’s what we’re doing with this code:</p>

<ol>
<li>We import data from the JSON file. This is for simplicity purposes. In the real app, you would likely fetch this data from a database by making an API call.</li>
<li>Then, we import the TypeScript model we created for <code>JobsList</code>.</li>
<li>Next, we create a new <code>job_list</code> variable and assign the imported data to it.</li>
<li>Last, we define a <code>load</code> function that will return an object with the assigned data. SvelteKit will automatically call this function when the page is requested. So, the magic for SSR code happens here as we fetch the data in the server and build the HTML with the data we get back.</li>
</ol>

<h2 id="accessing-data-from-the-job-board">Accessing Data From The Job Board</h2>

<p>SvelteKit makes accessing data relatively easy by passing data to the main job board page in a way that checks the types for errors in the process. We can import a type called <code>PageServerData</code> in the <code>+page.svelte</code> file. This type is autogenerated and will have the data returned by the <code>+page.server.ts</code> file. This is awesome, as we don’t have to define types again when using the data we receive.</p>

<p>Let’s update the code in the <code>+page.svelte</code> file, like the following:</p>

<pre><code class="language-javascript">&lt;script lang="ts"&gt;
  import JobDisplay from ’../components/JobDisplay.svelte’;
  import type { PageServerData } from ’./$types’;

  export let data: PageServerData;
&lt;/script&gt;

&lt;div class="home-page"&gt;
  &lt;h1&gt;Job Listing Home page&lt;/h1&gt;

  {#each data.job&#95;list as job}
    &lt;JobDisplay job={job}/&gt;
  {/each}
&lt;/div&gt;

&lt;style&gt;....&lt;/style&gt;
</code></pre>

<p>This is so cool because:</p>

<ol>
<li>The <code>#each</code> syntax is a Svelte benefit that can be used to repeat the <code>JobDisplay</code> component for all the jobs for which data exists.</li>
<li>At the top, we are importing both the JobDisplay component and <code>PageServerData</code> type from <code>./$types</code>, which is autogenerated by SvelteKit.</li>
</ol>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/6-job-listing-home-page.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="502"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/6-job-listing-home-page.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/6-job-listing-home-page.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/6-job-listing-home-page.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/6-job-listing-home-page.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/6-job-listing-home-page.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/6-job-listing-home-page.png"
			
			sizes="100vw"
			alt="Job listing home page"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/6-job-listing-home-page.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div class="partners__lead-place"></div>

<h2 id="deploying-the-app">Deploying The App</h2>

<p>We’re ready to compile and bundle this project in preparation for deployment! We get to use the same command in the Terminal as most other frameworks, so it should be pretty familiar:</p>

<pre><code class="language-bash">npm run build
</code></pre>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/7-build-output.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="474"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/7-build-output.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/7-build-output.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/7-build-output.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/7-build-output.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/7-build-output.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/7-build-output.png"
			
			sizes="100vw"
			alt="Build output"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/7-build-output.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Note</strong>: <em>You might get the following warning when running that command: “Could not detect a supported production environment.” We will fix that in just a moment, so stay with me.</em></p>

<p>From here, we can use the <code>npm run preview</code> command to check the latest built version of the app:</p>

<pre><code class="language-bash">npm run preview
</code></pre>

<p>This process is a new way to gain confidence in the build locally before deploying it to a production environment.</p>

<p>The next step is to deploy the app to the server. I’m using Netlify, but that’s purely for example, so feel free to go with another option. SvelteKit offers adapters that will deploy the app to different server environments. You can get the <a href="https://kit.svelte.dev/docs/adapters#supported-environments">whole list of adapters in the docs</a>, of course.</p>

<p>The real reason I’m using Netlify is that deploying there is super convenient for this tutorial, thanks to the <code>adapter-netlify</code> plugin that can be installed with this command:</p>

<pre><code class="language-bash">npm i -D @sveltejs/adapter-netlify
</code></pre>

<p>This does, indeed, introduce a new dependency in the <code>package.json</code> file. I mention that because you know how much I like to keep that list short.</p>

<p>After installation, we can update the <code>svelte.config.js</code> file to consume the adapter:</p>

<pre><code class="language-javascript">import adapter from ’@sveltejs/adapter-netlify’;
import { vitePreprocess } from ’@sveltejs/kit/vite’;

/&#42;&#42; @type {import(’@sveltejs/kit’).Config} &#42;/
const config = {
    preprocess: vitePreprocess(),

    kit: {
        adapter: adapter({
            edge: false, 
            split: false
        })
    }
};

export default config;
</code></pre>

<p>Real quick, this is what’s happening:</p>

<ol>
<li>The <code>adapter</code> is imported from <code>adapter-netlify</code>.</li>
<li>The new adapter is passed to the <code>adapter</code> property inside the <code>kit</code>.</li>
<li>The <code>edge</code> boolean value can be used to configure the deployment to a Netlify edge function.</li>
<li>The <code>split</code> boolean value is used to control whether we want to split each route into separate edge functions.</li>
</ol>

<h3 id="more-netlify-specific-configurations">More Netlify-Specific Configurations</h3>

<p>Everything from here on out is specific to Netlify, so I wanted to break it out into its own section to keep things clear.</p>

<p>We can add a new file called <code>netlify.toml</code> at the top level of the project folder and add the following code:</p>

<pre><code class="language-bash">[build]
  command = "npm run build"
  publish = "build"
</code></pre>

<p>I bet you know what this is doing, but now we have a new alias for deploying the app to Netlify. It also allows us to control deployment from a Netlify account as well, which might be a benefit to you. To do this, we have to:</p>

<ol>
<li>Create a new project in Netlify,</li>
<li>Select the “Import an existing project” option, and</li>
<li>Provide permission for Netlify to access the project repository. You get to choose where you want to store your repo, whether it’s GitHub or some other service.</li>
</ol>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/8-netlify-deploy.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="515"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/8-netlify-deploy.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/8-netlify-deploy.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/8-netlify-deploy.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/8-netlify-deploy.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/8-netlify-deploy.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/8-netlify-deploy.png"
			
			sizes="100vw"
			alt="Netlify deploy"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/8-netlify-deploy.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Since we have set up the <code>netlify.toml</code> file, we can leave the default configuration and click the “Deploy” button directly from Netlify.</p>

<p>Once the deployment is completed, you can navigate to the site using the provided URL in Netlify. This should be the final result:</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/9-final-output.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="502"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/9-final-output.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/9-final-output.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/9-final-output.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/9-final-output.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/9-final-output.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/9-final-output.png"
			
			sizes="100vw"
			alt="Final output"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/9-final-output.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Here’s something fun. Open up DevTools when viewing the app in the browser and notice that the HTML contains the actual data we fetched from the JSON file. This way, we know for sure that the right data is rendered and that everything is working.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/10-html-ssr-screenshot.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="431"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/10-html-ssr-screenshot.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/10-html-ssr-screenshot.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/10-html-ssr-screenshot.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/10-html-ssr-screenshot.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/10-html-ssr-screenshot.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/10-html-ssr-screenshot.png"
			
			sizes="100vw"
			alt="HTML SSR screenshot"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/build-server-side-rendered-svelte-apps-sveltekit/10-html-ssr-screenshot.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Note</strong>: <em>The source code of the whole project is <a href="https://github.com/sriram15/job-listing-ssr-sveltekit">available on GitHub</a>. All the steps we covered in this article are divided as separate commits in the <code>main</code> branch for your reference.</em></p>

<h2 id="conclusion">Conclusion</h2>

<p>In this article, we have learned about the basics of server-side rendered apps and the steps to create and deploy a real-life app using SvelteKit as the framework. Feel free to share your comments and perspective on this topic, especially if you are considering picking SvelteKit for your next project.</p>

<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2020/07/differences-static-generated-sites-server-side-rendered-apps/?_ga=2.58526638.50654286.1684748099-1129770734.1684748099">Differences Between Static Generated Sites And Server-Side Rendered Apps</a>, Timi Omoyeni</li>
<li><a href="https://deploy-preview-9126--smashing-prod.netlify.app/2022/05/google-crux-analysis-comparison-performance-javascript-frameworks/">How To Use Google CrUX To Analyze And Compare The Performance Of JS Frameworks</a>, Dan Shappir</li>
<li><a href="https://deploy-preview-9126--smashing-prod.netlify.app/2022/07/look-remix-differences-next/">A Look At Remix And The Differences With Next.js</a>, Facundo Giuliani</li>
<li><a href="https://www.smashingmagazine.com/2016/08/getting-started-koa-2-async-functions/">Building A Server-Side Application With Async Functions and Koa 2</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Rachel How</author><title>How To Boost Your Design Workflow With Setapp</title><link>https://www.smashingmagazine.com/2023/05/boost-design-workflow-setapp/</link><pubDate>Thu, 11 May 2023 14:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/05/boost-design-workflow-setapp/</guid><description>Trying to keep up with everything but still, constantly feeling overwhelmed with never-ending to-do lists? Spend some time now exploring efficient tools to save time in the future and speed up your workflow. Focus on what you do best &amp;mdash; designing high-quality work, and let these tools handle the rest!</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/05/boost-design-workflow-setapp/" />
              <title>How To Boost Your Design Workflow With Setapp</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How To Boost Your Design Workflow With Setapp</h1>
                  
                    
                    <address>Rachel How</address>
                  
                  <time datetime="2023-05-11T14:00:00&#43;00:00" class="op-published">2023-05-11T14:00:00+00:00</time>
                  <time datetime="2023-05-11T14:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>Setapp</b></p>
                

<p>As someone who wears multiple hats, it is challenging to balance a full-time job, freelance projects, and all sorts of creative endeavors.</p>

<p>This is how I started off: By day, I’m a full-time product designer. By night, I juggle all sorts of freelance work and creative projects.</p>

<p>I am currently self-employed. However, there are challenges that come with being my own boss: Working with clients, sales and negotiation, invoicing, building a personal brand, crafting a content strategy, time tracking, project management… The list goes on.</p>

<p>Trying to keep up with everything used to be tough. No matter how hard I tried, my to-do list always seemed never-ending. I was constantly feeling overwhelmed.</p>

<p>I thought to myself, “There’s got to be a better way.”</p>

<p>After analyzing my workflow, I realized that many tasks could be simplified or automated so that I could save time, focus on high-value tasks, and work fewer hours.</p>

<p>After years of trial and error, I discovered a range of tools and strategies that helped me save time and stay organized to focus on what <em>really</em> matters.</p>

<p>The apps mentioned in this guide are available on <a href="https://setapp.com/?utm_source=smashing&amp;utm_medium=article">Setapp</a>. Whether you’re a Mac user or not, these hacks will help you get more done in less time and improve your quality of life. I hope you find value in this guide.</p>

<h2 id="streamline-your-workflow-with-the-best-apps">Streamline Your Workflow With the Best Apps</h2>

<p>You can use <strong>Setapp</strong> to access 240+ apps on your Mac and iPhone under a single monthly subscription.</p>

<p>Personally, I use Setapp to do <strong>three things</strong>:</p>

<ol>
<li>Try out apps that could help save time. Some of these apps cost more than Setapp’s subscription, so it’s a relief that I do not need to pay for each one individually.</li>
<li>For apps that I only need to use occasionally, I can quickly install and uninstall them as needed, with no extra cost. This saves me precious space on my Mac and ensures that I’m not cluttering up my system with unnecessary apps.</li>
<li>Since Setapp’s library is updated regularly, I always get to try out new apps to further enhance my workflow.</li>
</ol>

<h2 id="track-time-eliminate-distractions">Track Time &amp; Eliminate Distractions</h2>

<p>As a freelance designer, I need to track how much time I spend on each project to calculate my billable hours. I used to manually create events on my calendar and calculate the hours spent on each project. It’s a waste of time, and sadly, it is inaccurate.</p>

<p>To solve this problem, you can use <a href="https://setapp.com/apps/timemator">Timemator</a> to track your time accurately and minimize distractions.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/1-timemator-interface.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="369"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/1-timemator-interface.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/1-timemator-interface.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/1-timemator-interface.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/1-timemator-interface.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/1-timemator-interface.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/1-timemator-interface.jpg"
			
			sizes="100vw"
			alt="Timemator interface on different devices"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image credit: <a href='https://setapp.com/apps/timemator'>Timemator</a>. (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/1-timemator-interface.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>With Timemator, you can set up auto time-tracking rules for specific apps, files, or websites. For example, you can set rules so that the timer starts tracking when you work on a specific project on Figma or Adobe Photoshop.</p>

<p>The timer runs quietly in the background so that you can stay focused without any interruptions. You no longer need to manually start or pause the timer.</p>

<p><strong>Pro tip:</strong> <em>Use it to reduce distractions! Set up auto-tracking to track how much time you spend on meetings, talking to teammates or clients on Slack, or watching Netflix.</em></p>

<p>To help you identify where you’ve spent your time, Timemator gives detailed reports and analytics so you can reduce or eliminate time-wasting activities and get more done in less time.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/2-timemator-reports.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="489"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/2-timemator-reports.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/2-timemator-reports.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/2-timemator-reports.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/2-timemator-reports.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/2-timemator-reports.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/2-timemator-reports.png"
			
			sizes="100vw"
			alt="Timemator reports"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image credit: <a href='https://setapp.com/apps/timemator'>Timemator</a>. (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/2-timemator-reports.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="the-only-font-manager-you-need">The Only Font Manager You Need</h2>

<p>As designers, we all know that font selection can make or break a creative project.</p>

<p>I was frustrated with Font Book (the default font manager on MacOS). It wasn’t user-friendly. Searching and comparing fonts was a chore.</p>

<p>I found <a href="https://setapp.com/apps/typeface">Typeface</a> to be useful &mdash; especially when you need to quickly browse through your font collection, customize the preview text and size <strong>in real-time</strong>, and compare to see how different fonts look side-by-side.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/3-typeface-fonts.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/3-typeface-fonts.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/3-typeface-fonts.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/3-typeface-fonts.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/3-typeface-fonts.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/3-typeface-fonts.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/3-typeface-fonts.png"
			
			sizes="100vw"
			alt="Different fonts compared side-by-side"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/3-typeface-fonts.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Over the years, I have saved up a huge font library. Typeface is able to load all my fonts quickly and <strong>remove duplicate fonts</strong> that bloat up my computer. It supports variable fonts and OpenType font features and has robust features for the busy designer.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/4-typaface-remove-duplicate-fonts.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/4-typaface-remove-duplicate-fonts.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/4-typaface-remove-duplicate-fonts.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/4-typaface-remove-duplicate-fonts.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/4-typaface-remove-duplicate-fonts.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/4-typaface-remove-duplicate-fonts.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/4-typaface-remove-duplicate-fonts.png"
			
			sizes="100vw"
			alt="Typeface’s feature to remove duplicate fonts"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/4-typaface-remove-duplicate-fonts.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>For fonts you don’t use often, you can choose to <strong>activate them only when necessary</strong>. This way, your computer stays clean and fast.</p>

<p>As a bonus, you can also easily organize fonts into custom collections or tags.</p>

<h2 id="fastest-way-to-create-device-mockups">Fastest Way To Create Device Mockups</h2>

<p>When designing, we often need to create high-quality, professional-looking phone, tablet, and computer mockups to showcase our designs.</p>

<p>I used to spend hours searching for device mockup templates and launch Adobe Photoshop in order to use those templates. The whole process was time-consuming, so I switched to a tool called <a href="https://setapp.com/apps/mockuuups-studio">Mockuuups Studio</a>.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/5-mockuuups-studio.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/5-mockuuups-studio.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/5-mockuuups-studio.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/5-mockuuups-studio.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/5-mockuuups-studio.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/5-mockuuups-studio.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/5-mockuuups-studio.png"
			
			sizes="100vw"
			alt="Examples of different mockups generated by Mockuuups Studio"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/5-mockuuups-studio.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>All you need to do is drag and drop a screenshot of your website or app into it, pick a scene, and it will generate thousands of mockups. It’s pretty neat.</p>

<p>You can filter through scenes, models, and devices to find the perfect mockup for your digital product. Then, add hands, overlays, realistic shadows, or backgrounds to your device mockups. In the example above, I have filtered ‘iPhone’ mockups only.</p>

<p>Since it’s cloud-based, you can access it anywhere and collaborate with your teammates in real time too.</p>

<p>To further speed up your workflow, you can use their Figma, Sketch, or Adobe XD plugin. This is their Figma plugin:</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/6-mockuuups-studio-figma-plugin.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="602"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/6-mockuuups-studio-figma-plugin.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/6-mockuuups-studio-figma-plugin.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/6-mockuuups-studio-figma-plugin.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/6-mockuuups-studio-figma-plugin.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/6-mockuuups-studio-figma-plugin.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/6-mockuuups-studio-figma-plugin.png"
			
			sizes="100vw"
			alt="Mockuuups Studio’s Figma plugin"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/6-mockuuups-studio-figma-plugin.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="create-screenshots-screen-recordings-fast">Create Screenshots &amp; Screen Recordings, Fast</h2>

<p>When presenting designs (especially when working remotely), I take screenshots and screen recordings for my clients every day.</p>

<p>But instead of using the default Mac screenshot tool, <a href="https://setapp.com/apps/cleanshot">CleanShot X</a> is a better solution. This is an essential tool for every Mac user.</p>

<p>To quickly take a screenshot, use this shortcut key on your Mac: <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>4</kbd>.</p>

<p>This tool gives you the convenience to record MP4 or GIF with your desktop icons hidden, capture scrollable content, and annotate, highlight, or blur screenshots to hide sensitive personal information.</p>

<p>An example of how I annotate my screenshots:</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/7-cleanshotx.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/7-cleanshotx.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/7-cleanshotx.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/7-cleanshotx.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/7-cleanshotx.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/7-cleanshotx.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/7-cleanshotx.png"
			
			sizes="100vw"
			alt="An example of an annotated screenshot"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/7-cleanshotx.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I’ve used this tool for years with zero complaints. This tool will make your life easier when sharing screenshots with clients or on social media.</p>

<p>A cool feature you’ll also love: You can capture and copy any text, so you’ll never have to manually retype it again!</p>

<p>Your workflow will become much more streamlined and efficient since you no longer get bogged down in the technical details.</p>

<h2 id="never-waste-time-searching-for-meeting-links-again">Never Waste Time Searching For Meeting Links Again</h2>

<p>It’s challenging to keep track of various meetings, their details, and attendees, especially when switching between Google Meet, Zoom, your email inbox, and calendars.</p>

<p>To solve this problem, you can use <a href="https://setapp.com/apps/meeter">Meeter</a> to schedule or join meetings with one click right from the menu bar on your Mac.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/8-meeter.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="577"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/8-meeter.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/8-meeter.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/8-meeter.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/8-meeter.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/8-meeter.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/8-meeter.png"
			
			sizes="100vw"
			alt="Meeter interface with scheduled google meets"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/8-meeter.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>It supports Google Meet, Zoom, and Microsoft Teams. When you want to join a meeting, you no longer have to waste time searching for meeting links, then copy and paste the link into the browser. Instead, you can now focus on being present in every meeting.</p>

<p>The tool allows you to directly call your FaceTime contacts and phone numbers and jump into recurring calls from the menu bar too. Pretty simple!</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/9-meeter-calls.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="514"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/9-meeter-calls.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/9-meeter-calls.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/9-meeter-calls.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/9-meeter-calls.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/9-meeter-calls.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/9-meeter-calls.png"
			
			sizes="100vw"
			alt="Quick call list on Meeter"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/9-meeter-calls.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="save-time-with-spotlight-on-mac">Save Time With Spotlight On Mac</h2>

<p>When working with multiple files and apps on your Mac, you need to be able to quickly find and access them instead of navigating through different folders.</p>

<p>With <a href="https://support.apple.com/guide/mac-help/search-with-spotlight-mchlp1008/mac">Spotlight</a>, you can do these things quickly. While this is not an app, it’s one of the most powerful features on Mac that can save you plenty of time.</p>

<p>To open Spotlight, simply hit <kbd>Command</kbd> + <kbd>Spacebar</kbd> on your keyboard and start typing.</p>

<p>Then, try these on Spotlight:</p>

<ul>
<li><strong>Perform quick calculations.</strong><br />
No need to open a calculator app. Simply type in your calculation in Spotlight and hit enter. It’s that easy.</li>
<li><strong>Search for apps.</strong><br />
Quickly find any app on your Mac.</li>
<li><strong>Search the internet.</strong><br />
Type your search term, and it will launch your default browser with the search results. You’ve just saved a few clicks.</li>
<li><strong>Find files or folders.</strong><br />
Type in the name of the file or folder, and you have it.</li>
</ul>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/10-spotlight-find-files-folders.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/10-spotlight-find-files-folders.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/10-spotlight-find-files-folders.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/10-spotlight-find-files-folders.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/10-spotlight-find-files-folders.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/10-spotlight-find-files-folders.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/10-spotlight-find-files-folders.png"
			
			sizes="100vw"
			alt="Find files or folders feature in Spotlight"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/10-spotlight-find-files-folders.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ul>
<li><strong>Check the weather.</strong><br />
Type “weather” followed by your location, and it will give you up-to-date information on the current weather conditions and forecast.</li>
</ul>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/11-spotlight-weather.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/11-spotlight-weather.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/11-spotlight-weather.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/11-spotlight-weather.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/11-spotlight-weather.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/11-spotlight-weather.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/11-spotlight-weather.png"
			
			sizes="100vw"
			alt="Check the weather feature in Spotlight"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/11-spotlight-weather.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Cool, right? Learning how to use Spotlight effectively is a game-changer. Give it a try, and see how much time you can save.</p>

<h2 id="design-accessible-interfaces">Design Accessible Interfaces</h2>

<p>As a product designer who also builds websites for clients, it’s a challenge to find and create the perfect color palettes while working on multiple projects at once. In the past, I&rsquo;ve had to rely on a combination of tools like swatch libraries and notes to keep track of my palettes.</p>

<p>If you’re a designer or a developer, you’ll love <a href="https://setapp.com/apps/sip">Sip</a> &mdash; a powerful color picker that can help you design beautiful and accessible interfaces easily.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/12-setapp-sip-color-picker.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="385"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/12-setapp-sip-color-picker.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/12-setapp-sip-color-picker.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/12-setapp-sip-color-picker.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/12-setapp-sip-color-picker.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/12-setapp-sip-color-picker.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/12-setapp-sip-color-picker.png"
			
			sizes="100vw"
			alt="A color picker"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/12-setapp-sip-color-picker.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>With Sip, you can quickly grab colors right from the Mac menu bar and drop them into any design or development tool, including Adobe Photoshop, Figma, and Sketch. This makes it easy to create custom color palettes that match the client’s brand.</p>

<p>You can create and save custom color palettes, and the quick access menu that floats on the side of your desktop gives you quick access to your color palettes.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/13-setapp-sip-color-palette.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="460"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/13-setapp-sip-color-palette.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/13-setapp-sip-color-palette.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/13-setapp-sip-color-palette.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/13-setapp-sip-color-palette.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/13-setapp-sip-color-palette.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/13-setapp-sip-color-palette.png"
			
			sizes="100vw"
			alt="A custom color palette"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/13-setapp-sip-color-palette.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Currently, it supports 24 of the most popular color formats in the industry, like Android XML, CSS hex, RGB, and CMYK.</p>

<p>Now, my favorite feature is Sip’s Contrast Checker. In the example below, you can use the color picker to check the contrast between the gray text and white background, ensuring that it meets accessibility standards and is legible for all users.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/14-setapp-sip-contrast-checker-tool.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/14-setapp-sip-contrast-checker-tool.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/14-setapp-sip-contrast-checker-tool.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/14-setapp-sip-contrast-checker-tool.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/14-setapp-sip-contrast-checker-tool.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/14-setapp-sip-contrast-checker-tool.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/14-setapp-sip-contrast-checker-tool.png"
			
			sizes="100vw"
			alt="Sip’s Contrast Checker"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/14-setapp-sip-contrast-checker-tool.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Tip</strong>: <em>Always make sure the contrast between the text and background is greater than or equal to <a href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html">4.5:1 for small text and 3:1 for large text</a>. If the color contrast fails, click on the ‘FIX’ button to improve it!</em></p>

<h2 id="declutter-your-mac-s-menu-bar">Declutter Your Mac’s Menu Bar</h2>

<p>If you have a bunch of apps running on your Mac, your menu bar may be cluttered with all sorts of icons and notifications.</p>

<p>Just like physical clutter, digital clutter takes up mental space and affects your focus, too! To solve this problem, you can use <a href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html">Bartender</a>.</p>

<p>Bartender allows you to organize your menu bar icons into neat and tidy groups or hide them completely &mdash; as simple as that. You can collapse your menu bar icons into a customizable dropdown menu so it remains clutter-free.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/15-setapp-bartender.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="250"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/15-setapp-bartender.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/15-setapp-bartender.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/15-setapp-bartender.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/15-setapp-bartender.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/15-setapp-bartender.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/15-setapp-bartender.png"
			
			sizes="100vw"
			alt="An example of a customizable dropdown menu to hide bar icons"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/boost-design-workflow-time-saving-tools-setapp/15-setapp-bartender.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In the above example, most of my menu icons are hidden, except Figma and the battery level indicator.</p>

<p>After using it for over a month, I am able to focus better. It’s one of those subtle quality-of-life improvements that can have a big impact on your productivity and mindset.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>I wish I had discovered these tools sooner!</p>

<p>The apps I’ve shared above are available on <strong>Setapp</strong>. With a single monthly subscription, you get access to 240+ Mac and iPhone apps. They offer a <a href="https://setapp.com/?utm_source=smashing&amp;utm_medium=article"><strong>free 7-day trial</strong></a>, so you can try it out and decide if it’s right for you.</p>

<p>These tools have completely transformed my workflow and helped me become more productive and less stressed. I hope that these tools will do the same for you so you can make the most of your time. After all, time is a limited resource, and it&rsquo;s up to us to use it wisely.</p>

<p>Thank you for reading. Have a productive day!</p>

<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2023/01/top-frontend-tools-2022/">Top Front-End Tools Of 2022</a>, Louis Lazaris</li>
<li><a href="https://www.smashingmagazine.com/2022/12/optimizing-design-workflow-tools/">Boosting Productivity For Designers With Efficient Tools</a>, Ashish Bogawat</li>
<li><a href="https://www.smashingmagazine.com/2022/07/overcoming-imposter-syndrome-developing-guiding-principles/">Overcoming Imposter Syndrome By Developing Your Own Guiding Principles</a>, Luis Ouriach</li>
<li><a href="https://www.smashingmagazine.com/2022/04/productivity-tips-tools/">Productivity Tips And Tools For A More Efficient Workflow</a>, Cosima Mielke</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Jessica Joseph</author><title>The Safest Way To Hide Your API Keys When Using React</title><link>https://www.smashingmagazine.com/2023/05/safest-way-hide-api-keys-react/</link><pubDate>Mon, 08 May 2023 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/05/safest-way-hide-api-keys-react/</guid><description>Want to make sure your API keys are safe and sound when working with React? Jessica Joseph’s got you covered! She will show you the best ways to hide your API keys, from using environment variables to building your own back-end proxy server.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/05/safest-way-hide-api-keys-react/" />
              <title>The Safest Way To Hide Your API Keys When Using React</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>The Safest Way To Hide Your API Keys When Using React</h1>
                  
                    
                    <address>Jessica Joseph</address>
                  
                  <time datetime="2023-05-08T13:00:00&#43;00:00" class="op-published">2023-05-08T13:00:00+00:00</time>
                  <time datetime="2023-05-08T13:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Back in the day, developers had to write all sorts of custom code to get different applications to communicate with each other. But, these days, <a href="https://developer.mozilla.org/en-US/docs/Glossary/API">Application Programming Interfaces</a> (APIs) make it so much easier. APIs provide you with everything you need to interact with different applications smoothly and efficiently, most commonly where one application requests data from the other application.</p>

<p>While APIs offer numerous benefits, they also present a significant risk to your application security. That is why it is essential to learn about their vulnerabilities and how to protect them. In this article, we’ll delve into the wonderful world of API keys, discuss why you should protect your API keys, and look at the best ways to do so when using React.</p>

<h2 id="what-are-api-keys">What Are API Keys?</h2>

<p>If you recently signed up for an API, you will get an API key. Think of API keys as secret passwords that prove to the provider that it is you or your app that’s attempting to access the API. While some APIs are free, others charge a cost for access, and because most API keys have zero expiration date, it is frightening not to be concerned about the safety of your keys.</p>

<h2 id="why-do-api-keys-need-to-be-protected">Why Do API Keys Need To Be Protected?</h2>

<p>Protecting your API keys is crucial for guaranteeing the security and integrity of your application. Here are some reasons why you ought to guard your API keys:</p>

<ul>
<li><strong>To prevent unauthorized API requests.</strong><br />
If someone obtains your API key, they can use it to make unauthorized requests, which could have serious ramifications, especially if your API contains sensitive data.</li>
<li><strong>Financial insecurity.</strong><br />
Some APIs come with a financial cost. And if someone gains access to your API key and exceeds your budget requests, you may be stuck with a hefty bill which could cost you a ton and jeopardize your financial stability.</li>
<li><strong>Data theft, manipulation, or deletion.</strong><br />
If a malicious person obtains access to your API key, they may steal, manipulate, delete, or use your data for their purposes.</li>
</ul>

<h2 id="best-practices-for-hiding-api-keys-in-a-react-application">Best Practices For Hiding API Keys In A React Application</h2>

<p>Now that you understand why API keys must be protected, let’s take a look at some methods for hiding API keys and how to integrate them into your React application.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h3 id="environment-variables">Environment Variables</h3>

<p><a href="https://en.wikipedia.org/wiki/Environment_variable">Environment variables</a> (<code>env</code>) are used to store information about the environment in which a program is running. It enables you to hide sensitive data from your application code, such as API keys, tokens, passwords, and just any other data you’d like to keep hidden from the public.</p>

<p>One of the most popular <code>env</code> packages you can use in your React application to hide sensitive data is the <a href="https://github.com/motdotla/dotenv"><code>dotenv</code></a> package. To get started:</p>

<ol>
  <li>Navigate to your react application directory and run the command below.<br />
<pre><code class="language-bash">npm install dotenv --save
</code></pre>
  </li>
  <li>Outside of the <code>src</code> folder in your project root directory, create a new file called <code>.env</code>.<br /><br />













<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/safest-way-hide-api-keys-react/1-env-file-project-root-directory.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="378"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/safest-way-hide-api-keys-react/1-env-file-project-root-directory.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/safest-way-hide-api-keys-react/1-env-file-project-root-directory.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/safest-way-hide-api-keys-react/1-env-file-project-root-directory.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/safest-way-hide-api-keys-react/1-env-file-project-root-directory.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/safest-way-hide-api-keys-react/1-env-file-project-root-directory.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/safest-way-hide-api-keys-react/1-env-file-project-root-directory.png"
			
			sizes="100vw"
			alt="A screenshot with a highlighted env file in the project root directory"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/safest-way-hide-api-keys-react/1-env-file-project-root-directory.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
  <li>In your <code>.env</code> file, add the API key and its corresponding value in the following format:<br />
<pre><code class="language-bash">// for CRA applications
REACT&#95;APP&#95;API&#95;KEY = A1234567890B0987654321C ------ correct

// for Vite applications
VITE&#95;SOME&#95;KEY = 12345GATGAT34562CDRSCEEG3T  ------ correct
</code></pre>
</li>
  <li>Save the <code>.env</code> file and avoid sharing it publicly or committing it to version control.</li>
  <li>You can now use the <code>env</code> object to access your environment variables in your React application.<br />
<pre><code class="language-bash">// for CRA applications
'X-RapidAPI-Key':process.env.REACT&#95;APP&#95;API&#95;KEY
// for Vite  applications
'X-RapidAPI-Key':import.meta.env.VITE&#95;SOME&#95;KEY
</code></pre>
</li>
  <li>Restart your application for the changes to take effect.</li>
</ol>

<p>However, running your project on your local computer is only the beginning. At some point, you may need to upload your code to GitHub, which could potentially expose your <code>.env</code> file. So what to do then? You can consider using the <code>.gitignore</code> file to hide it.</p>

<h3 id="the-gitignore-file">The <code>.gitignore</code> File</h3>

<p>The <code>.gitignore</code> file is a text file that instructs Git to ignore files that have not yet been added to the repository when it’s pushed to the repo. To do this, add the <code>.env</code> to the <code>.gitignore</code> file before moving forward to staging your commits and pushing your code to GitHub.</p>

<pre><code class="language-bash">// .gitignore
# dependencies
/node_modules
/.pnp
.pnp.js

# api keys
.env
</code></pre>

<p>Keep in mind that at any time you decide to host your projects using any hosting platforms, like <a href="https://vercel.com/">Vercel</a> or <a href="https://www.netlify.com/">Netlify</a>, you are to provide your environment variables in your project settings and, soon after, redeploy your app to view the changes.</p>

<div class="partners__lead-place"></div>

<h3 id="back-end-proxy-server">Back-end Proxy Server</h3>

<p>While environment variables can be an excellent way to protect your API keys, remember that they can still be compromised. Your keys can still be stolen if an attacker inspects your bundled code in the browser. So, what then can you do? Use a back-end proxy server.</p>

<p>A back-end proxy server acts as an intermediary between your client application and your server application. Instead of directly accessing the API from the front end, the front end sends a request to the back-end proxy server; the proxy server then retrieves the API key and makes the request to the API. Once the response is received, it removes the API key before returning the response to the front end. This way, your API key will never appear in your front-end code, and no one will be able to steal your API key by inspecting your code. Great! Now let’s take a look at how we can go about this:</p>

<ol>
<li><strong>Install necessary packages.</strong><br />To get started, you need to install some packages such as <a href="http://expressjs.com/">Express</a>, <a href="https://github.com/expressjs/cors">CORS</a>, <a href="https://axios-http.com/">Axios</a>, and <a href="https://nodemon.io/">Nodemon</a>. To do this, navigate to the directory containing your React project and execute the following command:<br />
<pre><code class="language-bash">npm install express cors axios nodemon
</code></pre>
</li>
<li><strong>Create a back-end server file.</strong><br />In your project root directory, outside your <code>src</code> folder, create a JavaScript file that will contain all of your requests to the API.<br /><br />













<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/safest-way-hide-api-keys-react/2-javascript-file-project-root-directory.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="365"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/safest-way-hide-api-keys-react/2-javascript-file-project-root-directory.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/safest-way-hide-api-keys-react/2-javascript-file-project-root-directory.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/safest-way-hide-api-keys-react/2-javascript-file-project-root-directory.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/safest-way-hide-api-keys-react/2-javascript-file-project-root-directory.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/safest-way-hide-api-keys-react/2-javascript-file-project-root-directory.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/safest-way-hide-api-keys-react/2-javascript-file-project-root-directory.png"
			
			sizes="100vw"
			alt="A screenshot with a highlighted server.js file in the project root directory"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/safest-way-hide-api-keys-react/2-javascript-file-project-root-directory.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
<li><strong>Initialize dependencies and set up an endpoint.</strong><br />In your backend server file, initialize the installed dependencies and set up an endpoint that will make a <code>GET</code> request to the third-party API and return the response data on the listened port. Here is an example code snippet:<br />
<pre><code class="language-javascript">// defining the server port
const port = 5000

// initializing installed dependencies
const express = require('express')
require('dotenv').config()
const axios = require('axios')
const app = express()
const cors = require('cors')
app.use(cors())

// listening for port 5000
app.listen(5000, ()=&gt; console.log(`Server is running on ${port}` ))

// API request
app.get('/', (req,res)=&gt;{    
    const options = {
        method: 'GET',
        url: 'https://wft-geo-db.p.rapidapi.com/v1/geo/adminDivisions',
        headers: {
            'X-RapidAPI-Key':process.env.REACT&#95;APP&#95;API&#95;KEY,
            'X-RapidAPI-Host': 'wft-geo-db.p.rapidapi.com'
        }
   };
   
    axios.request(options).then(function (response) {
        res.json(response.data);
    }).catch(function (error) {
        console.error(error);
    });
}
</code></pre>
</li>
<li>Add a script tag in your <code>package.json</code> file that will run the back-end proxy server.<br /><br />













<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/safest-way-hide-api-keys-react/3-script-tag-package-json-file.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="229"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/safest-way-hide-api-keys-react/3-script-tag-package-json-file.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/safest-way-hide-api-keys-react/3-script-tag-package-json-file.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/safest-way-hide-api-keys-react/3-script-tag-package-json-file.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/safest-way-hide-api-keys-react/3-script-tag-package-json-file.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/safest-way-hide-api-keys-react/3-script-tag-package-json-file.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/safest-way-hide-api-keys-react/3-script-tag-package-json-file.png"
			
			sizes="100vw"
			alt="A screenshot with a script tag in a package.json file"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/safest-way-hide-api-keys-react/3-script-tag-package-json-file.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
<li>Kickstart the back-end server by running the command below and then, in this case, navigate to <code>localhost:5000</code>.<br />
<pre><code class="language-bash">npm run start:backend
</code></pre>
</li>
<li>Make a request to the backend server (<code>http://localhost:5000/</code>) from the front end instead of directly to the API endpoint. Here’s an illustration:<br />
<pre><code class="language-javascript">import axios from "axios";
import {useState, useEffect} from "react"

function App() {

  const [data, setData] = useState(null)

  useEffect(()=&gt;{
    const options = {
      method: 'GET',
      url: "http://localhost:5000",
    }
    axios.request(options)
    .then(function (response) {
        setData(response.data.data)
    })
    .catch(function (error) {
        console.error(error);
    })  
  }, [])

  console.log(data)

  return (
    &lt;main className="App"&gt;
    &lt;h1&gt;How to Create a Backend Proxy Server for Your API Keys&lt;/h1&gt;
     {data && data.map((result)=&gt;(
      &lt;section key ={result.id}&gt;
        &lt;h4&gt;Name:{result.name}&lt;/h4&gt;
        &lt;p&gt;Population:{result.population}&lt;/p&gt;
        &lt;p&gt;Region:{result.region}&lt;/p&gt;
        &lt;p&gt;Latitude:{result.latitude}&lt;/p&gt;
        &lt;p&gt;Longitude:{result.longitude}&lt;/p&gt;
      &lt;/section>
    ))}
    &lt;/main&gt;
  )
}
export default App;
</code></pre>
</li>
</ol>

<p>Okay, there you have it! By following these steps, you&rsquo;ll be able to hide your API keys using a back-end proxy server in your React application.</p>

<h3 id="key-management-service">Key Management Service</h3>

<p>Even though environment variables and the back-end proxy server allow you to safely hide your API keys online, you are still not completely safe. You may have friends or foes around you who can access your computer and steal your API key. That is why data encryption is essential.</p>

<p>With a key management service provider, you can encrypt, use, and manage your API keys. There are tons of key management services that you can integrate into your React application, but to keep things simple, I will only mention a few:</p>

<ul>
<li><strong>AWS Secrets Manager</strong><br />
The <a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html">AWS Secrets Manager</a> is a secret management service provided by Amazon Web Services. It enables you to store and retrieve secrets such as database credentials, API keys, and other sensitive information programmatically via API calls to the AWS Secret Manager service. There are a ton of resources that can get you started in no time.</li>
<li><strong>Google Cloud Secret Manager</strong><br />
The <a href="https://cloud.google.com/secret-manager">Google Cloud Secret Manager</a> is a key management service provided and fully managed by the Google Cloud Platform. It is capable of storing, managing, and accessing sensitive data such as API keys, passwords, and certificates. The best part is that it seamlessly integrates with Google’s back-end-as-a-service features, making it an excellent choice for any developer looking for an easy solution.</li>
<li><strong>Azure Key Vault</strong><br />
The <a href="https://azure.microsoft.com/en-us/products/key-vault/">Azure Key Vault</a> is a cloud-based service provided by Microsoft Azure that allows you to seamlessly store and manage a variety of secrets, including passwords, API keys, database connection strings, and other sensitive data that you don’t want to expose directly in your application code.</li>
</ul>

<p>There are more key management services available, and you can choose to go with any of the ones mentioned above. But if you want to go with a service that wasn’t mentioned, that’s perfectly fine as well.</p>

<div class="partners__lead-place"></div>

<h2 id="tips-for-ensuring-security-for-your-api-keys">Tips For Ensuring Security For Your API Keys</h2>

<p>You have everything you need to keep your API keys and data secure. So, if you have existing projects in which you have accidentally exposed your API keys, don’t worry; I&rsquo;ve put together some handy tips to help you identify and fix flaws in your React application codebase:</p>

<ol>
<li>Review your existing codebase and identify any hardcoded API key that needs to be hidden.</li>
<li>Use environment variables with <code>.gitignore</code> to securely store your API keys. This will help to prevent accidental exposure of your keys and enable easier management across different environments.</li>
<li>To add an extra layer of security, consider using a back-end proxy server to protect your API keys, and, for advanced security needs, a key management tool would do the job.</li>
</ol>

<h2 id="conclusion">Conclusion</h2>

<p>Awesome! You can now protect your API keys in React like a pro and be confident that your application data is safe and secure. Whether you use environment variables, a back-end proxy server, or a key management tool, they will keep your API keys safe from prying eyes.</p>

<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>

<ul>
<li>“<a href="https://www.smashingmagazine.com/2021/12/protect-api-key-production-nextjs-api-route/">How To Protect Your API Key In Production With Next.js API Route</a>”, Caleb Olojo</li>
<li>“<a href="https://www.smashingmagazine.com/2021/10/react-apis-building-flexible-components-typescript/">Useful React APIs For Building Flexible Components With TypeScript</a>”, Gaurav Khanna</li>
<li>“<a href="https://www.smashingmagazine.com/2021/08/state-management-nextjs/">State Management In Next.js</a>”, Atila Fassina</li>
<li>“<a href="https://www.smashingmagazine.com/2023/03/internationalization-nextjs-13-react-server-components/">Internationalization In Next.js 13 With React Server Components</a>”, Jan Amann</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Iliya Garakh</author><title>The Growing Need For Effective Password Management</title><link>https://www.smashingmagazine.com/2023/05/effective-password-management/</link><pubDate>Thu, 04 May 2023 12:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/05/effective-password-management/</guid><description>Password management is a critical aspect of any company&amp;rsquo;s security infrastructure. With an increasing number of platforms, applications, and services requiring authentication, managing passwords and access rights has become a challenge for system administrators. Highlighting the advantages of self-hosted solutions over cloud-based alternatives and exploring collaborative password management and more, Pauline demonstrates how to organize convenient and secure password management in a company.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/05/effective-password-management/" />
              <title>The Growing Need For Effective Password Management</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>The Growing Need For Effective Password Management</h1>
                  
                    
                    <address>Iliya Garakh</address>
                  
                  <time datetime="2023-05-04T12:00:00&#43;00:00" class="op-published">2023-05-04T12:00:00+00:00</time>
                  <time datetime="2023-05-04T12:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>Passwork</b></p>
                

<p>As businesses rely more on digital services and platforms, the number of passwords and access credentials employees need to remember has grown exponentially. This can lead to the use of weak or duplicated passwords, posing a significant security risk. A centralized and secure password management system is essential for mitigating these risks and ensuring that sensitive information remains protected.</p>

<h2 id="self-hosted-vs-cloud-based-password-management-solutions">Self-Hosted vs. Cloud-Based Password Management Solutions</h2>

<p>When it comes to password management solutions, businesses have two primary options: <strong>self-hosted</strong> and <strong>cloud-based</strong>. While both have their merits, self-hosted solutions often provide a <strong>higher level of control and customization</strong>.</p>

<h3 id="advantages-of-self-hosted-solutions">Advantages Of Self-Hosted Solutions</h3>

<ul>
<li><strong>Greater control</strong><br />
A self-hosted solution allows administrators to have complete control over the password management infrastructure, enabling them to customize it according to their company’s needs;</li>
<li><strong>Enhanced security</strong><br />
By hosting the password management system on the company’s own servers, businesses can ensure that their sensitive data remains within their control, reducing the risks associated with third-party providers;</li>
<li><strong>Compliance</strong><br />
Self-hosted solutions make it easier for companies to meet industry-specific compliance requirements and data protection regulations.</li>
</ul>

<h3 id="limitations-of-cloud-based-solutions">Limitations Of Cloud-Based Solutions</h3>

<ul>
<li><strong>Dependency on third-party providers</strong><br />
With cloud-based solutions, businesses rely on external providers for the security and availability of their data. This can lead to potential vulnerabilities and the risk of data breaches;</li>
<li><strong>Limited customisation</strong><br />
Cloud-based solutions often have predefined features and settings, which may not align with a company’s unique requirements.</li>
</ul>

<h2 id="collaborative-password-management-in-companies">Collaborative Password Management In Companies</h2>

<p>In a company setting, employees often need to share passwords and access credentials for various applications and services. A collaborative password management system enables the secure sharing of these credentials, improving productivity and security.</p>

<h3 id="secure-sharing">Secure Sharing</h3>

<p>Collaborative password management systems, like <a href="https://passwork.pro/?utm_source=smashingmag&utm_medium=article&utm_campaign=pm">Passwork</a>, provide secure sharing options, allowing employees to share access credentials with colleagues without exposing sensitive data to unauthorized users. This is the kind of feature that a company needs for frictionless sharing in a collaborative environment, but without exposing sensitive information as you might through another platform, like email. This way, sharing happens securely through the password app’s service.</p>

<h3 id="permission-management">Permission Management</h3>

<p>To maintain control over who can access and modify shared passwords, a collaborative password management system should offer <strong>granular permission management</strong>. Administrators can assign different levels of access to individual users or groups, ensuring that employees have access to the information they need without compromising security.</p>

<p>Another benefit of permission management is that it provides you with an easy path to knowing <em>who</em> has access to certain information, as well as an easy way to assign and revoke permissions on an individual and group level.</p>

<h3 id="version-control">Version Control</h3>

<p>Have you ever created a new password for a service, then needed to reference the past password? There’s nothing worse than losing a password when you need it in a pinch, and in an environment where multiple users can update and modify shared passwords, version control becomes essential. Collaborative password management systems should provide a history of changes made to shared credentials, enabling administrators to track modifications and revert to previous versions if needed.</p>

<h2 id="access-rights-segregation">Access Rights Segregation</h2>

<p>To ensure that sensitive data remains protected, companies should implement access rights segregation within their password management system. This involves dividing users into different groups based on their roles and responsibilities and assigning appropriate access permissions accordingly.</p>

<h4 id="role-based-access-control-rbac">Role-Based Access Control (RBAC)</h4>

<p>RBAC is a widely used method for implementing access rights segregation. With RBAC, administrators can create roles that represent different job functions within the company and assign appropriate permissions to each role. Users are then assigned to roles, ensuring that they only have access to the information they need to perform their tasks.</p>

<h4 id="attribute-based-access-control-abac">Attribute-Based Access Control (ABAC)</h4>

<p>ABAC is a more flexible approach to access control, where permissions are granted based on a user’s attributes (e.g., job title, department, location, and so on) rather than predefined roles. This allows for greater customization and scalability, as administrators can create complex access rules that adapt to changing business requirements.</p>

<h2 id="auditing-and-monitoring-activity">Auditing And Monitoring Activity</h2>

<p>To maintain a secure password management system, administrators must be able to monitor and audit user activity. This sort of transparency allows you to know exactly who changed something at a particular point in time so you can take corrective action. This includes tracking changes to passwords, monitoring access attempts, and identifying potential security threats.</p>

<h3 id="activity-logging">Activity Logging</h3>

<p>A comprehensive password management system should log all user activity, including access attempts, password modifications, and sharing events. This information can be invaluable for detecting unauthorized access, troubleshooting issues, and conducting security audits.</p>

<p>For example, it’s nice to have a way to see who has used a particular password and when they used it, especially for troubleshooting permissions.</p>

<h3 id="real-time-notifications">Real-Time Notifications</h3>

<p>In addition to logging activity, real-time alerts can help administrators quickly identify and respond to potential security threats. A password management system that provides real-time notifications for suspicious activity, such as multiple failed login attempts or unauthorized password changes, can be instrumental in preventing data breaches.</p>

<h3 id="reporting">Reporting</h3>

<p>Generating reports on user activity, password strength, and compliance can help administrators assess the overall health of their password management system and identify areas for improvement. Regularly reviewing these reports can also ensure that the company remains compliant with relevant industry regulations and best practices.</p>

<h2 id="best-practices-for-implementing-a-password-management-system">Best Practices For Implementing A Password Management System</h2>

<p>To ensure the success of a password management system, it’s crucial to follow best practices for implementation and ongoing maintenance. You want to ensure that your passwords are managed in a way that is safe for everyone in your company while adhering to compliance guidelines for a secure environment.</p>

<h3 id="first-choose-the-right-solution">First, Choose The Right Solution</h3>

<p>Selecting the right password management system for your company is essential. Consider factors such as the size of your organization, the level of customization required, and your preferred hosting option (self-hosted vs. cloud-based) when evaluating solutions. <a href="https://passwork.pro/?utm_source=smashingmag&utm_medium=article&utm_campaign=pm">Passwork</a>, for example, offers a self-hosted solution with robust collaboration features, making it a suitable option for businesses looking for greater control and customization.</p>

<h3 id="next-train-employees">Next, Train Employees</h3>

<p>Employee training is crucial for the successful adoption of a password management system. Ensure that all users understand how to use the system, the importance of password security, and company policies related to password management.</p>

<h3 id="regularly-review-and-update-policies">Regularly Review And Update Policies</h3>

<p>As your business evolves, your password management policies should adapt accordingly. Regularly review and update your policies to ensure that they continue to meet your organization’s needs and maintain compliance with industry regulations.</p>

<h3 id="monitor-and-audit-system-activity">Monitor And Audit System Activity</h3>

<p>Stay vigilant by regularly monitoring and auditing your password management system. This will help you identify potential security threats and ensure that your system remains secure and up-to-date.</p>

<h2 id="password-policy-best-practices">Password Policy Best Practices</h2>

<p>To maintain a secure password management system, it’s essential to establish strong password policies and ensure that employees follow best practices.</p>

<h3 id="password-length-and-complexity">Password Length And Complexity</h3>

<p>A strong password policy should require a minimum password length and a combination of characters, including upper and lower case letters, numbers, and special characters. This helps to increase <strong>password entropy</strong>, making it more difficult for attackers to guess or crack passwords using brute force methods.</p>

<h3 id="password-expiration-and-rotation">Password Expiration And Rotation</h3>

<p>Regularly changing passwords can help to minimise the risk of unauthorized access, especially in cases where passwords have been compromised without the knowledge of the organization. Implementing a password expiration policy that requires users to change their passwords at regular intervals can enhance security.</p>

<h3 id="two-factor-authentication-2fa">Two-Factor Authentication (2FA)</h3>

<p>In addition to strong password policies, implementing two-factor authentication can provide an additional layer of security. By requiring users to provide a second form of verification, such as a code sent to a mobile device, 2FA reduces the risk of unauthorized access even if a password is compromised.</p>

<h3 id="prevent-reused-passwords">Prevent Reused Passwords</h3>

<p>Employees should be discouraged from using the same password across multiple accounts and services. Encourage the use of unique passwords for each account to minimise the risk of unauthorized access in case one password is compromised.</p>

<h2 id="integrations-and-compatibility">Integrations And Compatibility</h2>

<p>An effective password management system should be compatible with various platforms, applications, and services that your company uses. This ensures seamless integration and streamlined access management.</p>

<h4 id="single-sign-on-sso">Single Sign-On (SSO)</h4>

<p>SSO enables users to access multiple applications and services with a single set of credentials.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aBy%20integrating%20your%20password%20management%20system%20with%20SSO,%20you%20can%20simplify%20the%20login%20process%20for%20employees,%20reducing%20the%20need%20for%20multiple%20passwords%20and%20improving%20security.%0a&url=https://smashingmagazine.com%2f2023%2f05%2feffective-password-management%2f">
      
By integrating your password management system with SSO, you can simplify the login process for employees, reducing the need for multiple passwords and improving security.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<h3 id="browser-extensions-and-mobile-apps">Browser Extensions and Mobile Apps</h3>

<p>A password management system that offers browser extensions and mobile apps can help ensure that employees have access to their passwords and credentials wherever they are. This enhances productivity and encourages the adoption of the password management system.</p>

<h3 id="custom-integrations">Custom Integrations</h3>

<p>Depending on your company’s requirements, you may need to integrate your password management system with other tools, such as ticketing systems, customer relationship management platforms, or identity and access management solutions. Ensure that the password management system you choose is flexible and allows for custom integrations. Make sure that the password management system you decide to use has the flexibility to connect with the other services you rely on for your business.</p>

<h2 id="backup-and-disaster-recovery">Backup And Disaster Recovery</h2>

<p>A robust password management system should include backup and disaster recovery features to ensure the availability and integrity of your organization’s passwords and credentials.</p>

<h3 id="regular-backups">Regular Backups</h3>

<p>Implement a backup policy that includes regular backups of your password management system’s data. This helps to protect against data loss due to hardware failures, accidental deletions, or other unforeseen issues.</p>

<h3 id="encrypted-backups">Encrypted Backups</h3>

<p>Backups should be encrypted to protect the sensitive data they contain. Ensure that your password management system supports encrypted backups and uses strong encryption algorithms to safeguard your data.</p>

<h3 id="disaster-recovery-plan">Disaster Recovery Plan</h3>

<p>Develop a disaster recovery plan that outlines the steps to be taken in case of a system failure, data breach, or other security incidents. This plan should include procedures for restoring data from backups, as well as measures to prevent further damage or unauthorized access.</p>

<h2 id="evaluating-and-selecting-a-password-management-solution">Evaluating And Selecting A Password Management Solution</h2>

<p>When choosing a password management system, it’s important to thoroughly evaluate potential solutions and select the one that best meets your organization’s needs.</p>

<h3 id="security-features">Security Features</h3>

<p>Assess the security features offered by each solution, such as encryption algorithms, two-factor authentication support, and activity monitoring capabilities. Ensure that the solution adheres to industry standards and best practices for data security.</p>

<h3 id="scalability">Scalability</h3>

<p>Consider the scalability of the password management system, especially if your organization is growing or has plans for expansion. The solution should be able to handle an increasing number of users and passwords without compromising performance or security.</p>

<h3 id="ease-of-use">Ease of Use</h3>

<p>User adoption is crucial for the success of a password management system. Evaluate the user interface and overall ease of use of each solution, as this can have a significant impact on employee adoption and satisfaction.</p>

<h3 id="cost">Cost</h3>

<p>Consider the total cost of ownership for each password management system, including initial implementation costs, ongoing maintenance, and any additional fees for upgrades or add-on features. Be sure to weigh these costs against the potential benefits and savings offered by a more secure and efficient password management process.</p>

<h2 id="ongoing-maintenance-and-support">Ongoing Maintenance And Support</h2>

<p>Once your password management system is in place, it’s essential to keep it up-to-date and ensure that users receive the necessary support.</p>

<h3 id="software-updates">Software Updates</h3>

<p>Regularly update your password management system to benefit from the latest security patches, feature enhancements, and bug fixes. This helps to maintain the stability and security of the system.</p>

<h3 id="user-support">User Support</h3>

<p>Provide user support for your password management system, including training materials, FAQs, and access to technical assistance when needed. This ensures that employees can effectively use the system and resolve any issues that may arise.</p>

<h3 id="periodic-security-assessments">Periodic Security Assessments</h3>

<p>Conduct periodic security assessments of your password management system to identify any potential vulnerabilities and ensure that it continues to meet your organization’s security requirements. This may include penetration testing, vulnerability scanning, and other security assessments.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Organizing password management in a company is a critical task for system administrators. By selecting the right solution, implementing access rights segregation, fostering collaboration, and actively monitoring and auditing the system, administrators can create a secure and efficient password management environment. Additionally, establishing strong password policies, ensuring like <a href="https://passwork.pro/?utm_source=smashingmag&utm_medium=article&utm_campaign=pm">Passwork</a>, can offer businesses greater control and customization, providing a solid foundation for effective password management.</p>

<p>By following the best practices outlined in this guide, system administrators can enhance their organization’s overall security posture while improving productivity and streamlining access management.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Sarah Oke Okolo</author><title>Exploring The Potential Of Web Workers For Multithreading On The Web</title><link>https://www.smashingmagazine.com/2023/04/potential-web-workers-multithreading-web/</link><pubDate>Fri, 21 Apr 2023 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2023/04/potential-web-workers-multithreading-web/</guid><description>Multithreading is an important technique used in modern software development to enhance the performance and responsiveness of applications. However, it’s not a common practice on the web due to the single-threaded nature of JavaScript. To overcome this limitation, Web Workers were introduced as a way to enable this technique in web applications. In this article, Sarah Oke Okolo explores the importance of Web Workers for multithreading on the web, including the limitations and considerations of using them and the strategies for mitigating potential issues associated with Web Workers.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2023/04/potential-web-workers-multithreading-web/" />
              <title>Exploring The Potential Of Web Workers For Multithreading On The Web</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Exploring The Potential Of Web Workers For Multithreading On The Web</h1>
                  
                    
                    <address>Sarah Oke Okolo</address>
                  
                  <time datetime="2023-04-21T10:00:00&#43;00:00" class="op-published">2023-04-21T10:00:00+00:00</time>
                  <time datetime="2023-04-21T10:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Web Workers are a powerful feature of modern web development and were introduced as part of the <a href="https://en.wikipedia.org/wiki/Web_worker">HTML5 specification in 2009</a>. They were designed to provide a way to execute JavaScript code in the background, separate from the main execution thread of a web page, in order to improve performance and responsiveness.</p>

<p>The main thread is the single execution context that is responsible for rendering the UI, executing JavaScript code, and handling user interactions. In other words, <a href="https://developer.mozilla.org/en-US/docs/Glossary/Main_thread">JavaScript is “single-threaded”</a>. This means that any time-consuming task, such as complex calculations or data processing that is executed, would block the main thread and cause the UI to freeze and become unresponsive.</p>

<p>This is where Web Workers come in.</p>

<p>Web Workers were implemented as a way to address this problem by allowing time-consuming tasks to be executed in a separate thread, called a <strong>worker thread</strong>. This enabled JavaScript code to be executed in the background without blocking the main thread and causing the page to become unresponsive.</p>

<p>Creating a web worker in JavaScript is not much of a complicated task. The following steps provide a starting point for integrating a web worker into your application:</p>

<ol>
<li>Create a new JavaScript file that contains the code you want to run in the worker thread. This file should not contain any references to the DOM, as it will not have access to it.</li>
<li>In your main JavaScript file, create a new worker object using the <code>Worker</code> constructor. This constructor takes a single argument, which is the URL of the JavaScript file you created in step 1.<br />
<pre><code class="language-javascript">const worker = new Worker('worker.js');
</code></pre>
  </li>
<li>Add event listeners to the worker object to handle messages sent between the main thread and the worker thread. The <code>onmessage</code> event handler is used to handle messages sent from the worker thread, while the <code>postMessage</code> method is used to send messages to the worker thread.<br />
<pre><code class="language-javascript">worker.onmessage = function(event) {
  console.log('Worker said: ' + event.data);
};
worker.postMessage('Hello, worker!');
</code></pre>
  </li>
<li>In your worker JavaScript file, add an event listener to handle messages sent from the main thread using the <code>onmessage</code> property of the <code>self</code> object. You can access the data sent with the message using the <code>event.data</code> property.<br />
<pre><code class="language-javascript">self.onmessage = function(event) {
  console.log('Main thread said: ' + event.data);
  self.postMessage('Hello, main thread!');
};
</code></pre>
  </li>
</ol>

<p>Now let’s run the web application and test the worker. We should see messages printed to the console indicating that messages were sent and received between the main thread and the worker thread.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/potential-web-workers-multithreading-web/1-messages-console-between-main-worker-threads.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="412"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/potential-web-workers-multithreading-web/1-messages-console-between-main-worker-threads.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/potential-web-workers-multithreading-web/1-messages-console-between-main-worker-threads.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/potential-web-workers-multithreading-web/1-messages-console-between-main-worker-threads.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/potential-web-workers-multithreading-web/1-messages-console-between-main-worker-threads.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/potential-web-workers-multithreading-web/1-messages-console-between-main-worker-threads.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/potential-web-workers-multithreading-web/1-messages-console-between-main-worker-threads.png"
			
			sizes="100vw"
			alt="Messages in the console between the main thread and the worker thread"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/potential-web-workers-multithreading-web/1-messages-console-between-main-worker-threads.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>One key difference between Web Workers and the main thread is that Web Workers have <strong>no access to the DOM or the UI</strong>. This means that they cannot directly manipulate the HTML elements on the page or interact with the user.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aWeb%20Workers%20are%20designed%20to%20perform%20tasks%20that%20do%20not%20require%20direct%20access%20to%20the%20UI,%20such%20as%20data%20processing,%20image%20manipulation,%20or%20calculations.%0a&url=https://smashingmagazine.com%2f2023%2f04%2fpotential-web-workers-multithreading-web%2f">
      
Web Workers are designed to perform tasks that do not require direct access to the UI, such as data processing, image manipulation, or calculations.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Another important difference is that Web Workers are designed to run in a <strong>sandboxed environment</strong>, separate from the main thread, which means that they have limited access to system resources and cannot access certain APIs, such as the <a href="https://www.smashingmagazine.com/2010/10/local-storage-and-how-to-use-it/"><code>localStorage</code></a> or <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage"><code>sessionStorage</code></a> APIs. However, they can communicate with the main thread through a messaging system, allowing data to be exchanged between the two threads.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/touch-design-for-mobile-interfaces/">Touch Design for Mobile Interfaces</a></strong>, Steven Hoober’s brand-new guide on <strong>designing for mobile</strong> with proven, universal, human-centric guidelines. <strong>400 pages</strong>, jam-packed with in-depth user research and <strong>best practices</strong>.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/touch-design-for-mobile-interfaces/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/touch-design-for-mobile-interfaces/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/14bcab88-b622-47f6-a51d-76b0aa003597/touch-design-book-shop-opt.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b14658fc-bb2d-41a6-8d1a-70eaaf1b8ec8/touch-design-book-shop-opt.png"
    alt="Feature Panel"
    width="480"
    height="697"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="importance-and-benefits-of-web-workers-for-multithreading-on-the-web">Importance And Benefits Of Web Workers For Multithreading On The Web</h2>

<p>Web Workers provide a way for web developers to achieve multithreading on the web, which is crucial for building high-performance web applications. By enabling time-consuming tasks to be executed in the background, separate from the main thread, Web Workers improve the overall responsiveness of web pages and allow for a more seamless user experience. The following are some of the importance and benefits of Web Workers for multithreading on the Web.</p>

<h3 id="improved-resource-utilization">Improved Resource Utilization</h3>

<p>By allowing time-consuming tasks to be executed in the background, Web Workers make more efficient use of system resources, enabling faster and more efficient processing of data and improving overall performance. This is especially important for web applications that involve large amounts of data processing or image manipulation, as Web Workers can perform these tasks without impacting the user interface.</p>

<h3 id="increased-stability-and-reliability">Increased Stability And Reliability</h3>

<p>By isolating time-consuming tasks in separate worker threads, Web Workers help to prevent crashes and errors that can occur when executing large amounts of code on the main thread. This makes it easier for developers to write stable and reliable web applications, reducing the likelihood of user frustration or loss of data.</p>

<h3 id="enhanced-security">Enhanced Security</h3>

<p>Web Workers run in a sandboxed environment that is separate from the main thread, which helps to enhance the security of web applications. This isolation prevents malicious code from accessing or modifying data in the main thread or other Web Workers, reducing the risk of data breaches or other security vulnerabilities.</p>

<h3 id="better-resource-utilization">Better Resource Utilization</h3>

<p>Web Workers can help to improve resource utilization by freeing up the main thread to handle user input and other tasks while the Web Workers handle time-consuming computations in the background. This can help to improve overall system performance and reduce the likelihood of crashes or errors. Additionally, by leveraging multiple CPU cores, Web Workers can make more efficient use of system resources, enabling faster and more efficient processing of data.</p>

<p>Web Workers also enable better <a href="https://www.ibm.com/topics/load-balancing">load balancing</a> and scaling of web applications. By allowing tasks to be executed in parallel across multiple worker threads, Web Workers can help <strong>distribute the workload evenly across multiple cores or processors</strong>, enabling faster and more efficient processing of data. This is particularly important for web applications that experience high traffic or demand, as Web Workers can help to ensure that the application can handle an increased load without impacting performance.</p>

<h2 id="practical-applications-of-web-workers">Practical Applications Of Web Workers</h2>

<p>Let us explore some of the most common and useful applications of Web Workers. Whether you’re building a complex web application or a simple website, understanding how to leverage Web Workers can help you improve performance and provide a better user experience.</p>

<h3 id="offloading-cpu-intensive-work">Offloading CPU-Intensive Work</h3>

<p>Suppose we have a web application that needs to perform a large, CPU-intensive computation. If we perform this computation in the main thread, the user interface will become unresponsive, and the user experience will suffer. To avoid this, we can use a Web Worker to perform the computation in the background.</p>

<pre><code class="language-javascript">// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const result = event.data;
  console.log(result);
};

// Send a message to the worker to start the computation.
worker.postMessage({ num: 1000000 });

// In worker.js:

// Define a function to perform the computation.
function compute(num) {
  let sum = 0;
  for (let i = 0; i &lt; num; i++) {
    sum += i;
  }
  return sum;
}

// Define a function to handle messages from the main thread.
onmessage = function(event) {
  const num = event.data.num;
  const result = compute(num);
  postMessage(result);
};
</code></pre>

<p>In this example, we create a new Web Worker and define a function to handle messages from the worker. We then send a message to the worker with a parameter (<code>num</code>) that specifies the number of iterations to perform in the computation. The worker receives this message and performs the computation in the background. When the computation is complete, the worker sends a message back to the main thread with the result. The main thread receives this message and logs the result to the console.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/potential-web-workers-multithreading-web/2-console-number-message-main-thread.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="448"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/potential-web-workers-multithreading-web/2-console-number-message-main-thread.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/potential-web-workers-multithreading-web/2-console-number-message-main-thread.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/potential-web-workers-multithreading-web/2-console-number-message-main-thread.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/potential-web-workers-multithreading-web/2-console-number-message-main-thread.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/potential-web-workers-multithreading-web/2-console-number-message-main-thread.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/potential-web-workers-multithreading-web/2-console-number-message-main-thread.png"
			
			sizes="100vw"
			alt="A screenshot with a number in the console which is the result of the computation sent to the main thread"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/potential-web-workers-multithreading-web/2-console-number-message-main-thread.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>This task involves adding up all the numbers from <code>0</code> to a given number. While this task is relatively simple and straightforward for small numbers, it can become computationally intensive for very large numbers.</p>

<p>In the example code we used above, we passed the number <code>1000000</code> to the <code>compute()</code> function in the Web Worker. This means that the compute function will need to add up all the numbers from 0 to one million. This involves a large number of additional operations and can take a significant amount of time to complete, especially if the code is running on a slower computer or in a browser tab that is already busy with other tasks.</p>

<p>By offloading this task to a Web Worker, the main thread of the application can continue to run smoothly without being blocked by the computationally intensive task. This allows the user interface to remain responsive and ensures that other tasks, such as user input or animations, can be handled without delay.</p>

<h3 id="handling-network-requests">Handling Network Requests</h3>

<p>Let us consider a scenario where a web application needs to initiate a significant number of network requests. Performing these requests within the main thread could cause the user interface to become unresponsive and result in a poor user experience. In order to prevent this issue, we can utilize Web Workers to handle these requests in the background. By doing so, the main thread remains free to execute other tasks while the Web Worker handles the network requests simultaneously, resulting in improved performance and a better user experience.</p>

<pre><code class="language-javascript">// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const response = event.data;
  console.log(response);
};

// Send a message to the worker to start the requests.
worker.postMessage({ urls: ['https://api.example.com/foo', 'https://api.example.com/bar'] });

// In worker.js:

// Define a function to handle network requests.
function request(url) {
  return fetch(url).then(response =&gt; response.json());
}

// Define a function to handle messages from the main thread.
onmessage = async function(event) {
  const urls = event.data.urls;
  const results = await Promise.all(urls.map(request));
  postMessage(results);
};
</code></pre>

<p>In this example, we create a new Web Worker and define a function to handle messages from the worker. We then send a message to the worker with an array of URLs to request. The worker receives this message and performs the requests in the background using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API"><code>fetch</code> API</a>. When all requests are complete, the worker sends a message back to the main thread with the results. The main thread receives this message and logs the results to the console.</p>

<h3 id="parallel-processing">Parallel Processing</h3>

<p>Suppose we have a web application that needs to perform a large number of independent computations. If we perform these computations in sequence in the main thread, the user interface will become unresponsive, and the user experience will suffer. To avoid this, we can use a Web Worker to perform the computations in parallel.</p>

<pre><code class="language-javascript">// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const result = event.data;
  console.log(result);
};

// Send a message to the worker to start the computations.
worker.postMessage({ nums: [1000000, 2000000, 3000000] });

// In worker.js:

// Define a function to perform a single computation.
function compute(num) {
  let sum = 0;
  for (let i = 0; i &lt; num; i++) {
    sum += i;
}
  return sum;
}

// Define a function to handle messages from the main thread.
onmessage = function(event) {
  const nums = event.data.nums;
  const results = nums.map(compute);
  postMessage(results);
};
</code></pre>

<p>In this example, we create a new Web Worker and define a function to handle messages from the worker. We then send a message to the worker with an array of numbers to compute. The worker receives this message and performs the computations in parallel using the map method. When all computations are complete, the worker sends a message back to the main thread with the results. The main thread receives this message and logs the results to the console.</p>

<div class="partners__lead-place"></div>

<h2 id="limitations-and-considerations">Limitations And Considerations</h2>

<p>Web workers are a powerful tool for improving the performance and responsiveness of web applications, but they also have some limitations and considerations that you should keep in mind when using them. Here are some of the most important ones:</p>

<h3 id="browser-support">Browser Support</h3>

<p>Web workers are supported in all major browsers, including Chrome, Firefox, Safari, and Edge. However, there are still some other browsers that do not support web workers or may have limited support.</p>

<p>For a more extensive look at browser support, see <a href="https://caniuse.com/webworkers">Can I Use</a>.</p>

<p>It is important that you check out the browser support for any feature before using them in production code and test your application thoroughly to ensure compatibility.</p>

<h3 id="limited-access-to-the-dom">Limited Access To The DOM</h3>

<p>Web workers run in a separate thread and do not have access to the DOM or other global objects in the main thread. This means you <strong>cannot directly manipulate the DOM from a web worker or access global objects</strong> like windows or documents.</p>

<p>To work around this limitation, you can use the <code>postMessage</code> method to communicate with the main thread and update the DOM or access global objects indirectly. For example, you can send data to the main thread using <code>postMessage</code> and then update the DOM or global objects in response to the message.</p>

<p>Alternatively, there are some libraries that help solve this issue. For example, the <a href="https://github.com/ampproject/worker-dom">WorkerDOM</a> library enables you to run the DOM in a web worker, allowing for faster page rendering and improved performance.</p>

<h3 id="communication-overhead">Communication Overhead</h3>

<p>Web workers communicate with the main thread using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage"><code>postMessage</code></a> method, and as a result, could introduce communication overhead, which refers to the amount of time and resources required to establish and maintain communication between two or more computing systems, such as between a Web Worker and the main thread in a web application. This could result in a delay in processing messages and potentially slow down the application. To minimize this overhead, you should <strong>only send essential data</strong> between threads and <strong>avoid sending large amounts of data or frequent messages</strong>.</p>

<h3 id="limited-debugging-tools">Limited Debugging Tools</h3>

<p>Debugging Web Workers can be more challenging than debugging code in the main thread, as there are fewer debugging tools available. To make debugging easier, you can use the <code>console</code> API to log messages from the worker thread and use <a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools">browser developer tools</a> to inspect messages sent between threads.</p>

<h3 id="code-complexity">Code Complexity</h3>

<p>Using Web Workers can increase the complexity of your code, as you need to manage communication between threads and ensure that data is passed correctly. This can make it more difficult to write, debug, and maintain your code, so you should carefully consider whether using web workers is necessary for your application.</p>

<h2 id="strategies-for-mitigating-potential-issues-with-web-workers">Strategies For Mitigating Potential Issues With Web Workers</h2>

<p>Web Workers are a powerful tool for improving the performance and responsiveness of web applications. However, when using Web Workers, there are several potential issues that can arise. Here are some strategies for mitigating these issues:</p>

<h3 id="minimize-communication-overhead-with-message-batching">Minimize Communication Overhead With Message Batching</h3>

<p>Message batching involves grouping multiple messages into a single batch message, which can be more efficient than sending individual messages separately. This approach reduces the number of round-trips between the main thread and Web Workers. It can help to minimize communication overhead and improve the overall performance of your web application.</p>

<p>To implement message batching, you can <strong>use a queue to accumulate messages and send them together as a batch</strong> when the queue reaches a certain threshold or after a set period of time. Here’s an example of how you can implement message batching in your Web Worker:</p>

<pre><code class="language-javascript">// Create a message queue to accumulate messages.
const messageQueue = [];

// Create a function to add messages to the queue.
function addToQueue(message) {
  messageQueue.push(message);
  
  // Check if the queue has reached the threshold size.
  if (messageQueue.length &gt;= 10) {
    // If so, send the batched messages to the main thread.
    postMessage(messageQueue);
    
    // Clear the message queue.
    messageQueue.length = 0;
  }
}

// Add a message to the queue.
addToQueue({type: 'log', message: 'Hello, world!'});

// Add another message to the queue.
addToQueue({type: 'error', message: 'An error occurred.'});
</code></pre>

<p>In this example, we create a message queue to accumulate messages that need to be sent to the main thread. Whenever a message is added to the queue using the <code>addToQueue</code> function, we check if the queue has reached the threshold size (in this case, ten messages). If so, we send the batched messages to the main thread using the <code>postMessage</code> method. Finally, we clear the message queue to prepare it for the next batch.</p>

<p>By batching messages in this way, we can reduce the overall number of messages sent between the main thread and Web Workers,</p>

<h3 id="avoid-synchronous-methods">Avoid Synchronous Methods</h3>

<p>These are JavaScript functions or operations that block the execution of other code until they are complete. Synchronous methods can block the main thread and cause your application to become unresponsive. To avoid this, you should avoid using synchronous methods in your Web Worker code. Instead, use <strong>asynchronous methods</strong> such as <code>setTimeout()</code> or <code>setInterval()</code> to perform long-running computations.</p>

<p>Here is a little demonstration:</p>

<pre><code class="language-javascript">// In the worker
self.addEventListener('message', (event) =&gt; {
  if (event.data.action === 'start') {
    // Use a setTimeout to perform some computation asynchronously.
    setTimeout(() =&gt; {
      const result = doSomeComputation(event.data.data);

      // Send the result back to the main thread.
      self.postMessage({ action: 'result', data: result });
    }, 0);
  }
});
</code></pre>

<h3 id="be-mindful-of-memory-usage">Be Mindful Of Memory Usage</h3>

<p>Web Workers have their own memory space, which can be limited depending on the user’s device and browser settings. To avoid memory issues, you should be mindful of the amount of memory your Web Worker code is using and avoid creating large objects unnecessarily. For example:</p>

<div class="break-out">
<pre><code class="language-javascript">// In the worker
self.addEventListener('message', (event) =&gt; {
  if (event.data.action === 'start') {
    // Use a for loop to process an array of data.
    const data = event.data.data;
    const result = [];

    for (let i = 0; i &lt; data.length; i++) {
      // Process each item in the array and add the result to the result array.
      const itemResult = processItem(data[i]);
      result.push(itemResult);
    }

    // Send the result back to the main thread.
    self.postMessage({ action: 'result', data: result });
  }
});
</code></pre>
</div>

<p>In this code, the Web Worker processes an array of data and returns the result to the main thread using the <code>postMessage</code> method. However, the <code>for</code> loop used to process the data may be time-consuming.</p>

<p>The reason for this is that the code is processing an entire array of data at once, meaning that all the data must be loaded into memory at the same time. If the data set is very large, this can cause the Web Worker to consume a significant amount of memory, potentially exceeding the memory limit allocated to the Web Worker by the browser.</p>

<p>To mitigate this issue, you can consider using built-in JavaScript methods like <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach"><code>forEach</code></a> or <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce"><code>reduce</code></a>, which can process data one item at a time and avoid the need to load the entire array into memory at once.</p>

<div class="partners__lead-place"></div>

<h3 id="browser-compatibility">Browser Compatibility</h3>

<p>Web Workers are supported in most modern browsers, but some older browsers may not support them. To ensure compatibility with a wide range of browsers, you should test your Web Worker code in different browsers and versions. You can also use <strong>feature detection</strong> to check if Web Workers are supported before using them in your code, like this:</p>

<pre><code class="language-javascript">if (typeof Worker !== 'undefined') {
  // Web Workers are supported.
  const worker = new Worker('worker.js');
} else {
  // Web Workers are not supported.
  console.log('Web Workers are not supported in this browser.');
}
</code></pre>

<p>This code checks if Web Workers are supported in the current browser and creates a new Web Worker if they are supported. If Web Workers are not supported, the code logs a message to the console indicating that Web Workers are not supported in the browser.</p>

<p>By following these strategies, you can ensure that your Web Worker code is efficient, responsive, and compatible with a wide range of browsers.</p>

<h2 id="conclusion">Conclusion</h2>

<p>As web applications become increasingly complex and demanding, the importance of efficient multithreading techniques &mdash; such as Web Workers &mdash; is likely to increase. Web Workers are an essential feature of modern web development that allows developers to offload CPU-intensive tasks to separate threads, improving application performance and responsiveness. However, there are significant limitations and considerations to keep in mind when working with Web Workers, such as the lack of access to the DOM and limitations on the types of data that can be passed between threads.</p>

<p>To mitigate these potential issues, developers can follow strategies as mentioned earlier, such as using asynchronous methods and being mindful of the complexity of the task being offloaded.</p>

<p>Multithreading with Web Workers is likely to remain an important technique for improving web application performance and responsiveness in the future. While there are other techniques for achieving multithreading in JavaScript, such as using <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket">WebSockets</a> or <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer"><code>SharedArrayBuffer</code></a>, Web Workers have several advantages that make them a powerful tool for developers.</p>

<p>Adopting more recent technology such as <a href="https://developer.mozilla.org/en-US/docs/WebAssembly">WebAssembly</a> may open up new opportunities for using Web Workers to offload even more complex and computationally-intensive tasks. Overall, Web Workers are likely to continue to evolve and improve in the coming years, helping developers create more efficient and responsive web applications.</p>

<p>Additionally, many libraries and tools exist to help developers work with Web Workers. For example, <a href="https://github.com/GoogleChromeLabs/comlink">Comlink</a> and <a href="https://github.com/developit/workerize">Workerize</a> provide a simplified API for communicating with Web Workers. These libraries abstract away some of the complexity of managing Web Workers, making it easier to leverage their benefits.</p>

<p>Hopefully, this article has given you a good understanding of the potential of web workers for multithreading and how to use them in your own code.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item></channel></rss>