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.
DEFINE CLASS HelloController AS ApiController OLEPUBLIC
* GET /api/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 /api/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
ENDDEFINE Test
With any HTTP client or from the browser:
curl http://localhost:8080/api/hello
curl http://localhost:8080/api/hello/Irwin Example 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.
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 /api/productos
PROCEDURE GetProductos(req, res) HELP "GET: productos public"
LOCAL loRes, lnRecs
SELECT * FROM productos INTO CURSOR tmpProductos
lnRecs = _TALLY
IF lnRecs = 0
loRes = THIS.newObject("status,data,message")
loRes.status = "ok"
loRes.data = NULL
loRes.message = "No products found"
ELSE
loRes = THIS.newObject(TEXTMERGE("status,data[<<lnRecs>>],message"))
LOCAL i
i = 0
SCAN
i = i + 1
SCATTER MEMO NAME loRow
loRes.data[i] = loRow
ENDSCAN
loRes.status = "ok"
loRes.message = ""
ENDIF
res.Status(200).Json(THIS.ToJson(loRes))
ENDPROC
* GET /api/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 /api/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 /api/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 /api/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
ENDDEFINE JSON 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.
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 /api/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 /api/pedidos
PROCEDURE CreatePedido(req, res) HELP "POST: pedidos public"
LOCAL loRes
IF ISNULL(req.json) OR ISNULL(req.json.cliente)
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 req.json.cliente, total WITH 0
FOR i = 1 TO ALEN(req.json.items)
LOCAL loItem
loItem = req.json.items[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
ENDDEFINE JSON 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 →