IR
irwinrodriguez.dev
Zurück zur Dokumentation

Beispiele

Drei vollständige, einsatzbereite Beispiele, die die häufigsten FoxServer-Muster demonstrieren: ein minimaler Endpunkt, vollständiges CRUD und eine Master-Detail-Beziehung.

Verwenden Sie diese Beispiele als Ausgangspunkt. Passen Sie Tabellennamen, Felder und Routen an Ihr Projekt an.

Beispiel 1: Hello World

Ein minimaler Controller mit zwei Endpunkten: einer ohne Parameter und einer mit URL-Parameter.

HelloController: ein Gruß mit und ohne Parameter
DEFINE CLASS HelloController AS ApiController OLEPUBLIC

    * GET /hello  (oeffentlich, kein JWT)
    PROCEDURE GetHello(req, res) HELP "GET: hello public"
        LOCAL loRes
        loRes = THIS.newObject("status,data,message")
        loRes.status = "ok"
        loRes.data = "Hallo, Welt!"
        loRes.message = ""
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

    * GET /hello/{name}  (oeffentlich, mit Parameter)
    PROCEDURE GetHelloName(req, res) HELP "GET: hello/{name} public"
        LOCAL loRes, lcName
        lcName = req.GetParam("name", "Gast")
        loRes = THIS.newObject("status,data,message")
        loRes.status = "ok"
        loRes.data = "Hallo, " + lcName + "!"
        loRes.message = ""
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

ENDDEFINE

Test

Mit einem beliebigen HTTP-Client oder im Browser:

curl http://localhost:8099/hello
curl http://localhost:8099/hello/Irwin

Beispiel 2: Einfaches CRUD

Erstellen, Lesen, Aktualisieren und Löschen einer Produkttabelle. Zeigt die Verwendung von BeforeEndpoint / AfterEndpoint zum Öffnen und Schließen der Datenbank.

ProductosController: CRUD auf der Tabelle 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

    PROCEDURE GetProductos(req, res) HELP "GET: productos public as productos.list"
        * Ein ganzer Cursor als JSON, in einer Zeile: TableToJson
        * serialisiert ihn, und OkRaw gibt ihn unveraendert zurueck,
        * ohne ihn erneut zu parsen. Bei null Zeilen kommt ein leeres
        * Array heraus, und das ist richtig so.
        SELECT * FROM productos INTO CURSOR tmpProductos NOFILTER
        RETURN THIS.OkRaw(THIS.TableToJson("tmpProductos"))
    ENDPROC

    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 = "Produkt nicht gefunden"
            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

    PROCEDURE CreateProducto(req, res) HELP "POST: productos public"
        LOCAL loRes
        IF ISNULL(req.json)
            loRes = THIS.newObject("status,message")
            loRes.status = "error"
            loRes.message = "JSON-Body erwartet"
            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 = "Produkt erstellt"
        loRes.id = lcGuid
        res.Status(201).Json(THIS.ToJson(loRes))
    ENDPROC

    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 = "JSON-Body erwartet"
            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 = "Produkt nicht gefunden"
            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 = "Produkt aktualisiert"
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

    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 = "Produkt nicht gefunden"
            res.Status(404).Json(THIS.ToJson(loRes))
            RETURN
        ENDIF
        DELETE
        loRes = THIS.newObject("status,message")
        loRes.status = "ok"
        loRes.message = "Produkt gelöscht"
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

ENDDEFINE

JSON-Body für POST/PUT

{
  "nombre": "Mechanische Tastatur",
  "precio": 89.99,
  "stock": 50
}
Tabellenstruktur für productos: Dieses Beispiel setzt eine productos.dbf mit Feldern voraus: id (C,36), nombre (C,100), precio (N,10,2), stock (N,6). Passen Sie Typen und Langen an Ihr Schema an.

Beispiel 3: Master-Detail

Eine Bestellung zusammen mit ihren Positionen in einer einzigen hierarchischen JSON-Antwort abrufen.

PedidosController: Bestellungen mit ihren Positionen
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

    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 = "Bestellung nicht gefunden"
            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, i
        lnDets = RECCOUNT("tmpDetalles")
        loRes = THIS.newObject("status,pedido,message")
        loRes.status = "ok"
        loRes.pedido = loPedido
        loRes.message = ""
        i = 0
        DIMENSION laDetalles[MAX(lnDets,1)]
        SELECT tmpDetalles
        SCAN
            i = i + 1
            SCATTER MEMO NAME loDetalle
            laDetalles[i] = loDetalle
        ENDSCAN
        ADDPROPERTY(loRes, "detalles", @laDetalles)
        res.Status(200).Json(THIS.ToJson(loRes))
    ENDPROC

    * POST /pedidos -- legt eine Bestellung mit ihren Zeilen an
    PROCEDURE CreatePedido(req, res) HELP "POST: pedidos public"
        LOCAL loRes

        * Eine Eigenschaft zu lesen, die nicht kam, wirft Fehler 1734;
        * deshalb wird nicht mit ISNULL geprueft, sondern der Zugriff
        * eingepackt.
        LOCAL lcCliente
        lcCliente = ""
        TRY
            lcCliente = NVL(req.json.cliente, "")
        CATCH
        ENDTRY

        IF !req.HasJsonBody() OR EMPTY(lcCliente)
            RETURN THIS.BadRequest("cliente und items sind erforderlich", "bad_request")
        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

        * Arrays aus dem Rumpf kommen als Collection an, NICHT als
        * VFP-Array: ALEN() scheitert. Durchlaufen wird mit .Count und
        * .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("id,total")
        loRes.id    = lcPedidoId
        loRes.total = lnTotal
        RETURN THIS.Created(loRes, "/pedidos/" + lcPedidoId)
    ENDPROC

ENDDEFINE

JSON-Body zum Erstellen einer Bestellung

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

Best Practices

  • Verwenden Sie immer BeforeEndpoint und AfterEndpoint für das Öffnen und Schließen von Tabellen.
  • Geben Sie passende HTTP-Codes zurück: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Server Error.
  • Validieren Sie Eingabedaten vor der Verarbeitung. Vertrauen Sie dem Body niemals ohne Validierung.
  • Verwenden Sie THIS.NewGuid() für PKs. Verwenden Sie niemals Auto-Inkrement in gleichzeitigen Umgebungen.
  • Innerhalb von TRY/CATCH verwenden Sie EXIT anstelle von RETURN, um den Block zu verlassen.

Weiter: API-Referenz →