Tuesday, May 13, 2025

How to write Web Service API with Golf

How to build a Web Service API
This article will show a simple shopping web API with basic functions, such as adding customers, items and orders, as well as updating and deleting them. It's easy to write and easy to understand with Golf; that's the reason code doesn't have much in a way of comments - there's no need.

The API returns a valid JSON reply, even if it's just a single string (such as created ID for a customer, item or order). Listing an order returns a JSON document showing the order details.

If you'd like to be as RESTful as possible, you can use POST, PUT, GET or DELETE request methods. We'll cover that in another post.

The example is an open web service, i.e. it doesn't check for any permissions. You can add that separately (see the multitenant SaaS example).

This example will use PostgreSQL database, but you can use other databases too.
Keep this project in its own directory
We'll create a new directory ("shop") to keep everything tidy. You can call this directory whatever you want. Then we'll create a new Golf application ("shopping"):
mkdir shop
cd shop
gg -k shopping

Setup the database
Create the database "db_shopping":
echo "create user $(whoami);
create database db_shopping with owner=$(whoami);
grant all on database db_shopping to $(whoami);
\q"  | sudo -u postgres psql

Create the "customers", "items", "orders" and "orderItems" tables we'll use:
echo "drop table if exists customers; create table if not exists customers (firstName varchar(30), lastName varchar(30), customerID bigserial primary key);
drop table if exists items; create table if not exists items (name varchar(30), description varchar(200), itemID bigserial primary key);
drop table if exists orders; create table if not exists orders (customerID bigint, orderID bigserial primary key);
drop table if exists orderItems; create table if not exists orderItems (orderID bigint, itemID bigint, quantity bigint);" | psql -d db_shopping

Tell Golf about your database
First thing to do is to let Golf know what database it should use:
echo "user=$(whoami) dbname=db_shopping" > db

This tells Golf that a database "db" is setup (with database user being the same as your Operating System user, and the database name being "db_shopping"). Golf makes this easy by letting you use a given database's native format for client configuration, which you're likely to be familiar with. So if you used MariaDB or SQLite for instance, you would have used their native client configuration files.
Source code for web service
Here are the source code files.

- Add a new customer

Create file "add-customer.golf" and copy the following to it:
 begin-handler /add-customer
     out-header use content-type "application/json"
     get-param first_name
     get-param last_name
     // Add a customer SQL
     run-query @db ="insert into customers (firstName, lastName) \
             values ('%s', '%s') returning customerID" output customerID : \
             first_name, last_name
         @"<<print-out customerID>>"
     end-query
 end-handler

- Add new item for sale

Create file "add-item.golf" and copy the following to it:
 begin-handler /add-item
     out-header use content-type "application/json"
     get-param name
     get-param description
     // Add an item to inventory SQL
     run-query @db ="insert into items (name, description) \
         values ('%s', '%s') returning itemID" output item_id : name, description
         @"<<print-out item_id>>"
     end-query
 end-handler

- Add an item to an order

Create file "add-to-order.golf" and copy the following to it:
 begin-handler /add-to-order
     out-header use content-type "application/json"
     get-param order_id
     get-param item_id
     get-param quantity
     // SQL to add an item to an order
     run-query @db ="insert into orderItems (orderId, itemID, quantity) values  ('%s', '%s', '%s')" \
         : order_id, item_id, quantity no-loop affected-rows arows
     @"<<print-out arows>>"
 end-handler

- Create a new order

Create file "create-order.golf" and copy the following to it:
 begin-handler /create-order
     out-header use content-type "application/json"
     get-param customer_id
     // SQL to create an order
     run-query @db ="insert into orders (customerId) \
         values ('%s') returning orderID" output order_id : customer_id
         @"<<print-out order_id>>"
     end-query
 end-handler

- Delete an order

Create file "delete-order.golf" and copy the following to it:
 begin-handler /delete-order
     out-header use content-type "application/json"
     get-param order_id
     begin-transaction
     run-query @db ="delete from orders where orderID='%s'" : order_id \
         no-loop affected-rows order_rows
     run-query @db ="delete from orderItems where orderID='%s'" : order_id \
         no-loop affected-rows item_rows
     commit-transaction
     @{ "orders":"<<print-out order_rows>>", "items":"<<print-out item_rows>>" }
 end-handler

- Make a JSON document describing an order

Create file "json-from-order.golf" and copy the following to it:
 begin-handler /json_from_order
     get-param order_id type string
     get-param curr_order type number
     get-param order_count type number
     get-param customer_id type string
     get-param first_name type string
     get-param last_name type string
     @   {
     @       "orderID": "<<print-out order_id>>",
     @       "customer":
     @       {
     @           "customerID": "<<print-out customer_id>>",
     @           "firstName": "<<print-out first_name>>",
     @           "lastName": "<<print-out last_name>>"
     @       },
     @       "items": [
     set-number curr_item = 0
     // Query to get all items in an order
     run-query @db ="select i.itemID, t.name, t.description, i.quantity \
             from orderItems i, items t where i.orderID='%s' \
                 and t.itemID=i.itemID" \
             output itemID, itemName, itemDescription, itemQuantity : order_id \
             row-count item_count
         @       {
         @           "itemID": "<<print-out itemID>>",
         @           "itemName": "<<print-out itemName>>",
         @           "itemDescription": "<<print-out itemDescription>>",
         @           "itemQuantity": "<<print-out itemQuantity>>"
         // add a comma if there are more items after this
         set-number curr_item=curr_item+1
         if-true curr_item lesser-than item_count
             @       },
         else-if
             @       }
         end-if
     end-query
     @   ]
     // add a comma if there are more orders after this
     set-number curr_order = curr_order+1
     if-true curr_order lesser-than order_count
         @},
     else-if
         @}
     end-if
 end-handler

