IR
irwinrodriguez.dev
Back to docs

API Reference

The real surface of ApiController, req and res: what exists, with its signature. If a member is not here, it does not exist.

ApiController

Base class that every controller inherits from. It gives you the JSON utilities, the results with their status code, logging and the two lifecycle hooks. The JWT is NOT generated here: the middleware issues it when the login answers with X-Auth-Status.

Response helpers

Property / MethodTypeDescription
Ok(obj)200The normal result. Serializes the object and answers 200. It is the preferred form, and the one that respects the server's responseformat.
OkRaw(json)200The same, but with JSON you already have. It is the one that follows TableToJson().
Created(obj, location)201For what you have just created. location is optional and sets the Location header.
NoContent()204It went well and there is nothing to return. Typical of a DELETE.
BadRequest(msg, code)400What the client sent is not valid. code is yours and travels in the Problem Details.
Unauthorized(msg) · Forbidden(msg)401 · 403Not identified, or identified and not allowed.
NotFound(msg)404What was asked for does not exist.
Fail(status, msg, code)whatever you passAny other error, with the status code you give it.
newObject(fields)ObjectCreates a VFP object with those properties set to NULL. fields is a comma-separated list.
ToJson(obj) · ParseJson(txt)String · ObjectSerialize and deserialize, through JSONFox. The inverse of ToJson is called ParseJson: there is no FromJson.
LogInfo · LogWarning · LogError · LogFromExceptionVoidThey write to the server log. The last one dumps the whole exception, with its line.

What an error returns

Since 0.9.15, error results are described with Problem Details (RFC 9457), the same format ASP.NET uses, and are served with Content-Type: application/problem+json; charset=utf-8.

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "No existe el artículo ZZZZ",
  "instance": "/articulos/ZZZZ",
  "code": "not_found"
}

detail is your message, the one you passed to this.NotFound(...); code is yours and travels as an extension member, which is what the RFC allows for what is not its own; title is the code's reason phrase and is not translated, because it is machine readable; instance is set by the engine. A 200 does NOT change: it comes out with no envelope. And if your server declares responseformat: foxserver, that format does not change either.

Lifecycle hooks

Property / MethodTypeDescription
BeforeEndpoint(req, res)BooleanRuns before each endpoint OF THAT CONTROLLER. Returning .F. cuts the request off. This is where you open your tables.
AfterEndpoint(req, res)VoidRuns after each endpoint of that controller. This is where you close them.

FoxServerRequest

Object that encapsulates all information about the incoming HTTP request. Available as the req parameter in all endpoints.

Properties

Property / MethodTypeDescription
MethodStringGET, POST, PUT, PATCH, DELETE
uriObjectAn OBJECT, not a string: Scheme, Host, Port, AbsolutePath, PathAndQuery and AbsoluteUri. The path without the query is in uri.AbsolutePath.
ParamsObjectRoute parameters. req.Params.id from /{id}, or req.GetParam("id", "") with a default.
queryObjectQuery string. req.query.page from ?page=2, or req.GetQuery("page", 1).
HeadersObjectThe headers. Read with req.GetHeader("Authorization", "") and checked with req.HasHeader().
RawBodyStringThe unparsed body. Also req.GetRawBody(). This is what to read when the body may not arrive, or when a date must not go through JSONFox.
jsonObjectThe already parsed JSON body. req.HasJsonBody() says whether there is one, and req.HasJsonError() / req.JsonError if it could not be parsed.
ContentTypeStringThe request's Content-Type.
AuthenticatedUserObjectWith the token already validated: .id, .name and .role.
ClaimsObjectThe token's extra claims. req.GetClaim(name, default), req.HasClaim(name) and req.ClaimNames().
MultipartData · FormFieldsObjectUploads and forms: req.file(field), req.files(field), req.HasFiles(), req.GetFormField(field, default).
req.path, req.body, req.remoteIP and req.scheme do not exist And reading a property that does not exist returns empty without raising an error, which is the worst way to be wrong. The path is in req.uri.AbsolutePath, the body in req.RawBody and the method in req.Method. The client IP is NOT exposed to the controller.

