IR
irwinrodriguez.dev
Back to docs

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 →