- List orders

Create file "list-orders.golf" and copy the following to it:
 begin-handler /list-orders
     out-header use content-type "application/json"
     get-param order_id
     set-number curr_order = 0
     // Start JSON output
     @{ "orders": [
     if-true order_id not-equal ""
         // Query just a specific order
         run-query @db = "select o.orderID, c.customerID, c.firstName, c.lastName \
                 from orders o, customers c \
                 where o.customerID=c.customerID and o.orderId='%s'" \
                 output customer_id, first_name, last_name \
                 row-count order_count : order_id
                 set-param order_id, curr_order, order_count, customer_id, first_name, last_name
                 call-handler "/json_from_order"
         end-query
     else-if
         // Query to get all orders
         run-query @db ="select o.orderID, c.customerID, c.firstName, c.lastName \
                 from orders o, customers c \
                 where o.customerID=c.customerID order by o.orderId" \
                 output order_id, customer_id, first_name, last_name \
                 row-count order_count
                 set-param order_id, curr_order, order_count, customer_id, first_name, last_name
                 call-handler "/json_from_order"
         end-query
     end-if
     // Finish JSON output
     @   ]
     @}
 end-handler

- Update an order

Create file "update-order.golf" and copy the following to it:
 begin-handler /update-order
     out-header use content-type "application/json"
     get-param order_id, item_id, quantity
     set-number arows
     // If quantity update is 0, issue SQL to delete an item from order, otherwise update 
     if-true quantity equal "0"
         run-query @db ="delete from orderItems where orderID='%s' and itemID='%s'" \
             : order_id, item_id no-loop affected-rows arows
     else-if
         run-query @db ="update orderItems set quantity='%s' where orderID='%s' and itemID='%s'" \
             : quantity, order_id, item_id no-loop affected-rows arows
     end-if
     @"<<print-out arows>>"
 end-handler

Make the application
Specify the database "db" (remember we set it up above), and make all handlers public (i.e. they can handle external calls from the outside callers, and not just from within the application):
gg -q --db=postgres:db --public

Try it
The following is just playing with the API. Golf lets you run your web services from command line, so you can see byte-for-byte exactly what's the response. So the responses below include HTTP header, which in this case is very simple. You can disable HTTP output by specifying "--silent-header" in gg invocations below.

Add new customer:
gg -r --req="/add-customer/first-name=Mike/last-name=Gonzales" --exec

Resulting JSON (showing the ID of a new customer):
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

"1"

Add an item for sale (showing the ID of a newly added item):
gg -r --req="/add-item/name=Milk/description=Lactose-Free" --exec

The result:
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

"1"

Add a new order for the customer we created (showing the ID of order):
gg -r --req="/create-order/customer-id=1" --exec

The result:
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

"1"

Add an item to order, in quantity of 2 (showing number of items added, not the quantity):
gg -r --req="/add-to-order/order-id=1/item-id=1/quantity=2" --exec

The result:
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

"1"

List orders:
gg -r --req="/list-orders" --exec

Here's the JSON showing current orders (just one in our case):
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

{ "orders": [
   {
       "orderID": "1",
       "customer":
       {
           "customerID": "1",
           "firstName": "Mike",
           "lastName": "Gonzales"
       },
       "items": [
       {
           "itemID": "1",
           "itemName": "Milk",
           "itemDescription": "Lactose-Free",
           "itemQuantity": "2"
       }
   ]
}
   ]
}

Add another item for sale (showing the ID of this new item):
gg -r --req="/add-item/name=Bread/description=Sliced" --exec

The result (showing the ID of a newly added item):
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

"2"

Add a quantity of 3 of the new item we added (ID of 2):
gg -r --req="/add-to-order/order-id=1/item-id=2/quantity=3" --exec

The result (showing the number of items added, not the quantity):
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

"1"

List orders again:
gg -r --req="/list-orders" --exec

The result, now there's new items here:
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

{ "orders": [
   {
       "orderID": "1",
       "customer":
       {
           "customerID": "1",
           "firstName": "Mike",
           "lastName": "Gonzales"
       },
       "items": [
       {
           "itemID": "1",
           "itemName": "Milk",
           "itemDescription": "Lactose-Free",
           "itemQuantity": "2"
       },
       {
           "itemID": "2",
           "itemName": "Bread",
           "itemDescription": "Sliced",
           "itemQuantity": "3"
       }
   ]
}
   ]
}

Update order, by changing the quantity of item (we specify order ID, item ID and the new quantity):
gg -r --req="/update-order/order-id=1/item-id=2/quantity=4" --exec

The result showing number of items updated:
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

"1"

List orders to show the update:
gg -r --req="/list-orders" --exec

And it shows:
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

{ "orders": [
   {
       "orderID": "1",
       "customer":
       {
           "customerID": "1",
           "firstName": "Mike",
           "lastName": "Gonzales"
       },
       "items": [
       {
           "itemID": "1",
           "itemName": "Milk",
           "itemDescription": "Lactose-Free",
           "itemQuantity": "2"
       },
       {
           "itemID": "2",
           "itemName": "Bread",
           "itemDescription": "Sliced",
           "itemQuantity": "4"
       }
   ]
}
   ]
}

Delete an order:
gg -r --req="/delete-order/order-id=1" --exec

The result JSON (showing the number of items deleted in it):
Content-Type: application/json
Cache-Control: max-age=0, no-cache
Status: 200 OK

{ "orders":"1", "items":"2" }

Conclusion
You can see that building web services can be easy and fast. More importantly, what matters is also how easy it is to come back to it 6 months later and understand right away what's what. You can try that 6 months from now and see if it's true for you. I'd say chances are pretty good.