27. REST API Server
MALDA includes native support for creating REST API servers using function decorators.
27.1 Decorator Syntax
Functions can be decorated with HTTP method decorators to create REST endpoints:
@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);
}
Supported HTTP Methods
@GET(path)- Handle GET requests@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
Route Metadata Decorators
Routes can include optional metadata decorators. These are applied before route registration:
@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@Validate(schema)- request validation schema (JSON object or JSON string schema)
@RouteGroup("/api")
@Version("v1")
@Use("requireAuth")
@GET("/users/{id}")
function getUserById(id, req, res) {
return res.json(parseJSON("{\"id\":\"" + id + "\"}"));
}
27.2 RestServer Class
Constructor
var server = new RestServer(port);
// OR
var server = new RestServer(port, host); // host: "localhost" or "0.0.0.0"
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.
Methods
start(): Start the REST server and discover decorated functionsstop(): Stop the servergetRoutes(): Get array of registered routesenableCORS(enabled): Enable/disable CORS supportenableSwagger(enabled): Enable/disable Swagger/OpenAPI documentationuse(middleware): Register global middleware (function or transpiled function-name string)enableCsrf(secret, cookieName?, headerName?): Enable built-in CSRF protection middlewaredisableCsrf(): Disable CSRF protectionenableSession(secret, options?)/disableSession(): Same session/flash surface as HttpServer (req.session)- Deferred host:
new RestServer()(no port) forHttpServer.mount(api)single-port fullstack apps setRateLimit(limit, windowSeconds, keyStrategy?): Enable endpoint rate limitingsetRateLimitHeaders(enabled, includeRemaining?): Enable/disable rate-limit response headersconfigureTrustedProxy(enabled, headerName?, hopIndex?): Configure trusted proxy source IP behaviordisableRateLimit(): Disable endpoint rate limiting
27.3 Path Parameters
Path parameters are extracted from route patterns using {param} placeholders:
@GET("/api/users/{id}")
function getUserById(id) {
var jsonStr = "{\"id\": \"" + id + "\", \"name\": \"Alice\"}";
return parseJSON(jsonStr);
}
27.4 Query Parameters
Query parameters are automatically extracted 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);
}
27.5 Request Body
The request body is automatically parsed as JSON and bound to a parameter named body:
@POST("/api/users")
function createUser(body) {
var name = body.name;
var jsonStr = "{\"id\": 1, \"name\": \"" + name + "\"}";
return parseJSON(jsonStr);
}
27.6 Parameter Binding
Optional decorator-based binding for renaming parameters:
@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
}
27.7 Response Handling
Functions can return objects with status property to set HTTP status codes:
@POST("/api/users")
function createUser(body) {
var jsonStr = "{\"status\": 201, \"data\": {\"id\": 1, \"name\": \"" + body.name + "\"}}";
return parseJSON(jsonStr); // Returns 201 Created
}
Handlers can also accept a request/response context pair:
@GET("/api/users/{id}")
function getUser(req, res) {
var id = req.params.id;
return res.status(200).json(parseJSON("{\"id\": \"" + id + "\"}"));
}
27.8 Middleware and Request Context
Use server.use(...) for global middleware. Middleware receives three arguments: (req, res, next).
req.method,req.path,req.query,req.params,req.headers,req.cookies,req.body,req.correlationId,req.ip/req.remoteIpnext()continues the chain; omittingnext()short-circuits the request
Route-level middleware declared via @Use(...) / @Middleware(...) runs after global middleware and before parameter binding + handler invocation.
function logRequest(req, res, next) {
print(req.method + " " + req.path);
next();
}
var server = new RestServer(8080);
server.use(logRequest);
server.start();
Auth guards are composable middleware. Prefer req.auth.authenticateBearerJwt(secret) (or authenticateCookieJwt for browser sessions): it verifies the JWT, populates claims/roles/permissions, and throws standardized 401 errors on failure.
function requireAuth(req, res, next) {
req.auth.authenticateBearerJwt("my-jwt-secret");
next();
}
var server = new RestServer(8080);
server.use(requireAuth, {
"except": ["/api/health", "/api/readiness", "/metrics"]
});
server.start();
Skip public paths with use(middleware, { "except": [...] }). Exact paths and trailing * prefixes are supported. For ingress/gateway trust boundaries you can still call req.auth.setVerifiedSub(sub) after an upstream check; prefer authenticate* inside application middleware so roles and claims are available.
27.9 Response Helper API
Response context helper methods:
res.status(code)res.json(value)res.text(value)res.html(value)res.redirect(location, status?)res.header(name, value)res.cookie(name, value, options?)res.send(value)
res.cookie(...) uses secure defaults: HttpOnly=true, Secure=true, SameSite=Lax, Path=/.
27.10 CSRF and Rate Limiting
Use built-in middleware-style server configuration for request protection without ad-hoc per-route logic:
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();
Rate-limit key strategy supports ip, token, user, sub/verifiedSub/verifiedSubOrIp, and ipOrToken (default fallback).
Rate limiting runs after global and route middleware, so verifiedSub* strategies see subjects set by req.auth.authenticateBearerJwt / authenticateCookieJwt. When no verified subject is present, keying falls back to IP/token behavior.
Header aliases (X-Malda-Auth-Verified/X-Malda-Auth-Sub and legacy X-Auth-*) remain available for compatibility at ingress boundaries, but application middleware should use req.auth.
Trusted proxy defaults are secure: proxy headers are ignored unless explicitly enabled via configureTrustedProxy(...).
When setRateLimitHeaders(true) is enabled, responses include Retry-After (on 429), X-RateLimit-Limit, and optionally X-RateLimit-Remaining.
CSRF behavior:
- Safe methods (GET/HEAD/OPTIONS) issue/refresh CSRF cookie when missing or invalid
- State-changing methods (POST/PUT/PATCH/DELETE) require matching cookie + request token
- Request token is read from
X-CSRF-Token,body.csrfToken, orbody._csrf
CSRF, auth, and rate-limit failures all use the same standardized error payload and include correlation ID headers/payload fields.
27.11 Error Handling
Functions can throw objects with status property to return custom HTTP status codes:
@GET("/api/users/{id}")
function getUserById(id) {
if (!userExists(id)) {
var error = parseJSON("{\"status\": 404, \"message\": \"User not found\"}");
throw error;
}
// ...
}
REST framework errors (handler failures, middleware/auth failures, and validation failures) use one JSON contract:
{
"status": 401,
"error": "InvalidToken",
"message": "Invalid token signature.",
"correlationId": "...",
"details": [ ... ] // optional, present for validation errors
}
Correlation ID propagation is consistent across all framework-generated failures:
- Response header:
X-Correlation-ID - Payload field:
correlationId
27.12 Validation
Use @Validate(...) to validate path, query, and body before your handler runs. Invalid requests return HTTP 400 with field-level details and do not execute handler logic.
@GET("/search/{id}")
@Validate("{\"path\":{\"id\":\"int|required|min=1\"},\"query\":{\"q\":\"string|required|minLength=2\"}}")
function search(id, q) {
return parseJSON("{\"ok\": true}");
}
Schema values support string DSL rules such as required, min=, max=, minLength=, maxLength=, and pattern=.
When Swagger is enabled, validation metadata is reflected in OpenAPI parameter/request-body schemas (type, required, and common constraints).
27.13 CORS Support
var server = new RestServer(8080);
server.enableCORS(true);
server.setCORSOrigin("*");
server.start();
27.14 Swagger/OpenAPI Documentation
var server = new RestServer(8080);
server.enableSwagger(true);
server.start();
// Access at: http://localhost:8080/swagger.json
27.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);
}
See Also
- 9. Functions - Function decorators
- 24. Web UI Server Components - @ACTION, fragments, and
ui.*controls - 28. REST Web Client - Calling APIs from MALDA with RestClient
- 29. Full-Stack Development with MALDA - End-to-end architecture and full-stack example