DEV Community

Collabier
Collabier

Posted on

Why your next web app might not need a backend for every operation

I've noticed a pattern in a lot of web applications I've worked on.

Something happens in the browser, so we send it to the server.

The server processes it, sends the result back, and the browser renders it.

For most applications, that's completely reasonable.

But sometimes we build an API endpoint simply because that's how we've always built web applications.

Take a few simple examples:

  • Formatting JSON
  • Generating a UUID
  • Calculating a subnet
  • Hashing a string
  • Converting an image
  • Encoding a URL
  • Parsing a JWT
  • Doing a percentage calculation

None of these inherently require a server.

That realization became more interesting to me when I started building Omnikite, a collection of developer and productivity utilities.

The project became an experiment in figuring out how much work I could reasonably move into the browser.

The browser is much more capable now

When I started writing frontend applications, I mostly thought of the browser as a place for rendering UI and making HTTP requests.

That's obviously an outdated mental model.

Modern browsers provide APIs for things such as:

  • Cryptographic operations
  • File handling
  • Background processing
  • Binary data
  • Image manipulation
  • Persistent storage
  • Streams
  • WebAssembly
  • Workers

Once you start using these APIs, some traditional backend operations start looking unnecessary.

Not all of them, obviously.

But more than I initially expected.


Start with the simplest question: does the server actually need the data?

This became my rule of thumb while working on the project.

Before creating an API endpoint, I ask:

Does the server actually need to see this input?

If the answer is no, I look for a client-side implementation first.

For example, suppose I want to calculate a SHA-256 hash.

There is no reason to send the input to an API just to calculate the hash.

The browser already has the Web Crypto API:

async function sha256(value: string) {
  const data = new TextEncoder().encode(value);

  const hash = await crypto.subtle.digest("SHA-256", data);

  return Array.from(new Uint8Array(hash))
    .map(byte => byte.toString(16).padStart(2, "0"))
    .join("");
}
Enter fullscreen mode Exit fullscreen mode

The implementation is small, and more importantly, the original input doesn't need to leave the browser for this operation.

That distinction matters when the input happens to be sensitive.


File processing is where things get interesting

PDFs and images initially seemed like a different category.

Large files can be expensive to process, and traditionally it's common to upload them to a server and let some backend worker do the heavy lifting.

But browsers can work with binary files directly.

A file selected through an <input type="file"> is available to JavaScript as a File object.

From there you can work with:

const buffer = await file.arrayBuffer();
Enter fullscreen mode Exit fullscreen mode

Now you have the file's binary data in the browser.

For some workloads, that's enough.

For more CPU-intensive processing, WebAssembly can be useful because existing native libraries can sometimes be compiled to run inside the browser.

That's the approach behind some of the file utilities in the Omnikite PDF tools.

There is an important caveat, though.

Client-side processing doesn't magically make large files cheap.

You still have memory limits, CPU limitations, mobile-device constraints, and browser-specific behavior to consider.

"Runs in the browser" isn't the same thing as "runs instantly."


Web Crypto is another good example

I still see projects pulling in additional dependencies for operations that modern browsers already support.

For basic cryptographic operations, the Web Crypto API provides primitives such as:

  • SHA-256
  • HMAC
  • AES
  • PBKDF2
  • Random values

For example:

const bytes = new Uint8Array(32);

crypto.getRandomValues(bytes);
Enter fullscreen mode Exit fullscreen mode

That's enough to generate cryptographically strong random bytes using the browser's secure random number generator.

The important part is understanding what the API actually provides.

A browser API doesn't automatically make an application secure.

You still need to choose appropriate algorithms, parameters, key sizes, storage mechanisms, and threat models.

Moving something client-side is an architectural decision, not a security guarantee.


Calculators don't need APIs

This sounds obvious, but it's surprisingly easy to over-engineer.

If a calculator needs to perform:

principal × rate × time
Enter fullscreen mode Exit fullscreen mode

there isn't much value in:

Browser
   ↓
POST /calculate
   ↓
Server
   ↓
JSON response
   ↓
Browser
Enter fullscreen mode Exit fullscreen mode

The browser can perform the calculation immediately.

That's why tools such as the IPv4 subnet calculator can update their results as the user types.

There's no loading state because there isn't a network operation to wait for.

