
Design Patterns in Practice: When They Help and When They Complicate Code
Strategy, Adapter, and Factory solve different kinds of change. Learn how to recognize the moment each pattern earns its place and when a direct function is better.
Compare Next.js with Axum, Askama, and htmx through browser state, contracts, caching, deployment, security, mobile clients, and project handover.

The choice between Next.js and Axum, Askama, and htmx is not a contest between modern JavaScript and fast Rust, however the talks frame it. It is a decision about who owns the user interface.
Next.js gives substantial responsibility to React's component model and, where required, to the browser. htmx does the opposite. It keeps the final HTML on the server, swaps only the fragment the response was asked for, while any local state needs a separate design.
The short verdict first. Next.js is a strong default for customer-facing products and interfaces with rich local state. Axum, Askama, and htmx are a strong choice for server-driven CRUD, for administration, and for the workflow applications that somebody approves step by step. A Rust API with Next.js is justified only when the product demonstrably needs both a rich client and an independently demanding or security-sensitive backend, which is rare.
Three outcomes of the architecture decision
Choose the outcome from the interface workload and proven backend needs.
Next.js
Rich local state, an editor, a map or anything that must react instantly in the browser.
Axum + htmx
Forms, tables, and sequential workflows whose truth belongs on the server.
Rust API + Next.js
Rich UI plus proven compute, concurrency, or multi-client backend requirements.
The examples illustrate architecture, not a list of currently secure releases. Pin the versions for your implementation and read their release notes and security advisories.
Rust here means Axum for HTTP and Askama for templates, with htmx in the browser. Leptos, Yew and Rust SPA frameworks use a different model and are outside this comparison.
A React Server Component executes on the server and produces an RSC payload. On the first load, Next.js also supplies prerendered HTML. The browser uses the payload to reconcile the tree and JavaScript to hydrate Client Components. The Next.js documentation distinguishes this from subsequent navigation.
Interactive behaviour still needs a Client Component boundary. Hydration and browser state come with it wherever the interface behaves like an application.
An htmx fragment is an ordinary server response, the same HTML a full page reload would bring, only less of it, and without the rest of the document. A button declares hx-post, gets its destination from hx-target, and how the swap happens is hx-swap. The server applies a rule. Askama renders the row or the panel and htmx drops it exactly where it belongs.
The public contract is not a React tree or necessarily a JSON object. It is an HTTP request plus HTML whose structure must fit the target.
Two ways a request becomes an interface
Next.js transfers the result of a component tree. htmx swaps an HTML fragment prepared by the server.
Next.js
Next.js request
The browser requests a route or invokes a Server Action.
Component result
The server renders components and the client merges the transferred result with its state.
Axum + htmx
htmx event
A click or form sends a regular HTTP request from an HTML attribute.
HTML fragment
Axum enforces the rules, Askama renders the result, and htmx replaces the chosen part of the page.
Consider an invoice list. An authorised person selects Approve. The server checks the organisation and the document state, writes the transition and shows the new status, and that chain must hold when two people press the button in the same second. A form can invoke a Server Action.
'use server';
import { revalidatePath } from 'next/cache';
export async function approveInvoice(formData: FormData) {
const invoiceId = String(formData.get('invoiceId') ?? '');
const user = await requireUser();
await invoices.approve({
invoiceId,
organizationId: user.organizationId,
});
revalidatePath('/invoices');
}
A real handler also needs identifier validation, permission checks for the operation, replay protection, an audit trail. A Server Action is a publicly reachable server entry point rather than a trusted internal function.
After success, Next.js can revalidate a route or a precise cache tag. The client may show an optimistic state and restore it if the request fails.
In the server-driven version, the button submits POST /invoices/{id}/approve. Axum extracts the authenticated user. The application service checks the organisation and the state transition, and Askama returns the updated table row, which is the only part of the document that changed.
use askama::Template;
use axum::{extract::{Path, State}, response::Html};
use uuid::Uuid;
async fn approve_invoice(
State(state): State<AppState>,
user: AuthenticatedUser,
Path(invoice_id): Path<Uuid>,
) -> Result<Html<String>, AppError> {
let invoice = state
.invoices
.approve(invoice_id, user.organization_id)
.await?;
let html = InvoiceRowTemplate { invoice }.render()?;
Ok(Html(html))
}
The row can declare hx-post, take its destination from hx-target="closest tr", and describe the swap with hx-swap="outerHTML". Only that element changes after the response. AppError must convert the template error too, and an error response should hand back something the person can act on rather than the empty space where that row used to be.
The domain transition belongs in the service and not in a template or htmx attribute.
This route is direct for forms and tables. After every step the server owns both truth and presentation. The balance changes with bulk selection, offline work, demanding drag and drop, or several panels that have to react together, and the DOM then starts carrying state nobody documented.
The team then either adds a small JavaScript island or admits that the interface now has a client application model.
A list with URL filters and one open dialog works in either stack. The difference shows up only when state has to respond immediately, with no server round trip in between.
An editor with undo history, a map with thousands of objects, a planner with draggable work, a configurator with continuous calculations. Every one has a natural browser model, and Next.js combines server loading with isolated client islands for exactly that case.
htmx is at its best when state is safely held by the database, the URL, the submitted form, the HTML already on screen. Instead of synchronising a client store after every mutation the server returns a fresh view of truth, so no two copies of the same value exist to drift apart. Less duplication, fewer classes of defect.
The original advantage disappears the moment important data migrates into data-* attributes, a hand-rolled event bus starts coordinating the page, and five fragments need updating by hand.

