IR
irwinrodriguez.dev
Volver a documentación

Sistema de rutas

FoxServer utiliza comentarios HELP en los métodos VFP para declarar rutas. No hay archivos de configuración de rutas separados: la declaración vive junto al código del controlador.

Sintaxis del comentario HELP

Cada método endpoint debe tener un comentario HELP con este formato:

La cláusula HELP de un endpoint
PROCEDURE NombreMetodo(req, res) HELP "VERBO: ruta/path [public] [as capacidad]"
ParteDescripciónEjemplo
Verbo HTTPGET, POST, PUT, PATCH o DELETE. HEAD no se declara: el motor lo sirve solo, como el GET de la misma ruta.GET
RutaRelativa al prefijo del servidor. Puede llevar {parámetros}.products/{id}
Visibilidadpublic = sin JWT. Sin esa palabra, el middleware JWT exige el token.public
Alias de capacidadOpcional, y es la pieza que más rinde: as <nombre> le da a la ruta un nombre estable, evita colisiones y la publica ADEMÁS como método JSON-RPC 2.0 y como herramienta MCP.as productos.buscar
Una cláusula HELP no puede pasar de 255 caracteres A 256, VFP no compila y dice „Command contains unrecognized phrase/keyword“, que no menciona la longitud por ningún lado.

El alias «as», y por qué compensa ponerlo

Sin alias, la capacidad de una ruta se deriva del verbo y del path, y eso colisiona antes de lo que parece: GET products y GET products/{id} derivan la misma capacidad, products.get, aunque una liste y la otra traiga uno. Y el motor tiene sus propias rutas, que no pueden renunciar a su nombre: docs.get, openapi.json.get, mcp.post, rpc.post y server.status.*. Si chocas con una de esas, la que pierde es la tuya.

Pero lo que hace que el alias valga la pena no es evitar la colisión: es que la misma función se publica tres veces. Una ruta con capacidad se sirve como endpoint REST, como método JSON-RPC 2.0 en /rpc y como herramienta MCP en /mcp. Escribes una función VFP, y un agente de IA la llama igual que la llama tu frontend.

Alias «as»: dos rutas que ya no colisionan
* Dos rutas que sin alias colisionarian en products.get
PROCEDURE pListar(req, res)  HELP "GET: products as products.list"
PROCEDURE pUno(req, res)     HELP "GET: products/{id} as products.get"

* Y la misma, llamada de las tres maneras:
*   REST      GET /products/42
*   JSON-RPC  POST /rpc   {"method":"products.get","params":{"id":"42"}}
*   MCP       la herramienta products.get

Métodos HTTP soportados

MétodoDescripciónTiene body?Ejemplo de ruta
GETRecuperar datosNoGET: products public
POSTCrear recursoSiPOST: products public
PUTActualizar (completo)SiPUT: products/{id} public
PATCHActualizar (parcial)SiPATCH: products/{id} public
DELETEEliminar recursoNoDELETE: products/{id} public

Parámetros de URL

Parámetros de ruta con 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

Los parámetros de query string (?clave=valor) se acceden vía req.query:

SearchProducts: leer la 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

Objeto Request: propiedades

PropiedadTipoDescripción
MethodStringEl verbo de la petición
uriObjectUn objeto, no una cadena: Scheme, Host, Port, AbsolutePath, PathAndQuery y AbsoluteUri
ParamsObjectParámetros de la ruta ({id} → req.Params.id), o req.GetParam("id", "")
queryObjectQuery string (?clave=valor), o req.GetQuery("clave", defecto)
HeadersObjectCabeceras. Se leen con req.GetHeader(clave, defecto)
RawBodyStringEl cuerpo sin parsear. También req.GetRawBody()
jsonObjectEl cuerpo JSON ya parseado. req.HasJsonBody() dice si lo hay
ContentTypeStringEl Content-Type de la petición
AuthenticatedUserObject.id, .name y .role, con el token ya validado
ClaimsObjectLos claims extra: req.GetClaim(nombre, defecto)

Objeto Response: métodos

MétodoFirmaDescripción
status(code)res.status(200)Establece el código HTTP. Retorna res para encadenamiento.
json(jsonStr)res.json('{"ok":true}')Envía el cuerpo JSON. Pone Content-Type: application/json.
send(text)res.send("Hello")Envía texto plano o HTML.
header(key, val)res.header("X-Id","123")Añade o sobrescribe una cabecera de respuesta.
location(url)res.location("/api/v2")Establece el header Location (para redirects).
sendFile(path, disp)res.sendFile("C:\\rep.pdf","attachment")Envía un fichero. disp: "inline" o "attachment".
Encadenar cabeceras y respuesta JSON
* Chaining example — multiple headers + JSON response
res.status(201)
   .header("X-Resource-Id", lcNewId)
   .header("Cache-Control", "no-cache")
   .json(THIS.ToJson(loResponse))

Ciclo de vida del controlador

Cada controlador puede definir hooks que se ejecutan antes y después de cada endpoint:

ProductsController: abrir y cerrar la tabla en los 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

Rutas públicas y rutas protegidas

La visibilidad se controla con la palabra clave public en el comentario HELP:

Un endpoint público y uno protegido
* 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
Con el middleware JWT activo, los endpoints que no llevan public exigen que el cliente mande el token en la cabecera Authorization: Bearer {token}.

Siguiente: Middleware →