Overview
References in Ponzu allow you to create relationships between your Content types.
Ponzu uses an embedded database, rather than a more traditional relational database
with SQL support. This may seem unnatural since there is no native concept of
"foreign keys" or "joins" like you may be used to. Instead, Ponzu wires up your
data using references, which are simply URL paths, like /api/content?type=Post&id=1
A foreign key as a URL path?! Am I crazy? No! For the purpose Ponzu serves, this structure works quite well, especially given its creation was specifically tuned for HTTP/2 features such as "Request/Response Multiplexing" and "Server Push."
There is a deeper dive into the HTTP/2 concepts below, but first we'll walk through a quick tutorial on Ponzu's references.
To generate references from the CLI, please read through the documentation. The example below assumes you understand the syntax.
Create Your Content Types¶
Here we are creating two Content types, Author
and Book
. A Book
will keep
a reference to an Author
in the sense that an author wrote the book.
$ ponzu gen c author name:string photo:string:file bio:string:textarea $ ponzu gen c book title:string author:@author,name pages:int year:int
The structs generated for each look like:
content/author.go
type Author struct { item.Item Name string `json:"name"` Photo string `json:"photo"` Bio string `json:"bio"` }
content/book.go
type Book struct { item.Item Title string `json:"title"` Author string `json:"author"` Pages int `json:"pages"` Year int `json:"year"` }
Notice how the Author
field within the Book
struct is a string
type, not
an Author
type. This is because the Author
is stored as a string
in our
database, as a reference to the Author
, instead of embedding the Author
data
inside the Book
.
Some example JSON data for the two structs looks like:
GET /api/content?type=Author&id=1
(Author
)
{ "data": [ { "uuid": "024a5797-e064-4ee0-abe3-415cb6d3ed18", "id": 1, "slug": "item-id-024a5797-e064-4ee0-abe3-415cb6d3ed18", "timestamp": 1493926453826, "updated": 1493926453826, "name": "Shel Silverstein", "photo": "/api/uploads/2017/05/shel-silverstein.jpg", "bio": "Sheldon Allan Silverstein was an American poet..." } ] }
GET /api/content?type=Book&id=1
(Book
)
{ "data": [ { "uuid": "024a5797-e064-4ee0-abe3-415cb6d3ed18", "id": 1, "slug": "item-id-024a5797-e064-4ee0-abe3-415cb6d3ed18", "timestamp": 1493926453826, "updated": 1493926453826, "title": "The Giving Tree", "author": "/api/content?type=Author&id=1", "pages": 57, "year": 1964 } ] }
As you can see, the Author
is a reference as the author
field in the JSON
response for a Book
. When you're building your client, you need to make a second
request for the Author
, to the URL path found in the author
field of the Book
response.
For example, in pseudo-code:
# Request 1: $book = GET /api/content?type=Book&id=1 # Request 2: $author = GET $book.author # where author = /api/content?type=Author&id=1
Until recently, this would be considered bad practice and would be costly to do over HTTP. However, with the wide availability of HTTP/2 clients, including all modern web browsers, mobile devices, and HTTP/2 libraries in practically every programming language, this pattern is fast and scalable.
Designed For HTTP/2¶
At this point, you've likely noticed that you're still making two independent HTTP requests to your Ponzu server. Further, if there are multiple references or more than one item, you'll be making many requests -- how can that be efficient?
There are two main concepts at play: Request/Response Multiplexing and Server Push.
Request/Response Multiplexing¶
With HTTP/2, a client and server (peers) transfer data over a single TCP connection, and can send data back and forth at the same time. No longer does a request need to wait to be sent until after an expected response is read. This means that HTTP requests can be sent much faster and at the same time on a single connection. Where previously, a client would open up several TCP connections, the re-use of a single connection reduces CPU overhead and makes the server more efficient.
This feature is automatically provided to you when using HTTP/2 - the only
requirement is that you connect via HTTPS and have active TLS certificates, which
you can get for free by running Ponzu with the --https
flag and configuring it
with a properly set, active domain name of your own.
Server Push¶
Another impactful feature of HTTP/2 is "Server Push": the ability to preemptively send a response from the server to a client without waiting for a request. This is where Ponzu's reference design really shows it's power. Let's revisit the example from above:
# Request 1: $book = GET /api/content?type=Book&id=1 # Request 2: $author = GET $book.author # where author = /api/content?type=Author&id=1
Instead of waiting for the server to respond with the data for $book.author
,
the response data is already in the client's cache before we even make the request!
Now there is no round-trip made to the server and back, and the client reads the
pushed response from cache in fractions of a millisecond.
But, how does the server know which response to push and when? You'll need to
specify which fields of the type you've requested should be pushed. This is done
by implementing the item.Pushable
interface.
See the example below which demonstrates a complete implementation on the Book
struct, which has a reference to an Author
.
Example¶
content/book.go
... type Book struct { item.Item Title string `json:"title"` Author string `json:"author"` Pages int `json:"pages"` Year int `json:"year"` } func (b *Book) Push(res http.ResponseWriter, req *http.Request) ([]string, error) { return []string{ // the json struct tag is used to tell the server which // field(s) it should push - only URL paths originating // from your server can be pushed! "author", }, nil } ...
Now, whenever a single Book
is requested, the server will preemptively push the
Author
referenced by the book. The response for the Author
will already be
on the client and will remain there until a request for the referenced Author
has been made.
What else can I Push?
Only fields that are URL paths originating from your server can be pushed.
This means that you could also implement item.Pushable
on the Author
type, and return []string{"photo"}, nil
to push the Author's image!
Other Considerations¶
HTTP/2 Server Push is a powerful feature, but it can be abused just like anything
else. To try and help mitigate potential issues, Ponzu has put some "stop-gaps"
in place. Server Push is only activated on single item API responses, so you
shouldn't expect to see references or files pushed from the /api/contents
endpoint.
An exception to this is the /api/search
endpoint, which only the first
result is pushed (if applicable) no matter how many items are in the response.
You should take advantage of HTTP/2 in Ponzu and get the most out of the system. With the automatic HTTPS feature, there is no reason not to and you gain the additional benefit of encrypting your traffic - which your users will appreciate!