I have a dilemma whether to choose REST-like interface or RPC.
META API structure should be objective - resources are structured as objects with properties and methods.
RPC
Pros:
- Variable methods - not fixed to HTTP (GET, POST, PUT...)
- Each method can have custom arguments
- Simple structure
Cons:
- Not so native for HTTP - data must be retrieved using POST as method call, eg.: POST /contacts/1$get
- Incompatibility with Server sent events which should be retrieved using GET method
- Not so expressive
REST-like
Pros:
- Native to web and HTTP
- Expressive usage - GET for fetch, POST for create, PUT for replace, PATCH for update, etc...
- Custom methods can be used as POST /contacts/1$sendEmail
- Compatibility with native technologies, eg.: server sent events
Cons:
- HTTP methods are bound to resource, so describing resource will be more difficult and less understandable when using custom methods
Example resource schema using RPC
Methods are not bound to HTTP words. Each method has arguments specified so collection record fields can be different than new record fields.
But this collection description is too common. It is difficult to understand which method is used for retrieving data and what arguments mean, eg.: which are for filtering and which are for sorting? This can be solved by specifying that interface (type) "@meta.collection" must implement methods with specific arguments.
Each method endpoint can has it's own schema and META UI can work with it and display arguments as proper fields.
{
"@doctype": "@meta.schema",
"type": "@meta.collection",
"title": "Contacts",
"record": {
"properties": {
"first_name": "...",
"last_name": "..."
}
},
"methods": {
"query": {
"label": "Query contacts collection",
"arguments": {
"limit": {
"type": "meta.integer",
"label": "Limit",
"required": false
}
}
},
"create": {
"label": "Create contact",
"arguments": {
"first_name": {
"type": "meta.text",
"label": "First name",
"required": true
},
"last_name": "..."
}
},
"sendNewsletter": {
"label": "Send newsletter to all contacts",
"arguments": {
"subject": "...",
"content": "..."
}
}
}
}
Methods can be then called using following URLs:
# Returns resource META API schema
GET /contacts
# Query collection, pass arguments as JSON formatted request body
POST /contacts$query
Example resource schema using REST-like
This si more understandable that GET method retrieves data from collection and that POST methods creates new record. But what i new record needs different arguments? And how META UI should display view for new record? In this case it is not so under control.
In this schema we clearly see definition of list records, filters and methods.
{
"@doctype": "@meta.schema",
"type": "@meta.collection",
"title": "Contacts",
"record": {
"properties": {
"first_name": "...",
"last_name": "..."
}
},
"filters": {
"first_name": {
"type": "@meta.text",
"label": "First name"
},
"last_name": {
"type": "@meta.text",
"label": "Last name"
},
"active": {
"type": "@meta.boolean",
"label": "Is active"
}
},
"methods": {
"GET": {
"label": "Retrieve data"
},
"POST": {
"label": "Add new contact",
"arguments": {
"first_name": {
"type": "meta.text",
"label": "First name",
"required": true
},
"last_name": "..."
}
},
"$sendNewsletter": {
"label": "Send newsletter to all contacts",
"arguments": {
"subject": "...",
"content": "..."
}
}
}
}
Calls:
#Retrieve data
GET /contacts
# Create record
POST /contacts
# Call custom method
POST /contacts$sendNewsletter
# Retrieve resource schema
OPTIONS /contacts
Hybrid model
I'am also thinking about combination of these two approaches.
Let's suppose that resources are same as objects in OOP and has properties. Each property has a getter and a setter. And methods are special kind of properties where setting them means calling them. We can also use other HTTP words eg. DELETE which can reset property value to it's default state (if supported).
And URL structure represents this property relations so we can access each property individually including method which can be then represented in UI separately.
Resource schema:
{
"@doctype": "@meta.object",
"@title": "Contacts",
"records": {
"type": "@meta.collection",
"label": "Contacts",
"readonly": true
},
"create": {
"type": "@meta.method",
"label": "Create contact",
"properties": {
"first_name": {
"type": "@meta.text",
"label": "First name",
"required": true
},
"last_name": "..."
}
},
"sendNewsletter": {
"type": "@meta.method",
"label": "Send newsletter",
"properties": {
"recipients": {
"type": "@meta.list",
"itemType": "@meta.integer",
"label": "IDs of recipient contacts",
"required": true
},
"subject": "...",
"content": "..."
}
}
}
Method schema:
{
"@doctype": "@meta.property",
"@title": "Create contact",
"@type": "@meta.method",
"first_name": {
"type": "@meta.text",
"label": "First name",
"required": true
},
"last_name": "..."
}
Record schema:
{
"@doctype": "@meta.object",
"@title": "#{first_name} #{last_name}",
"first_name": {
"type": "@meta.text",
"label": "First name",
"required": true
},
"last_name": "..."
}
Calls:
# Retrieve schema
OPTIONS /contacts
# Fetch records
GET /contacts/records
# Create record
POST /contacts/create
# Send newsletter
POST /contacts/sendNewsletter
# Retrieve method schema
OPTIONS /contacts/sendNewsletter
# Update record
PUT /contacts/records/1
But this approach seems to me still complicated.
Another hybrid model
We can simplify previous model by separating object related properties (such as records or first_name, last_name) with extending properties. It means that simple (flat) properties can be accessed within same resource URI and extending properties (such as methods or collection properties) can be accessed using extended URI structure.
We can also specify filters which tells client that data can be filtered - for collections and also for single record - why not?
Contacts schema:
{
"@doctype": "@meta.object",
"@implements": "@meta.collection",
"methods": ["GET", "LIVE"],
"title": "Contacts",
"properties": {
"records": {
"type": "@meta.list.object",
"label": "Contacts",
"readonly": true,
"properties": {
"id": {
"type": "@meta.integer",
"label": "Record ID",
"private": true,
"..comment..": "Private tells that UI should not display this field by default."
},
"first_name": {
"type": "@meta.text",
"label": "First name"
},
"last_name": "..."
}
},
"count": {
"type": "@meta.integer",
"label": "Total count",
"readonly": true
},
"..comment..": "These properties can be accessed within same resource as this schema using GET, PUT or PATCH methods. In this case of collection only GET method is supported."
},
"query": {
"first_name": {
"type": "@meta.text",
"label": "First name"
},
"last_name": "...",
"..comment..": "Specifies query variables that can be used to filter returned properties."
},
"relations": {
"{records.id}": {
"label": "Record detail"
},
"..comment..": "Specifies next URI endpoints."
},
"actions": {
"create": {
"label": "Create new contact",
"..comment..": "Method arguments (properties) can be retrieved by fetching schema for method URI."
},
"sendNewsletter": {
"label": "Send newsletter to contacts"
}
}
}
Method schema:
{
"@doctype": "@meta.method",
"title": "Create contact",
"methods": ["SET"],
"properties": {
"first_name": {
"type": "@meta.text",
"label": "First name",
"required": true
},
"last_name": "...",
"..comment..": "These properties are method arguments. Method endpoint acts as separated data model."
}
}
Record schema:
{
"@doctype": "@meta.object",
"title": "#{first_name} #{last_name}",
"methods": ["GET", "SET", "DELETE", "LIVE", "LOCK"],
"properties": {
"id": {
"type": "@meta.integer",
"label": "Record ID",
"private": true,
"..comment..": "Private tells that UI should not display this field by default."
},
"first_name": {
"type": "@meta.text",
"label": "First name"
},
"last_name": "..."
},
"relations": {
"invoices": {
"label": "Contact invoices"
}
}
}
Calls:
# Retrieve contacts schema
OPTIONS /contacts
# Fetch records
GET /contacts
# Fetch create method schema
OPTIONS /contacts$create
# Create contact
PUT /contacts$create
# Retrieve contact schema
OPTIONS /contacts/123
# Retrieve contact data
GET /contacts/123
# Update contact data
PUT /contacts/123
# Delete record
DELETE /contacts/123
# Access contact invoices collection schema
OPTIONS /contacts/123/invoices
# Get contacts as live data stream
GET /contacts$$live
# Get contact lock
GET /contacts/123$$lock
# Lock record
PUT /contacts/123$$lock
# Release record lock
DELETE /contacts/123$$lock
This approach seems to me most expressive. It describes data model of current resource, supported access methods (GET, SET, etc...), it's relations separately as links and actions (custom methods) separately as links. It also provides information about query capabilities.
It is also UI friendly because creation of new contact has separated schema which can be displayed as separated UI view (add page). Single record supports GET and SET methods which means, that UI can display it as single page with "Save" button. And query capabilities can tell UI which filter fields can be displayed.
I've also added new methods such as LIVE or LOCK because I want to have server sent events and record locking as standard (but optional) features of META API resources.