Edge, Client, API, Data: A Pragmatic Web App Architecture
Most architecture diagrams lie a little. They show clean boxes with clean arrows, and they hide the fact that every real system leaks responsibilities across its boundaries. A pragmatic web app architecture is not about drawing the prettiest diagram. It is about deciding, on purpose, where each concern lives so that you can reason about the system when something breaks at 2am. We think in four layers: edge, client, API, and data. Each one has a job. Trouble starts when a job ends up in the wrong layer.
The edge is your first decision, not your last
The edge is everything that happens before a request reaches your application code: the CDN, TLS termination, caching, redirects, rate limiting, and increasingly, small compute functions that run close to the user. It is tempting to treat the edge as plumbing you configure once and forget. That is a mistake.
The edge is where you decide what never has to hit your servers at all. Static assets, cached HTML, and read-only responses can be served in milliseconds without touching your API. This is the cheapest performance you will ever buy. A few honest rules:
- Cache what is safe to cache, and set explicit cache headers rather than trusting defaults.
- Terminate TLS and enforce HTTPS at the edge so downstream services never see plaintext.
- Put coarse rate limiting and basic bot mitigation here, before requests reach code that costs you money to run.
We run our own edge with Caddy and Oracle in production, so we tend to favor configuration you can read and version in git over opaque dashboards. The point is not the tool. The point is that the edge should absorb load, not just forward it.
The client should render, not run your business
The client layer is the browser: HTML, CSS, and JavaScript. Modern frameworks make it easy to push more and more logic into the client, and some of that is genuinely good. Interactive forms, optimistic updates, and instant navigation all make products feel alive.
But the client is the one layer you do not control. It runs on someone else's device, on a network you cannot predict, and it can be inspected and modified by anyone. So the rule is simple: the client renders and reacts, it does not decide anything that matters. Pricing, permissions, and validation must be confirmed on the server, even if you also check them in the browser for a nicer experience.
Decide early how much rendering happens where. Server rendering gives you fast first paint and better indexing. Client rendering gives you rich interaction after load. Most real apps want a blend, and the honest work is choosing which pages lean which way, per route, based on what the page is for.
The API is the contract everyone depends on
If the edge is about traffic and the client is about experience, the API is about truth. It is the single place where your rules live and the only interface the client is allowed to trust. A good API layer is boring in the best way: predictable inputs, predictable outputs, clear errors.
Three habits keep an API healthy. First, validate every input at the boundary and reject bad data loudly rather than letting it drift inward. Second, version deliberately so you can change behavior without breaking existing clients. Third, keep the API stateless where you can, so any instance can serve any request and scaling is just adding more instances.
The API is also where authorization belongs. Not at the edge, which is too coarse, and not in the client, which cannot be trusted. Every request should answer the question "is this specific user allowed to do this specific thing" in one consistent place.
The data layer is where you tell the truth
Underneath everything sits the data layer: your database, your caches, and the boundaries around them. This is the layer that outlives every rewrite. Frameworks change, but the shape of your data and the guarantees around it will follow you for years.
Keep the rules that protect data close to the data. Use constraints, foreign keys, and transactions rather than hoping application code always remembers to be careful. Treat schema changes as first-class work with real migrations, not as edits you run by hand and forget. When you add a cache, be explicit about how it gets invalidated, because a cache without an invalidation story is just a slow, confusing bug waiting to happen.
We tend to reach for Postgres because it is honest about what it guarantees, and because it lets you push correctness down to the layer that can actually enforce it.
Where the layers meet is where you get paid
The interesting decisions in web app architecture are never inside a layer. They are at the seams. Should this validation run at the edge, in the API, or both? Should this page render on the server or the client? Does this rule belong in application code or as a database constraint? There is rarely one correct answer, only a defensible one you can explain later.
A pragmatic architecture is one where every concern has an obvious home, the boundaries are enforced rather than assumed, and a new engineer can trace a request from edge to data without surprises. That clarity is not a luxury. It is what lets you move fast without quietly accumulating the kind of debt that eventually stops you cold.