The same principle applies to things like:

  • Unit conversion
  • Percentage calculations
  • Date calculations
  • CIDR calculations
  • Compound interest
  • Number conversions
  • Color conversion

For small deterministic operations, local computation is usually the simpler architecture.


Where the backend still makes sense

This experiment also made me more aware of where a backend is genuinely necessary.

You still need a server when you have things such as:

Authentication

User accounts, sessions, permissions, password management, and identity systems require server-side infrastructure.

Persistent shared data

If multiple users need to access the same state, local browser storage obviously isn't enough.

Secrets

API keys and other server-side credentials shouldn't be moved into frontend JavaScript just because you're trying to avoid a backend.

Anything shipped to the browser should be considered accessible to the user.

Heavy processing

Some workloads simply don't belong on a user's laptop or phone.

A 10-year-old laptop and a modern desktop shouldn't necessarily be expected to perform the same computation.

External integrations

If you're communicating with a private database, payment provider, internal service, or another system requiring credentials, a backend is often the correct boundary.

So I don't think the lesson is:

"Backends are dead."

It's closer to:

Don't create a backend operation when the browser can safely and efficiently do the job itself.


The architecture I ended up with

For Omnikite, I wanted the application layer to remain relatively simple.

The general structure is roughly:

Next.js
   │
   ├── Static UI
   │
   ├── Tool pages
   │
   ├── Browser APIs
   │     ├── Web Crypto
   │     ├── File APIs
   │     ├── Canvas
   │     └── Web Workers
   │
   └── WebAssembly
         │
         └── CPU-heavy processing
Enter fullscreen mode Exit fullscreen mode

The exact implementation varies from tool to tool.

That's another thing I learned during the project: there isn't one "client-side architecture."

A JSON formatter has completely different requirements from a PDF processor.

A calculator has completely different requirements from an image converter.

The right architecture depends on the workload.


Next.js is still useful even when the tool itself is client-side

One misconception I had initially was that a client-side application somehow means you don't need a framework like Next.js.

That's not really the case.

Next.js can still handle:

  • Routing
  • Metadata
  • Static generation
  • SEO
  • Layouts
  • Code splitting
  • UI composition

The actual tool operation can then happen entirely in the browser.

For a utility website, this combination works particularly well because each tool can have its own URL.

For example:

JSON Formatter

can exist independently from:

JWT Debugger

while still sharing the same application shell.


Performance isn't the only reason

Initially, I was mostly interested in performance.

No request means no network round trip.

That obviously helps.

But after building more tools, I started appreciating another benefit.

Privacy becomes easier to reason about.

If a tool processes a piece of text locally, there's no server endpoint sitting between the user and the result.

That's not a substitute for proper security engineering, but it can remove an entire category of data-handling concerns.

There's also less infrastructure to maintain.

No API endpoint.

No request validation for that operation.

No server-side worker.

No temporary upload bucket.

No cleanup job.

Sometimes the simplest architecture is simply the one with fewer moving parts.


There are trade-offs

Client-side processing isn't automatically better.

You have to think about:

  • Browser compatibility
  • Memory consumption
  • CPU usage
  • Mobile performance
  • Large files
  • Web Worker communication overhead
  • WebAssembly bundle size
  • Accessibility
  • Error handling

And some operations are simply better suited to a server.

The goal isn't to move everything into JavaScript running in the browser.

The goal is to put the computation in the place where it makes the most sense.


What I'm taking away from the experiment

The biggest lesson for me wasn't a specific API.

It was questioning the default architecture.

When building something new, it's easy to start with:

Frontend → API → Database
Enter fullscreen mode Exit fullscreen mode

because that's the architecture we're used to.

But sometimes the better architecture is:

Frontend → Browser API → Result
Enter fullscreen mode Exit fullscreen mode

And sometimes it's:

Frontend → Web Worker → Result
Enter fullscreen mode Exit fullscreen mode

And sometimes:

Frontend → API → Worker → Database
Enter fullscreen mode Exit fullscreen mode

The interesting part is deciding which one you actually need.

I've been using that mindset while continuing to build Omnikite.

It's basically become my testbed for small browser-based utilities and experiments.

If you're building a developer tool yourself, I'd recommend starting with a simple question:

Does this operation actually need my server to exist?

You might be surprised by how often the answer is no.

Top comments (0)