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
ENDDEFINEPrueba
Con cualquier cliente HTTP o desde el navegador:
curl http://localhost:8099/hello
curl http://localhost:8099/hello/IrwinEjemplo 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
ENDDEFINEBody 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
ENDDEFINEBody 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 →