Examples
Three complete, ready-to-use examples demonstrating the most common FoxServer patterns: a minimal endpoint, full CRUD and a master-detail relationship.
Use these examples as a starting point. Adjust table names, fields and routes to match your project.
Example 1: Hello World
A minimal controller with two endpoints: one without parameters and one with a URL parameter.
HelloController: a greeting with and without a parameter
DEFINE CLASS HelloController AS ApiController OLEPUBLIC
* GET /hello (public, no JWT)
PROCEDURE GetHello(req, res) HELP "GET: hello public"
LOCAL loRes
loRes = THIS.newObject("status,data,message")
loRes.status = "ok"
loRes.data = "Hello, world!"
loRes.message = ""
res.Status(200).Json(THIS.ToJson(loRes))
ENDPROC
* GET /hello/{name} (public, with parameter)
PROCEDURE GetHelloName(req, res) HELP "GET: hello/{name} public"
LOCAL loRes, lcName
lcName = req.GetParam("name", "Guest")
loRes = THIS.newObject("status,data,message")
loRes.status = "ok"
loRes.data = "Hello, " + lcName + "!"
loRes.message = ""
res.Status(200).Json(THIS.ToJson(loRes))
ENDPROC
ENDDEFINETest
With any HTTP client or from the browser:
curl http://localhost:8099/hello
curl http://localhost:8099/hello/IrwinExample 2: Basic CRUD
Create, Read, Update and Delete operations on a products table. Shows how to use BeforeEndpoint / AfterEndpoint to open and close the database.
ProductosController: CRUD on the productos table
DEFINE CLASS ProductosController AS ApiController OLEPUBLIC
FUNCTION BeforeEndpoint(req, res) AS BOOLEAN
SET DELETED ON
SET EXCLUSIVE OFF
SET SAFETY OFF
SET CENTURY ON
SELECT 0
USE (THIS.cPath + "data\productos") SHARED
RETURN .T.
ENDFUNC
PROCEDURE AfterEndpoint(req, res)
CLOSE DATABASES ALL
ENDPROC
* GET /productos
PROCEDURE GetProductos(req, res) HELP "GET: productos public as productos.list"
* A whole cursor to JSON in one line: TableToJson serializes it
* and OkRaw returns it as it is, without parsing it again. With
* zero rows you get an empty array, which is the right answer.
SELECT * FROM productos INTO CURSOR tmpProductos NOFILTER
RETURN THIS.OkRaw(THIS.TableToJson("tmpProductos"))
ENDPROC
* GET /productos/{id}
PROCEDURE GetProducto(req, res) HELP "GET: productos/{id} public"
LOCAL loRes, lcId
lcId = req.params.id
SELECT * FROM productos WHERE id = lcId INTO CURSOR tmpProducto
IF _TALLY = 0
loRes = THIS.newObject("status,data,message")
loRes.status = "error"
loRes.data = NULL
loRes.message = "Product not found"
res.Status(404).Json(THIS.ToJson(loRes))
ELSE
SCATTER MEMO NAME loRow
loRes = THIS.newObject("status,data,message")
loRes.status = "ok"
loRes.data = loRow
loRes.message = ""
res.Status(200).Json(THIS.ToJson(loRes))
ENDIF
ENDPROC
* POST /productos
PROCEDURE CreateProducto(req, res) HELP "POST: productos public"
LOCAL loRes
IF ISNULL(req.json)
loRes = THIS.newObject("status,message")
loRes.status = "error"
loRes.message = "JSON body expected"
res.Status(400).Json(THIS.ToJson(loRes))
RETURN
ENDIF
LOCAL lcGuid
lcGuid = THIS.NewGuid()
SELECT productos
APPEND BLANK
REPLACE id WITH lcGuid, nombre WITH req.json.nombre, precio WITH req.json.precio, stock WITH req.json.stock
loRes = THIS.newObject("status,message,id")
loRes.status = "ok"
loRes.message = "Product created"
loRes.id = lcGuid
res.Status(201).Json(THIS.ToJson(loRes))
ENDPROC
* PUT /productos/{id}
PROCEDURE UpdateProducto(req, res) HELP "PUT: productos/{id} public"
LOCAL loRes, lcId
lcId = req.params.id
IF ISNULL(req.json)
loRes = THIS.newObject("status,message")
loRes.status = "error"
loRes.message = "JSON body expected"
res.Status(400).Json(THIS.ToJson(loRes))
RETURN
ENDIF
SELECT productos
LOCATE FOR id = lcId
IF !FOUND()
loRes = THIS.newObject("status,message")
loRes.status = "error"
loRes.message = "Product not found"
res.Status(404).Json(THIS.ToJson(loRes))
RETURN
ENDIF
IF TYPE("req.json.nombre") = "C"
REPLACE nombre WITH req.json.nombre
ENDIF
IF TYPE("req.json.precio") = "N"
REPLACE precio WITH req.json.precio
ENDIF
IF TYPE("req.json.stock") = "N"
REPLACE stock WITH req.json.stock
ENDIF
loRes = THIS.newObject("status,message")
loRes.status = "ok"
loRes.message = "Product updated"
res.Status(200).Json(THIS.ToJson(loRes))
ENDPROC
* DELETE /productos/{id}
PROCEDURE DeleteProducto(req, res) HELP "DELETE: productos/{id} public"
LOCAL loRes, lcId
lcId = req.params.id
SELECT productos
LOCATE FOR id = lcId
IF !FOUND()
loRes = THIS.newObject("status,message")
loRes.status = "error"
loRes.message = "Product not found"
res.Status(404).Json(THIS.ToJson(loRes))
RETURN
ENDIF
DELETE
loRes = THIS.newObject("status,message")
loRes.status = "ok"
loRes.message = "Product deleted"
res.Status(200).Json(THIS.ToJson(loRes))
ENDPROC
ENDDEFINEJSON body for POST/PUT
{
"nombre": "Mechanical keyboard",
"precio": 89.99,
"stock": 50
} Table structure for productos: This example assumes a productos.dbf with fields: id (C,36), nombre (C,100), precio (N,10,2), stock (N,6). Adjust types and lengths to match your schema.
Example 3: Master-Detail
Fetch an order along with its line items in a single hierarchical JSON response.
PedidosController: orders with their detail lines
DEFINE CLASS PedidosController AS ApiController OLEPUBLIC
FUNCTION BeforeEndpoint(req, res) AS BOOLEAN
SET DELETED ON
SET EXCLUSIVE OFF
SET SAFETY OFF
SET CENTURY ON
SELECT 0
USE (THIS.cPath + "data\pedidos") SHARED
SELECT 0
USE (THIS.cPath + "data\pedido_detalle") SHARED
RETURN .T.
ENDFUNC
PROCEDURE AfterEndpoint(req, res)
CLOSE DATABASES ALL
ENDPROC
* GET /pedidos/{id}
PROCEDURE GetPedido(req, res) HELP "GET: pedidos/{id} public"
LOCAL loRes, lcId
lcId = req.params.id
SELECT * FROM pedidos WHERE id = lcId INTO CURSOR tmpPedido
IF _TALLY = 0
loRes = THIS.newObject("status,data,message")
loRes.status = "error"
loRes.data = NULL
loRes.message = "Order not found"
res.Status(404).Json(THIS.ToJson(loRes))
RETURN
ENDIF
SELECT * FROM pedido_detalle WHERE pedido_id = lcId INTO CURSOR tmpDetalles
SELECT tmpPedido
SCATTER MEMO NAME loPedido
LOCAL lnDets, i
lnDets = RECCOUNT("tmpDetalles")
loRes = THIS.newObject("status,pedido,message")
loRes.status = "ok"
loRes.pedido = loPedido
loRes.message = ""
i = 0
DIMENSION laDetalles[MAX(lnDets,1)]
SELECT tmpDetalles
SCAN
i = i + 1
SCATTER MEMO NAME loDetalle
laDetalles[i] = loDetalle
ENDSCAN
ADDPROPERTY(loRes, "detalles", @laDetalles)
res.Status(200).Json(THIS.ToJson(loRes))
ENDPROC
* POST /pedidos
PROCEDURE CreatePedido(req, res) HELP "POST: pedidos public"
LOCAL loRes
* Leer una propiedad que no vino da error 1734, asi que no se
* comprueba con ISNULL: se envuelve el acceso.
LOCAL lcCliente
lcCliente = ""
TRY
lcCliente = NVL(req.json.cliente, "")
CATCH
ENDTRY
IF !req.HasJsonBody() OR EMPTY(lcCliente)
loRes = THIS.newObject("status,message")
loRes.status = "error"
loRes.message = "cliente and items are required"
res.Status(400).Json(THIS.ToJson(loRes))
RETURN
ENDIF
LOCAL lcPedidoId, lnTotal, i
lcPedidoId = THIS.NewGuid()
lnTotal = 0
SELECT pedidos
APPEND BLANK
REPLACE id WITH lcPedidoId, fecha WITH DATE(), cliente WITH lcCliente, total WITH 0
* Los arrays del cuerpo llegan como Collection, NO como array de
* VFP: ALEN() falla con "'X' is not an array". Se recorren con
* .Count y .Item(n).
FOR i = 1 TO req.json.items.Count
LOCAL loItem
loItem = req.json.items.Item(i)
SELECT pedido_detalle
APPEND BLANK
REPLACE pedido_id WITH lcPedidoId, linea WITH i, ;
producto WITH loItem.producto, ;
cantidad WITH loItem.cantidad, ;
precio WITH loItem.precio, ;
importe WITH loItem.cantidad * loItem.precio
lnTotal = lnTotal + (loItem.cantidad * loItem.precio)
ENDFOR
SELECT pedidos
LOCATE FOR id = lcPedidoId
REPLACE total WITH lnTotal
loRes = THIS.newObject("status,message,id,total")
loRes.status = "ok"
loRes.message = "Order created"
loRes.id = lcPedidoId
loRes.total = lnTotal
res.Status(201).Json(THIS.ToJson(loRes))
ENDPROC
ENDDEFINEJSON body to create an order
{
"cliente": "C001",
"items": [
{ "producto": "P001", "cantidad": 2, "precio": 19.99 },
{ "producto": "P002", "cantidad": 1, "precio": 29.99 }
]
} Required tables: pedidos (id C36, fecha D, cliente C50, total N10.2) and pedido_detalle (pedido_id C36, linea N4, producto C50, cantidad N8, precio N10.2, importe N12.2).
Best practices
- Always use BeforeEndpoint and AfterEndpoint to manage table open/close.
- Return appropriate HTTP codes: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Server Error.
- Validate input data before processing. Never trust the body without validation.
- Use THIS.NewGuid() for PKs. Never use auto-increment sequences in concurrent environments.
- Inside TRY/CATCH use EXIT instead of RETURN to exit the block.
Next: API Reference →