Install
openclaw skills install phoenix-api-genGenerate a full Phoenix JSON API from an OpenAPI spec or natural language description. Creates contexts, Ecto schemas, migrations, controllers, JSON views/renderers, router entries, ExUnit tests with factories, auth plugs, and tenant scoping. Use when building a new Phoenix REST API, adding CRUD endpoints, scaffolding resources, or converting an OpenAPI YAML into a Phoenix project.
openclaw skills install phoenix-api-gensecuritySchemes.YYYYMMDDHHMMSS)*JSON modules, or *View for older)See references/phoenix-conventions.md for project structure, naming, context patterns.
Key rules:
Accounts, Billing, Notifications).MyApp.Accounts.User.{:ok, resource} or {:error, changeset}.FallbackController with action_fallback/1 to handle error tuples.See references/ecto-patterns.md for schema, changeset, migration details.
Key rules:
timestamps(type: :utc_datetime_usec).@primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id.create_changeset/2 and update_changeset/2 when create/update fields differ.Add tenant_id :binary_id to every tenant-scoped table. Pattern:
# In context
def list_resources(tenant_id) do
Resource
|> where(tenant_id: ^tenant_id)
|> Repo.all()
end
# In plug — extract tenant from conn and assign
defmodule MyAppWeb.Plugs.SetTenant do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
tenant_id = get_req_header(conn, "x-tenant-id") |> List.first()
assign(conn, :tenant_id, tenant_id)
end
end
Always add a composite index on [:tenant_id, <resource_id or lookup field>].
defmodule MyAppWeb.Plugs.ApiKeyAuth do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
with [key] <- get_req_header(conn, "x-api-key"),
{:ok, account} <- Accounts.authenticate_api_key(key) do
assign(conn, :current_account, account)
else
_ -> conn |> send_resp(401, "Unauthorized") |> halt()
end
end
end
defmodule MyAppWeb.Plugs.BearerAuth do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
{:ok, claims} <- MyApp.Token.verify(token) do
assign(conn, :current_user, claims)
else
_ -> conn |> send_resp(401, "Unauthorized") |> halt()
end
end
end
scope "/api/v1", MyAppWeb do
pipe_through [:api, :authenticated]
resources "/users", UserController, except: [:new, :edit]
resources "/teams", TeamController, except: [:new, :edit] do
resources "/members", MemberController, only: [:index, :create, :delete]
end
end
See references/test-patterns.md for ExUnit, Mox, factory patterns.
Key rules:
async: true on all tests that don't share state.Ecto.Adapters.SQL.Sandbox for DB isolation.ex_machina or hand-rolled build/1, insert/1.Mox — define behaviours, set expectations in test.defmodule MyAppWeb.UserControllerTest do
use MyAppWeb.ConnCase, async: true
import MyApp.Factory
setup %{conn: conn} do
user = insert(:user)
conn = put_req_header(conn, "authorization", "Bearer #{token_for(user)}")
{:ok, conn: conn, user: user}
end
describe "index" do
test "lists users", %{conn: conn} do
conn = get(conn, ~p"/api/v1/users")
assert %{"data" => users} = json_response(conn, 200)
assert is_list(users)
end
end
describe "create" do
test "returns 201 with valid params", %{conn: conn} do
params = params_for(:user)
conn = post(conn, ~p"/api/v1/users", user: params)
assert %{"data" => %{"id" => _}} = json_response(conn, 201)
end
test "returns 422 with invalid params", %{conn: conn} do
conn = post(conn, ~p"/api/v1/users", user: %{})
assert json_response(conn, 422)["errors"] != %{}
end
end
end
defmodule MyAppWeb.UserJSON do
def index(%{users: users}), do: %{data: for(u <- users, do: data(u))}
def show(%{user: user}), do: %{data: data(user)}
defp data(user) do
%{
id: user.id,
email: user.email,
inserted_at: user.inserted_at
}
end
end
timestamps(type: :utc_datetime_usec){:error, changeset} and {:error, :not_found}