Ejemplos prácticos de FoxServer

Los siguientes ejemplos muestran cómo implementar diferentes tipos de funcionalidades con FoxServer. Puedes usar estos ejemplos como punto de partida para tus propios proyectos.

Ejemplo 1: Hello World

Un controlador simple que responde con un mensaje "Hola Mundo" y demuestra el uso básico de FoxServer.

Controlador (HelloController.prg)


DEFINE CLASS HelloController AS ApiController OLEPUBLIC

    * Endpoint simple que devuelve un saludo
    PROCEDURE GetHello(req, res) HELP "GET: hello public"
        LOCAL loResponse
        
        loResponse = THIS.newObject("status,data,message,timestamp")
        loResponse.status = "success"
        loResponse.data = "¡Hola, Mundo!"
        loResponse.message = "Este es un endpoint de ejemplo"
        loResponse.timestamp = DATETIME()                

        res.Status(200).Json(this.ToJson(loResponse))
    ENDPROC
    
    * Endpoint que recibe un parámetro "name" y devuelve un saludo personalizado
    PROCEDURE GetHelloName(req, res) HELP "GET: hello/{name} public"
        LOCAL loResponse, lcName
        
        lcName = req.GetParam("name", "Invitado")
        * Si estás seguro que el parámetro se envía entonces se puede leer así:
        * lcName = req.params.name
        
        loResponse = THIS.newObject("status,data,message,timestamp")
        loResponse.status = "success"
        loResponse.data = "¡Hola, " + lcName + "!"
        loResponse.message = "Este es un saludo personalizado"
        loResponse.timestamp = DATETIME()
        
        res.Status(200).Json(this.ToJson(loResponse))
    ENDPROC
    
ENDDEFINE
                        

Prueba

Puedes probar estos endpoints con cualquier cliente HTTP o desde el navegador:


# Obtener saludo general
curl http://localhost:8080/api/v1/hello

# Obtener saludo personalizado
curl http://localhost:8080/api/v1/hello/Juan
                        

Nota: Este ejemplo muestra cómo crear un controlador simple y definir endpoints con y sin parámetros.

Ejemplo 2: CRUD Básico

Un ejemplo que implementa operaciones CRUD (Crear, Leer, Actualizar, Eliminar) en una tabla de productos.

Controlador (ProductosController.prg)


