29. REST API Server
MALDA includes native support for JSON-first REST APIs. Decorate functions with HTTP methods, then host them on RestServer (standalone) or mount that server on HttpServer for a single-port full-stack app.
RestServer, HttpServer, and sessions run on the interpreter and C# transpile. They are not available on the JavaScript backend — keep the server on the host and call it from the browser (30. REST Web Client). See the backend capability matrix. Interpret vs transpile handler notes are in 29.14.
@PAGE, HTTPS, static files, multipart uploads, and SSE live in 27. HttpServer & HTML UI Generation. Calling APIs from MALDA is 30. REST Web Client. JWT, CSRF, and password helpers are in 29.16 (name index: 13.25).
Examples/Web/rest_api_server.malda. Start from 25. Web UI Overview. Course: Learn MALDA — After the core path.
29.1 Chapter Map
- 29.1.1 Concepts primer — REST, middleware, auth, CSRF, CORS, HTTPS, security map
- 29.2 HTTP and route-metadata decorators
- 29.3
RestServerconstructor, properties, and methods - 29.4 Request pipeline order
- 29.5 Path, query, body, and decorator binding
- 29.6
req,req.auth, andreq.session - 29.7 Return shapes and
reshelpers - 29.8 Global and route middleware
- 29.9 CSRF, rate limiting, and trusted proxy
- 29.10
@Validateand handler-time validation - 29.11 Error contract
- 29.12 CORS and Swagger/OpenAPI
- 29.13 Mounting on
HttpServer - 29.14 Interpret vs transpile notes
- 29.15 Complete example and templates
- 29.16 Password, JWT, and cookie helpers
29.1.1 Concepts primer
The later sections assume a few web terms. This primer is enough to follow the MALDA APIs without an external glossary; it is not a full HTTP textbook. Skim it once, then jump to the section that names the API you need. A one-page threat-to-defense map sits at the end.
REST and HTTP
HTTP is the request/response protocol the browser and your server already speak. A client sends a method (GET, POST, PUT, PATCH, DELETE, OPTIONS) plus a path, optional headers, and sometimes a body. The server answers with a status code plus headers and a body.
REST here means: expose operations as HTTP routes that exchange JSON. @GET("/api/users/{id}") is a read; @POST("/api/users") creates; PUT/PATCH update; DELETE removes.
| Piece of the request | Example | Where MALDA puts it |
|---|---|---|
| Method + path | GET /api/users/12 | @GET("/api/users/{id}"); id is a path parameter |
| Query string | ?status=open | Handler argument or req.query |
| Headers | Authorization: Bearer … | req.headers, or req.auth after JWT middleware |
| JSON body | {"name": "Ada"} | Handler body argument or req.body |
Status families: 2xx success, 4xx the client sent something the server will not accept, 5xx the server failed. Codes you will see often: 200 OK, 400 validation failed, 401 not authenticated, 403 authenticated but not allowed, 404 no such route or resource, 429 rate limited, 500 uncaught server error. MALDA's JSON error envelope is 29.11.
GET and PUT are usually idempotent (repeating them does not create a second resource). POST often is not. That matters for retries — see 30.1.1.
Middleware
A route handler should do the business work (load a user, save a row). Middleware is a function that runs before that handler, for work that many routes share: logging, authentication, attaching a tenant id. Think of a conveyor: every request passes through the same gates, then the specific handler.
The MALDA contract is (req, res, next). Call next() to continue the chain; skip next() (or send a response) to stop — that is how you reject an unauthenticated caller without running the handler. Global middleware is server.use(...); per-route is @Use("name"). Details: 29.8. Pipeline order: 29.4.
function logRequest(req, res, next) {
print(req.method + " " + req.path);
next();
}
function requireAuth(req, res, next) {
req.auth.authenticateBearerJwt("replace-me");
next();
}
var server = new RestServer(8080);
server.use(logRequest);
server.use(requireAuth, { "except": ["/api/health"] });
Request flow, left to right: CSRF (if enabled) → global middleware → route @Use → rate limit → @Validate → handler. Auth middleware must run before the rate limiter if you want to key limits on the verified user.
Authentication vs authorization
Authentication answers “who is this?”. Authorization answers “may they do this?”. Login proves identity; a role or permission check decides access. In MALDA, req.auth.authenticateBearerJwt(secret) authenticates; req.auth.requireRole("admin") authorizes. A missing or bad token is HTTP 401; a known user lacking a role is 403. Never treat “the request has an X-User-Id header” as authentication — anyone can send that header. See 29.6.2.
JWT and Bearer tokens
A JWT (JSON Web Token) is a signed string: a JSON payload (claims such as sub, role, expiry) plus a signature the server verifies with a secret. The client usually sends it as Authorization: Bearer <token>.
Three words that are easy to mix up: encoding (Base64, reversible, not a secret), hashing (one-way, for passwords — next heading), signing (proves the token was minted with your secret; MALDA JWTs are signed, not encrypted). Anyone who has the JWT can read the claims, so do not put passwords in them, treat the token like a password, keep the signing secret off the client, and set a short expiry.
Create tokens with createJwt (29.16); verify them in middleware with authenticateBearerJwt. Cookie-based JWTs use authenticateCookieJwt instead of the Authorization header.
Password hashing
Never store a login password as plaintext. A password hash is a one-way value: given the hash you cannot recover the password, but verifyPassword can check a later login against it. MALDA uses salted PBKDF2 (hashPassword / verifyPassword in 29.16). A salt is random data mixed in so two users with the same password do not get the same hash. This is not JWT signing and not HTTPS encryption — it is only for credentials at rest.
Sessions and cookies
A cookie is a small name/value the server sets on the response; the browser sends it back on later requests to the same site. Flags that matter:
- HttpOnly — JavaScript on the page cannot read it (helps against stolen-session XSS).
- Secure — send it only over HTTPS.
- SameSite (Lax / Strict / None) — limits sending the cookie on cross-site requests. Lax is the MALDA default and blocks most CSRF from other sites on POST; Strict is tighter; None requires Secure and is for cross-site APIs that deliberately use cookies.
A session is server-side storage keyed by a session-id cookie — req.session after enableSession. Prefer sessions or cookie JWTs for browser apps (the token is not sitting in JavaScript). Prefer Bearer JWTs for programmatic API clients (scripts, other servers, mobile). Cookie defaults: 29.7.2. Session API: 29.6.3.
CSRF
CSRF (Cross-Site Request Forgery) is a trick where another site causes the user's browser to send a request to yours with the user's cookies. The user is logged in, so the request looks legitimate — for example a hidden form on evil.example that POSTs to https://your-app/api/transfer.
Defense: the real page includes a secret token (header or hidden form field) that the attacker site cannot read (the browser's same-origin policy blocks that). Safe methods (GET/HEAD/OPTIONS) may mint the cookie; POST/PUT/PATCH/DELETE must present a matching token. Bearer JWT APIs called from non-browser clients do not send cookies, so CSRF usually does not apply to them; it does apply to cookie sessions and cookie JWTs. Enable with enableCsrf. Details: 29.9.
CORS
Browsers isolate pages by origin (scheme + host + port). https://app.example and https://api.example are different origins; so are http://localhost:5173 and http://localhost:8080. A page cannot read a response from another origin unless that API opts in with CORS headers. The browser may first send an OPTIONS preflight asking “may I call you this way?”.
CORS is not authentication and not CSRF. Same-origin calls (the MALDA UI and API on one HttpServer port) do not need it. Host-side RestClient is not a browser, so CORS does not apply to it. Enable CORS only if browsers on another origin will call you. Details: 29.12 and 30.9.
XSS
XSS (Cross-Site Scripting) is injecting script into a page the victim's browser will run — for example concatenating a username into HTML without escaping. The injected script can then read non-HttpOnly cookies or call your API as that user. MALDA does not auto-escape strings you put in HTML: treat every untrusted value as data, not markup, especially in @PAGE handlers (chapter 27). HttpOnly cookies reduce the blast radius; they do not fix XSS.
Rate limiting and trusted proxies
Rate limiting caps how many requests a key (IP, user, token) may make in a window, so a buggy client or attacker cannot overwhelm the server or brute-force a login. MALDA's limiter runs after middleware so it can key on a verified JWT subject.
A reverse proxy (nginx, Caddy, a cloud load balancer) sits in front of your app, often terminating TLS. The TCP peer your process sees is then the proxy, not the user. X-Forwarded-For carries the client IP — but anyone can spoof that header unless you only trust it from your proxy (configureTrustedProxy). Details: 29.9.
HTTPS / TLS
HTTPS is HTTP over TLS: the connection is encrypted and the server presents a certificate so clients know they reached the right host. Without it, cookies, passwords, and Bearer tokens travel in the clear on the network. Enable TLS on HttpServer with enableHttps (chapter 27). Standalone RestServer does not terminate TLS; mount it on HttpServer or put a reverse proxy in front. Keep signing secrets and certificate passwords in environment variables, not in source.
Correlation IDs, validation, and OpenAPI
A correlation ID is a string that tags one request across logs and error payloads so you can find “what happened to this call”. MALDA honors inbound X-Correlation-ID or generates one (29.11).
Validation checks that path, query, and body match a schema before your handler runs, so bad input fails with 400 instead of crashing later. That is @Validate (29.10) — related to, but not the same as, prompt/LLM validate() in chapter 10.
OpenAPI (often called Swagger) is a machine-readable description of your routes, parameters, and schemas. MALDA can serve it as JSON when you call enableSwagger(true) (29.12). Humans and code generators read it; it is not a security control.
Which defense for which threat
| If you worry about… | Concept | MALDA switch |
|---|---|---|
| A caller pretending to be a user | Authentication | authenticateBearerJwt / cookie JWT / session |
| A logged-in user doing admin work | Authorization | requireRole / requirePermission |
| Passwords stolen from your database | Password hashing | hashPassword / verifyPassword |
| Another site submitting a form as the user | CSRF | enableCsrf (cookie sessions / cookie JWTs) |
| A page on another origin calling your API | CORS | enableCORS only if you want that |
| Script injected into your HTML | XSS | Do not concatenate untrusted strings into HTML |
| Tokens or cookies sniffed on the wire | HTTPS | enableHttps or a TLS reverse proxy |
| Floods or login brute-force | Rate limiting | setRateLimit |
| Wrong client IP behind nginx | Trusted proxy | configureTrustedProxy |
29.2 Decorator Syntax
Functions decorated with an HTTP method become REST endpoints when a RestServer starts (or is mounted):
@GET("/api/users")
function getUsers() {
return parseJSON("[{\"id\": 1, \"name\": \"Alice\"}]");
}
@POST("/api/users")
function createUser(body) {
var jsonStr = "{\"id\": 3, \"name\": \"" + body.name + "\"}";
return parseJSON(jsonStr);
}
29.2.1 Supported HTTP Methods
@GET(path)- Handle GET requests.HEADmatches the same route; the body is omitted.@POST(path)- Handle POST requests@PUT(path)- Handle PUT requests@DELETE(path)- Handle DELETE requests@PATCH(path)- Handle PATCH requests@OPTIONS(path)- Handle OPTIONS requests (CORS preflight is handled separately when CORS is enabled)
Duplicate method+path pairs throw at startup. Use getRoutes() to inspect the registered table.
29.2.2 Route Metadata Decorators
Optional metadata decorators are applied before the route is registered. Prefixes compose as group + version + path.
@RouteGroup(prefix)/@Group(prefix)/@Prefix(prefix)- prepend a group prefix@Version(version)/@ApiVersion(version)- prepend a version segment@Use("middlewareName")/@Middleware("middlewareName")- route-level middleware by function name (a string, not an inline function)@Validate(schema)- request validation schema (object or JSON string)
@RouteGroup("/api")
@Version("v1")
@Use("requireAuth")
@GET("/users/{id}")
function getUserById(id, req, res) {
return res.json(parseJSON("{\"id\":\"" + id + "\"}"));
}
29.3 RestServer Class
29.3.1 Constructor
var server = new RestServer(port);
// OR
var server = new RestServer(port, host); // host: "localhost" or "0.0.0.0"
// OR
var api = new RestServer(); // port 0: deferred / mounted
port must be 0 (deferred/mounted) or an integer in 1-65535. Privileged ports (1-1023) are allowed; binding may still require elevated permissions or OS URL reservations. Host defaults to localhost. Pass 0.0.0.0 to listen on all interfaces. Unlike HttpServer, there is no setHost method and no MALDA_HTTP_HOST override.
29.3.2 Properties
| Property | Type | Notes |
|---|---|---|
port | int | Configured port; 0 when deferred/mounted |
isRunning | bool | Standalone listener, or mirrors the host after mount |
29.3.3 Methods
| Method | Notes |
|---|---|
start() | Scan decorated functions, reject route conflicts, print the route table, bind HttpListener. Mounted: no listener; isRunning follows the host |
stop() | Stop the standalone listener |
getRoutes() | Array of {method, path} |
use(middleware, options?) | Global middleware: function, or function-name string in transpile. options.except skips exact paths or trailing-* prefixes |
enableCORS(enabled) / setCORSOrigin(origin) / setCORSMethods(array) / setCORSHeaders(array) | CORS. Defaults: origin *, methods GET/POST/PUT/DELETE/PATCH/OPTIONS, headers Content-Type and Authorization |
enableSwagger(enabled) | Serve OpenAPI JSON (see 29.12) |
enableCsrf(secret, cookieName?, headerName?) / disableCsrf() | Built-in CSRF. Defaults: cookie csrf_token, header X-CSRF-Token |
enableSession(secret, options?) / disableSession() | Signed session cookie and req.session. Options: cookieName (default malda_session), maxAge seconds (default 14 days), store "memory" or "sqlite", sqlitePath |
setRateLimit(limit, windowSeconds, keyStrategy?) / disableRateLimit() | Per-key limit. Strategies: ip, token, user (X-User-Id), sub / verifiedSub / verifiedSubOrIp, ipOrToken (default) |
setRateLimitHeaders(enabled, includeRemaining?) | X-RateLimit-Limit, optional X-RateLimit-Remaining, and Retry-After on 429 |
configureTrustedProxy(enabled, headerName?, hopIndex?) | Off by default. When enabled, read client IP from X-Forwarded-For (or headerName) at hopIndex (default 0) |
29.3.4 What RestServer does not host
Standalone RestServer is API-only. These belong on HttpServer (chapter 27): HTTPS / enableHttps, setHost / MALDA_HTTP_HOST, static files, @PAGE / @AIPAGE, multipart/form-data uploads, and SSE ({"sse": true}). The shared pieces are req / res / req.auth / req.session, CSRF, rate limits, and route metadata.
29.4 Request Pipeline
Every request runs this order. Knowing it matters for auth keys, CSRF, and validation:
- Resolve correlation ID: honor inbound
X-Correlation-IDor generate one; echo it on the response - CORS preflight (
OPTIONS) when CORS is enabled, then CORS headers on the real request - Swagger static paths when enabled
- Route match; no match → 404 standard envelope
- Parse body for POST / PUT / PATCH
- CSRF (before any application middleware)
- Global middleware (
server.use) - Route middleware (
@Use/@Middleware) - Rate limit (after middleware, so
verifiedSub*seesreq.auth) @Validateschema- Parameter binding and handler
- Serialize or commit the response
Interpreted handlers run on an isolated interpreter per request. In interpret mode, assigning req.tenant = "acme" in middleware is visible to later middleware and the handler on that request only.
29.5 Parameter Binding
29.5.1 Path parameters
Placeholders in the route pattern bind by name:
@GET("/api/users/{id}")
function getUserById(id) {
var jsonStr = "{\"id\": \"" + id + "\", \"name\": \"Alice\"}";
return parseJSON(jsonStr);
}
29.5.2 Query parameters
Unmatched parameter names bind from the query string:
@GET("/api/users")
function getUsers(limit, offset) {
// 'limit' and 'offset' extracted from ?limit=10&offset=0
var jsonStr = "{\"users\": [], \"limit\": " + limit + "}";
return parseJSON(jsonStr);
}
29.5.3 Request body
A parameter named body receives the parsed payload. Content types:
application/json→ object / array (invalid JSON falls back to the raw string)application/x-www-form-urlencoded→ object of string fields- anything else → raw string
RestServer does not parse multipart/form-data. Use HttpServer for file uploads.
@POST("/api/users")
function createUser(body) {
var name = body.name;
var jsonStr = "{\"id\": 1, \"name\": \"" + name + "\"}";
return parseJSON(jsonStr);
}
29.5.4 Special parameter names
These names always bind, in both name-based and decorator modes:
| Parameter | Binds to |
|---|---|
req / request | Request context |
res / response | Response context |
params / pathParams | Path-parameter object |
query / queryParams | Query-parameter object |
headers | Request headers |
cookies | Request cookies |
body | Parsed body (name-based mode) |
29.5.5 Decorator binding
Optional renaming. If any parameter has a decorator, undecorated parameters bind null (special names above still bind):
@PUT("/api/users/{id}/posts/{postId}")
function updatePost(@PathParam("id") userId, @PathParam("postId") postId,
@QueryParam("limit") maxResults, @Body() updateData) {
// userId = path param {id}
// postId = path param {postId}
// maxResults = query param ?limit=10
// updateData = request body
}
In transpile mode the compiler registers routes with paramDecorators = null, so @PathParam / @QueryParam / @Body renaming works in the interpreter only. Prefer matching parameter names to path/query/body when you compile.
29.6 Request Context
29.6.1 req properties and helpers
| Member | Notes |
|---|---|
method, path, pathBase, url, queryString | Request line |
scheme / protocol, host, contentType | Connection and content |
query / queryParams, params / pathParams, headers, cookies, body | Parsed maps / payload |
correlationId | Inbound or generated; also sent as X-Correlation-ID |
ip / remoteIp | Peer IP; proxy headers ignored unless configureTrustedProxy is on |
auth / authContext | See 29.6.2 |
session | See 29.6.3 |
verifiedSub | Verified subject or null |
header(name, default?), queryParam(name, default?), param(name, default?), cookie(name, default?) | Lookups |
hasHeader / hasQueryParam / hasParam / hasCookie | Boolean presence |
validate(schema) | Returns {ok, errors, message?} without throwing |
requireValid(schema) | Throws a 400 ValidationError envelope |
29.6.2 req.auth
Prefer req.auth.authenticateBearerJwt(secret) (APIs) or authenticateCookieJwt(cookieName, jwtSecret, cookieSecret?) (browser sessions). Both verify the JWT, populate claims/roles/permissions, and throw a standardized 401 on failure. Create tokens with createJwt (29.16).
| Member | Notes |
|---|---|
verified | True after a successful authenticate / setVerifiedSub |
sub / subject | Verified subject, or null |
claims | JWT payload object |
roles, permissions | From role/roles and permission/permissions/scope/scp |
token / bearerToken | Raw bearer token, or null |
authenticateBearerJwt(secret) | Authorization Bearer JWT |
authenticateCookieJwt(cookieName, jwtSecret, cookieSecret?) | JWT in a (optionally signed) cookie |
claim(name, default?), hasClaim(name) | Claim lookup |
hasRole(role), requireRole(role) | requireRole throws 403 Forbidden |
hasPermission(p), requirePermission(p) | Same pattern for permissions |
requireVerified() | Throws 401 if anonymous |
setVerifiedSub(sub) | Trust-boundary helper after an upstream check |
setAnonymous() / clear() | Clear the principal |
function requireAuth(req, res, next) {
req.auth.authenticateBearerJwt("my-jwt-secret");
next();
}
@GET("/api/admin/stats")
function adminStats(req) {
req.auth.requireRole("admin");
return {
"users": 2
};
}
var server = new RestServer(8080);
server.use(requireAuth, {
"except": ["/api/health", "/api/login"]
});
server.start();
Skip public paths with use(middleware, { "except": [...] }). Exact paths and trailing * prefixes are supported. Ingress aliases X-Malda-Auth-Verified / X-Malda-Auth-Sub (and legacy X-Auth-* / X-Verified-Sub) can pre-mark a subject; application middleware should still call authenticate* so roles and claims are present.
29.6.3 req.session
Available after enableSession(secret, options?). Same surface as HttpServer:
| Member | Notes |
|---|---|
id | Session id |
get(key) / set(key, value) / delete(key) / clear() | Server-side bag |
flash(key, value), getFlash(key), getFlashes() | One-request notices (typical after redirect) |
29.7 Response Handling
29.7.1 Return shapes
- Plain object / array → JSON 200
- Plain string →
text/plain200 { "status": 201, "data": ... }→ HTTP status fromstatus; that field is stripped from the JSON body- Standard error object
{status, error, message}(see 29.11) → that status, body unchanged res.status(...).json(...)(or other send helpers) → already committed; the return value is ignored
@POST("/api/users")
function createUser(body) {
var jsonStr = "{\"status\": 201, \"data\": {\"id\": 1, \"name\": \"" + body.name + "\"}}";
return parseJSON(jsonStr); // Returns 201 Created
}
@GET("/api/users/{id}")
function getUser(req, res) {
var id = req.params.id;
return res.status(200).json(parseJSON("{\"id\": \"" + id + "\"}"));
}
29.7.2 Response helper API
| Member | Notes |
|---|---|
status(code) | Set status; chainable |
json / text / html / redirect / send / sendBase64 / fragment | Send and close immediately |
header(name, value), setContentType(type) | Headers before send |
cookie(name, value, options?), clearCookie(name, options?) | Defaults: HttpOnly=true, Secure=true, SameSite=Lax, Path=/. Options: domain, maxAge, sameSite, httpOnly, secure, path |
flush() | Send whatever is already committed (including status-only) |
statusCode, contentType / type, headers, body | Readable snapshot |
committed / sent / isCommitted, flushed / isFlushed | Send state |
res.send(value) UTF-8-encodes a string body. Use res.sendBase64(b64) for binary payloads (PDF, DOCX, and similar): it decodes the base64 string and writes those bytes. Invalid base64 throws.
After a send helper, the handler may keep running (background work). Headers and cookies set after send throw response already sent. res.fragment(targetId, html) is the early-send form of componentFragment(targetId, html): same X-Malda-Fragment headers, but the client receives the fragment before the handler returns.
29.8 Middleware
Middleware is a shared function that runs before the route handler — see 29.1.1 if the term is new. Use server.use(...) for global middleware. The contract is (req, res, next). Call next() to continue; omitting it short-circuits. Route-level @Use("name") runs after global middleware and before validation + the handler. Transpiled apps should pass the function name as a string: server.use("requireAuth").
function logRequest(req, res, next) {
print(req.method + " " + req.path);
next();
}
var server = new RestServer(8080);
server.use(logRequest);
server.start();
29.9 CSRF, Rate Limiting, and Trusted Proxy
What these defenses are, in plain language: CSRF, rate limiting and trusted proxies.
var server = new RestServer(8080);
server.enableCsrf("my-csrf-secret"); // cookie: csrf_token, header: X-CSRF-Token
server.setRateLimit(120, 60, "verifiedSubOrIp"); // after auth middleware; prefer subject when verified
server.setRateLimitHeaders(true, true); // add X-RateLimit-* + Retry-After headers
server.configureTrustedProxy(true, "X-Forwarded-For", 0); // trust first forwarded hop
server.use(requireAuth, { "except": ["/api/health", "/metrics"] });
server.start();
CSRF runs before middleware. Safe methods (GET/HEAD/OPTIONS) issue or refresh the CSRF cookie when missing or invalid. State-changing methods (POST/PUT/PATCH/DELETE) require a matching cookie plus request token from X-CSRF-Token, body.csrfToken, or body._csrf. Pair this with generateCsrfToken / csrfField from chapter 13 for browser clients.
Rate limiting runs after global and route middleware. verifiedSub* strategies key on the subject set by authenticateBearerJwt / authenticateCookieJwt; otherwise they fall back to IP/token. user keys on the X-User-Id header. Proxy headers are ignored until configureTrustedProxy(...) is enabled.
CSRF, auth, and rate-limit failures use the standard error envelope and include the correlation ID.
29.10 Validation
@Validate(...) checks path, query, and body before the handler. Invalid requests return HTTP 400 with field-level details and do not run handler logic. Schema values may be a JSON string or an object. Rules are a pipe DSL (int|required|min=1) or an object {type, required, min, max, minLength, maxLength, pattern}.
@GET("/search/{id}")
@Validate("{\"path\":{\"id\":\"int|required|min=1\"},\"query\":{\"q\":\"string|required|minLength=2\"}}")
function search(id, q) {
return parseJSON("{\"ok\": true}");
}
For checks that depend on loaded data, call req.requireValid(schema) or req.validate(schema) inside the handler. Validation details items are {location, field, message}. When Swagger is enabled, @Validate metadata is reflected in OpenAPI parameter and request-body schemas.
29.11 Error Handling
Framework failures (404, CSRF, auth, rate limit, @Validate / req.requireValid, uncaught handler errors) use one JSON contract:
{
"status": 401,
"error": "InvalidToken",
"message": "Invalid token signature.",
"correlationId": "...",
"details": [ ... ] // optional, present for validation errors
}
- Response header:
X-Correlation-ID(honors an inbound value) - Payload field:
correlationId - Validation
details: array of{location, field, message} - Set
MALDA_WEB_DEBUG_ERRORS=1(ortrue) to add adiagnosticsobject on 500s
A route miss is {"status": 404, "error": "NotFound", ...}. Uncaught throw of a plain object is not a custom HTTP status: it becomes 500 InternalServerError. To send a structured client error, return the standard envelope or use res.status(code).json(...):
@GET("/api/users/{id}")
function getUserById(id, res) {
if (!userExists(id)) {
return {
"status": 404,
"error": "NotFound",
"message": "User not found"
};
}
return res.json({
"id": id
});
}
req.auth.requireRole, requirePermission, requireVerified, and req.requireValid already throw that envelope.
29.12 CORS and Swagger/OpenAPI
CORS is the browser's cross-origin gate; host-side RestClient ignores it. Primer: 29.1.1 CORS.
var server = new RestServer(8080);
server.enableCORS(true);
server.setCORSOrigin("*");
server.setCORSMethods(["GET", "POST", "OPTIONS"]);
server.setCORSHeaders(["Content-Type", "Authorization"]);
server.enableSwagger(true);
server.start();
// OpenAPI JSON: /swagger.json, /openapi.json, /swagger/openapi.json
With Swagger enabled, GET those three paths (same document). Validation constraints from @Validate appear in the generated schemas.
29.13 Mounting on HttpServer
For a single listener that serves API routes first, then pages/components/static files, construct a deferred RestServer and mount it. Do not give the API its own port, and do not start() it as a standalone listener before mount.
var rest = new RestServer();
var web = new HttpServer(8080);
function requireAuth(req, res, next) {
req.auth.authenticateBearerJwt(jwtSecret);
next();
}
@GET("/api/health")
function health() {
return {
"status": "ok"
};
}
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 - Session cookies often live on
HttpServer(pages + API share the listener) - Mounted
api.start()does not bind a port; it only marks the API as running with the host - A
RestServerthat already has its own listener cannot be mounted - Canonical starter:
malda new fullstack→Templates/fullstack/backend/app.malda
29.14 Interpret vs Transpile
- Name-based binding,
@RouteGroup/@Version/@Use/@Validate, andreq/reswork in both modes @PathParam/@QueryParam/@Bodyrenaming is interpret-only (see 29.5.5)- In transpile,
@GETand@POSTare also registered onHttpServer. A compiled process that constructs both servers can see the same GET/POST routes on each - Pass middleware to
useas a function-name string in transpile:server.use("requireAuth", { "except": ["/api/health"] }) - Route-level
@Usealways takes a function name string
29.15 Complete Example
function json(str) {
return parseJSON(str);
}
@GET("/api/health")
function healthCheck() {
return json("{\"status\": \"healthy\"}");
}
@GET("/api/users/{id}")
function getUserById(id) {
var jsonStr = "{\"id\": \"" + id + "\", \"name\": \"Alice\"}";
return json(jsonStr);
}
@POST("/api/users")
function createUser(body) {
var jsonStr = "{\"status\": 201, \"data\": {\"id\": 3, \"name\": \"" + body.name + "\"}}";
return json(jsonStr);
}
var server = new RestServer(8080);
server.start();
while (server.isRunning) {
sleep(1000);
}
Richer starters in the repo:
Examples/Web/rest_api_server.malda/Examples/rest_api_example.malda— CRUD + SwaggerExamples/Web/rest_bearer_jwt.malda— login, Bearer JWT,requireRolemalda new webapi— API-only templatemalda new fullstack— mountedRestServer+HttpServer
29.16 Password, JWT, and cookie helpers
These helpers implement the tokens and hashes that this chapter and 27. HttpServer wire into HTTP. Primers: password hashing, JWT, CSRF. Handler surface: 29.6.2 req.auth. Name index: 13.25.
var passwordHash = hashPassword("my-password"); // PBKDF2-SHA256 hash
var isValid = verifyPassword("my-password", passwordHash); // true/false
var claims = {"sub": "user-123", "role": "admin"};
var token = createJwt(claims, "my-jwt-secret", 3600); // expires in 3600s
var token2 = createJwt(claims, "my-jwt-secret", {
"expiresInSeconds": 3600,
"issuer": "malda-core",
"audience": "malda-apps",
"notBeforeSeconds": 0
});
var verifiedClaims = verifyJwt(token2, "my-jwt-secret", {
"issuer": "malda-core",
"audience": "malda-apps"
});
print(verifiedClaims.sub);
var csrfToken = generateCsrfToken("csrf-secret", 3600);
var csrfOk = verifyCsrfToken(csrfToken, "csrf-secret"); // true/false
var setCookieHeader = createSecureCookie("session", "user-123", "cookie-secret");
// send with res.header("Set-Cookie", setCookieHeader) or use res.cookie(...)
var cookieValue = split(split(setCookieHeader, ";")[0], "=")[1];
var session = readSecureCookie(urlDecode(cookieValue), "cookie-secret");
hashPassword(password, iterations?)uses PBKDF2 with SHA-256 and random saltverifyPassword(password, passwordHash)performs constant-time comparisoncreateJwt(payload, secret, expiresInSeconds? | options?)signs HS256 tokens and addsiat; options may setexpiresInSeconds,issuer(iss),audience(aud), andnotBeforeSeconds(nbf)verifyJwt(token, secret, options?)validates signature,nbf,exp, and optional expectedissuer/audienceverifyJwtthrows standardized auth errors for missing, invalid, not-yet-valid, or expired tokens- In HTTP/REST handlers prefer
req.auth.authenticateBearerJwt(secret)orreq.auth.authenticateCookieJwt(cookieName, jwtSecret, cookieSecret?)— they verify the token and populatereq.authclaims/roles. Usereq.auth.requireRole/requirePermissionfor authorization.setVerifiedSubremains for ingress/gateway cases without a local JWT. generateCsrfToken(secret, ttlSeconds?)creates signed CSRF tokens for form and API mutation flowsverifyCsrfToken(token, secret)validates signature + expiration and returns booleancsrfField(secret, ttlSeconds?)returns a hidden_csrfinput HTML fragment for forms (token matchesenableCsrf)bindForm(body, fields)binds/validates form fields (name,required,trim,minLength/maxLength,pattern: "email") into{ ok, values, errors }formErrors(errors)renders an escaped HTML error listpageLayout(title, bodyHtml, options?)thin full-document HTML chrome for@PAGEstring apps (preferui.layoutfor rich layouts)createSecureCookie(name, value, secret, options?)returns a signedSet-Cookieheader string with secure defaultsreadSecureCookie(cookieValue, secret)verifies a signed cookie value and returns plaintext ornull
See Also
- 9. Functions - Function decorators
- 13.25 Password, JWT, and request auth — name index
- 26. Web UI Server Components - @ACTION, fragments, and
ui.*controls - 27. HttpServer & HTML UI Generation - Pages, HTTPS, static files, mount host
- 30. REST Web Client - Calling APIs from MALDA with RestClient
- 31. Full-Stack Development with MALDA - End-to-end architecture