The backend can serve HTML applications without a Codename One client. Write HTML with a supported subset of Thymeleaf attributes, return view names from Java controllers, and use htmx when an interaction should update part of the page. The build parses the templates and produces Java renderers. Native packaging translates those renderers through ParparVM to C along with the rest of the server. There is no template engine, expression interpreter or compiler in the request path.
The runnable example is scripts/backend-mvc. It includes an in-memory product
catalog, form errors, CSRF protection, shared page fragments, and a pinned local
htmx script. Its forms work with JavaScript disabled too.
Controllers and models
Use com.codename1.backend.annotations.Controller for HTML controllers and
com.codename1.backend.mvc.Model for their model. Existing REST controllers retain
their response semantics. An HTML controller uses the same mapping annotations,
constructor injection, sessions and security chains as a REST controller.
@GetMapping("/products")
public synchronized String list(Model model) {
model.addAttribute("products", new ArrayList<HtmlProduct>(products));
return "products";
}
A string return value selects a compiled view. products selects
src/main/resources/templates/products.html; products :: rows selects that
file’s named fragment. redirect:/products redirects to a local absolute path:
303 for normal requests, or 200 with HX-Redirect for requests from htmx. A null
return is 404. Unknown view names fail the request; they never resolve file paths.
ModelAndView carries a view name and attributes added with addObject.
@ResponseBody, on the method or controller, retains the REST response rules.
An explicit HttpServer.Response is returned unchanged. A view’s default status
is 200; @ResponseStatus can change it.
Typed HTML
Declare model types with comments. Names are explicit and types are fully qualified:
<!-- cn1:model products java.util.List<com.codenameone.developerguide.backend.HtmlProduct> -->
<ul th:fragment="rows">
<li th:each="product : ${products}">
<a th:href="@{/products/{id}(id=${product.id})}"
th:text="${product.name}">Example product</a>
</li>
</ul>
The build resolves public JavaBean getters or public fields and emits direct access. Generic type arguments are preserved through getters, fields, and inherited classes or interfaces. A missing property or undeclared model name fails the build with the file and source position. At runtime, a referenced model must be present and have the declared type. A present null renders as empty text; property navigation propagates null. Numeric comparisons require non-null operands. Fragments only require model names they read. Collection loops check element types before access.
Supported directives:
th:text,th:if,th:unless, andth:each="item, status : ${items}". The optional status exposesindex,count,size,first,last,even, andodd.th:href,src,action,value,id,name,class,title,alt,placeholder,method, andfor, plusth:classappend.Boolean attributes
checked,selected,disabled,readonly,multiple,required,autofocus, andhidden.th:attr="attribute=expression, other=expression", including htmx URLs.th:fragment="name",th:insert="~{layout :: name}", and- `th:replace="~{layout
name}"`. References are static, fragments have no parameters, and recursive inclusion is a build error.
th:blockadds no wrapper.
th:object,th:field, andth:errorsfor forms.
Expressions support ${model.property}, typed list/array/map indexing, string,
number, boolean and null literals, boolean operators, equality, numeric comparisons,
addition/subtraction, and condition ? yes : no. Selection expressions *{field}
use the enclosing th:object. URL expressions use local paths with encoded path
and query arguments, such as @{/search(q=${query})}.
Dynamic text and attributes are HTML-escaped. URL arguments use URL encoding
before attribute escaping. Dynamic URL attributes reject executable URL schemes.
Dynamic htmx attributes that evaluate expressions (hx-on*, hx-vars, hx-vals,
hx-headers, hx-request, and hx-trigger) are rejected, including data-hx-*
aliases. Define those attributes as static template content.
Script sources, link-resource URLs (including stylesheets), and base URLs must be
static template attributes. Meta http-equiv directives, including refresh content,
must also be static. Named metadata such as a description can use dynamic content.
Dynamic attributes are evaluated once per rendered
element, including each iteration of a loop.
Raw HTML, dynamic script/style content, arbitrary method calls, expression
preprocessing, message expressions, inline expressions, custom dialects, and
Spring EL are outside this subset. Unsupported th: attributes fail the build.
This is a syntax-compatible subset, not the Thymeleaf library.
Forms and validation
A mapped method can receive a dedicated form DTO:
@PostMapping("/products")
public synchronized String save(@ModelAttribute("form") ProductForm form,
BindingResult errors, Model model) {
if (form.name == null || form.name.trim().isEmpty()) {
errors.rejectValue("name", "Enter a name.");
}
if (errors.hasErrors()) {
return "edit";
}
products.add(new HtmlProduct(products.size() + 1, form.name));
return "redirect:/products";
}
Form DTOs need a public no-argument constructor and writable scalar fields or setters. Supported values are strings, numeric primitives/boxes, booleans and characters. Nested objects, collections, file uploads through the form DTO, and ORM entities are refused. Bind identifiers through route parameters. Unknown submitted fields are ignored; no property path from a request is executed.
BindingResult must immediately follow its form parameter. Conversion errors are
recorded before the controller runs. Without a BindingResult, conversion failure
returns 400. reject(message) adds a global error; rejectValue(field, message)
adds a field error. Validation is application code; Bean Validation annotations
aren’t implemented.
<!-- cn1:model form com.codenameone.developerguide.backend.ProductForm -->
<form method="post" action="/products" th:object="${form}">
<label for="name">Name</label>
<input th:field="*{name}">
<span th:errors="*{name}"></span>
<button type="submit">Save</button>
</form>
th:field renders the name, a default id, and the submitted value if binding failed.
It supports inputs, textareas, scalar selects, checkboxes and radio buttons.
Boolean checkboxes use field presence, including with custom values. They emit a
hidden marker so an unchecked field binds to false. The marker shares the
checkbox’s disabled state and form association. Without a marker, Boolean fields
use strict value conversion.
th:errors="{}" renders all errors for the selected form. Use distinct explicit
ids for radio buttons in a group. Multiple-selection collection binding is deferred.
Each @ModelAttribute parameter on a route must have a distinct name; duplicate
names fail the build instead of overwriting form objects and validation results.
htmx, CSRF and assets
Ordinary hx-* attributes pass through. Put hx-post on a form and target a
named fragment’s wrapper. Htmx.isRequest(request) distinguishes fragment requests
from normal navigation and history restoration. A controller chooses the view;
request headers never select a template themselves. Return form-error fragments
with status 200 so htmx swaps them normally. Htmx.redirect, refresh, and trigger
provide response-header helpers.
The existing security chain remains responsible for CSRF enforcement. The model
contains _csrf, whose type is declared automatically. Unsafe forms include a
hidden CSRF token when one is available; htmx serializes it with the other fields.
Form action, submit-control formaction, mutating htmx URLs (hx-post,
hx-put, hx-patch, hx-delete), and static base URLs must be local
absolute paths such as /products/save, or empty to use the current document.
This applies to static and dynamic form destinations, including controls in fragments,
to prevent submissions from disclosing the generated token to another origin.
For hx-get, generated hx-params filtering excludes the CSRF parameter while
preserving the token for a native POST fallback. Explicit parameter allow lists
and exclusion lists are retained; included fragments receive their parent’s
original parameter filter, so nested POST requests don’t inherit the GET-only
CSRF exclusion. The configured CSRF parameter name is used.
When a CSRF token is available, submit controls can’t override the native method
with GET or an invalid value that defaults to GET. This restriction also applies
to controls in fragments or controls linked to a form by its ID. Use a separate
GET form for searches and previews. POST and dialog overrides remain supported.
Form enctype and submit-control formenctype support
application/x-www-form-urlencoded and multipart/form-data. Unsupported static
encodings fail the build; unsupported dynamic values fail rendering. In particular,
text/plain isn’t supported by form binding.
For requests outside forms, supply the existing token header explicitly. Defining
HTML controllers doesn’t enable a security chain automatically.
Files in src/main/resources/static are embedded at build time and served under
/static/ after controller routes. Only enumerated assets are accessible; templates
are private. Embedded assets use Cache-Control: no-cache and nosniff. Use URL-safe
ASCII filenames. Each embedded asset is limited to 2 MiB; use the existing
cn1.static.root facility for larger files.
Building and porting
Maven compiles views during process-annotations in process-classes; native
packaging also runs this compiler. Gradle uses the same engine and tracks templates
and static assets as compilation inputs. Rebuild after editing HTML. Generated
view and asset registries are replaced on each build, including after file deletion.
The first request only executes generated code.
To port a small Spring MVC application, change the controller/model imports, add
template model declarations, use dedicated scalar form DTOs, and replace unsupported
expressions or dialect features. Keep existing HTML and htmx attributes within the
supported subset. Controller advice, method-level @ModelAttribute, flash attributes,
forward: views, custom converters, dynamic fragment selectors, and internationalized
message bundles aren’t part of this first version.