DEFINE CLASS ProductosController AS ApiController OLEPUBLIC

    FUNCTION BeforeEndpoint(req, res) AS BOOLEAN
        * Setear configuraciones iniciales
        SET DELETED ON
        SET EXCLUSIVE OFF        
        SET CENTURY ON
        SET TABLEPROMPT OFF
        SET SAFETY OFF
        SET TALK OFF

        * Abrir tabla de productos
        SELECT 0
        USE (this.cPath + "data\productos") SHARED
        RETURN .T.
    ENDFUNC
    
    PROCEDURE AfterEndpoint(req, res)
        * Cerrar toda la base de datos
        CLOSE DATABASES ALL
    ENDPROC
    
    * Obtener todos los productos
    PROCEDURE GetProductos(req, res) HELP "GET: productos public"
        LOCAL loResponse, lnProductos
        * Crear cursor con todos los productos
        SELECT * FROM productos INTO CURSOR tmpProductos
        
        lnProductos = _TALLY                                    

        SELECT tmpProductos
        IF EOF()
            loResponse = THIS.newObject("status,data,message")
            loResponse.status = "success"
            loResponse.data = null
            loResponse.message = "No se encontraron productos"
        ELSE
            * Creamos la respuesta pero con data como un array
            loResponse = THIS.newObject(TEXTMERGE("status,data[<>],message"))
            LOCAL i
            i = 0
            * Recorremos el cursor y llenamos el array
            SCAN
                i = i + 1
                SCATTER MEMO NAME loRow
                loResponse.data[i] = loRow
            ENDSCAN
        ENDIF
        * Devolver la respuesta
        res.Status(200).Json(this.ToJson(loResponse))                
    ENDPROC
    
    * Obtener un producto específico
    PROCEDURE GetProducto(req, res) HELP "GET: productos/{id} public"
        LOCAL loResponse, lcId
        
        lcId = req.params.id
        
        * Buscar el producto
        SELECT * FROM productos WHERE id = lcId INTO CURSOR tmpProducto
        
        IF _TALLY = 0
            loResponse = THIS.newObject("status,data,message")
            loResponse.status = "error"
            loResponse.data = null
            loResponse.message = "Producto no encontrado"
            res.Status(404).Json(this.ToJson(loResponse))
        ELSE
            SELECT tmpProducto
            SCATTER MEMO NAME loRow
            loResponse = THIS.newObject("status,data,message")
            loResponse.status = "success"
            loResponse.data = loRow
            loResponse.message = "Producto encontrado"
            res.Status(200).Json(this.ToJson(loResponse))
        ENDIF
    ENDPROC
    
    * Crear un nuevo producto
    PROCEDURE CreateProducto(req, res) HELP "POST: productos public"
        LOCAL loResponse
        
        IF ISNULL(req.json)
            loResponse = THIS.newObject("status,error,message")
            loResponse.status = "error"
            loResponse.error = "Se esperaba un cuerpo JSON"
            loResponse.message = "No se proporcionaron datos para crear el producto"
            res.Status(400).Json(this.ToJson(loResponse))            
            RETURN
        ENDIF
        
        * Insertar nuevo producto
        LOCAL lcGuid
        lcGuid = this.NewGuid()
        SELECT productos
        APPEND BLANK
        REPLACE id WITH lcGuid
        REPLACE nombre WITH req.json.nombre
        REPLACE precio WITH req.json.precio
        REPLACE stock WITH req.json.stock
        

        loResponse = THIS.newObject("status,message,producto")
        loResponse.status = "success"
        loResponse.message = "Producto creado correctamente"
        
        * Devolver el producto creado usando location
        res.status(201).location(lcGuid).Json(this.ToJson(loResponse))                
    ENDPROC
    
    * Actualizar un producto
    PROCEDURE UpdateProducto(req, res) HELP "PUT: productos/{id} public"
        LOCAL loResponse, lcId
        
        lcId = req.params.id
        
        IF ISNULL(req.json)
            loResponse = THIS.newObject("status,error,message")
            loResponse.status = "error"
            loResponse.error = "Se esperaba un cuerpo JSON"
            loResponse.message = "No se proporcionaron datos para actualizar el producto"
            res.Status(400).Json(this.ToJson(loResponse))
            RETURN            
        ENDIF
        
        * Buscar y actualizar el producto
        SELECT productos
        LOCATE FOR id = lcId
        
        IF !FOUND()
            loResponse = THIS.newObject("status,error,message")
            loResponse.status = "error"
            loResponse.error = "Producto no encontrado"
            loResponse.message = "No se pudo actualizar el producto porque no existe"
            res.Status(404).Json(this.ToJson(loResponse))
            RETURN
        ENDIF
        
        * Actualizar campos
        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                
        
        * Crear respuesta
        loResponse = THIS.newObject("status,message,producto")
        loResponse.status = "success"
        loResponse.message = "Producto actualizado correctamente"
        
        * Devolver el producto actualizado
        res.Status(200).Json(this.ToJson(loResponse))                
    ENDPROC
    
    * Eliminar un producto
    PROCEDURE DeleteProducto(req, res) HELP "DELETE: productos/{id} public"
        LOCAL loResponse, lcId
        
        lcId = req.params.id
        
        * Buscar el producto
        SELECT productos
        LOCATE FOR id = lcId
        
        IF !FOUND()
            loResponse = THIS.newObject("status,error,message")
            loResponse.status = "error"
            loResponse.error = "Producto no encontrado"
            loResponse.message = "No se pudo eliminar el producto porque no existe"
            res.Status(404).Json(this.ToJson(loResponse))
            RETURN
        ENDIF
        
        * Eliminar producto
        DELETE
        
        loResponse = THIS.newObject("status,message")
        loResponse.status = "success"
        loResponse.message = "Producto eliminado correctamente"
        res.Status(200).Json(this.ToJson(loResponse))

    ENDPROC
    
ENDDEFINE
                        

Importante

Este ejemplo asume que tienes una tabla llamada "productos" con campos id, nombre, precio y stock. Ajusta la estructura según tus necesidades.

Para utilizar los métodos hooks BeforeEndpoint y AfterEndpoint, asegúrate de marcar el check "Hooks" en la creación de tu proyecto.

Ejemplo de JSON para crear un producto


{
  "nombre": "Teclado mecánico",
  "precio": 89.99,
  "stock": 50
}
                            

Ejemplo 3: Maestro-Detalle

Un ejemplo que muestra cómo trabajar con relaciones maestro-detalle para pedidos y sus líneas.

Controlador (PedidosController.prg)


