IR
irwinrodriguez.dev
Zurück zur Dokumentation

Routing

FoxServer verwendet HELP-Kommentare in VFP-Methoden zur Routendeklaration. Es gibt keine separaten Routenkonfigurationsdateien: die Deklaration befindet sich direkt neben dem Controller-Code.

HELP-Kommentar-Syntax

Jede Endpunkt-Methode muss einen HELP-Kommentar in diesem Format haben:

Die HELP-Klausel eines Endpunkts
PROCEDURE NombreMetodo(req, res) HELP "VERBO: ruta/path [public] [as capacidad]"
TeilBeschreibungBeispiel
HTTP-VerbGET, POST, PUT, PATCH oder DELETE. HEAD wird nicht deklariert: der Motor bedient es von selbst, wie das GET derselben Route.GET
RoutePfad relativ zum Server-Präfix. Kann {Parameter} enthalten.products/{id}
Sichtbarkeitpublic = kein JWT erforderlich. Ohne dieses Token = JWT obligatorisch.public
Eine HELP-Klausel darf 255 Zeichen nicht überschreiten Bei 256 kompiliert VFP nicht und meldet „Command contains unrecognized phrase/keyword“ – von der Länge steht dort nichts.

Der Alias „as“, und warum er sich lohnt

Ohne Alias wird die Fähigkeit einer Route aus Verb und Pfad abgeleitet, und das kollidiert früher als man denkt: GET products und GET products/{id} leiten dieselbe Fähigkeit ab, products.get, obwohl die eine auflistet und die andere einen Datensatz holt. Und der Motor hat eigene Routen, die ihren Namen nicht abgeben können: docs.get, openapi.json.get, mcp.post, rpc.post und server.status.*. Bei einer Kollision verliert Ihre.

Was den Alias aber wirklich lohnend macht, ist nicht die vermiedene Kollision: es ist, dass dieselbe Funktion dreifach veröffentlicht wird. Eine Route mit Fähigkeit wird als REST-Endpunkt, als JSON-RPC-2.0-Methode unter /rpc und als MCP-Tool unter /mcp bedient. Sie schreiben eine VFP-Funktion, und ein KI-Agent ruft sie genauso auf wie Ihr Frontend.

Alias „as“: zwei Routen, die nicht mehr kollidieren
* Zwei Routen, die ohne Alias auf products.get kollidieren wuerden
PROCEDURE pListe(req, res)  HELP "GET: products as products.list"
PROCEDURE pEine(req, res)   HELP "GET: products/{id} as products.get"

* Und dieselbe, auf drei Arten aufgerufen:
*   REST      GET /products/42
*   JSON-RPC  POST /rpc   {"method":"products.get","params":{"id":"42"}}
*   MCP       das Tool products.get

Unterstutzte HTTP-Methoden

MethodeBeschreibungHat Body?Routenbeispiel
GETDaten abrufenNeinGET: products public
POSTRessource erstellenJaPOST: products public
PUTAktualisieren (vollständig)JaPUT: products/{id} public
PATCHAktualisieren (partiell)JaPATCH: products/{id} public
DELETERessource löschenNeinDELETE: products/{id} public

URL-Parameter

Routenparameter mit req.Params
* Single parameter
PROCEDURE GetProduct(req, res) HELP "GET: products/{id} public"
    LOCAL lcId
    lcId = req.Params.id          && /products/42 -> "42"
    res.status(200).json(...)
ENDPROC

* Multiple parameters
PROCEDURE GetVariant(req, res) HELP "GET: products/{id}/variants/{vid} public"
    LOCAL lcProductId, lcVariantId
    lcProductId = req.Params.id   && /products/42/variants/black -> "42"
    lcVariantId = req.Params.vid  && "black"
    res.status(200).json(...)
ENDPROC

Query-String

Query-String-Parameter (?Schlüssel=Wert) werden über req.query abgerufen:

