MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.39

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).

Why this page is not a lesson: Full-stack samples bind a port or compile to JavaScript. That cannot run in the manual snippet harness. Start from 25. Web UI Overview and 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:

These map directly to existing manual chapters: Web UI Generation, REST API Server, REST Web Client, Database Support, and Agent Orchestration.

Web vocabulary: Middleware, authentication vs authorization, JWT, CSRF, CORS, rate limits, and HTTPS are explained in 29.1.1 Concepts primer. Client-side HTTP status vs transport errors: 30.1.1.

31.1.1 Runtime boundary: MALDA vs JavaScript

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

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:

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:

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();

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:

ModelWhen to choose
MALDA-native full stackFast delivery, fewer moving parts, server-driven pages are enough
Hybrid full stackRich 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:

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

  1. Keep existing @PAGE endpoints stable (no breaking migration required).
  2. Extract view logic into component functions.
  3. Add @ACTION handlers for interactive updates and return componentFragment(...).
  4. Add @LIVE endpoint + componentLiveEmit(...) for push notifications.
  5. 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:

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:

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

  1. Browser hits one origin, for example http://localhost:8080/items or POST /api/items
  2. HttpServer receives the request on that listener
  3. If a RestServer is 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
  4. No API match: pages, components, @ACTION, @LIVE, static files (27. HttpServer & HTML UI Generation)
  5. API handler validates and persists the item, optionally calls a triage agent, then returns JSON
  6. 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:

Relevant built-ins are documented in 13. Built-in Functions.

31.10 Practical checklist for new full-stack apps

31.10.1 Testing and troubleshooting (Web UI + API)

When integrating Web UI with REST APIs, use these practical checks:

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