Pie Crust
1 Quick Start
1.1 Minimal Example
1.1.1 Generated endpoints and request formats
1.1.2 Notes on the generated API
1.2 Extended Example
1.2.1 API hooks
2 API Documentation
crud-op/  c
create-api-manager
join-spec/  c
api-dispatcher
api-db
exn:  fail:  api:  error
8.2

Pie Crust

Nadeem Abdul Hamid <nadeem@acm.org>

 (require piecrust) package: piecrust

Pie Crust is an automated (RESTful) CRUD API generator for use with the library for Web Applications in Racket.

Warning: This library should be treated as an alpha release. It has not been stress-tested or bullet-proofed. Bug reports, feature requests, and pull requests are welcomed at the Github repo.

In particular:
  • This library has only been tested/used with small SQLite databases.

  • It assumes either TEXT or INTEGER fields. Conversion to and from JSON and SQL data types is minimal to non-existent at this point.

  • Although there shouldn’t be any (i.e. properly parameterized queries are generated and used), it has not been thoroughly vetted for safety from injection attacks.

  • The first time you try to use it, something’s bound to not work. (One cannot be too pessimistic.)

With all that said, feel free to try it out and have fun! Use cases and ideas for improvement are welcome, as noted above.

1 Quick Start

Pie Crust provides a lightweight way to bind an SQL database table(s) to a REST-based API providing responses in JSON format. It enables you to generate API endpoints for creating, reading/querying, updating, and deleting data in the underlying table(s) by defining a dispatching function (based on dispatch-rules) that can be used with serve/servlet.

1.1 Minimal Example

(Running code for this example is in the piecrust/tests/minimal-example.rkt file.)

Consider an SQLite database set up like this:

>  CREATE TABLE Stores ( Store_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,

                         Name TEXT NOT NULL,

                         Address TEXT);

 

   INSERT INTO Stores (Store_id, Name, Address) VALUES (1, 'Adli', '123 Main');

   INSERT INTO Stores (Store_id, Name, Address) VALUES (2, 'BeeJays', '456 First');

   INSERT INTO Stores (Store_id, Name) VALUES (3, 'Warmalt');

The following code will launch a JSON-based API on http://localhost:8000.

(require piecrust)
 
(define dbc (sqlite3-connect ...))
 
(define store-api
  (create-api-manager "/api-endpoint"
                      dbc          ; database connection
                      "Stores"     ; table name
                        ; table column schema
                      `((["Store_id" id] integer "Internal ID")
                        ("Name" text #f "Store name")
                        ("Address" text))
                      "Store_id")) ; primary key
 
(serve/servlet (api-dispatcher store-api)
               #:servlet-regexp #rx""
               #:servlet-path "/")

There are several overloaded forms to specifying the column schema. See the full documentation for create-api-manager below for details.

1.1.1 Generated endpoints and request formats

With the server above running, the follow endpoints are provided:

1.1.2 Notes on the generated API

1.2 Extended Example

(Running code for this example is in the piecrust/tests/shopping-list-example.rkt file.)

Consider an SQLite database set up with two tables like this:

>  CREATE TABLE Stores ( Store_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,

                         Name TEXT NOT NULL,

                         Address TEXT);

 

   CREATE TABLE Items ( ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,

                         Store_id INTEGER NOT NULL,

                         Name TEXT NOT NULL,

                         Brand TEXT,

                         FOREIGN KEY (Store_id) REFERENCES Stores (Store_id));

 

   INSERT INTO Stores (Store_id, Name, Address) VALUES (1, 'Adli', '123 Main');

   INSERT INTO Stores (Store_id, Name, Address) VALUES (2, 'BeeJays', '456 First');

   INSERT INTO Stores (Store_id, Name) VALUES (3, 'Warmalt');

 

   INSERT INTO Items (ID, Store_id, Name, Brand) VALUES (101, 2, 'Chips', "Louie's");

   INSERT INTO Items (ID, Store_id, Name, Brand) VALUES (102, 1, 'Donuts', 'Delites');

   INSERT INTO Items (ID, Store_id, Name, Brand) VALUES (103, 1, 'Eggs', 'Fresh');

   INSERT INTO Items (ID, Store_id, Name, Brand) VALUES (104, 3, 'Flour', "Baker's");

   INSERT INTO Items (ID, Store_id, Name) VALUES (105, 2, 'Grapes');

The following code will set up and launch a JSON-based API on http://localhost:8000.

(require piecrust)
 
(define dbc (sqlite3-connect ...))
 
; A default generic request handler for root path "/" requests
(define (main-servlet req)
  (if (equal? "" (path/param-path (first (url-path (request-uri req)))))
      (response/xexpr `(body (h1 "Shopping List - API Endpoints")
                             (a ([href "/api/stores"]) "Stores") (br)
                             (a ([href "/api/v1/items"]) "Items")))
      (next-dispatcher))) ; hand everything else off to the next dispatcher
 
