A Phoenix controller module that validates requests and responses using typespecs.
When you use PhoenixSpectral.Controller, your controller actions use the convention
(conn, path_args, query_params, headers, body) instead of the standard
Phoenix (conn, params). Request data is decoded and validated against your typespecs,
and responses are encoded automatically.
Usage
defmodule MyAppWeb.UserController do
use PhoenixSpectral.Controller
@spec show(Plug.Conn.t(), %{id: String.t()}, %{}, %{}, nil) :: {200, %{}, User.t()}
def show(_conn, path_args, _query_params, _headers, _body) do
user = Repo.get!(User, path_args.id)
{200, %{}, user}
end
@spec create(Plug.Conn.t(), %{}, %{}, %{}, UserInput.t()) :: {201, %{}, User.t()} | {422, %{}, Error.t()}
def create(_conn, _path_args, _query_params, _headers, body) do
case Repo.insert(body) do
{:ok, user} -> {201, %{}, user}
{:error, changeset} -> {422, %{}, format_errors(changeset)}
end
end
endOptions
Options are forwarded to use Phoenix.Controller (e.g. formats: [:json]).
Required vs optional query params and headers
%{required(:key) => type}— missing key returns400 Bad Request%{optional(:key) => type}— missing key is omitted from the decoded map
Path parameters are always required.
OpenAPI annotations with spectral/1
use PhoenixSpectral.Controller implies use Spectral. Use spectral/1 before
@spec to annotate the endpoint with summary: and description:. On type
aliases, spectral description: "..." adds a description to the parameter in
the OpenAPI output.
Actions without @spec
Actions without a @spec crash on dispatch and during OpenAPI generation — use
a plain use Phoenix.Controller module to bypass PhoenixSpectral entirely.
Using conn
conn is always passed as the first argument. Use it for out-of-band context —
conn.assigns (auth from upstream plugs), conn.remote_ip, conn.host,
conn.method, conn.private, etc.
Do not use conn to access data that is already decoded and validated by the
framework: use path_args, query_params, headers, and body instead.
Reading from conn.path_params, conn.query_params, conn.req_headers, or
conn.body_params directly bypasses type validation.
An action may also return conn directly for streaming, file sends, or any
other response that cannot be expressed as {status, headers, body}. In that case,
PhoenixSpectral passes the conn through without schema validation — the typespec still
documents the endpoint, but the response is the caller's responsibility.
How It Works
- Extracts path params, query params, headers, and body from
conn - Decodes and validates them against the action's typespec via
Spectral.decode - Calls your handler as
action(conn, path_args, query_params, headers, decoded_body) - Encodes the
{status, headers, body}response viaSpectral.encode - Sends the response on
conn - On validation failure, returns a 400 response