API services¶
Services provide a generalized way to create API calls. These calls automatically use the authentication mechanism (session id or OAuth) to perform access checks.
A Zotonic service is created in a separate service (sub)module. Here you typically define the method name, the type of access (GET or POST) and if authentication is required.
How service names are mapped to URLs¶
The URL to call an API service is defined by the module and the method name.
In a vanilla Zotonic install, there is one single URL namespace under which all API services can be accessed. controller_api by default intercepts all URLs according to the following patterns:
/api/:module/:method
/api/:module
On these URLs, a lookup is done to find the corresponding Zotonic module inside the services subdirectory of the module:
mod_modulename/
mod_modulename.erl
services/
service_modulename_methodname.erl
If no method name is used in the /api/:module
URL, the method name will be
equal to the module name - see /api/search
in the table below.
Examples of URLs that correspond to service handlers:
URL | Module | Method | Located in service .erl file |
---|---|---|---|
/api/base/export | mod_base | export | mod_base/services/service_base_export.erl |
/api/base/info | mod_base | info | mod_base/services/service_base_info.erl |
/api/search | mod_search | search | mod_search/services/service_search_search.erl |
For creating services at alternative URLs, see Creating services at a non-standard URL in the controller_api documentation.
Service naming in detail¶
As stated above, a service module is defined as:
service_<modulename>_<processname>.erl
And is then reachable on the URL
http://<hostname>/api/<module_name>/<process_name>
.
The module module_name to be activated for the API call to work.
If you have a module named mod_something that needs a service to return stats, your directory would look like this:
mod_something/
mod_something.erl
services/
service_something_stats.erl
The url for this service will be http://<site_addr>/api/something/stats
The key is that an activated module (minus the mod_
prefix if you use
them!) should be part of the service name. Zotonic parses the service
modules filename to identify what module a service relates to and what
process should be called. It checks to make sure that module is
activated and it also uses that same information when matching a
service url. So, reversly, service_something_stats.erl
is served by
http://<hostname>/api/something/stats
.
Service metadata¶
Like stated, any service is a regular Erlang module. There are
however a few extra attributes for use in the service which describe
it more. Firstly, there is svc_title
:
-svc_title("Retrieve uptime statistics.").
The title of a service should be a human-readable, one-line description of what the services does. This title is used in the OAuth authentication dialog: when authorizing an application, the titles of the services that it wants to access are listed, for the authorizing user’s consideration.
Secondary there is svc_needauth
:
-svc_needauth(false).
This is a boolean value which tells the system whether or not a user needs to be authorized in order to use the service.
If authentication is needed for a service, a service can only be accessed either by using the session cookie or by using an authorized OAuth (1.0a) token.
Creating a GET service¶
By implementing the process_get/2
function in your service module,
it indicates that it is able to handle GET requests. A full example
of a services which handles a GET request is listed below:
-module(service_something_stats).
-author("Arjan Scherpenisse <arjan@scherpenisse.net>").
-svc_title("Retrieve uptime statistics of the system.").
-svc_needauth(true).
-export([process_get/2]).
-include_lib("zotonic.hrl").
process_get(_ReqData, _Context) ->
Stats = [{count, 12310}, {uptime, 399}],
z_convert:to_json(Stats).
This module could be called service_something_stats.erl
and then
gets served at /api/something/stats
. Its output is a JSON object
containing a count
and an uptime
field, containing some values.
Of course, you would write real code there which retrieves actual stats. If your
module something
contains the function stats_data/1
, call it from the
process function like this:
process_get(_ReqData, Context) ->
Stats = mod_something:stats_data(Context),
z_convert:to_json(Stats).
Creating a POST service¶
Similar to GET, by implementing the process_post/2
function in
your service module, it indicates that it is able to handle POST
requests. The POST parameters are accessible to you by using
z_context:get_q/2
.
A full example of a services which handles a POST request is listed below:
-module(service_something_process).
-author("Arjan Scherpenisse <arjan@scherpenisse.net>").
-svc_title("Processes the given id.").
-svc_needauth(true).
-export([process_post/2]).
-include_lib("zotonic.hrl").
process_post(_ReqData, Context) ->
Id = z_context:get_q("id", Context),
%% Do some processing here...
Response = [{result, Id}],
z_convert:to_json(Response).
This module could be called service_something_process.erl
and then
gets served at /api/something/process
. It requires authentication,
and is only accessible with POST and expects an id
argument to be
posted.
Again, its output is a JSON object containing a result
field.
Setting response headers¶
You can set response headers by returning a {Result, #context{}}
tuple from the process_get/2
and process_post/2
calls:
process_get(_ReqData, Context) ->
Stats = mod_something:stats_data(Context),
Result = {struct, [{count, 100}]},
Context1 = z_context:set_resp_header("Cache-Control", "max-age=3600", Context),
{Result, Context1}.
Uploading files¶
The simplest way to upload files is to use the ready-made API service media_upload. But if you want to have different behavior (for instance to connect an uploaded user picture to a user page), it is easy to create your own.
The post payload should be multipart/form-data
encoded (which is the standard for file uploads).
The posted data is automatically retrieved by Zotonic and made available via z_context
. If you use "upload"
for the form data name
field, you get the upload data from z_context:get_q("upload", Context)
. The resulting value is an #upload{}
record, and can be passed directly to m_media:insert_file
:
Upload = z_context:get_q("upload", Context),
m_media:insert_file(Upload, Context)
Error handling¶
An HTTP status error code will be generated when process_get
or process_post
returns an error object:
{error, error_name, DetailsString}
{error, error_name, DetailsString, ErrorData}
Additionally, you may also throw()
these error structures inside
process_get
and process_post
, to easily short-circuit your
error handling (e.g. for input validation):
Title = z_context:get_q("title", Context),
z_utils:is_empty(Title) andalso
throw({error, missing_arg, "title"}),
Simple error feedback¶
By providing the error name, a corresponding HTTP status code and message will be set. Supported error names are:
Name | Generated message | Status code |
---|---|---|
missing_arg |
Missing argument: + Details | 400 |
unknown_arg |
Unknown argument: + Details | 400 |
syntax |
Syntax error: + Details | 400 |
unauthorized |
Unauthorized. | 401 |
access_denied |
Access denied. | 403 |
not_exists |
Resource does not exist: + Details | 404 |
unprocessable |
Unprocessable entity: + Details | 422 |
(other) | Generic error. | 500 |
For example:
process_post(_ReqData, Context) ->
%% Do some processing here...
case Error of
true ->
{{error, missing_arg, "username"}, Context};
false ->
{z_convert:to_json(Data), Context}
end.
Working with Error Objects¶
In some cases it is useful to return more detailed error feedback. The JSON API has specified a format for this. The thinking behind the format is that the server, after encountering an error, may continue to process information, and instead of returning a single error code, returns multiple found errors.
Taking this approach, this error information is returned as a JSON array, with a top key entry errors
:
["errors": {
"detail": "...",
"source": "...",
"status": "...",
"title": "..."
}]
Of course there is no obligation to use JSON API structure, but if you want, the code of one of those functions - for instance to log on - could look like this:
case User of
undefined ->
{error, [
{status, 422},
{source, "mod_webapp:logon"},
{title, "No user found"},
{detail, "Could not log on user"}
]};
_ ->
{ok, User}
end.
The return data of multiple functions may then be aggregated into a single error data object and returned as a list of Error Objects:
process_post(_ReqData, Context) ->
%% Do some processing here...
%% Accumulate all data...
%% Handle return:
case Data of
{error, ErrData} ->
{{error, unprocessable, "", z_convert:to_json(ErrData)}, Context};
_ ->
{z_convert:to_json(Data), Context}
end.
Enabling Cross-Origin Resource Sharing (CORS)¶
By default the server has a same-origin policy: scripts that access the API must reside on the same server.
Cross-origin resource sharing allows cross-domain requests for apps outside of the server domain. CORS header settings define which requests are (and are not) allowed.
In-depth background information is available at https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
CORS settings are defined in the site’s config.
Site config settings
{service_api_cors, false}
- Set to
true
to enable CORS
{'Access-Control-Allow-Origin', "*"}
{'Access-Control-Allow-Credentials', undefined}
{'Access-Control-Max-Age', undefined}
{'Access-Control-Allow-Methods', undefined}
{'Access-Control-Allow-Headers', undefined}
Note
The config file can be modified without a site restart.
The “Access-Control” settings only work if
service_api_cors
is set to true.The setting name is an Erlang atom and must be in single quotes.
Setting values are either
undefined
or a string value. Multiple values can be set as a comma-separated string, for instance:{'Access-Control-Allow-Headers', "authorization, X-Requested-With, Content-Type"}
Service authentication¶
Like stated, authentication and authorization is done either through
the Zotonic session or through a custom notification hook,
#service_authorize{}
.
For session authentication, you need to have a valid session id (z_sid
)
cookie. This method of authentication is the easiest when you are
accessing the services from JavaScript from the same domain as your
user is logged in to.
When no session is available, but the called services requires
authentication (according to its svc_needauth
metadata attribute),
a notification hook with the name
service_authorize
is called.
In a default Zotonic install, this service_authorize
hook is
handled by the OAuth module, but can be replaced by
a different service authentication module.
The module implementing the service_authorize
hook is expected to
return either undefined (when the request is not applicable) or a
response which must conform to the Webmachine is_authorized/2
return format.