An interface with several connected panels needs immediate responses and shared browser state. That is a natural fit for Next.js.
Photo by Neil Fernandez on UnsplashServer-rendered HTML is not a deficiency when the web interface is the only client, and for an internal approval system it may be the most useful contract you have. The form, the validation error, and the row that redraws after it are one result, with no DTO mapping to keep in step.
The answer changes when a native mobile application, a partner integration or a public API needs the same capability, and an HTML fragment stops being the domain contract.
JSON endpoints can live beside HTML handlers, but copying business rules into two paths leaves two systems that agree only by coincidence.
Both should call the same application service and receive contract tests.
Next.js does not automatically solve API design either. A Server Action is convenient for one React interface, not a durable partner API, so any product with several clients needs an explicit contract whatever renders the web.
Next.js offers several caching and invalidation layers. That helps on content and data-heavy routes, but the team has to know what is static, what revalidates, what belongs to one person only, and to know it before the first deployment.
Failures often appear because two parts of the team assume different data lifetimes rather than because the cache is broken.
htmx does not prescribe cache policy. It uses HTTP. Full pages and safe GET fragments can use response headers, a reverse proxy, a CDN, which is to say the machinery the web ran on long before anyone had to reason about framework invalidation.
Personal fragments need correct Cache-Control and Vary behaviour. A mutation returns a fresh representation or triggers a follow-up read, or else the person sees the old row and concludes the button is broken, which is an expensive bug.
Next.js can run on a managed platform or in a self-hosted Node.js or container environment. Self-hosting requires a compatible reverse proxy, static asset handling, multiple instances, coordinated caches, consistent build identifiers.
The framework handles much of the application work, but its production topology needs deliberate design.
An Axum application can become one Rust binary with templates embedded at compile time and a directory of static assets beside it, with a separate build and release process. That is an attractive operating shape. Compilation, cross builds, database migrations, telemetry, incident procedures. All of that remains.
Fewer runtime layers do not mean no operational work.
Rust can offer predictable resource use and precise control over concurrent work. A cheaper product does not follow, and nobody knows without measurement, because latency is normally dominated by the database, third-party calls, images and that one badly written query.
We therefore avoid synthetic requests-per-second figures taken from applications that do not perform comparable work.

A single Rust binary simplifies the runtime, not operations. Proxies, migrations, telemetry, and incident response remain part of the system.
Photo by Kevin Ache on UnsplashNext.js and htmx share ordinary web security boundaries. The server must verify identity, organisation, permission, current object state on every mutation, even when it already checked the same thing while the page was loading. Never trust a hidden input, React prop or DOM attribute.
Output must be escaped, and any deliberate HTML injection needs narrowly defined sanitisation.
Cookie authentication requires a CSRF assessment based on the method, on the request origin, and on how the cookies happen to be configured. An htmx header or Server Action does not replace authorisation, while Rust ownership prevents one broad category of memory faults.
It does not prevent tenant isolation mistakes, flawed SQL, or sensitive values leaking into logs.
The safer stack is the one where the team can maintain one authoritative domain path, controlled dependencies, prompt updates, useful observability. A framework can provide good defaults, but it cannot infer business permissions.
The Rust pool is smaller and hiring can take longer. Its type system and its explicit error paths narrow the space for accidental change. So does a compiler that refuses to move on. If the future owner does not know Rust, that elegant binary becomes an organisational risk, and the first security update waits until somebody who can compile it turns up. Technical fit without an available owner is not sustainable.
In custom software development, we therefore assess whether the client can operate the system after handover, not just how quickly the first release can ship.
Local setup, migrations, incident response, dependency upgrades are architectural deliverables, not an appendix to them.

