29. 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. 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).
29.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.
29.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.
29.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.
29.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.
29.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"
}
29.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().
29.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();
var insert = 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
}
);
return {"status": 201, "data": {"id": insert.lastInsertId, "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});
}
Production recommendation: add validation, auth middleware, and consistent error payloads. See 27. REST API Server.
29.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.
29.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.
29.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};
}
29.5.1 MALDA server-side page handlers
@PAGE("/items")
function itemsPage() {
return "<html><body><h1>Items</h1></body></html>";
}
29.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.
29.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.
29.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 28. 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>"};
}
var id = dbExecute("INSERT INTO items (...) VALUES (...)", body).lastInsertId;
return RedirectTo("/web/items?added=" + id);
}
For full schema structure and template paths, see 24. Web UI Server Components (24.3.1.3 CRUD schema reference).
29.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
For provider setup and connection details, see 16. Database Support.
29.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.
29.8 End-to-end request flow
- User submits item form in Web UI
- UI sends
POST /api/items - API validates and persists initial item
- API optionally calls triage agent
- API updates priority/queue and returns JSON
- UI refreshes list via
GET /api/items
29.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.
29.10 Practical checklist for new full-stack apps
- Define API contracts first (payloads, status codes, error format)
- 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
29.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, ensure the API returns consistent JSON ({"status": 200, "data": [...]}). Use 27. REST API Server validation and error payloads. - CSRF: If
enableCsrfis on, the client must send the token. Check that forms include the hidden field and fetch requests use the correct header. - 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 24. Web UI Server Components (24.3.15 Common pitfalls and debugging, 24.6 Testing and Troubleshooting).
See Also
- 24. Web UI Server Components - Building browser-facing pages and UI flows
- 27. REST API Server - Route decorators, middleware, validation, responses
- 28. REST Web Client - Calling APIs from UI/services
- 16. Database Support - Persistence patterns and providers
- 18. Agent Orchestration - AI agents and tool-based workflows