IR
irwinrodriguez.dev
Volver a documentacion

Ejemplos

Tres ejemplos completos y listos para usar que demuestran los patrones mas comunes de FoxServer: un endpoint minimo, CRUD completo y una relacion maestro-detalle.

Puedes usar estos ejemplos como punto de partida. Ajusta los nombres de tablas, campos y rutas segun tu proyecto.

Ejemplo 1: Hello World

Un controlador minimo con dos endpoints: uno sin parametros y otro con parametro en la URL.

DEFINE CLASS HelloController AS ApiController OLEPUBLIC

    * GET /api/hello  (publico, sin JWT)
    PROCEDURE GetHello(req, res) HELP "GET: hello public"
        LOCAL loRes
        loRes = THIS.newObject("status,data,message")
        loRes.status = "ok"
        loRes.data = "Hola, mundo!"
        loRes.message = ""
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

    * GET /api/hello/{name}  (publico, con parametro)
    PROCEDURE GetHelloName(req, res) HELP "GET: hello/{name} public"
        LOCAL loRes, lcName
        lcName = req.GetParam("name", "Invitado")
        loRes = THIS.newObject("status,data,message")
        loRes.status = "ok"
        loRes.data = "Hola, " + lcName + "!"
        loRes.message = ""
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

ENDDEFINE

Prueba

Con cualquier cliente HTTP o desde el navegador:

curl http://localhost:8080/api/hello
curl http://localhost:8080/api/hello/Irwin

Ejemplo 2: CRUD basico

Operaciones Crear, Leer, Actualizar y Eliminar sobre una tabla de productos. Ilustra el uso de BeforeEndpoint / AfterEndpoint para abrir y cerrar la base de datos.

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 = "Sin productos"
            res.Status(200).Json(THIS.ToJson(loRes))
        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 = ""
            res.Status(200).Json(THIS.ToJson(loRes))
        ENDIF
    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 = "Producto no encontrado"
            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 = "Se esperaba un body JSON"
            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 = "Producto creado"
        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 = "Se esperaba un body JSON"
            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 = "Producto no encontrado"
            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 = "Producto actualizado"
        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 = "Producto no encontrado"
            res.Status(404).Json(THIS.ToJson(loRes))
            RETURN
        ENDIF
        DELETE
        loRes = THIS.newObject("status,message")
        loRes.status = "ok"
        loRes.message = "Producto eliminado"
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

ENDDEFINE

Body JSON para POST/PUT

{
  "nombre": "Teclado mecanico",
  "precio": 89.99,
  "stock": 50
}
Estructura de la tabla productos: Este ejemplo asume una tabla productos.dbf con campos: id (C,36), nombre (C,100), precio (N,10,2), stock (N,6). Ajusta los tipos y longitudes segun tu esquema.

Ejemplo 3: Maestro-Detalle

Obtener un pedido junto con sus lineas de detalle en una sola respuesta JSON jerarquica.

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 = "Pedido no encontrado"
            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
        SELECT tmpDetalles
        lnDets = RECCOUNT("tmpDetalles")
        loRes = THIS.newObject("status,pedido,message")
        loRes.status = "ok"
        loRes.pedido = loPedido
        loRes.message = ""
        * Usar MasterToJSON para la relacion jerarquica
        * o construir manualmente el array de detalles
        LOCAL i
        i = 0
        DIMENSION laDetalles[MAX(lnDets,1)]
        SELECT tmpDetalles
        SCAN
            i = i + 1
            SCATTER MEMO NAME loDetalle
            laDetalles[i] = loDetalle
        ENDSCAN
        * Agregar detalles al objeto de respuesta
        ADDPROPERTY(loRes, "detalles", @laDetalles)
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

    * POST /api/pedidos  -- crea pedido con sus lineas
    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 = "Se requiere cliente e items"
            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 = "Pedido creado"
        loRes.id = lcPedidoId
        loRes.total = lnTotal
        res.Status(201).Json(THIS.ToJson(loRes))
    ENDPROC

ENDDEFINE

Body JSON para crear un pedido

{
  "cliente": "C001",
  "items": [
    { "producto": "P001", "cantidad": 2, "precio": 19.99 },
    { "producto": "P002", "cantidad": 1, "precio": 29.99 }
  ]
}
Tablas necesarias: pedidos (id C36, fecha D, cliente C50, total N10.2) y pedido_detalle (pedido_id C36, linea N4, producto C50, cantidad N8, precio N10.2, importe N12.2).

Buenas practicas

  • Usa siempre BeforeEndpoint y AfterEndpoint para gestionar la apertura y cierre de tablas.
  • Retorna codigos HTTP apropiados: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Server Error.
  • Valida los datos de entrada antes de procesarlos. Nunca confies en el body sin validar.
  • Usa THIS.NewGuid() para generar PKs. Nunca uses secuencias autonumericas en entornos concurrentes.
  • Dentro de TRY/CATCH usa EXIT en lugar de RETURN para salir del bloque.

Siguiente: Referencia API →