Manuale di riferimento MALDA™

Il linguaggio di programmazione AI-First - Versione 1.0.11

27. Server REST API

MALDA include il supporto nativo per creare server REST API usando i decoratori di funzione.

27.1 Sintassi dei decoratori

Le funzioni possono essere decorate con decoratori di metodo HTTP per creare endpoint REST:

@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);
}

Metodi HTTP supportati

Decoratori di metadati delle route

Le route possono includere decoratori di metadati opzionali. Vengono applicati prima della registrazione della route:

@RouteGroup("/api")
@Version("v1")
@Use("requireAuth")
@GET("/users/{id}")
function getUserById(id, req, res) {
    return res.json(parseJSON("{\"id\":\"" + id + "\"}"));
}

27.2 Classe RestServer

Costruttore

var server = new RestServer(port);
// OR
var server = new RestServer(port, host);  // host: "localhost" or "0.0.0.0"

port deve essere 0 (deferred/mounted) oppure un intero in 1-65535. Le porte privilegiate (1-1023) sono ammesse; il bind può comunque richiedere permessi elevati o reservation URL del sistema operativo.

Metodi

27.3 Path parameter

I path parameter vengono estratti dai pattern di route usando i placeholder {param}:

@GET("/api/users/{id}")
function getUserById(id) {
    var jsonStr = "{\"id\": \"" + id + "\", \"name\": \"Alice\"}";
    return parseJSON(jsonStr);
}

27.4 Query parameter

I query parameter vengono estratti automaticamente dalla 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

Il body della richiesta viene parsato automaticamente come JSON e bound a un parametro di nome body:

@POST("/api/users")
function createUser(body) {
    var name = body.name;
    var jsonStr = "{\"id\": 1, \"name\": \"" + name + "\"}";
    return parseJSON(jsonStr);
}

27.6 Parameter binding

Binding opzionale basato su decoratori per rinominare i parametri:

@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 Gestione della response

Le funzioni possono restituire oggetti con la proprietà status per impostare i codici di status HTTP:

@POST("/api/users")
function createUser(body) {
    var jsonStr = "{\"status\": 201, \"data\": {\"id\": 1, \"name\": \"" + body.name + "\"}}";
    return parseJSON(jsonStr);  // Returns 201 Created
}

Gli handler possono anche accettare una coppia di contesto request/response:

@GET("/api/users/{id}")
function getUser(req, res) {
    var id = req.params.id;
    return res.status(200).json(parseJSON("{\"id\": \"" + id + "\"}"));
}

27.8 Middleware e contesto della richiesta

Usa server.use(...) per il middleware globale. Il middleware riceve tre argomenti: (req, res, next).

Il middleware a livello di route dichiarato via @Use(...) / @Middleware(...) gira dopo il middleware globale e prima del parameter binding + invocazione dell'handler.

function logRequest(req, res, next) {
    print(req.method + " " + req.path);
    next();
}

var server = new RestServer(8080);
server.use(logRequest);
server.start();

Le guardie di auth sono middleware componibili. Preferisci req.auth.authenticateBearerJwt(secret) (o authenticateCookieJwt per le sessioni browser): verifica il JWT, popola claim/ruoli/permessi e lancia errori 401 standardizzati in caso di fallimento.

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

Salta i path pubblici con use(middleware, { "except": [...] }). Sono supportati path esatti e prefissi con * finale. Per i confini di trust di ingress/gateway puoi ancora chiamare req.auth.setVerifiedSub(sub) dopo un controllo upstream; preferisci authenticate* nel middleware applicativo così ruoli e claim sono disponibili.

27.9 API helper di response

Metodi helper del contesto di response:

res.cookie(...) usa default sicuri: HttpOnly=true, Secure=true, SameSite=Lax, Path=/.

27.10 CSRF e rate limiting

Usa la configurazione built-in del server in stile middleware per proteggere le richieste senza logica ad-hoc per route:

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

La strategia di chiave del rate-limit supporta ip, token, user, sub/verifiedSub/verifiedSubOrIp e ipOrToken (fallback di default).

Il rate limiting gira dopo il middleware globale e di route, così le strategie verifiedSub* vedono i subject impostati da req.auth.authenticateBearerJwt / authenticateCookieJwt. Quando non c'è un subject verificato, la chiave ricade sul comportamento IP/token.

Gli alias di header (X-Malda-Auth-Verified/X-Malda-Auth-Sub e i legacy X-Auth-*) restano disponibili per compatibilità ai confini di ingress, ma il middleware applicativo dovrebbe usare req.auth.

I default del proxy fidato sono sicuri: gli header del proxy vengono ignorati finché non li abiliti esplicitamente via configureTrustedProxy(...).

Quando setRateLimitHeaders(true) è abilitato, le response includono Retry-After (su 429), X-RateLimit-Limit e, opzionalmente, X-RateLimit-Remaining.

Comportamento CSRF:

I fallimenti di CSRF, auth e rate-limit usano tutti lo stesso payload di errore standardizzato e includono header/campi di correlation ID.

27.11 Gestione degli errori

Le funzioni possono lanciare oggetti con la proprietà status per restituire codici di status HTTP personalizzati:

@GET("/api/users/{id}")
function getUserById(id) {
    if (!userExists(id)) {
        var error = parseJSON("{\"status\": 404, \"message\": \"User not found\"}");
        throw error;
    }
    // ...
}

Gli errori del framework REST (fallimenti di handler, fallimenti di middleware/auth e fallimenti di validazione) usano un unico contratto JSON:

{
  "status": 401,
  "error": "InvalidToken",
  "message": "Invalid token signature.",
  "correlationId": "...",
  "details": [ ... ] // optional, present for validation errors
}

La propagazione del correlation ID è coerente su tutti i fallimenti generati dal framework:

27.12 Validazione

Usa @Validate(...) per validare path, query e body prima che l'handler giri. Le richieste non valide restituiscono HTTP 400 con dettagli a livello di campo e non eseguono la logica dell'handler.

@GET("/search/{id}")
@Validate("{\"path\":{\"id\":\"int|required|min=1\"},\"query\":{\"q\":\"string|required|minLength=2\"}}")
function search(id, q) {
    return parseJSON("{\"ok\": true}");
}

I valori di schema supportano regole DSL stringa come required, min=, max=, minLength=, maxLength= e pattern=.

Quando Swagger è abilitato, i metadati di validazione si riflettono negli schema OpenAPI di parameter/request-body (tipo, required e vincoli comuni).

27.13 Supporto CORS

var server = new RestServer(8080);
server.enableCORS(true);
server.setCORSOrigin("*");
server.start();

27.14 Documentazione Swagger/OpenAPI

var server = new RestServer(8080);
server.enableSwagger(true);
server.start();

// Access at: http://localhost:8080/swagger.json

27.15 Esempio completo

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

Vedi anche