Methods

Reading parameters, headers and the JSON body
* Un parametro de ruta, con valor por defecto
lcId = req.GetParam("id", "")

* Una cabecera: GetHeader, no req.headers[...]
IF req.HasHeader("X-Api-Key")
    lcKey = req.GetHeader("X-Api-Key", "")
ENDIF

* El JSON del cuerpo, con cuidado: leer una propiedad que no vino
* da error 1734, asi que se comprueba antes o se envuelve en TRY.
IF req.HasJsonBody() AND !ISNULL(req.json)
    TRY
        lcCiudad = NVL(req.json.direccion.ciudad, "")
    CATCH
        lcCiudad = ""
    ENDTRY
ENDIF

FoxServerResponse

Object to build and send the HTTP response. Available as the res parameter in all endpoints. All methods are chainable (return res).

Property / MethodDescription
status(code)Sets the HTTP status code (200, 201, 400, 401, 404, 500...)
json(jsonStr)Sends JSON body. Sets Content-Type: application/json automatically.
send(text)Sends plain text or HTML as body.
header(key, value)Adds or overwrites a response header. Chainable.
location(url)Sets the Location header. Use with status(301) or status(302) for redirect.
sendFile(path, disp)Sends a file. disp can be "inline" (display in browser) or "attachment" (force download).
events(tipo)Declares an SSE event type this endpoint supports. Without declaring it, nobody can subscribe to it.
emit(tipo, json)Emits an SSE event from any endpoint. Framing, retry and pings are handled by the middleware.

Example

Responding by chaining the res methods
* Standard success response
res.status(200).json(THIS.ToJson(loResult))

* Created with location header
res.status(201)
   .header("Location", "/api/v1/users/" + lcNewId)
   .json(THIS.ToJson(loResult))

* Error response
res.status(400).json('{"error":"Name is required"}')

* File download
res.status(200)
   .header("Content-Disposition", 'attachment; filename="report.pdf"')
   .sendFile("C:\reports\report.pdf", "attachment")

ApiController: detailed methods

Object and JSON management

Property / MethodDescription
NewObject(fields) Creates a VFP object with properties initialized to NULL. fields is a comma-separated list: "status,data,message".
NewObjectFromCursor(cursor, session) Creates a VFP object mirroring a cursor structure. Useful for creating templates based on table schemas.
ToJson(obj) Serializes a VFP object to a JSON string via JSONFox.
ParseJson(jsonStr) Deserializes a JSON string to a VFP object via JSONFox.
TableToJson(cursor, currentRow) Converts a cursor to JSON. currentRow=.T. returns the current record as an object; .F. returns all records as an array.
TableToJsonObject(cursor, currentRow, session) Like TableToJson but returns a VFP object collection instead of a JSON string. Useful for manipulating data before final serialization.
MasterToJSON(master, detail, expr, attr) Generates hierarchical master-detail JSON. expr is the join condition (e.g. "master.id = detail.master_id"), attr is the detail array name in the output JSON.
MasterToJSON: orders with their lines
* Ejemplo: maestro-detalle con MasterToJSON
SELECT id, fecha, cliente FROM pedidos INTO CURSOR curPedidos
SELECT id, pedido_id, producto, cantidad FROM lineas INTO CURSOR curLineas
lcJson = THIS.MasterToJSON("curPedidos", "curLineas", "curPedidos.id = curLineas.pedido_id", "lineas")
res.status(200).json(lcJson)

Text and formatting utilities

Property / MethodDescription
Format(template, p0..p10) Substitutes {0}, {1}, ... placeholders in the template string. Similar to C# String.Format.
EscapeCharsToJSON(text) Escapes special characters for safe use in JSON strings.
EscapeCharsToHTML(text) Escapes special characters for use in HTML output.
Content(req, source) Serializes source (cursor, object or string) to the format requested by the client via Accept header or ?format= (json, xml, csv).
TableToXml(cursor, currentRow) Converts a cursor to XML.
TableToCsv(cursor, currentRow) Converts a cursor to CSV.

Date, encoding and identifier utilities

