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:
PROCEDURE NombreMetodo(req, res) HELP "VERBO: ruta/path [public] [as capacidad]"| Parte | Descripción | Ejemplo |
|---|---|---|
| Verbo HTTP | GET, POST, PUT, PATCH o DELETE. HEAD no se declara: el motor lo sirve solo, como el GET de la misma ruta. | GET |
| Ruta | Relativa al prefijo del servidor. Puede llevar {parámetros}. | products/{id} |
| Visibilidad | public = sin JWT. Sin esa palabra, el middleware JWT exige el token. | public |
| Alias de capacidad | Opcional, 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 |
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.
* 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.getMétodos HTTP soportados
| Método | Descripción | Tiene body? | Ejemplo de ruta |
|---|---|---|---|
GET | Recuperar datos | No | GET: products public |
POST | Crear recurso | Si | POST: products public |
PUT | Actualizar (completo) | Si | PUT: products/{id} public |
PATCH | Actualizar (parcial) | Si | PATCH: products/{id} public |
DELETE | Eliminar recurso | No | DELETE: products/{id} public |
Parámetros de URL
* 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
Los parámetros de query string (?clave=valor) se acceden vía req.query:
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(...)
ENDPROCObjeto Request: propiedades
| Propiedad | Tipo | Descripción |
|---|---|---|
Method | String | El verbo de la petición |
uri | Object | Un objeto, no una cadena: Scheme, Host, Port, AbsolutePath, PathAndQuery y AbsoluteUri |
Params | Object | Parámetros de la ruta ({id} → req.Params.id), o req.GetParam("id", "") |
query | Object | Query string (?clave=valor), o req.GetQuery("clave", defecto) |
Headers | Object | Cabeceras. Se leen con req.GetHeader(clave, defecto) |
RawBody | String | El cuerpo sin parsear. También req.GetRawBody() |
json | Object | El cuerpo JSON ya parseado. req.HasJsonBody() dice si lo hay |
ContentType | String | El Content-Type de la petición |
AuthenticatedUser | Object | .id, .name y .role, con el token ya validado |
Claims | Object | Los claims extra: req.GetClaim(nombre, defecto) |
Objeto Response: métodos
| Método | Firma | Descripció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". |
* 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:
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
ENDDEFINERutas públicas y rutas protegidas
La visibilidad se controla con la palabra clave public en el comentario HELP:
* 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(...)
ENDPROCSiguiente: Middleware →