Basics
This crash course covers the core 85% of Phoenix Framework that you’ll use daily, giving you enough foundation to explore the remaining 15% on your own.
Introduction to Phoenix Framework
Phoenix is a web framework written in Elixir that implements the server-side MVC (Model-View-Controller) pattern. It’s built to handle high concurrency, provide fault tolerance, and enable real-time communication.
Key Benefits:
- Exceptional performance (millions of WebSocket connections on a single server)
- High developer productivity with concise, maintainable code
- Built-in developer tools (formatter, linter, test framework)
- Real-time capabilities through LiveView without writing JavaScript
- Powerful data handling with Ecto (the database wrapper)
graph TD A[Elixir Language] --> B[Phoenix Framework] B --> C[Controllers] B --> D[Views/Templates] B --> E[LiveView] B --> F[Contexts] B --> G[Ecto ORM] B --> H[Plugs] B --> I[Channels]
Prerequisites and Installation
Before installing Phoenix, you need:
- Elixir and Erlang installed
- A PostgreSQL database (default, though other databases are supported)
- Node.js (for asset compilation)
Installation Steps
- Install Hex (Elixir’s package manager):
mix local.hex
- Install the Phoenix application generator:
mix archive.install hex phx_new
- Create a new Phoenix project:
mix phx.new my_app
# Common flags:
# --no-ecto (skip database)
# --no-html (API-only)
# --live (include LiveView)
- Set up the database:
# Configure database in config/dev.exs
mix ecto.create
- Start the server:
cd my_app
mix phx.server
# Visit http://localhost:4000
Project Structure
Phoenix follows a well-defined project structure that separates business logic from the web interface:
my_app/
├── lib/
│ ├── my_app/ # Business Logic
│ └── my_app_web/ # Web Interface
├── assets/ # CSS, JS, etc.
├── config/ # Configuration
├── priv/ # Assets and DB
└── test/ # Test Files
Key Directories and Files:
lib/my_app/
: Business logic and contextslib/my_app_web/
: Web interface (controllers, views, templates)lib/my_app_web/router.ex
: Defines routeslib/my_app_web/endpoint.ex
: Entry point for HTTP requestsassets/
: Frontend assets (CSS, JavaScript)config/
: Environment-specific configurationpriv/
: Database migrations and static assetstest/
: Test files
The MVC Pattern in Phoenix
Phoenix implements the Model-View-Controller pattern with some Elixir-specific adaptations:
graph TD A[Browser/Client] -->|Request| B[Router] B -->|Routes to| C[Controller] C -->|Gets data from| D[Context/Model] D -->|Returns data to| C C -->|Renders| E[View/Template] E -->|Response| A
Controllers
Controllers handle incoming HTTP requests, interact with business logic, and prepare data for views:
defmodule MyAppWeb.ProductController do
use MyAppWeb, :controller
alias MyApp.Store # Context module
def index(conn, _params) do
# Get data from context
products = Store.list_products()
# Render view with data
render(conn, :index, products: products)
end
def show(conn, %{"id" => id}) do
# Pattern matching on params
product = Store.get_product!(id)
render(conn, :show, product: product)
end
def new(conn, _params) do
# Create empty changeset for form
changeset = Store.change_product(%Store.Product{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"product" => product_params}) do
case Store.create_product(product_params) do
{:ok, product} ->
conn
|> put_flash(:info, "Product created successfully.")
|> redirect(to: ~p"/products/#{product}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
end
Views and Templates
Modern Phoenix uses HTML modules for views. Templates are embedded Elixir (HEEx) files that define the HTML structure:
# View module (lib/my_app_web/controllers/product_html.ex)
defmodule MyAppWeb.ProductHTML do
use MyAppWeb, :html
# Automatically embeds templates from this directory
embed_templates "product_html/*"
# Helper function available in templates
def product_price(product) do
"$#{Decimal.to_string(product.price)}"
end
end
# Template (lib/my_app_web/controllers/product_html/index.html.heex)
<h1>Products</h1>
<div class="products-list">
<%= for product <- @products do %>
<div class="product-card">
<h3><%= product.name %></h3>
<p><%= product_price(product) %></p>
<.link href={~p"/products/#{product}"}>View Details</.link>
</div>
<% end %>
</div>
Routing in Phoenix
Routes map HTTP requests to controller actions. Phoenix’s router provides a powerful DSL for defining routes:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# Define pipelines (middleware groups)
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
# Browser routes
scope "/", MyAppWeb do
pipe_through :browser
get "/", PageController, :index
# RESTful resources (creates 7 standard routes)
resources "/products", ProductController
# LiveView route
live "/dashboard", DashboardLive
end
# API routes
scope "/api", MyAppWeb do
pipe_through :api
resources "/products", ProductAPIController, except: [:new, :edit]
end
end
Common route types:
- Simple routes:
get "/about", PageController, :about
- Resource routes:
resources "/users", UserController
- Live routes:
live "/dashboard", DashboardLive
- Nested resources:
resources "/users", UserController do resources "/posts", PostController end
Contexts: Organizing Business Logic
Contexts are modules that encapsulate and group related functionality, separating business logic from the web layer:
graph TD A[Web Layer] -->|Uses| B[Contexts] B -->|Manages| C[Schemas] B -->|Talks to| D[External Services] B -->|Contains| E[Business Logic]
Example context:
defmodule MyApp.Store do
import Ecto.Query
alias MyApp.Repo
alias MyApp.Store.Product
# List all products
def list_products do
Repo.all(Product)
end
# Get a product by ID
def get_product!(id) do
Repo.get!(Product, id)
end
# Create a product
def create_product(attrs \\ %{}) do
%Product{}
|> Product.changeset(attrs)
|> Repo.insert()
end
# Update a product
def update_product(%Product{} = product, attrs) do
product
|> Product.changeset(attrs)
|> Repo.update()
end
# Delete a product
def delete_product(%Product{} = product) do
Repo.delete(product)
end
# Create empty changeset for forms
def change_product(%Product{} = product, attrs \\ %{}) do
Product.changeset(product, attrs)
end
end
Database Integration with Ecto
Ecto is Phoenix’s database wrapper that provides schema definitions, changesets for validation, and a query DSL.
Schemas and Changesets
Schemas define the structure of your database tables:
defmodule MyApp.Store.Product do
use Ecto.Schema
import Ecto.Changeset
schema "products" do
field :name, :string
field :description, :string
field :price, :decimal
field :slug, :string
belongs_to :category, MyApp.Store.Category
has_many :reviews, MyApp.Store.Review
timestamps() # Adds inserted_at and updated_at
end
# Changeset handles validation and type casting
def changeset(product, attrs) do
product
|> cast(attrs, [:name, :description, :price, :slug, :category_id])
|> validate_required([:name, :price])
|> validate_length(:name, min: 2, max: 100)
|> validate_number(:price, greater_than: 0)
|> unique_constraint(:slug)
|> generate_slug()
end
# Custom changeset function to generate slug
defp generate_slug(changeset) do
case get_change(changeset, :name) do
nil -> changeset
name ->
slug = name |> String.downcase() |> String.replace(~r/[^a-z0-9]+/, "-")
put_change(changeset, :slug, slug)
end
end
end
Migrations
Migrations define database schema changes over time:
defmodule MyApp.Repo.Migrations.CreateProducts do
use Ecto.Migration
def change do
create table(:products) do
add :name, :string, null: false
add :description, :text
add :price, :decimal, null: false, precision: 10, scale: 2
add :slug, :string
add :category_id, references(:categories, on_delete: :nothing)
timestamps()
end
create unique_index(:products, [:slug])
create index(:products, [:category_id])
end
end
Generate and run migrations:
# Generate a migration
mix ecto.gen.migration create_products
# Run migrations
mix ecto.migrate
Seeding the Database
Create seed files in priv/repo/seeds.exs
:
# priv/repo/seeds.exs
# Categories
alias MyApp.Repo
alias MyApp.Store.Category
alias MyApp.Store.Product
# Clear existing data (optional)
Repo.delete_all(Product)
Repo.delete_all(Category)
# Insert categories
{:ok, electronics} = Repo.insert(%Category{name: "Electronics"})
{:ok, books} = Repo.insert(%Category{name: "Books"})
{:ok, clothing} = Repo.insert(%Category{name: "Clothing"})
# Insert products
Repo.insert!(%Product{
name: "Laptop",
description: "High-performance laptop",
price: Decimal.new("999.99"),
category_id: electronics.id
})
Repo.insert!(%Product{
name: "Phoenix Programming",
description: "Learn Phoenix Framework",
price: Decimal.new("39.99"),
category_id: books.id
})
Repo.insert!(%Product{
name: "T-shirt",
description: "Comfortable cotton t-shirt",
price: Decimal.new("19.99"),
category_id: clothing.id
})
IO.puts "Seed data inserted successfully!"
Run seeds:
mix run priv/repo/seeds.exs
Authentication
Phoenix doesn’t include authentication out of the box, but you can add it using phx_gen_auth
:
# Add phx_gen_auth to your dependencies
# mix.exs
{:phx_gen_auth, "~> 0.7.0", only: [:dev], runtime: false}
# Generate authentication system
mix phx.gen.auth Accounts User users
This generates:
- User schema and context
- Registration and login forms
- Password hashing with BCrypt
- Session management
- Account confirmation and reset functions
Deployment
Here’s a basic deployment workflow for Phoenix applications:
graph TD A[Local Development] --> B[Configure Production Settings] B --> C[Compile Assets] C --> D[Build Release] D --> E[Deploy to Server] E --> F[Run Migrations] F --> G[Start Phoenix Server]
Basic Deployment Steps
- Configure production settings in
config/runtime.exs
:
# config/runtime.exs
import Config
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
raise "DATABASE_URL environment variable is missing"
config :my_app, MyApp.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
# Other production settings...
end
- Build a release:
MIX_ENV=prod mix release
- Deploy the release to your server:
# Example using scp (secure copy)
scp -r _build/prod/rel/my_app user@your-server:/app
- On the server, run:
# Set environment variables
export DATABASE_URL=postgres://username:password@hostname/database_name
export SECRET_KEY_BASE=$(mix phx.gen.secret)
export PHX_HOST=your-domain.com
export PORT=4000
# Run migrations
/app/my_app/bin/my_app eval "MyApp.Release.migrate"
# Start the application
/app/my_app/bin/my_app start
Docker Deployment
Create a Dockerfile
:
FROM elixir:1.15-alpine AS build
# Install build dependencies
RUN apk add --no-cache build-base npm git
WORKDIR /app
# Copy config files
COPY config ./config
COPY mix.exs mix.lock ./
# Install dependencies
RUN mix deps.get --only prod
RUN mix deps.compile
# Copy assets
COPY assets ./assets
COPY priv ./priv
# Compile assets
RUN mix assets.deploy
# Copy application code
COPY lib ./lib
# Compile and build release
RUN mix compile
RUN mix release
# Start a new stage for the final image
FROM alpine:3.18 AS app
RUN apk add --no-cache libstdc++ openssl ncurses-libs
WORKDIR /app
# Copy the release from the build stage
COPY --from=build /app/_build/prod/rel/my_app ./
# Run the application
CMD ["bin/my_app", "start"]
The Remaining 15%: Advanced Topics to Explore
These advanced topics represent the remaining 15% that you can explore as needed:
OTP (Open Telecom Platform)
- GenServers for stateful processes
- Supervisors for fault tolerance
- Applications and supervision trees
- Process communication patterns
LiveView
- Real-time UI updates without JavaScript
- Event handling and state management
- Live components and hooks
- Form handling and validation
Channels
- WebSocket communication
- Custom protocol implementations
- Client libraries for JavaScript, iOS, Android
Phoenix PubSub
- Distributed real-time communication
- Broadcasting messages across a cluster
- Implementing presence features
Advanced Ecto Features
- Complex queries and associations
- Database functions and fragments
- Multi-database setups
- Custom types and validations
Performance Optimization
- Profiling and benchmarking
- Database query optimization
- Cache strategies
- Distributed systems
Custom Plugs
- Building authentication systems
- Request transformation
- Response composition
- Building middleware pipelines
API Development
- JSON API best practices
- API versioning
- Rate limiting
- GraphQL with Absinthe
Testing Strategies
- Property-based testing
- Integration testing
- End-to-end testing
- Load testing
- Mocking external services
Summary
This crash course covered the core 85% of Phoenix Framework that you’ll use daily:
- Project setup and structure
- MVC architecture implementation
- Routing configuration
- Context organization
- Database integration with Ecto
- Authentication and deployment
Phoenix’s excellent documentation (https://hexdocs.pm/phoenix) and growing community make it easy to explore the advanced topics as your needs evolve. The framework’s focus on developer productivity and runtime performance makes it an excellent choice for building modern web applications.