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:

PROCEDURE NombreMetodo(req, res) HELP "METODO: ruta/path [public]"
PartDescriptionExample
HTTP MethodGET, POST, PUT, PATCH, DELETE, HEADGET
RoutePath relative to the server prefix. Can contain {parameters}.products/{id}
Visibilitypublic = no JWT required. Without this token = JWT mandatory.public

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
HEADHeaders onlyNoHEAD: products public

URL parameters

* Single parameter
PROCEDURE GetProduct(req, res) HELP "GET: products/{id} public"
    LOCAL lcId
    lcId = req.params.id          && /api/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   && /api/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:

PROCEDURE SearchProducts(req, res) HELP "GET: products/search public"
    * GET /api/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
methodStringHTTP method (GET, POST, etc.)
urlUriFull request URL
pathStringPath without query string
paramsObjectURL parameters ({id} → params.id)
queryObjectQuery string parameters (?key=val)
headersDictHTTP headers (case-insensitive)
bodyStringRequest body as plain text
jsonObjectParsed JSON body (if Content-Type: application/json)
contentTypeStringContent-Type header value
remoteIPStringClient IP address

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 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:

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:

* 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 →