Setting Up a Phoenix API Project
A Phoenix API project typically starts with mix phx.new my_app --no-html --no-assets, which skips the HTML/asset pipeline entirely and scaffolds a project split into two top-level directories: lib/my_app, the pure business-logic layer with no knowledge of HTTP (Ecto schemas, contexts, business rules), and lib/my_app_web, the web layer containing the router, controllers, and JSON rendering modules. This separation means your core domain logic can be tested and reused independently of whether it's exposed over HTTP, a GraphQL endpoint, or a background job.
Cricket analogy: It's like a franchise separating its scouting and player-development department (the domain logic) from its matchday broadcast operations (the web layer) -- the same players and stats feed both, but they're organizationally independent.
Routing and Controllers
Phoenix's router defines pipelines -- named chains of plugs -- and then scopes routes through them; an API typically uses an :api pipeline that plugs Plug.Parsers for JSON bodies (skipping :fetch_session and CSRF protection, which are HTML-only concerns) and the resources "/posts", PostController, except: [:new, :edit] macro to generate the conventional RESTful routes (index, show, create, update, delete) in one line. Each controller action receives the connection struct (conn) and a params map, performs its work, and returns a transformed conn via functions like json/2 or render/3 -- controllers themselves should stay thin, delegating actual business logic to a context module rather than calling Repo functions directly.
Cricket analogy: A router pipeline is like a fixed pre-match routine every team follows -- toss, pitch inspection, warm-up -- before any match-specific play begins, just as the :api pipeline runs standard plugs before any controller action executes.
Contexts and Ecto for Data Access
Phoenix's convention is to wrap all database access behind a context module -- for example, a Blog context exposing functions like Blog.list_posts/0, Blog.get_post!/1, and Blog.create_post/1 -- so that controllers never call Repo.insert/1 or Repo.get/2 directly. Inside the context, Ecto.Changeset handles validation and casting: Post.changeset(%Post{}, attrs) declares which fields are required, applies validations like validate_length/3, and returns a changeset that's either valid (ready to insert) or carries structured errors, keeping validation logic centralized in the schema rather than scattered across controllers.
Cricket analogy: A context module is like a team's designated team manager who's the only one authorized to communicate with the board office -- players (controllers) never contact the board directly, they go through the manager, keeping communication consistent and auditable.
Rendering JSON Responses
In modern Phoenix (1.7+), a controller action like def show(conn, %{"id" => id}) fetches the resource through its context and calls render(conn, :show, post: post), which Phoenix dispatches to a matching PostJSON.show/1 function -- a plain module that explicitly builds the response map (e.g. %{data: %{id: post.id, title: post.title}}) rather than serializing the Ecto struct directly. For error cases, the idiomatic pattern is a FallbackController registered via action_fallback MyAppWeb.FallbackController, which pattern-matches on {:error, changeset} returned from a context function and renders a 422 with structured validation errors, keeping every controller action free of repetitive error-handling boilerplate.
Cricket analogy: A PostJSON module is like an official scorer who converts raw match data into the specific format published on the scoreboard, rather than fans reading straight off a player's private notebook -- you never expose the raw internal record directly.
# router.ex
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", MyAppWeb do
pipe_through :api
resources "/posts", PostController, except: [:new, :edit]
end
# post_controller.ex
defmodule MyAppWeb.PostController do
use MyAppWeb, :controller
alias MyApp.Blog
action_fallback MyAppWeb.FallbackController
def index(conn, _params) do
render(conn, :index, posts: Blog.list_posts())
end
def create(conn, %{"post" => post_params}) do
with {:ok, post} <- Blog.create_post(post_params) do
conn
|> put_status(:created)
|> render(:show, post: post)
end
end
end
# post_json.ex
defmodule MyAppWeb.PostJSON do
def index(%{posts: posts}), do: %{data: for(p <- posts, do: data(p))}
def show(%{post: post}), do: %{data: data(post)}
defp data(post), do: %{id: post.id, title: post.title, body: post.body}
endFor authentication, most Phoenix APIs use a token-based scheme (a Plug that validates a Bearer token, or the Guardian library for JWT) rather than session cookies -- sessions rely on :fetch_session, which the :api pipeline deliberately skips.
Never render an Ecto struct straight to JSON with something like Jason.encode!(post) -- unloaded associations show up as #Ecto.Association.NotLoaded<> structs that will raise a Protocol.UndefinedError, and even loaded structs may leak internal fields like password_hash. Always go through an explicit JSON view module that builds the response map field by field.
Testing the API
Phoenix API tests typically use ConnCase, which sets up a conn via build_conn() and provides helpers like json_response(conn, 200) to assert both the HTTP status code and parse the JSON body in one call; a typical test posts to /api/posts with valid params, asserts a 201 and that json_response(conn, 201)["data"]["title"] matches, then asserts a 422 with structured errors when required fields are missing. Test data is usually built with a factory library like ExMachina (insert(:post)) rather than hand-writing Repo.insert! calls in every test, keeping tests focused on behavior rather than setup boilerplate.
Cricket analogy: ConnCase is like a standardized net-practice setup that every batsman uses to test their technique under repeatable conditions, and json_response/2 is like a coach checking both footwork and shot outcome in a single glance rather than two separate reviews.
- Scaffold API-only Phoenix projects with mix phx.new --no-html --no-assets to skip the HTML/asset pipeline entirely.
- The :api router pipeline uses Plug.Parsers for JSON and skips session/CSRF plugs, which are HTML-specific concerns.
- Keep controllers thin by delegating all business logic and database access to context modules (e.g. Blog.create_post/1).
- Use Ecto.Changeset inside schemas to centralize validation and casting logic rather than scattering checks across controllers.
- Render JSON through explicit view modules (e.g. PostJSON) rather than encoding Ecto structs directly, to control exactly what's exposed.
- Use action_fallback with a FallbackController to centralize error-to-status-code mapping instead of repeating it in every action.
- Test with ConnCase, json_response/2, and a factory library like ExMachina for concise, behavior-focused API tests.
Practice what you learned
1. Which command scaffolds a Phoenix project without the HTML/asset pipeline?
2. What is the recommended place for database access logic in a Phoenix app?
3. Why does the :api pipeline skip :fetch_session and CSRF protection?
4. What does action_fallback MyAppWeb.FallbackController accomplish?
5. Why should you avoid encoding an Ecto struct directly to JSON?
Was this page helpful?
You May Also Like
Elixir vs Ruby
A practical comparison of Elixir and Ruby covering syntax, concurrency, fault tolerance, mutability, and when to reach for each language.
Elixir Best Practices
Idiomatic conventions and patterns for writing maintainable, robust Elixir code, from naming and pattern matching to OTP and testing.
Elixir Quick Reference
A condensed cheat-sheet of core Elixir syntax, data types, collection tradeoffs, and standard-library functions for quick lookup.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics