IR
irwinrodriguez.dev
Back to docs

Routing

FoxServer uses HELP comments in VFP methods to declare routes. There are no separate route configuration files: the declaration lives alongside the controller code.

HELP comment syntax

Each endpoint method must have a HELP comment in this format:

The HELP clause of an endpoint
PROCEDURE NombreMetodo(req, res) HELP "VERBO: ruta/path [public] [as capacidad]"
PartDescriptionExample
HTTP verbGET, POST, PUT, PATCH or DELETE. HEAD is not declared: the engine serves it on its own, as the GET of the same route.GET
RoutePath relative to the server prefix. Can contain {parameters}.products/{id}
Visibilitypublic = no JWT required. Without this token = JWT mandatory.public
A HELP clause cannot exceed 255 characters At 256, VFP will not compile and says "Command contains unrecognized phrase/keyword", which mentions the length nowhere.

The "as" alias, and why it pays to use it

Without an alias, a route's capability is derived from the verb and the path, and that collides sooner than you would think: GET products and GET products/{id} derive the same capability, products.get, even though one lists and the other fetches one. And the engine has routes of its own that cannot give up their names: docs.get, openapi.json.get, mcp.post, rpc.post and server.status.*. If you collide with one of those, the one that loses is yours.

But what makes the alias worth it is not avoiding the collision: it is that the same function gets published three times. A route with a capability is served as a REST endpoint, as a JSON-RPC 2.0 method at /rpc and as an MCP tool at /mcp. You write one VFP function, and an AI agent calls it exactly as your front end does.

The "as" alias: two routes that no longer collide
* Two routes that without an alias would collide on products.get
PROCEDURE pList(req, res)  HELP "GET: products as products.list"
PROCEDURE pOne(req, res)   HELP "GET: products/{id} as products.get"

* And the same one, called in all three ways:
*   REST      GET /products/42
*   JSON-RPC  POST /rpc   {"method":"products.get","params":{"id":"42"}}
*   MCP       the products.get tool

Supported HTTP methods

MethodDescriptionHas body?Route example
GETRetrieve dataNoGET: products public
POSTCreate resourceYesPOST: products public
PUTUpdate (full)YesPUT: products/{id} public
PATCHUpdate (partial)YesPATCH: products/{id} public
DELETEDelete resourceNoDELETE: products/{id} public

URL parameters

Route parameters with 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 parameters (?key=value) are accessed via req.query:

SearchProducts: reading the query string
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 object: properties

PropertyTypeDescription
MethodStringThe request verb
uriObjectAn object, not a string: Scheme, Host, Port, AbsolutePath, PathAndQuery and AbsoluteUri
ParamsObjectRoute parameters ({id} → req.Params.id), or req.GetParam("id", "")
queryObjectQuery string (?key=value), or req.GetQuery("key", default)
HeadersObjectHeaders. Read with req.GetHeader(key, default)
RawBodyStringThe unparsed body. Also req.GetRawBody()
jsonObjectThe already parsed JSON body. req.HasJsonBody() says whether there is one
ContentTypeStringThe request Content-Type
AuthenticatedUserObject.id, .name and .role, with the token already validated
ClaimsObjectThe extra claims: req.GetClaim(name, default)

Response object: methods

MethodSignatureDescription
status(code)res.status(200)Sets the HTTP code. Returns res for chaining.
json(jsonStr)res.json('{"ok":true}')Sends JSON body. Sets Content-Type: application/json.
send(text)res.send("Hello")Sends plain text or HTML.
header(key, val)res.header("X-Id","123")Adds or overwrites a response header.
location(url)res.location("/api/v2")Sets the Location header (for redirects).
sendFile(path, disp)res.sendFile("C:\\rep.pdf","attachment")Sends a file. disp: "inline" or "attachment".
Chaining headers and a JSON response
* Chaining example — multiple headers + JSON response
res.status(201)
   .header("X-Resource-Id", lcNewId)
   .header("Cache-Control", "no-cache")
   .json(THIS.ToJson(loResponse))

Controller lifecycle

Each controller can define hooks that run before and after each endpoint:

ProductsController: opening and closing the table in the hooks
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

Public vs. protected routes

Visibility is controlled with the public keyword in the HELP comment:

A public endpoint and a protected one
* 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
When JWT middleware is active, endpoints without public require the client to send the token in the Authorization: Bearer {token} header.

Next: Middleware →