
One Backend, No Regrets: Server Actions at Scale
You can build a serious product without a separate API tier. Here is how we structure Next.js server actions so the logic stays sane as the app grows.

For years the default shape of a web app was two codebases pretending to be one. A frontend, a separate backend API, and a layer of glue in the middle whose only job was to move data across the gap and keep two sets of types in sync. Most of that glue existed to solve a problem the framework can now solve for you.
We build most products as a single Next.js app where the server logic lives in server actions. No separate API service, no client that spends half its code fetching and reshaping. It scales further than people expect, as long as you are disciplined about structure. Here is the discipline.
The default is one codebase
A separate API tier is the right call when something genuinely needs it. A public API for third parties. Multiple clients sharing one backend. A service with its own scaling profile. If you do not have one of those reasons, a second codebase is mostly a tax you pay in glue, duplicated types, and an extra deploy to keep in step.
With server actions, a button calls a function that runs on the server, and the type of what it returns is known on both sides automatically. The network boundary stops being a translation layer you maintain by hand. That removal is most of the win.
Two codebases that have to agree on everything are usually one codebase that got split too early.
Structure is what saves you
The danger with server actions is that they are easy. Easy things sprawl. Drop database calls directly into components and you will have logic scattered everywhere within a month, impossible to find and impossible to test.
So we keep a clear shape. Actions live in their own layer and own the business logic. Helpers, schemas, and types for a feature live together so a feature is a place, not a scattering. Components call actions, they do not reach past them. The rule is boring and the boringness is the point. A new engineer can guess where a thing lives, because everything of that kind lives in the same place.
Guard at the boundary, every time
The one mistake that actually bites with server actions is forgetting that they are public endpoints. A server action is a door into your backend. If it does not check who is knocking, it is open.
We enforce auth and role checks at the start of every action that touches anything sensitive, not in the component that happens to call it. The component is a suggestion. The action is the gate. Every action returns a predictable, typed result, so the caller handles success and failure the same way every time and nothing leaks through a forgotten branch.
Validate the input as if it is hostile
Because an action can be called directly, you treat its input as untrusted, always. We validate every payload against a schema at the door before any logic runs. The schema is the same one the form uses, so the client and server agree on what valid means without anyone maintaining two copies. Bad input bounces at the boundary instead of becoming a corrupt row three functions deep.
When to add the tier
This approach has a ceiling, and you should know where it is. When you genuinely need to serve other clients, expose a stable public contract, or scale a piece independently, add the service. Do it because a real requirement arrived, not because a diagram in a blog post said real apps have a backend folder.
Most products never hit that ceiling. They ship faster, with less to maintain and fewer ways to drift, on one well-structured codebase. Start there. Add complexity the day the product earns it, and not a day sooner.
Have something to build?
Let's turn your vision into a shipped product, fast.



