Tutorial: Building a Pet Shop API
Build a Pet Shop API with controller stubs, Ecto schemas, and a PostgreSQL backend from a Paradox specification. You define your types and URI endpoints in .dox, paradox-phoenix generates the project scaffolding, and you fill in the handler logic.
What You Will Build
A Phoenix application with these API endpoints:
| Method | Path | Description |
|---|---|---|
|
|
Create a pet |
|
|
Get a pet by ID |
|
|
List all pets |
Prerequisites
-
The
paradoxCLI on yourPATH -
paradox-phoenixon yourPATH -
Elixir 1.14+ and Erlang/OTP 25+
-
A running PostgreSQL database
|
|
Step 1: Write the Specification
Create petshop.dox:
import std
type Pet
name: Text
species: Text
age: Integer
valid Pet
name != "" | Name is required
species != "" | Species is required
age > 0 | Age must be positive
createPet: URI
http://localhost/pets POST (Pet. Pet)
getPet: URI
http://localhost/pets/${Integer} GET (Unit. Pet)
listPets: URI
http://localhost/pets GET (Unit. [Pet])
Each URI constant defines an API endpoint with a path, HTTP method, and input/output types.
Step 2: Run the Generator
paradox-phoenix --title "PetShop" --uris createPet --uris getPet --uris listPets
Output:
Generating Paradox base output...
Parsing specification...
Resolving URI endpoints...
Found 3 URI endpoints:
POST /pets (Pet -> Pet)
GET /pets/${Integer} (Unit -> Pet)
GET /pets (Unit -> [Pet])
Generating Phoenix project: PetShop
Done! Your Phoenix app is at: .dox/pet_shop/
To get started:
cd .dox/pet_shop
mix deps.get
mix ecto.create
mix ecto.migrate
mix phx.server
Step 3: Boot the App
cd .dox/pet_shop
mix deps.get
mix ecto.create
mix ecto.migrate
mix phx.server
The server starts on http://localhost:4000.
Step 4: Fill in the Stubs
Open .dox/pet_shop/lib/pet_shop_web/controllers/pets_controller.ex. You’ll see stub handlers that return 501 Not Implemented. Replace each one with Ecto-backed logic using the generated schema.
First, add the aliases and a shared error helper at the top of the module:
defmodule PetShopWeb.PetsController do
use PetShopWeb, :controller
alias PetShop.Repo
alias PetShop.Schemas.Pet
import Ecto.Query
create_pet — POST /pets
Insert a new pet via the generated changeset (which includes Dox validation bridging):
def create_pet(conn, params) do
changeset = Pet.changeset(%Pet{}, params)
case Repo.insert(changeset) do
{:ok, pet} ->
conn |> put_status(:created) |> json(Pet.to_json(pet))
{:error, changeset} ->
conn |> put_status(:unprocessable_entity) |> json(%{errors: format_errors(changeset)})
end
end
get_pet — GET /pets/:integer
Look up a single pet by primary key:
def get_pet(conn, %{"integer" => id}) do
case Repo.get(Pet, id) do
nil ->
conn |> put_status(:not_found) |> json(%{error: "not found"})
pet ->
json(conn, Pet.to_json(pet))
end
end
list_pets — GET /pets
Return all pets as a JSON array:
def list_pets(conn, _params) do
pets = Repo.all(Pet)
json(conn, Enum.map(pets, &Pet.to_json/1))
end
Error formatting helper
Add a private function to translate changeset errors into JSON:
defp format_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
end
The Ecto schema, changeset validations, and Dox validation bridging are all generated for you — the Pet.changeset/2 call handles both standard Ecto validations (validate_required) and your Paradox valid rules (e.g. name != "").
The Pet.to_json/1 helper is also generated, converting schema structs to plain maps suitable for JSON encoding.
|
Step 5: Test It
# Create a pet
curl -X POST http://localhost:4000/api/pets \
-H "Content-Type: application/json" \
-d '{"name": "Buddy", "species": "Dog", "age": 3}'
# List all pets
curl http://localhost:4000/api/pets
# Get a pet
curl http://localhost:4000/api/pets/1
# Validation error -- empty name
curl -X POST http://localhost:4000/api/pets \
-H "Content-Type: application/json" \
-d '{"name": "", "species": "Cat", "age": 2}'
# => 422 Unprocessable Entity
Optional: Add LiveView
Re-run the generator with --live for stub LiveView pages:
paradox-phoenix --title "PetShop" --uris createPet --uris getPet --uris listPets --live
This adds a browser scope with HomeLive and stub LiveView index pages. Open http://localhost:4000/ to see the home page with links to your API endpoints.
Next Steps
-
Reference — Full flag reference, validation bridging, and architecture
-
Elixir Code Generation — Details on generated Elixir modules and validation
-
SQL Code Generation — Details on generated DDL
-
Express Tutorial — The same workflow in Express.js