SearchProducts: den Query-String lesen
PROCEDURE SearchProducts(req, res) HELP "GET: products/search public"
    * GET /products/search?name=Widget&minprice=10&page=2
    LOCAL lcName, lnMin, lnPage
    lcName  = req.query.name               && "Widget"
    lnMin   = VAL(req.query.minprice)      && 10
    lnPage  = IIF(EMPTY(req.query.page), 1, VAL(req.query.page))
    res.status(200).json(...)
ENDPROC

Request-Objekt: Eigenschaften

EigenschaftTypBeschreibung
MethodStringDas Verb der Anfrage
uriObjectEin Objekt, keine Zeichenkette: Scheme, Host, Port, AbsolutePath, PathAndQuery und AbsoluteUri
ParamsObjectParameter der Route ({id} → req.Params.id), oder req.GetParam("id", "")
queryObjectQuery-String (?schluessel=wert), oder req.GetQuery("schluessel", Vorgabe)
HeadersObjectHeader. Gelesen mit req.GetHeader(Schlüssel, Vorgabe)
RawBodyStringDer unverarbeitete Rumpf. Auch req.GetRawBody()
jsonObjectDer bereits geparste JSON-Rumpf. req.HasJsonBody() sagt, ob es ihn gibt
ContentTypeStringDer Content-Type der Anfrage
AuthenticatedUserObject.id, .name und .role, bei geprüftem Token
ClaimsObjectDie zusätzlichen Claims: req.GetClaim(Name, Vorgabe)

Response-Objekt: Methoden

MethodeSignaturBeschreibung
status(code)res.status(200)Setzt den HTTP-Code. Gibt res für Verkettung zurück.
json(jsonStr)res.json('{"ok":true}')Sendet JSON-Body. Setzt Content-Type: application/json.
send(text)res.send("Hallo")Sendet Klartext oder HTML.
header(key, val)res.header("X-Id","123")Fügt einen Antwort-Header hinzu oder überschreibt ihn.
location(url)res.location("/api/v2")Setzt den Location-Header (für Weiterleitungen).
sendFile(path, disp)res.sendFile("C:\\rep.pdf","attachment")Sendet eine Datei. disp: "inline" oder "attachment".
Header und JSON-Antwort verketten
* Chaining example — multiple headers + JSON response
res.status(201)
   .header("X-Resource-Id", lcNewId)
   .header("Cache-Control", "no-cache")
   .json(THIS.ToJson(loResponse))

Controller-Lebenszyklus

Jeder Controller kann Hooks definieren, die vor und nach jedem Endpunkt ausgeführt werden:

ProductsController: Tabelle in den Hooks öffnen und schließen
DEFINE CLASS ProductsController AS ApiController OLEPUBLIC

    FUNCTION BeforeEndpoint(req, res) AS BOOLEAN
        SET DELETED ON
        SET EXCLUSIVE OFF
        USE data\products SHARED
        RETURN .T.   && .F. here would abort the request
    ENDFUNC

    PROCEDURE AfterEndpoint(req, res)
        CLOSE DATABASES ALL
    ENDPROC

    PROCEDURE GetProducts(req, res) HELP "GET: products public"
        && Products table is already open (BeforeEndpoint)
        && It will be closed automatically (AfterEndpoint)
        res.status(200).json(...)
    ENDPROC

ENDDEFINE

Öffentliche vs. geschutzte Routen

Die Sichtbarkeit wird mit dem public-Schlusselwort im HELP-Kommentar gesteuert:

Ein öffentlicher und ein geschützter Endpunkt
* PUBLIC endpoint — no JWT needed
PROCEDURE GetProducts(req, res) HELP "GET: products public"
    res.status(200).json(...)
ENDPROC

* PRIVATE endpoint — JWT required (omit "public")
PROCEDURE DeleteProduct(req, res) HELP "DELETE: products/{id}"
    && Client must send: Authorization: Bearer eyJhb...
    res.status(200).json(...)
ENDPROC
Wenn JWT-Middleware aktiv ist, müssen Endpunkte ohne public den Token im Header Authorization: Bearer {token} senden.

Weiter: Middleware →