
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.

The choice between Next.js and Axum, Askama, and htmx is not a contest between modern JavaScript and fast Rust. 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 keeps the final HTML under server control and replaces selected fragments in the page.
Our short verdict is direct. 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, administrative, and workflow applications. 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.
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 immediate browser interactions.
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.
Verified versions
- Next.js 16.2.x
- htmx 2.0.10 as the stable line
- htmx 4.0.0-beta5 as preview only
We exclude the unconfirmed claim about a specific Next.js 16.2.11 security release. Before implementation, lock versions, read the relevant release notes, and review current security advisories.
Rust here means Axum for HTTP, Askama for typed templates, and htmx for targeted requests and HTML swaps. It does not mean Leptos, Yew, or another Rust SPA layer. Those options have a different application model and deserve a separate comparison.
A React Server Component executes on the server. Its result travels in React's transport format, and the client reconciles it with the existing component tree. A Server Component can load data close to its source without shipping its own JavaScript.
Interactive behaviour still needs a Client Component boundary, hydration, and browser state wherever the interface behaves like an application.
An htmx fragment is an ordinary server response. A button may declare hx-post, hx-target, and hx-swap. The server applies a rule, Askama renders a row or panel, and htmx places it in the DOM.
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 document state, writes the transition, and the interface shows the new status. In Next.js, 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, and an audit trail. A Server Action is a publicly reachable server entry point, not 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 state transition, and Askama returns the updated table row.
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, hx-target="closest tr", and hx-swap="outerHTML". Only that element changes after the response. AppError must convert the template error too, and an error response should return a fragment that helps the person recover.
The domain transition belongs in the service, 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 must react together.
At that point the team either adds a small JavaScript island or acknowledges that the interface now has a client application model.
A list with URL filters and one open dialog works in either stack. The difference becomes clear when state must respond immediately without a server round trip.
An editor with undo history, a map with thousands of objects, a planner with draggable work, or a configurator with continuous calculations has a natural browser model. Next.js combines server loading with isolated client islands for these cases.
htmx is at its best when state is safely represented by the database, URL, submitted form, or current HTML. Instead of synchronising a client store after every mutation, the server returns a fresh view of truth. Less duplication often removes entire classes of defects.
The original advantage disappears if important data migrates into data-* attributes, a custom event bus coordinates the page, and five fragments require manual updates.

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. For an internal approval system, HTML may be the most useful contract. The form, validation error, and new row are one consistent result without parallel DTO and template mappings.
The answer changes when a native mobile application, a partner, or a public API needs the same capability. An HTML fragment is not a suitable general domain contract. JSON endpoints can live beside HTML handlers, but business rules must not be copied into two paths.
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. A product with several clients needs an explicit contract whatever renders the web interface. Decide the API boundary separately from the rendering model.
Next.js offers several caching and invalidation layers. That is useful for content and data-heavy routes, but the team must know what is static, what revalidates, and what is personal.
Failures often appear because two parts of the team assume different data lifetimes rather than because the cache itself is broken.
htmx does not prescribe cache policy. It uses HTTP. Full pages and safe GET fragments can use response headers, a reverse proxy, or a CDN.
Personal fragments need correct Cache-Control and Vary behaviour. Mutations should return a fresh representation or initiate a follow-up read. A smaller abstraction does not forgive incorrect HTTP semantics.
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, and consistent build identifiers.
The framework handles much of the application work, but its production topology still needs deliberate design.
An Axum application can become one Rust binary with templates embedded at compile time and a directory of static assets. That is an attractive operating shape. Compilation, cross builds, database migrations, telemetry, and incident procedures remain.
Fewer runtime layers do not mean no operational work.
Rust can offer predictable resource use and precise control over concurrent work. That does not prove a lower total product cost. The database, third-party calls, images, and one poor query often dominate latency.
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, and current object state for every mutation. A hidden input, React prop, or DOM attribute is untrusted.
Output must be escaped, and any deliberate HTML injection needs narrowly defined sanitisation.
Cookie authentication requires a CSRF assessment based on the method, request origin, and cookie policy. An htmx header or Server Action does not replace authorisation. Rust ownership prevents a 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 in which the team can maintain one authoritative domain path, controlled dependencies, prompt updates, and 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, explicit error paths, and compiler can reduce some accidental changes. If the future owner does not know Rust, however, an elegant binary becomes an organisational risk. 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, and dependency upgrades are architectural deliverables.

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 basic forms only because it knows React. Unnecessary hydration, duplicated state, complicated invalidation, and Server Action boundaries can obscure a simple business process.
The framework also changes quickly enough that upgrades require regular attention.
Another failure appears when Next.js becomes an accidental integration layer and domain backend without modules. Interface code, authorisation, provider calls, and long-running jobs mix inside routes. The product then has no clear mobile contract and no obvious place to test a business rule safely.
Where htmx + Rust fails
Rust can slow a team that is still discovering the product and learning the language. The compiler finds technical disagreement, but it cannot decide whether a screen is useful. During rapid discovery, the immediate cost of types and compile cycles can exceed the current risk.
htmx fails when fragments turn into an invisible client state architecture. One response updates several targets, events trigger more events, and custom JavaScript accumulates. The result is difficult to trace.
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, and 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. The backend owns compute-heavy work, strict latency targets, substantial concurrency, or a separate contract for several clients.
Teams can test, version, and operate the boundary independently.
The hybrid is not insurance against making a choice. It adds two toolchains, network failures, an API schema, authentication between layers, correlated logs, and 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 measurements support it, not because the architecture diagram looks balanced.
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 roughly 80 percent of screens are forms, tables, and sequential workflows whose state naturally lives on the server, Axum, Askama, and htmx deserve serious consideration. 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 then evaluate a hybrid.
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, and the time needed for a safe change.
This gives a more relevant basis than a generic benchmark.
Confirm ownership too. Decide who updates the framework, responds to incidents, understands migrations, and can accept the product two years later. Our guide to choosing an IT partner explains why those answers belong in the proposal and handover plan.
Close the decision only after the vertical slice and an ownership agreement. The architecture is defensible when the future team can explain its boundaries, measure them, and change them safely.
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.