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.

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

ENDDEFINE

Test

With any HTTP client or from the browser:

curl http://localhost:8099/hello
curl http://localhost:8099/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.

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

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.

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

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 →