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 →