Property / MethodTypeDescription
NewGuid() String Generates a GUID without braces or dashes. Use for all PKs.
DTOUNX(dt, useMsec) Number Converts a VFP DateTime to a Unix timestamp. useMsec=.T. returns milliseconds.
UNXTOD(timestamp) DateTime Converts a Unix timestamp (seconds or milliseconds) to a VFP DateTime.
Base64URLEncode(input) String Encodes to Base64URL (used in JWTs and safe URLs).
Base64URLDecode(input) String Decodes from Base64URL.
Unix dates, GUIDs and Format
* Conversiones de fecha
lnTs = THIS.DTOUNX(DATETIME(), .F.)   && segundos
ldFecha = THIS.UNXTOD(1716825600)

* Identificadores
lcId = THIS.NewGuid()   && "a1b2c3d4e5f647g8h9i0j1k2l3m4n5o6"

* Formato de mensajes
lcMsg = THIS.Format("Usuario {0} accedio a {1} desde {2}", lcUser, lcUrl, lcIp)

Lifecycle and resources

Property / MethodDescription
Initialize() Runs automatically when an instance is created. Configures cPath, loads JSONFox and ApiService.
LoadJsonFox() Loads JSONFox.app if not yet available. Called internally by Initialize().
GetGlobalResource(name) Gets a shared global resource (connections, configs) through the application lifecycle.
SetWorkingDirectory(path) Sets cPath and calls Initialize(). Useful for testing outside the FoxServer cycle.
CleanUp() / Release() / Dispose() Releases resources at the end of the controller lifecycle. Invoked automatically by FoxServer.

FoxServerRequest: detailed methods

Property / MethodDescription
GetHeader(key, default) Returns the value of the specified HTTP header. Returns default if not present.
GetParam(key, default) Returns a URL route parameter (e.g. {id}). Returns default if not present.
GetQuery(key, default) Returns a query string parameter (?key=value). Returns default if not present.
GetQueryParams() Returns all query parameters as a FoxServerTupleDictionary.
HasJsonBody() .T. if the request has a valid, parsed JSON body. Check before accessing req.json.*.
GetRawBody() Returns the raw unparsed request body. Required for webhook signature verification.
GetFormField(key, default) Returns the value of a multipart or urlencoded form field.
HasFormField(key) .T. if the form field exists in the request.
File(field) Returns the object for a file uploaded via multipart. Exposes FileName and SaveToFile(path).
Files(field) Returns a collection of files uploaded under the same field name.
Headers, body, uploaded files and webhook signature
* Leer headers y params
lcToken = req.GetHeader("Authorization", "")
lcId    = req.GetParam("id", "")
lcPage  = req.GetQuery("page", "1")

* Verificar body antes de acceder
IF req.HasJsonBody()
    lcEmail = req.json.email
ELSE
    res.status(400).json('{"error":"JSON body required"}')
    RETURN
ENDIF

* Subir archivo
loFile = req.File("avatar")
IF !ISNULL(loFile)
    loFile.SaveToFile(THIS.cPath + "uploads\" + loFile.FileName)
ENDIF

* Webhook: raw body para HMAC
lcRaw = req.GetRawBody()
lcSig = req.GetHeader("X-Signature", "")

Collections

FoxServerTupleDictionary

Key-value dictionary used for Headers, Params and Query. Case-insensitive access.

Property / MethodDescription
CountNumber of entries.
GetKey(n)Key at position n (1-based).
GetValue(n)Value at position n.
Contains(key).T. if the key exists.
GetValueByKey(key)Value by key name.
Iterating over all query parameters
* Iterar todos los query params
loParams = req.GetQueryParams()
FOR lnI = 1 TO loParams.Count
    lcKey = loParams.GetKey(lnI)
    lcVal = loParams.GetValue(lnI)
    THIS.LogInfo(THIS.Format("Param {0}={1}", lcKey, lcVal))
ENDFOR

FoxServerTuple

Simple key-value pair. Internal component of FoxServerTupleDictionary.

Property / MethodDescription
KeyKey name.
ValueAssociated value.

Next: FAQ →