Web API framework with a need for speed
Maverick builds on top of the Plug.Conn.Adapter
for your supported webserver and creates
the webserver handler by reading annotations on functions you want to expose as part of your API. To publish a function, add use Maverick
to your module and the @route
attribute to
the relevant functions. Pass a keyword list of options to the route attribute including the
:path
and you're off to the races.
Once you add the Maverick.Api
to your application supervision tree and start the app,
Maverick compiles your routes into a handler module and sends incoming requests
to your functions, taking care to wrap the return values accordingly.
With Maverick added to your application's dependencies, create a module that implements the
use Maverick.Api
macro and pass it, at a minimum, the name of your application:
defmodule CoolApp.Api do
use Maverick.Api, otp_app: :cool_app
end
Then, in your application supervision tree, add your Maverick Api to the list of children:
children =
[
maybe_a_database,
other_cool_stuff,
{CoolApp.Api, name: :cool_app_web, port: 443},
anything_else
]
Now it's time to annotate the functions you want served over HTTP. These can be anywhere within
your project structure that make sense to you, Maverick will compile them all into your callback
handler. While it's considered best practice to structure your code to separate domain concerns
and maintain good abstractions, in practice this organization has no effect on what functions are
available to be routed to by Maverick. Add use Maverick
to a module that will be
serving functions and then any public function (sorry, no macros) with the @route
annotation will do:
defmodule CoolApp.BusinessLogics do
use Maverick, scope: "api/v1"
@route path: "do/stuff"
def do_stuff(%{"stuff" => what_needs_doing}) do
what_needs_doing |> transform_process_etc
end
end
Once the app is started, you can reach your function at the path, method, etc. configured on
the route annotation, such as: curl -XPOST -d '{\"stuff\":\"transform-me!\"}' "http://host:port/api/v1/do/stuff"
The following options can configure functions annotated with the @route
attribute:
-
:path
- The path, as a string, at which the function should be accessible. Prefix elements of the path with a colon (":") if they should be treated as variables on requests, such asapi/customers/:customer_id
(required). -
:args
- The format of the argument that will be passed to the internal function, which can be one of:params
(the default),{:required_params: [atom()]}
, or:request
.-
:params
- The argument passed to the function will be a string-keyed map merging the path parameters map, query parameters map, and the request body map. -
{:required_params, [atom()]}
- The second element is a list of atoms representing keys that must appear and have non-nil
values in the request params map, this subset of key/value pairs will be sent as a string-keyed map. -
:request
- The entire Maverick request struct will be sent. This is good for handling requests that need access to deeper elements of the HTTP request like the source IP, scheme, port, manipulating headers, etc.
-
-
:method
- The HTTP method the function should respond to as an atom or a string. Defaults to"POST"
(all methods are converted to uppercase strings so follow your personal tastes). -
:success_code
- In the event of success, the HTTP status code that will be returned (defaults to 200). -
:error_code
- In the event of an error (an explicit{:error, term()}
), the HTTP status code that will be returned. -
:scope
- This special option is set at the module level (use Maverick.Api, scope: "path/prefix"
), not on the@route
attribute, to set a shared prefix for all routes handled within the module.
Maverick aims to be simple to use, passing some form of map to your function (see :args
in the above
"Route Options" section) and taking virtually any JSON-encodable term as a result to return to the client.
That said, the following are acceptable return values for your routed functions; use the simplest one that meets your needs and it will be converted accordingly to allow the webserver to hand it back to the client:
-
response()
- Any raw term will be assumed to be a successful request; must be encodable to JSON. -
{:ok, response()}
- An explicit success; the term must be encodable to JSON. -
{:error, response()}
- An explicit (non-exception) failure; the term must be encodable to JSON. -
{code(), headers(), response()}
- A 3-tuple with an explicit integer status code, map of response headers, and response body which, you guessed it, must be encodable to JSON.
In the absence of a full-detailed response return value, Maverick will apply the :success_code
for
the status code of {:ok, response()}
and implicitly successful response()
results and the
:error_code
for the status code of {:error, response()}
results.
When response headers are returned, they are expected to be in the form of a map, as this is the format
in which the function will receive request headers. They will be converted to [{String.t(), String.t()}]
before handing back to the webserver to return to the client. If no response headers are returned, Maverick
will automatically add the {"Content-Type", "application/json"}
header.
In the event an exception is raised during handling of a request, the Handler functions will automatically
rescue and construct a response by calling functions from the the Maverick.Exception
protocol on the exception. See the exception protocol module for implementing it for specific exceptions.
A full web framework with support for live content updates and server-side rendering is great, but sometimes you just want a way to handle JSON-based service requests over HTTP without all the bells and whistles.
Maverick aims to fill the niche between a full Phoenix Framework just for providing a web-based API and hand-rolling a Plug API.
- Make it work
- Support Elli webserver
- Native telemetry integration
- Implement basic auth middleware hooks in the API module (via Plug support)
- Absinthe/GraphQL integration (via Plug support)
- Profit?
The package can be installed by adding maverick
to your list of dependencies in mix.exs
:
def deps do
[
{:maverick, "~> 0.2.0"}
]
end
The docs can be found at https://hexdocs.pm/maverick.