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:
PROCEDURE NombreMetodo(req, res) HELP "VERBO: ruta/path [public] [as capacidad]"| Teil | Beschreibung | Beispiel |
|---|---|---|
| HTTP-Verb | GET, POST, PUT, PATCH oder DELETE. HEAD wird nicht deklariert: der Motor bedient es von selbst, wie das GET derselben Route. | GET |
| Route | Pfad relativ zum Server-Präfix. Kann {Parameter} enthalten. | products/{id} |
| Sichtbarkeit | public = kein JWT erforderlich. Ohne dieses Token = JWT obligatorisch. | public |
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.
* 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.getUnterstutzte HTTP-Methoden
| Methode | Beschreibung | Hat Body? | Routenbeispiel |
|---|---|---|---|
GET | Daten abrufen | Nein | GET: products public |
POST | Ressource erstellen | Ja | POST: products public |
PUT | Aktualisieren (vollständig) | Ja | PUT: products/{id} public |
PATCH | Aktualisieren (partiell) | Ja | PATCH: products/{id} public |
DELETE | Ressource löschen | Nein | DELETE: products/{id} public |
URL-Parameter
* 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(...)
ENDPROCQuery-String
Query-String-Parameter (?Schlüssel=Wert) werden über req.query abgerufen:
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(...)
ENDPROCRequest-Objekt: Eigenschaften
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
Method | String | Das Verb der Anfrage |
uri | Object | Ein Objekt, keine Zeichenkette: Scheme, Host, Port, AbsolutePath, PathAndQuery und AbsoluteUri |
Params | Object | Parameter der Route ({id} → req.Params.id), oder req.GetParam("id", "") |
query | Object | Query-String (?schluessel=wert), oder req.GetQuery("schluessel", Vorgabe) |
Headers | Object | Header. Gelesen mit req.GetHeader(Schlüssel, Vorgabe) |
RawBody | String | Der unverarbeitete Rumpf. Auch req.GetRawBody() |
json | Object | Der bereits geparste JSON-Rumpf. req.HasJsonBody() sagt, ob es ihn gibt |
ContentType | String | Der Content-Type der Anfrage |
AuthenticatedUser | Object | .id, .name und .role, bei geprüftem Token |
Claims | Object | Die zusätzlichen Claims: req.GetClaim(Name, Vorgabe) |
Response-Objekt: Methoden
| Methode | Signatur | Beschreibung |
|---|---|---|
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". |
* 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:
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:
* 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(...)
ENDPROCWeiter: Middleware →