; Stores API endpoint
(define store-api
  (create-api-manager "api/stores" ; API endpoint
                      dbc          ; database connection
                      "Stores"     ; table name
                      `((["Store_id" id] integer "Internal ID")
                        ("Name" text "Store name")
                        ("Address" text))  ; table columns
                      "Store_id" ; primary key
 
                      #:request-wrapper
                      (λ (req crud-op api-handler)
                        (match crud-op
                          ['delete (response/jsexpr "not supported" #:code 400)]
                          [_ (api-handler req)]))
 
                      #:fallback-request-handler main-servlet))
 
; Items API endpoint
(define items-api
  (create-api-manager "api/v1/items"
                      dbc
                      "Items"
                      `(("ID" integer)
                        (["Store_id" store-id] integer #f "Store ID")
                        ("Name" text #f) ; must be non-null
                        ("Brand" text))
                      "ID"
 
                      #:response-wrapper
                      (λ (js crud-op)
                        (response/jsexpr (make-immutable-hasheq
                                          `((meta . #hasheq((endpoint . "api/v1/items")))
                                            (data . ,js)))))
 
                      #:error-wrapper
                      (λ (exn crud-op resp-code jsexpr)
                        (response/jsexpr
                         (make-immutable-hasheq `((operation . ,(symbol->string crud-op))
                                                                  (message . ,jsexpr)))
                         #:code resp-code))
 
                      #:fallback-request-handler (api-dispatcher store-api)))
 
 
(serve/servlet (api-dispatcher items-api)
                 #:servlet-regexp #rx"" #:servlet-path "/" #:launch-browser? #f)
1.2.1 API hooks

The extended example above demonstrates several hooks that are supplied with the create-api-manager form.

2 API Documentation

procedure

crud-op/c : contract?

 = (symbols 'create 'query 'read 'update 'delete)

syntax

(create-api-manager
                    [endpoint-path string?]
                    [database-conn connection?]
                    [table-name string?]
                    [columns [listof col-schema]]
                    [primary-key string?]
                    joins-clause
                    fallback-handler-clause
                    request-wrapper-clause
                    response-wrapper-clause
                    error-wrapper-clause
                    field-name-generator-clause)
 
col-schema = ([col-name json-key] sql-type null-ok? col-description)
  | (col-name sql-type null-ok? col-description)
  | ([col-name json-key] sql-type col-description)
  | ([col-name json-key] sql-type null-ok?)
  | (col-name sql-type col-description)
  | (col-name sql-type null-ok?)
  | (col-name sql-type)
     
joins-clause = 
  | #:joins joins-list
     
fallback-handler-clause = 
  | #:fallback-request-handler disp-fun
     
request-wrapper-clause = 
  | #:request-wrapper pre-fun
     
response-wrapper-clause = 
  | #:response-wrapper post-fun
     
error-wrapper-clause = 
  | #:error-wrapper err-fun
     
field-name-generator-clause = 
  | #:field-name-generator gen-fun
 
  col-name : string?
  json-key : symbol?
  sql-type : (or/c 'text 'integer)
  null-ok? : boolean?
  col-description : string?
  joins-list : (listof join-spec/c)
  disp-fun : (request? . -> . any)
  pre-fun : (request? crud-op/c (request? . -> . response?) . -> . response?)
  post-fun : (jsexpr? crud-op/c . -> . response?)
  err-fun : (exn? crud-op/c response-code/c jsexpr? . -> . response?)
  gen-fun : (string? . -> . symbol?)
Produces an API manager instance.

The endpoint-path should be a slash-separated path string specifying the base endpoint from which the API will be served, for example, "/myapi/v1".

The most comprehensive form of the column schema is:

syntax

(list (list "sql-column-name" json-key)
      sql-type  null-ok?  "col-description")

sql-column-name should be the name of the column in the SQL table. json-key-name will be used as the key label for matching fields in the JSON output. Requests to create, update, or query data must also use the JSON key name, as the SQL column name is only used internally in the API to bind to the database. If a json-key-name is not specified, the function supplied to the #:field-name-generator option is used to build the JSON key name from the SQL column name. The default generator is (compose string->symbol string-downcase). The null-ok? parameter corresponds to (the absence of) a NOT NULL constraint in the SQL table. If not specified, it is assumed to be #t. The column description is only used for metadata purposes, and if not specified is just the column name.

See the API hooks section for descriptions of the optional keyword arguments.

Details on the #:joins clause

The #:joins clause specifies additional tables to which the primary one may be joined, based on its primary key. It supports three scenarios, described in the next paragraph. In the description that follows, "primary table" refers to the table declared in the create-api-manager clause. The "secondary table" is the one from which associated data is nested in the primary JSON output. Example code for each of the three scenarios is provided in the piecrust/tests/book-author-example.rkt file of the Github repo.)

procedure

join-spec/c : contract?

 = 
(or/c (list/c symbol? (list/c string? (listof col-schema)) string?)
      (list/c symbol? (list/c string? (listof col-schema) string?) string?)
      (list/c symbol? (list/c string? (listof col-schema) string?)
              (list/c string? string?) string?))

procedure

(api-dispatcher an-api-manager)  (request? . -> . any)

  an-api-manager : api?
Produces the dispatch function for the given API manager instance (generated by create-api-manager), suitable for use with serve/servlet.

procedure

(api-db an-api-manager)  connection?

  an-api-manager : api?
Produces the database connection to which the given API manager instance is bound.

struct

(struct exn:fail:api:error exn:fail ()
    #:extra-constructor-name make-exn:fail:api:error)
Represents an API error, for example, a missing field name in the body of a request, or an invalid parameter.