IR
irwinrodriguez.dev
Volver a documentación

Ejemplos

Tres ejemplos completos y listos para usar que demuestran los patrones más comunes de FoxServer: un endpoint mínimo, CRUD completo y una relación maestro-detalle.

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

Ejemplo 1: Hello World

Un controlador mínimo con dos endpoints: uno sin parámetros y otro con parámetro en la URL.

HelloController: un saludo con y sin parámetro
DEFINE CLASS HelloController AS ApiController OLEPUBLIC

    * GET /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 /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:8099/hello
curl http://localhost:8099/hello/Irwin

Ejemplo 2: CRUD básico

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.

ProductosController: CRUD de la tabla productos
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"
        * Un cursor entero a JSON en una linea: TableToJson lo serializa
        * y OkRaw lo devuelve tal cual, sin volver a parsearlo. Con cero
        * filas sale un array vacio, que es lo correcto.
        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 = "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 /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 /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 /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 según tu esquema.

Ejemplo 3: Maestro-Detalle

Obtener un pedido junto con sus líneas de detalle en una sola respuesta JSON jerarquica.

PedidosController: pedidos con sus líneas de detalle
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 = "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 /pedidos  -- crea pedido con sus lineas
    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 = "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 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 = "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, línea 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 códigos 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 →