DEFINE CLASS PedidosController AS ApiController OLEPUBLIC

    FUNCTION BeforeEndpoint(req, res) AS BOOLEAN
        * Configuraciones iniciales
        SET DELETED ON
        SET EXCLUSIVE OFF
        SET CENTURY ON
        SET TABLEPROMPT OFF
        SET SAFETY OFF
        SET TALK OFF

        * Abrir tablas necesarias
        SELECT 0
        USE (this.cPath + "data\pedidos") SHARED
        
        SELECT 0
        USE (this.cPath + "data\pedido_detalle") SHARED
        
        RETURN .T.
    ENDFUNC
    
    PROCEDURE AfterEndpoint(req, res)
        * Cerrar tablas
        CLOSE DATABASES ALL
    ENDPROC
    
    * Obtener un pedido con sus detalles
    PROCEDURE GetPedido(req, res) HELP "GET: pedidos/{id} public"
        LOCAL loResponse, lcId
        
        lcId = req.params.id
        
        * Obtener el pedido
        SELECT * FROM pedidos WHERE id = lcId INTO CURSOR tmpPedido
        
        IF _TALLY = 0
            loResponse = THIS.newObject("status,error,message")
            loResponse.status = "error"
            loResponse.error = "Pedido no encontrado"
            loResponse.message = "No se pudo encontrar el pedido con ID " + lcId
            res.Status(404).Json(this.ToJson(loResponse))
            RETURN
        ENDIF
        
        * Obtener los detalles del pedido
        SELECT * FROM pedido_detalle WHERE pedido_id = lcId INTO CURSOR tmpDetalles
        
        SELECT tmpPedido
        SCATTER MEMO NAME loPedido

        * Crear respuesta
        loResponse = THIS.newObject("status,pedido,detalles[1],message")
        loResponse.status = "success"
        loResponse.pedido = loPedido

        LOCAL i
        i = 0
        * Recorremos los detalles y los agregamos al array
        SELECT tmpDetalles
        SCAN
            i = i + 1
            SCATTER MEMO NAME loDetalle
            loResponse.detalles[i] = loDetalle
        ENDSCAN
        loResponse.message = "Pedido encontrado correctamente"        
        res.Status(200).Json(this.ToJson(loResponse))                
    ENDPROC
    
    * Crear un nuevo pedido con sus detalles
    PROCEDURE CreatePedido(req, res) HELP "POST: pedidos public"
        LOCAL loResponse, lcPedidoId
        
        IF ISNULL(req.json) OR ISNULL(req.json.cliente) OR ISNULL(req.json.items)
            loResponse = THIS.newObject("status,error,message")
            loResponse.status = "error"
            loResponse.error = "Datos incompletos"
            loResponse.message = "Se esperaba un cuerpo JSON con cliente e items"
            res.Status(400).Json(this.ToJson(loResponse))
            RETURN
        ENDIF
        
        * Generar ID para el pedido
        lcPedidoId = this.NewGuid()
        
        * Insertar el pedido
        SELECT pedidos
        APPEND BLANK
        REPLACE id WITH lcPedidoId
        REPLACE fecha WITH DATE()
        REPLACE cliente WITH req.json.cliente
        REPLACE total WITH 0
        
        * Insertar los detalles y calcular total
        LOCAL lnTotal, i
        lnTotal = 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
            REPLACE linea WITH i
            REPLACE producto WITH loItem.producto
            REPLACE cantidad WITH loItem.cantidad
            REPLACE precio WITH loItem.precio
            REPLACE importe WITH loItem.cantidad * loItem.precio
            
            lnTotal = lnTotal + (loItem.cantidad * loItem.precio)
        NEXT
        
        * Actualizar total en el pedido
        SELECT pedidos
        LOCATE FOR id = lcPedidoId
        REPLACE total WITH lnTotal
        
        * Devolver el pedido creado
        SELECT * FROM pedidos WHERE id = lcPedidoId INTO CURSOR tmpPedido
        SELECT * FROM pedido_detalle WHERE pedido_id = lcPedidoId INTO CURSOR tmpDetalles
        
        SELECT tmpPedido
        SCATTER MEMO NAME loPedido
        loResponse = THIS.newObject("status,pedido,detalles[1],message")
        loResponse.status = "success"
        loResponse.pedido = loPedido
        
        LOCAL i
        i = 0
        * Recorremos los detalles y los agregamos al array
        SELECT tmpDetalles
        SCAN
            i = i + 1
            SCATTER MEMO NAME loDetalle
            loResponse.detalles[i] = loDetalle
        ENDSCAN
        loResponse.message = "Pedido creado correctamente"
        
        res.Status(201).Json(this.ToJson(loResponse))                
    ENDPROC
    
ENDDEFINE
                        

Ejemplo de JSON para crear un pedido


{
  "cliente": "C001",
  "items": [
    {
      "producto": "P001",
      "cantidad": 2,
      "precio": 19.99
    },
    {
      "producto": "P002",
      "cantidad": 1,
      "precio": 29.99
    }
  ]
}
                        

Estructura de tablas necesarias

  • Tabla pedidos

    Campos: id (C), fecha (D), cliente (C), total (N)

  • Tabla pedido_detalle

    Campos: pedido_id (C), linea (N), producto (C), cantidad (N), precio (N), importe (N)

Conclusión

Estos ejemplos muestran algunas de las funcionalidades básicas de FoxServer. Para casos más avanzados, consulta la Referencia API o revisa el código fuente del proyecto.

Buenas prácticas

  • Utiliza siempre BeforeEndpoint y AfterEndpoint para gestionar recursos como conexiones a bases de datos.
  • Devuelve códigos HTTP apropiados según el resultado de la operación.
  • Valida siempre los datos de entrada antes de procesarlos.
  • Documenta tus endpoints con comentarios claros.