31. Full-Stack Development with MALDA
This chapter shows how to use MALDA as a complete full-stack platform. The goal is to connect concepts already documented in dedicated chapters into one end-to-end architecture. The canonical single-port host is new RestServer() plus HttpServer.mount(api) — that is what malda new fullstack emits. For runnable starters, use malda new webapi, malda new fullstack, malda new game --fullstack (canvas client + score API), and the examples under Examples/Web/ (for example rest_api_server.malda and ui_controls_showcase_minimal.malda).
Examples/Web/. Course: Learn MALDA — After the core path.
31.1 What "full-stack" means in MALDA
In MALDA, a full-stack app typically combines:
- Web UI for user interactions (forms, lists, dashboards)
- REST API for business operations and integration boundaries
- Database layer for persistence
- Optional AI agent for triage, summaries, and automation
These map directly to existing manual chapters: Web UI Generation, REST API Server, REST Web Client, Database Support, and Agent Orchestration.
31.1.1 Runtime boundary: MALDA vs JavaScript
- MALDA runtime (server-side): your API handlers, database access, agent orchestration, and optional server-rendered HTML via
HttpServer,@PAGE, or@AIPAGE - Browser runtime (client-side): JavaScript/TypeScript UI code that renders the interface and calls MALDA APIs over HTTP
So the UI layer can be either server-rendered by MALDA, client-rendered in JavaScript, or a hybrid of both.
31.1.2 Delivery models
- MALDA-native full stack: MALDA backend + MALDA server-rendered UI (
@PAGE/@AIPAGE) - Hybrid full stack: MALDA backend + external browser framework UI (React/Vue/Angular/Svelte/etc.)
- Backend-only: MALDA APIs/services without a user-facing UI layer
Both MALDA-native and hybrid are full-stack because they include UI + API + data layers end to end. MALDA-native apps should share one listener: mount the API on HttpServer (31.3.1). A second port with new RestServer(8080) is the API-only template (malda new webapi), not the full-stack host.
31.1.3 Single-source target decorators
MALDA now supports target decorators so one .malda source can contain server-only, client-only, and shared code in a controlled way:
@server()/@csharp(): include declaration in C# transpilation, exclude from JavaScript transpilation.@client()/@javascript(): include declaration in JavaScript transpilation, exclude from C# transpilation.@shared(): include declaration in both C# and JavaScript transpilation targets.
Existing route decorators (@GET, @POST, @PUT, @DELETE, @PATCH, @OPTIONS, @PAGE, @AIPAGE, @ACTION, @COMPONENT, @LIVE) remain runtime/web decorators and continue to imply server participation.
Validation rules: combining @client() with route decorators is invalid, and @shared() should only use cross-target-safe helpers. Target-specific operations such as direct filesystem/database/server APIs or browser DOM/local storage helpers should stay in explicit server/client declarations.
Desktop IDE workflow: for single-source full-stack files, the Desktop IDE can render one physical .malda as multiple virtual tabs when the source opts in with explicit // @malda-section Name separators. Saving still writes one physical file, and regular include files continue to open/edit as normal physical documents. F5 starts dual debug: the interpreter on the host partition and Web Preview on the client (see 2.6.6). Ctrl+F5 still offers the Server / Client preview / Full stack run dialog.
31.2 Example scenario: Simple items app
A typical sample app supports:
- Create items from a web form
- List open items with status and priority
- Update status (open, in-progress, closed)
- Use an AI helper to classify urgency and suggest next action
Minimal data model:
{
"id": 101,
"ownerName": "Alice",
"email": "alice@example.com",
"title": "Cannot login",
"description": "Password reset link expired",
"status": "open",
"priority": "high",
"createdAt": "2026-02-14T09:15:00Z"
}
31.3 Recommended project structure
/app
/api
items-api.malda
/ui
dashboard.malda
/shared
item-model.malda
/agents
triage-agent.malda
Keep business logic in reusable functions/classes, then expose it through both API handlers and UI handlers. This prevents duplicated logic between front-end and back-end flows.
When using single-source target decorators, you can also choose a flatter structure (for example one app.malda entry) while still separating concerns by marking declarations with @server(), @client(), and @shared().
31.3.1 Single-port host: HttpServer.mount
Construct a deferred RestServer (no port), construct HttpServer with the public port, then mount before web.start(). Do not give the API its own listener, and do not start() the RestServer as a standalone server. Matching API routes run first; unmatched paths fall through to pages, @ACTION, @LIVE, and static files.
var rest = new RestServer();
var web = new HttpServer(8080);
@GET("/api/items")
function getItems(status) {
return {"status": 200, "data": []};
}
@PAGE("/items")
function itemsPage() {
return "<html><body><h1>Items</h1></body></html>";
}
web.mount(rest);
web.enableSession(sessionSecret);
rest.enableCsrf(csrfSecret);
rest.setRateLimit(200, 60, "verifiedSubOrIp");
rest.use(requireAuth, {
"except": ["/api/health"]
});
web.start();
- Configure CSRF, rate limits, and API middleware on the
RestServerinstance (29. REST API Server §29.4, §29.8–29.9, §29.13) - Session cookies usually live on
HttpServerso pages and API share the same cookie jar - A
RestServerthat already has its own listener cannot be mounted; oneHttpServermounts at most oneRestServer - Canonical starter:
Templates/fullstack/backend/app.malda - Host methods (HTTPS, static files,
setHost) stay onHttpServer— 27. HttpServer & HTML UI Generation
31.4 API layer (Item CRUD + workflow)
Define REST routes for item operations:
function ok(data) {
return {"status": 200, "data": data};
}
@GET("/api/items")
function getItems(status) {
// status can come from query string ?status=open
var items = dbQuery("SELECT * FROM Items WHERE (@status IS NULL OR status = @status)", {"status": status});
return ok(items);
}
@POST("/api/items")
function createItem(body) {
var now = dateNow();
dbExecute(
"INSERT INTO Items (ownerName, email, title, description, status, priority, createdAt) VALUES (@ownerName, @email, @title, @description, 'open', 'normal', @createdAt)",
{
"ownerName": body.ownerName,
"email": body.email,
"title": body.title,
"description": body.description,
"createdAt": now
}
);
var row = dbQuery("SELECT last_insert_rowid() AS id")[0];
return {"status": 201, "data": {"id": row.id, "createdAt": now}};
}
@PATCH("/api/items/{id}/status")
function updateItemStatus(id, body) {
dbExecute("UPDATE Items SET status = @status WHERE id = @id", {"id": id, "status": body.status});
return ok({"id": id, "status": body.status});
}
Host these routes on the mounted RestServer (31.3.1). Do not new RestServer(8080) beside the UI server in a full-stack app. Production: add @Validate, auth middleware, and the standard error envelope from 29. REST API Server (§29.4 pipeline, §29.10 validation, §29.11 errors). Returning {"status": 200, "data": ...} is an application envelope; HTTP status is still set by the framework unless you use res.status(...).
31.5 UI layer (what technology to use)
You have two common approaches:
- Server-rendered UI in MALDA: build pages with
@PAGE/@AIPAGEand serve from MALDA directly - Client-rendered UI in JavaScript/TypeScript: build a SPA/static frontend and call MALDA REST endpoints
| Model | When to choose |
|---|---|
| MALDA-native full stack | Fast delivery, fewer moving parts, server-driven pages are enough |
| Hybrid full stack | Rich SPA UX, large frontend teams, strong JS ecosystem requirements |
Framework options for the browser UI include vanilla JavaScript, React, Vue, Angular, Svelte, Next.js (frontend/API-consumer mode), Nuxt, Solid, and lightweight options like HTMX or Alpine.js. MALDA is backend-agnostic here as long as the frontend can call HTTP APIs.
31.5.0 MALDA-native component pattern
For MALDA-native full stack, this provides a pragmatic server-component baseline:
component Name(...) { ... }for server-rendered component entry points@ACTION(path)for fragment/form updates via POST@LIVE(path)for live channels (SSE contract)componentState*,componentFragment(...), andcomponentLiveEmit(...)helpers
How to choose transport: use fragment HTTP as the default for user-initiated actions (forms/buttons). Add LIVE/SSE when the UI must update without a user action (notifications, counters, queues).
Note: LIVE endpoints support channel subscriptions (for example /items/live?channel=items) and componentLiveEmit("items", ...) publishes to that channel only. The malda new fullstack scaffold also ships a sample component at /components/TicketBoard for a quick local demo.
31.5.0.1 Small items example
component ItemBoard() {
var items = componentStateGet("board", "items", []);
return "<h1>Open: " + items.length + "</h1>";
}
@ACTION("/items/add")
function addItem(body) {
var items = componentStateGet("board", "items", []);
items.append({"title": body.title, "status": "open"});
componentStateSet("board", "items", items);
componentLiveEmit("items", {"count": items.length}, "item-added");
return componentFragment("item-list", "<ul><li>" + body.title + "</li></ul>");
}
@LIVE("/items/live")
function itemsLive() {
return {"sse": true};
}
31.5.1 MALDA server-side page handlers
@PAGE("/items")
function itemsPage() {
return "<html><body><h1>Items</h1></body></html>";
}
31.5.2 Browser-side JavaScript calling MALDA API
async function loadItems() {
const r = await fetch("/api/items?status=open");
const json = await r.json();
return json.data;
}
async function closeItem(id) {
await fetch(`/api/items/${id}/status`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: "closed" })
});
}
This separation keeps UI concerns focused on rendering and interaction, while MALDA remains the source of truth for business logic and data access. Same-origin fetch to a mounted API does not need CORS. Server-side MALDA talking to an external API uses RestClient / httpGet — 30. REST Web Client.
31.5.3 Migration path from @PAGE to components
- Keep existing
@PAGEendpoints stable (no breaking migration required). - Extract view logic into
componentfunctions. - Add
@ACTIONhandlers for interactive updates and returncomponentFragment(...). - Add
@LIVEendpoint +componentLiveEmit(...)for push notifications. - Adopt this incrementally page-by-page rather than rewriting the entire UI.
Incremental migration tip: start with one form workflow (for example "create item"), validate that fragment updates and state handling are clear to the team, then expand to other pages.
31.5.4 End-to-end UI example
For a complete list/detail flow with CRUD-style controls, start from Examples/Web/ui_controls_showcase_minimal.malda, Examples/Web/ui_form_workflow.malda, and Examples/Web/rest_api_server.malda, then compose pages with ui.crudControls, @PAGE, and @POST. Key patterns:
- List page: an
@PAGEhandler builds filters/buttons/grid host viaui.crudControls(...). - Detail / relation navigation: query params drive master-detail views (for example
?ownerId=5). - CRUD: forms use
method="post"with matching@POSThandlers that validate, persist, and redirect or return a fragment. - API calls: the grid host fetches envelope JSON; the server builds
ui.dataGridfrom rows and returns a mount/patch envelope. For external APIs, useRestClient; see 30. REST Web Client. - Optimistic / error handling: show a toast on success; on validation or server error, return
componentFragment("form-errors", errorHtml)and re-render the form with inline errors.
function crudPage(req, schema, pageTitle, lookups) {
var queryParams = req.query == null ? {} : req.query;
var sessionId = ui.sessionId(req, schema.sessionDefault);
var controlsHtml = ui.crudControls(schema, sessionId, queryParams, lookups);
return controlsHtml;
}
@PAGE("/web/items")
function itemsPage(req) { return crudPage(req, itemSchema, "Items", {}); }
@POST("/web/items/add")
function addItem(req, body, res) {
var errors = [];
if (body.title == null || body.title == "") errors.append("Title required");
if (errors.length > 0) {
return {"status": 400, "body": "<ul class='error-list'><li>" + errors[0] + "</li></ul>"};
}
dbExecute("INSERT INTO items (...) VALUES (...)", body);
var row = dbQuery("SELECT last_insert_rowid() AS id")[0];
return RedirectTo("/web/items?added=" + row.id);
}
For full schema structure and template paths, see 26. Web UI Server Components (26.3.1.3 CRUD schema reference).
31.6 Database integration pattern
A practical approach is to use:
- Small repository-like functions per aggregate (for example
saveItem,findItemsByStatus) - Parameterized SQL for all input values
- Simple mapping functions from row objects to domain objects
execute / insert return the rows-affected count, not a new key. After an SQLite insert, read the id with SELECT last_insert_rowid() AS id on the same connection (SQL Server: SCOPE_IDENTITY(); PostgreSQL: INSERT … RETURNING id). There is no lastInsertId field. See 17. Database Support §17.6.
31.7 AI triage workflow (optional but powerful)
Use an agent to classify incoming item urgency and propose next action:
var client = new OpenRouterClient();
var triageAgent = new Agent(
"TriageAgent",
"item-triage",
"Classify items by priority and suggest next action.",
client
);
function triageItem(item) {
var prompt = "Classify this item and return JSON with priority and queue: " + toJSON(item);
var result = triageAgent.think(prompt);
return parseJSON(result.content);
}
Common integration pattern: call triage right after item creation, then persist priority and queue fields before returning the response.
31.8 End-to-end request flow
- Browser hits one origin, for example
http://localhost:8080/itemsorPOST /api/items HttpServerreceives the request on that listener- If a
RestServeris mounted, it tries the path first. A match runs the API pipeline (CSRF → global middleware → route middleware → rate limit →@Validate→ handler). Details: 29. REST API Server §29.4 - No API match: pages, components,
@ACTION,@LIVE, static files (27. HttpServer & HTML UI Generation) - API handler validates and persists the item, optionally calls a triage agent, then returns JSON
- UI refreshes via
GET /api/items(same origin) or a fragment@ACTION
31.9 Configuration and secrets
Keep credentials and environment-specific values outside source code:
- Use environment variables for API keys and DB credentials
- Use
getMaldaConfig()for local project/user configuration where appropriate - Centralize endpoint URLs and model names in one config module
Relevant built-ins are documented in 13. Built-in Functions.
31.10 Practical checklist for new full-stack apps
- Define API contracts first (payloads, status codes, error format)
- Mount a deferred
RestServeronHttpServerinstead of two listeners - Implement database access through small reusable functions
- Keep UI rendering separate from transport and data logic
- Add request validation and auth middleware before production use
- Add logging/correlation IDs for tracing cross-layer issues
- Introduce AI automation only where it saves measurable effort
31.10.1 Testing and troubleshooting (Web UI + API)
When integrating Web UI with REST APIs, use these practical checks:
- Fragment responses: Verify
X-Malda-FragmentandX-Malda-Fragment-Targetheaders when testing@ACTIONor@POSThandlers. The client must update only the target element. - Session continuity: Pass
sessionin query or form;ui.sessionId(req, default)reads it. Without it,componentState*andui.statemay not persist across requests. - API errors: When the UI calls
GET /api/items, distinguish HTTPresponse.statusfrom a JSON envelope. Framework failures use{"status", "error", "message", "correlationId"}— see 29. REST API Server §29.11. Application lists often use{"status": 200, "data": [...]}. - CSRF: Enable it on the mounted
RestServer(rest.enableCsrf) for JSON mutations. HTML forms needcsrfField(and the matching cookie if you also enable CSRF onHttpServer). Fetch must sendX-CSRF-Token(or your configured header). See 29.9 and the comments inTemplates/fullstack/backend/app.malda. Do not name the server variableapi— that is a keyword. - Lookups: For schema-driven CRUD, ensure
lookupscontains all keys referenced indialogLookupOptions(e.g.owners,tags). Missing lookups produce empty dropdowns.
For Web UI-specific debugging (tree divergence, event handlers, template cache), see 26. Web UI Server Components (26.3.15 Common pitfalls and debugging, 26.6 Testing and Troubleshooting).
See Also
- 26. Web UI Server Components - Building browser-facing pages and UI flows
- 27. HttpServer & HTML UI Generation - Listener, pages, HTTPS, static files,
mounthost - 29. REST API Server - Route decorators, pipeline, middleware, validation, mount
- 30. REST Web Client - Calling APIs from UI/services
- 17. Database Support - Persistence patterns and providers
- 19. Agent Orchestration - AI agents and tool-based workflows