A lot of modern backend code looks roughly the same.
Define a route.
Write a handler.
Validate input.
Query data.
Return a response.
It works.
But after building enough APIs, something starts to become obvious:
Most of the code isn’t unique.
It’s repetition wrapped in slightly different logic.
The Traditional Route Handler Model
Most applications are structured around handlers.
Something like this:
app.get("/posts/:id", async (req, res) => {
const id = req.params.id;
if (!id) {
return res.status(400).json({
error: "Missing id"
});
}
const post = await db.posts.findById(id);
if (!post) {
return res.status(404).json({
error: "Post not found"
});
}
return res.json(post);
});
There’s nothing inherently wrong with this.
But when systems grow, a few problems start appearing:
- Validation logic gets repeated
- Transport concerns leak everywhere
- Execution patterns drift
- Every route becomes slightly different
The issue isn’t functionality.
It’s structure.
The Shift: Define Behavior Instead of Rewriting It
Instead of implementing every route directly, another option is to define routes as structured contracts.
Example:
const routes = {
postById: {
method: "GET",
endpoint: "/posts/:id"
}
};
At first glance, this might seem overly simple.
But the important shift is this:
The route is now a definition.
Not an implementation.
That distinction changes the architecture significantly.
Moving Execution Into the Runtime
Once behavior is defined structurally, execution can move into a shared runtime layer.
Example:
export const getPostById =
createWordPressRoute(routes.postById);
Now the runtime becomes responsible for:
- Request construction
- Parameter handling
- Transport behavior
- Response normalization
- Shared execution flow
Instead of every route manually implementing these concerns, the runtime handles them consistently.
The route definition becomes input to the engine.
Why This Matters
In traditional systems, logic tends to spread outward.
Every route becomes its own mini-system.
Over time:
- Patterns drift
- Behavior becomes inconsistent
- Refactoring becomes harder
A runtime-based approach centralizes execution.
That creates:
- Predictable flow
- Reusable behavior
- Easier debugging
- Easier maintenance
Adapters Instead of Hardcoded Behavior
One interesting side effect of this approach is the ability to create adapter-style abstractions.
Example:
createAliasedQueryRoute(
routes.postBySlug,
"posts",
"slug"
);
Here, the system translates application intent into backend-specific query behavior.
The important part is not the helper itself.
It’s the separation.
The frontend or consuming layer doesn’t need to know:
- How WordPress structures queries
- How parameters are mapped
- How transport formatting works
The runtime handles translation.
This creates cleaner boundaries between systems.
Domain Layers Become Simpler
Once execution logic moves into shared runtimes, higher-level APIs become significantly cleaner.
Example:
getBySlug(slug: string) {
return this.read(getPostBySlug, { slug });
}
Notice what is missing here:
- No transport logic
- No validation duplication
- No query construction
- No response formatting
The method simply describes intent.
That’s an important architectural difference.
Shared Execution Paths
One of the biggest advantages of system-first architecture is centralized execution.
Example:
return this.read(...)
return this.mutate(...)
Those shared execution paths become natural locations for:
- Validation
- Authentication
- Logging
- Caching
- Transformation
- Retry logic
- Error handling
Instead of scattering those concerns across handlers, they exist in one predictable flow.
This aligns closely with pipeline-oriented architecture.
From Handlers to Systems
At a small scale, route handlers feel straightforward.
At larger scales, they often become difficult to reason about because behavior becomes distributed across the application.
System-first architecture attempts to solve that by shifting focus away from individual handlers and toward:
- Definitions
- Contracts
- Pipelines
- Runtimes
- Engines
The goal is not to eliminate flexibility.
The goal is to create consistency.
This Is Not About Removing Code
A system-first approach does not reduce software down to configuration files.
The code still exists.
It simply moves to a different layer.
Instead of repeatedly writing endpoint behavior, the focus becomes building:
- Execution engines
- Validation systems
- Shared runtimes
- Transformation pipelines
The implementation effort shifts upward into architecture.
The Long-Term Benefit
The biggest advantage of this style of architecture usually does not appear immediately.
It appears later.
As systems grow, consistent execution models become increasingly valuable.
They reduce:
- Drift
- Repetition
- Hidden behavior
- Structural inconsistency
And they make systems easier to evolve over time.
Final Thought
Most backend applications are built as collections of handlers.
System-first architecture approaches the problem differently.
Instead of asking:
“How should this route work?”
It asks:
“How should the system work?”
That shift changes everything that follows.
Top comments (0)