Rest Api Builder
This library helps to create Plug compatible Rest API routers that can be manually created or generated based upon a Resource Provider library.
Provider Implementations
- rest_api_builder_essp - Implements a resource provider based upon the Ecto Schema Store library.
Installation
def deps do
[{:rest_api_builder, "~> 0.6"}]
end
Creating an API resource router
All API routers are based upon Plug.Router
and may be included into any standard Plug.Router
path
or forwarded to in a Phoenix Router definition.
In a API module the following paths are built in:
- index -
GET /
- create -
POST /
- show -
GET /:id
- update -
PUT /:id
orPATCH /:id
- delete -
DELETE /:id
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
import Plug.Conn
# Called before a specific resource is loaded on show, update, or delete.
# You will generally want to set the value into Plug.Conn.assigns
def handle_preload(%Plug.Conn{path_params: %{"id" => id}, assigns: assigns} = conn) do
assign(conn, :resource, %{id: id})
end
def handle_index(conn) do
# Will send back a 200 status with the resource in the body as JSON.
send_resource conn, [%{id: 1}, %{id: 2}]
end
def handle_create(conn) do
# Will send back a 201 status with the resource in the body as JSON.
send_resource conn, %{id: 3}
end
def handle_show(%Plug.Conn{assigns: %{resource: resource}} = conn) do
# Will send back a 200 status with the resource in the body as JSON.
send_resource conn, resource
end
def handle_update(%Plug.Conn{assigns: %{resource: resource}} = conn) do
# Will send back a 403 status with an errors JSON body containing the content provided.
send_errors conn, 403, "Cannot update resource"
end
def handle_delete(%Plug.Conn{assigns: %{resource: resource}} = conn) do
# Will send back a 204 status with no content.
send_resource conn, nil
end
end
Reference API Provider
Other libraries can implement a macro that will fill in the functions above based upon some factor relevent to the library.
For reference implementation purposes, the following documentation will use the ecto_schem_store
api provider.
There will be other implementations but based upon this early release the reference implementation is based upon another
library I wrote for simplicity purposes.
The EctoSchemaStore library builds upon ecto to create customizable CRUD modules that utilize and Ecto Repo and Schema.
defmodule Customer do
use EctoTest.Web, :model
schema "customers" do
field :name, :string
field :email, :string
field :account_closed, :boolean, default: false
timestamps
end
def changeset(model, params) do
model
|> cast(params, [:name, :email])
end
end
You can create a store with the following:
defmodule CustomerStore do
use EctoSchemaStore, schema: Customer, repo: MyApp.Repo
end
To learn more about how to use that libary please visit that project. This will be enough for the following examples.
Using a Provider
This library provides a macro to load a provider module into your API.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
end
That’s it. Since the EctoSchemaStore library knows how to interact with itself, it will generate the REST action functions for us.
This same process can be followed any library that adds use RestApiBuilder.Provider
to a module. More on building an API Provider later.
Adding to Plug Router
If you have an existing Plug.Router
module you can add your API with the following:
forward "/customers", to: CustomerApi
Adding to Phoenix Router
forward "/customers", CustomerApi
Any pipeline of plugs can be applied before forwarding if you would like.
Include Other API routers
Any API module can act as a base for other API modules which will utilize the plural name provided in those modules. A better example of a real API would be to set up a version collection.
defmodule ApiV1 do
use RestApiBuilder
include CustomersApi
include PartnersApi
end
Then include this module in your router.
# Plug Router
forward "/api/v1", to: ApiV1
# Phoenix Router
forward "/api/v1", ApiV1
To get to the rest resources you would submit to /api/v1/customers
or to /api/v1/partners
. The resource names were pulled from the child modules.
You can include any REST API module into any other.
Activating Actions
By default no REST actions are activated and an eror will be returned if you attempt to use one of the built in actions.
To activate all you can provided a option on the use
statement.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
end
Actions can also be activate one at a time.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
activate :index
activate :show
activate :create
activate :update
activate :delete
end
Or combined into list.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
activate [:index, :show, :create, :update, :delete]
end
:all
is the same as listing all of the actions. Providing a subset of the list of action will only turn on those actions producing an error for others.
Plugs
Since a REST API module is based upon Plug Router, it predefines plugs that facilitate its functionality. To append your own plugs, you must tell the library to allow for custom plugs.
Although you could use the plug statment directly, it would never fire since the :dispatcher plug will have already fired.
The plugs command is provided to allow you to drop your plugs into the middle of the process. Your plugs will fire after
the preload
function has done its work if you would like to use the loaded resource.
All plugs will be applied at every level of a REST path so any parent resources will have any security checks applied before any children. More on children later.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all, default_plugs: false
import Plug.Conn
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
plugs do
if Mix.env == :dev do
plug :verify_active_customer, verify: false
else
plug :verify_active_customer, verify: true
end
plug prevent_inactive
end
def verify_active_customer(%{assigns: %{current: customer}} = conn, verify: true) do
assign conn, :verified, !customer.account_closed
end
def verify_active_customer(conn, _opts) do
assign conn, :verified, true
end
def prevent_inactive(%{assigns: %{verified: true}} = conn, _opts), do: conn
def prevent_inactive(%{assigns: %{verified: false}} = conn, _opts) do
conn
|> send_resp(404, "Not Found")
|> halt
end
end
Links
A REST API module can add links to the resource. Some basic ones are automatically provided. A provider module may also add additional. Links exist for both the entire resource group and for individual resources.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
group_link :google, "http://www.google.com"
link :author, "/author"
export_links()
end
The export_links
macro will write out the links to the resource depending upon the encoding used for the resource.
Custom links can also be added using group_link
and link
.
Plug Router Matching
You can provide Plug Router level matching since Plug,Router is imported into the API module. You need to make sure you activate any REST actions after the custom matching or the the REST actions may pre-empt the path.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
import Plug.Conn
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
post "/my_action" do
conn
|> send_resp(200, "You got to the action.")
end
activate :all
get "/:id/my_action" do
# Any route with :id in the first level will have the preload plug load the resource.
send_resource conn, CustomerStore.to_map(conn.assigns[:current])
end
group_link :my_action, "/my_action"
link :my_action, "/my_action"
export_links()
end
Features
Some helper macros are provided to help setup route matching. These add ons to the standard REST action are called features in this library. There are group features which apply to the base path or regular features which apply to an individual resource. Using features also sets up the link path for the proivided feature to be added to the resource.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
import Plug.Conn
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
group_feature :my_action do
conn
|> send_resp(200, "You got to the action.")
end
activate :all
feature :my_action do
# Any route with :id in the first level will have the preload plug load the resource.
send_resource conn, CustomerStore.to_map(conn.assigns[:current])
end
export_links()
end
Children
Similar include
the children macro will load another REST API module into the path but associated with a resource.
Providers can then use the parent resource to narrow down the child resources.
# Child Resource
defmodule MessagesApi do
use RestApiBuilder, plural_name: :messages, singular_name: :message, activate: :all
provider RestApiBuilder.EctoSchemaStoreProvider, store: MessageStore,
parent: :customer_id
export_links()
end
# Parent Resource
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
activate :all
children MessagesApi
export_links()
end
The messages resource can be reach via /api/v2/customers/12/messages
a single child resource could be retrieved
via /api/v2/customers/12/messages/37
.
The child resource will be filtered down by the API Provider library based upon the parent. If message 37 existed but did not belong to customer 12, then a HTTP 404 would be returned even though the message id is valid. If customer 12 did not exist or the user does not have access, then the message would never be looked up and the parents error would be returned.
The exact enforcement of relationships is defined by the API provider. If you write your own then you will have to define this enforcement and relatioship yourself.
If the relationships are validated, then links will be created for parenbt and children on the current resource.
Modifying Actions
If the provider allows, then the actions can be overloaded in your code. This will allow you to perform some action before or after the provider’s default action.
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
def handle_create(conn) do
# Do some action before the resource is created by the provider.
conn = super(conn)
# Do some action after the resource is created by the provider.
conn
end
end
Event Announcements
An API module supports the concept of an event through the Event Queues library on Hex. Event Queues must be included in your application and each queue and handler added to your application supervisor. Visit the instructions at (https://hexdocs.pm/event_queues) for more details.
Events:
:after_create
:after_update
:after_delete
Macros:
create_queue
- Creates a Queue for instances where one is not already set up. Accessible at {api module name}.Queueannounces
- Register a what events to announce and what modules to send the event. By default will use {api module name}.Queue
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
create_queue()
announces events: [:after_create, :after_update, :after_delete]
defmodule Handler do
use EventQueues, type: :handler, subscribe: SampleApi.Apis.UsersApi.Queue
def handle(event) do
IO.inspect event
end
end
end
The event queue and any handlers must be started as part of the application.
# Start the event queue and handler.
CustomersApi.Queue.start_link
CustomersApi.Handler.start_link
# As part of the App supervisor
worker(CustomersApi.Queue, []),
worker(CustomersApi.Handler, [])
Direct Access
The API can be accessed internally to your application without needing to make an HTTP call. When directly accessed
via other application code, the Plug.Conn being passed contains the :direct_access
value in assigns
set to true
.
Direct access does not consume JSON text or generate the JSON response. These translations are skipped over as that converting to JSON and back provides no advantage internally. Therefore, you may receive a more complete resource from direct access then you would get as an HTTP call.
Execute process:
process(http_method, path, opts)
or process!(http_method, path, opts)
The http_method can be :get
, :post
, :put
, :patch
, or :delete
.
Options:
params
- Will set the params on Plug.Conn. Keyword list or map.assigns
- Will set values on the assigns of the Plug.Conn passed in. Keyword list or map.headers
- Map or list of tuples with headers.
The following convience methods can be used to access the standard REST paths.
Functions:
- run_index, run_index!
- run_show, run_show!
- run_create, run_create!
- run_update, run_update!
- run_delete, run_delete!
All functions share the same parameters (path, opts)
.
The path is always relative to the API module you are directly calling on.
# Equivalent paths
{:ok, customers} = ApiV1.process :get, "/customers"
{:ok, customers} = ApiV1.run_index "/customers"
{:ok, customers} = CustomersApi.run_index "/"
# Submitting resource
{:ok, customer} = ApiV1.run_create "/customers", params: %{name: "Bob Person"}
customer = ApiV1.run_create! "/customers", params: %{name: "Bob Person"}
Direct Access can be used to build an internal api. This will allow you to use your REST API as a traditional API.
defmodule InternalApi do
def list_customers do
ApiV1.run_index! "/customers"
end
def create_customer(name) do
ApiV1.run_create "/customers", params: %{name: name}
end
def create_customer!(name) do
ApiV1.run_create! "/customers", params: %{name: name}
end
end
When designing plugs, you will want to consider both web access and direct access and may want alternate functionality or
less security when the :direct_access
value is present in assigns.
Testing
Any API module can be tested using Plug.Test
or the direct access methods directly on the modules. You can also
perform controller style tests like you would normally when using Phoenix Framework.
defmodule CustomersApiTest do
use MyApp.ConnCase
test "Test Creating a Customer" do
customer = ApiV1.run_create! "/customers", params: %{name: "Bob Person"}
assert "Bob Person" == customer.name
end
test "Test Creating a Customer using Test Conn", %{conn: conn} do
response =
conn
|> post("/api/v1/customers", %{customer: %{name: "Bob"}})
|> json_response(201)
assert "Bob Person" == response["customer"]["name"]
end
end