A named team must be able to take ownership of the architecture. Rust's technical advantages fade when no maintainer is available after handover.
Photo by Vitaly Gariev on UnsplashNext.js is a poor choice when a team builds a large client application for a handful of basic forms, only because React is what it already knows. Unnecessary hydration, duplicated state, complicated invalidation, Server Action boundaries. Between them they can bury a business process that fits in three sentences.
The framework also changes quickly enough that upgrades require regular attention.
Another failure appears when Next.js becomes an accidental integration layer and a domain backend at once, and nobody bothers to draw the boundary between those modules. Interface code, authorisation, provider calls, long-running jobs. All of it mixes inside routes. The product then has no clear mobile contract and nowhere safe to test a business rule.
Where htmx + Rust fails
Rust can slow a team that is still discovering the product and picking up the language as it goes. The compiler finds disagreement. It cannot decide whether a screen is useful, and during discovery the cost of types plus compile cycles can exceed the risk that those types remove.
htmx fails when fragments turn into an invisible client state architecture, something with no name, no existence outside the head of whoever wrote it. One response updates several targets, events trigger further events, custom JavaScript accumulates.
Choosing React at that point is not a defeat. It is an accurate description of the interface.
The smaller pool of people who understand Rust, server templates, progressive enhancement, accessibility is another risk. A simple runtime can hide an expensive handover.
The hybrid fits when two independent needs are proven. The frontend has rich state and benefits from the React ecosystem, while the backend owns compute-heavy work, strict latency targets, substantial concurrency, possibly one separate contract for several clients.
Teams have to be able to test that boundary, version it, and operate it without coordinating every release.
The hybrid is not insurance against making a choice. It adds two toolchains, network failures, an API schema, authentication between layers, correlated logs, coordinated change.
If a Rust API only moves one form into a database and Next.js is its sole client, the system pays for distribution without gaining independence.
A product can start in one stack while preserving domain boundaries. The same reasoning appears in our comparison of a modular monolith and microservices.
Extract an API because ownership and measurement support it, rather than the appearance of the diagram.
On a smaller screen, scroll the table horizontally.
| Product | Recommendation |
|---|---|
| Customer portal | Next.js. Choose Axum, Askama, and htmx when the portal is mostly forms with little rich local state. |
| Internal system | Axum, Askama, and htmx. Choose Next.js for complex planning, editing, or offline work. |
| E-commerce | Next.js. Server hypermedia fits a simple catalogue and controlled purchase flow without application-like interaction. |
| Editor | Next.js. Use htmx only for simple form blocks without local history. |
| Map | Next.js. Add a Rust API when measurements confirm demanding spatial computation or a large data stream. |
| Dashboard | Next.js. Use htmx for server filters, tables, and periodic refresh without tightly linked visualisations. |
| Marketing site | Next.js or a simple static solution. Use Rust with htmx only when a significant server workflow belongs to the site. |
At Rise we use a practical heuristic. If most screens are forms, tables and sequential workflows whose state lives on the server, then Axum with Askama and htmx deserve a look, well before anyone orders React. This is not a scientific threshold or a universal rule.
It signals that React may be solving a problem the product does not have.
If a substantial part of the value comes from immediate browser manipulation, Next.js is the safer default. If both qualities are required, define the real domain contract first, and evaluate the hybrid only after that.
Handover check
Start with the decision inputs.
Then build a thin vertical slice of one representative workflow. Measure browser state, network steps, failure behaviour, transferred assets, the time one safe change takes.
This gives a more relevant basis than a generic benchmark.
Confirm ownership too, namely who updates the framework, who responds to incidents, who understands the migrations, and who can accept the product two years from now. Our guide to choosing an IT partner puts those four answers in the proposal.
Close the decision only after the vertical slice and an ownership agreement. The architecture is defensible when the future team can explain its boundaries and measure them, then change them without asking permission.
Maroš Bednár prepared the article with AI support for research and language editing. He reviewed and approved the technical conclusions, examples, sources, and final text.

Strategy, Adapter, and Factory solve different kinds of change. Learn how to recognize the moment each pattern earns its place and when a direct function is better.

Microservices buy independent change at a real operating cost. Compare both models through boundaries, ownership, data, deployment, and signals that justify extraction.

A buyer checklist for security, integrations, data roles, ownership, support, and exit planning before signing a software contract.