Placid
A REST toolkit for building highly-scalable and fault-tolerant HTTP APIs with Elixir.
Configuration
HTTP
Options
port
- Port to listen for HTTP requests.
HTTPS
By default, connecting to a Placid-based API will require all requests to be made over HTTPS, responding to HTTP requests with a 403 Forbidden
. If desired, the https_only
option may be set allow HTTP requests to be served by your application. Take a look at the kitchen sink example to see this in action.
Options
certfile
- Path to the certificate file.keyfile
- Path to the certificate key file.otp_app
- If present,certfile
andkeyfile
can be relative paths with respect tootp_app
'spriv
directory.port
- Port to listen for HTTPS requests.
Note: Create a self-signed certificate for easy testing.
# Generate a keyfile $ openssl genrsa -out key.pem 2048 # Create a CSR $ openssl req -new -key key.pem -out request.pem # Generate a certfile that expires in $NUM_DAYS $ openssl x509 -req -days $NUM_DAYS -in request.pem -signkey key.pem -out cert.pem
Routing
defmodule Router do
use Placid.Router
# Define one of the versions of the API
# with a simple version number "1"
# or following semver "1.0.0"
# or date of release "2014-09-06"
version "1" do
# Define your routes here
get "/", Handlers.V1.Pages, :index
get "/pages", Handlers.V1.Pages, :create
post "/pages", Handlers.V1.Pages, :create
put "/pages/:page_id" when id == 1,
Handlers.V1.Pages, :update_only_one
get "/pages/:page_id", Handlers.V1.Pages, :show
# Auto-create a full set of routes for resources
#
resource :users, Handlers.V1.User, arg: :user_id
#
# Generates:
#
# get "/users", Handlers.V1.User, :index
# post "/users", Handlers.V1.User, :create
# get "/users/:user_id", Handlers.V1.User, :show
# put "/users/:user_id", Handlers.V1.User, :update
# patch "/users/:user_id", Handlers.V1.User, :patch
# delete "/users/:user_id", Handlers.V1.User, :delete
#
# options "/users", "HEAD,GET,POST"
# options "/users/:_user_id", "HEAD,GET,PUT,PATCH,DELETE"
end
# An updated version of the AP
version "2" do
get "/", Handlers.V2.Pages, :index
post "/pages", Handlers.V2.Pages, :create
get "/pages/:page_id", Handlers.V2.Pages, :show
put "/pages/:page_id", Handlers.V2.Pages, :update
raw :trace, "/trace", Handlers.V2.Tracer, :trace
resource :users, Handlers.V2.User
resource :groups, Handlers.V2.Group
end
end
get/3
, post/3
, put/3
, patch/3
, delete/3
, options/2
, and any/3
are already built-in as described. resource
exists but will need modifications to create everything as noted.
raw/4
allows for using custom HTTP methods, allowing your application to be HTTP spec compliant.
version/2
will need to be created outright. Will allow requests to contained endpoints when version exists in either Accepts
header or URL (which ever is defined in app config).
Extra routes will need to be added for *.json
, *.xml
, etc. requests for optionally specifying desired content type without the use of the Accepts
header. These should match parsing/rendering abilities of Placid.
Should required/optional params be gathered for matching purposes? Only return a matched route when all required params are present?
Handlers
defmodule Handlers.V2.Pages do
use Placid.Handler
@doc """
List all available pages
"""
def index(conn, []) do
# Somehow get our content
pages = Queries.Page.all
render conn, pages
end
@doc """
Show an individual page
"""
def show(conn, args) do
result = case Integer.parse args["page_id"] do
:error ->
%Error{ id: "no_page_id",
message: "A valid page_id is required." }
{i, _} ->
Queries.Page.get i
end
render conn, result
end
@doc """
Create a new page
"""
def create(conn, args) do
render conn, Queries.Page.create(args), status: :created
end
@doc """
Update an individual page
"""
def update(conn, args) do
result = case Integer.parse args["page_id"] do
:error ->
%Error{ id: "no_page_id",
message: "A valid page_id is requried." }
{i, _} ->
Queries.Page.update i, args
end
render conn, result
end
end
Actions in handler modules are responsible for handling a request once it has been routed. These actions typically generate a response, whether that be an error, a result, or a result set, so that it can be rendered to the client with the correct content type further up the stack.
CORS
Should have an option to respect Cross-origin resource sharing (CORS) when desired.
Main response headers:
Access-Control-Allow-Origin
: *|[list of allowed hosts]Access-Control-Allow-Credentials
: true (or ignore header)Access-Control-Allow-Methods
: [list of allowed methods]Access-Control-Allow-Headers
: [list of allowed beyond simple]Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
Access-Control-Max-Age
: [# of seconds]
Should check over the HTML5 Rocks CORS flowchart as much as possible.
Is JSON-P
still a thing? Should it be supported? What happens with non-GET
requests?
Request Parsing
Parsing request bodies from their content type to Elixir terms allows the handler actions to easily use that data in responding to the client. There should be one parser for each supported response content type, with an additional parser for form encoded data.
Current Parsers:
- JSON - Encoded into standard map
- XML - Encoded into a list of maps, each containing a representation of XML nodes from the request body
- WWW-Encoded and Multipart form data - Encoded into a standard map
Rendering
Render layer serializes/encodes data based on the requested content type unless overridden for whatever reason in the response stack.
Rendering engine behavior:
defmodule Placid.Response.Rendering.Engine do
use Behaviour
@type data :: Keyword | Map | List
defcallback serialize(data, type, subtype) :: { :ok, binary } | :next
end
defmodule Placid.Response.Rendering.JSON do
@behaviour Placid.Response.Rendering.Engine
@types ["application", "text"]
def serialize(data, type, "json") when type in @types do
{ :ok, data |> Poison.encode!(string: true) }
end
def serialize(_, _, _), do: :next
end
Internationalization
I18n should always be considered when producing an API.
Linguist is already a part of the project's dependencies. Need to think of ways to make translations seemless as possible, similar to rendering.
TODO
- Respects HTTP specifications (7230, 7231, 7232, 7233, 7234, 7235)
- Compatibility with web frameworks via umbrella projects.
- Would be nice to offer tight integration when available, e.g.
Phoenix.Topic
notifications
- Would be nice to offer tight integration when available, e.g.
- Foundations
- Prefer TLS. Require clients to use TLS when enabled in server
- Version with
Accept
header. Fallback to URL versioning - Support caching with
Etags
- Trace requests with Request-Ids
- Paginate with ranges. Mostly lies on end-developer, but we should provide facility to easily set headers
- Requests
- Return appropriate status codes. Mostly lies on end-developer, but we should return them when appropriate
- Provide full resources where available
- Accept serialized JSON/XML in request bodies
- Downcase paths and attributes
- Support non-id dereferencing for convenience. No type checking occurs on parameters
- Minimize path nesting. Mostly lies on end-developer, but we should ensure generated route paths are as simple as posisble
- Responses
- Provide resource (UU)IDs
- Provide standard timestamps
- Use UTC times formatted in ISO8601
- Nest foreign key relations
- Generate structured errors
- Show rate limit status
- Keep JSON minified in all responses
- Artifacts
- Provide machine-readable JSON schema
- Provide human-readable docs
- Provide executable examples
- Describe stability
This list comes primarily from the HTTP API Design Guide by @interagent and friends but will be updated to fit the needs of the project.
License
Placid is released under the MIT License.
See LICENSE for details.