About us Guides Projects Contacts
Админка
please wait

Elixir is a functional, concurrent programming language built on the Erlang VM (BEAM), known for its fault tolerance and scalability. Phoenix is the premier web framework for Elixir, offering Rails-like productivity with the performance characteristics that made Erlang famous in telecom systems. This guide covers building production-ready web applications with Phoenix from a senior developer's perspective.

Why Elixir and Phoenix

Elixir brings several unique advantages:

  1. Concurrency: Lightweight processes (not OS threads) allow millions of concurrent connections
  2. Fault Tolerance: Supervisor trees automatically restart failed processes
  3. Hot Code Reloading: Deploy updates without dropping connections
  4. Functional Programming: Immutable data structures prevent entire classes of bugs
  5. Real-time Built-in: WebSockets and PubSub are first-class citizens

Setting Up an Elixir Project

Install Elixir on your system first. On CentOS/RHEL:

# Add the EPEL repository
sudo dnf install epel-release
sudo dnf install elixir erlang

Install the Phoenix helper and create a project:

mix archive.install hex phx_new
mix phx.new my_app
cd my_app
mix ecto.setup
mix phx.server

Phoenix Project Structure

Understanding the folder structure is crucial:

my_app/
├── lib/
│ ├── my_app/ # Business logic (contexts)
│ │ ├── accounts.ex # Accounts context
│ │ └── accounts/
│ │ ├── user.ex # User schema
│ │ └── credential.ex
│ └── my_app_web/ # Web layer
│ ├── controllers/
│ ├── views/
│ ├── templates/
│ ├── channels/ # WebSocket channels
│ └── router.ex
├── priv/
│ └── repo/migrations/ # Database migrations
├── assets/ # Frontend assets
└── config/ # Configuration files

Elixir Syntax Essentials

Elixir has a distinct syntax that leverages pattern matching extensively:

defmodule MyApp.Calculator do
# Pattern matching in function heads
def divide(_, 0) do
{:error, "Division by zero"}
end
def divide(x, y) do
{:ok, x / y}
end
# Pipe operator for function chaining
def process_data(data) do
data
|> String.trim()
|> String.downcase()
|> String.split(",")
end
# Anonymous functions
def map_values(list) do
Enum.map(list, fn x -> x * 2 end)
# Or use shorter syntax: Enum.map(list, &(&1 * 2))
end
end

Pattern matching for extracting values:

# Extract from map
%{id: user_id} = %{id: 123, name: "John"}
# user_id is now 123
# Extract from string
"rows:" <> count = "rows:456"
# count is now "456"

Building a CRUD Application

Define Routes

In lib/myappweb/router.ex:

defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {MyAppWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/", MyAppWeb do
pipe_through :browser
# RESTful resources
resources "/users", UserController
# Or define individually:
get "/posts", PostController, :index
get "/posts/new", PostController, :new
post "/posts", PostController, :create
get "/posts/:id/edit", PostController, :edit
put "/posts/:id", PostController, :update
delete "/posts/:id", PostController, :delete
end
end

Create Migrations

Generate a migration:

mix ecto.gen.migration create_posts

Edit the migration file in priv/repo/migrations/:

defmodule MyApp.Repo.Migrations.CreatePosts do
use Ecto.Migration
def change do
create table(:posts) do
add :title, :string, null: false
add :body, :text
add :published, :boolean, default: false
add :user_id, references(:users, on_delete: :delete_all), null: false
timestamps()
end
create index(:posts, [:user_id])
create index(:posts, [:published])
end
end

Run migrations:

mix ecto.migrate

Define Schemas (Models)

Create lib/my_app/blog/post.ex:

defmodule MyApp.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
# Configure JSON serialization
@derive {Jason.Encoder, only: [:id, :title, :body, :published, :inserted_at]}
schema "posts" do
field :title, :string
field :body, :string
field :published, :boolean, default: false
belongs_to :user, MyApp.Accounts.User
has_many :comments, MyApp.Blog.Comment
timestamps()
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body, :published])
|> validate_required([:title])
|> validate_length(:title, min: 3, max: 255)
|> assoc_constraint(:user)
end
end

Create Context (Business Logic)

Create lib/my_app/blog.ex:

defmodule MyApp.Blog do
import Ecto.Query
alias MyApp.Repo
alias MyApp.Blog.Post
def list_posts do
Post
|> order_by(desc: :inserted_at)
|> Repo.all()
|> Repo.preload(:user)
end
def list_published_posts do
Post
|> where(published: true)
|> order_by(desc: :inserted_at)
|> Repo.all()
|> Repo.preload(:user)
end
def get_post!(id) do
Post
|> Repo.get!(id)
|> Repo.preload([:user, :comments])
end
def create_post(user, attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Ecto.Changeset.put_assoc(:user, user)
|> Repo.insert()
end
def update_post(%Post{} = post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
end
def delete_post(%Post{} = post) do
Repo.delete(post)
end
end

Implement Controller

Create lib/myappweb/controllers/post_controller.ex:

defmodule MyAppWeb.PostController do
use MyAppWeb, :controller
alias MyApp.Blog
alias MyApp.Blog.Post
def index(conn, _params) do
posts = Blog.list_posts()
render(conn, "index.html", posts: posts)
end
def new(conn, _params) do
changeset = Post.changeset(%Post{}, %{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"post" => post_params}) do
user = conn.assigns.current_user
case Blog.create_post(user, post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post created successfully.")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
post = Blog.get_post!(id)
render(conn, "show.html", post: post)
end
def edit(conn, %{"id" => id}) do
post = Blog.get_post!(id)
changeset = Post.changeset(post, %{})
render(conn, "edit.html", post: post, changeset: changeset)
end
def update(conn, %{"id" => id, "post" => post_params}) do
post = Blog.get_post!(id)
case Blog.update_post(post, post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post updated successfully.")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, changeset} ->
render(conn, "edit.html", post: post, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
post = Blog.get_post!(id)
{:ok, _post} = Blog.delete_post(post)
conn
|> put_flash(:info, "Post deleted successfully.")
|> redirect(to: Routes.post_path(conn, :index))
end
end

Create Templates

Create lib/myappweb/templates/post/index.html.heex:

<h1>Posts</h1>
<%= for post <- @posts do %>
<div class="post-item">
<h2><%= link post.title, to: Routes.post_path(@conn, :show, post) %></h2>
<p>By <%= post.user.name %> on <%= post.inserted_at %></p>
<div class="actions">
<%= link "Edit", to: Routes.post_path(@conn, :edit, post) %>
<%= link "Delete", to: Routes.post_path(@conn, :delete, post),
method: :delete, data: [confirm: "Are you sure?"] %>
</div>
</div>
<% end %>
<%= link "New Post", to: Routes.post_path(@conn, :new), class: "btn btn-primary" %>

Authentication with Sessions

Implement user authentication following Phoenix patterns.

Create Credentials Schema

mix phx.gen.context Accounts Credential credentials email:string:unique password_hash:string user_id:references:users

Session Controller

Create lib/myappweb/controllers/session_controller.ex:

defmodule MyAppWeb.SessionController do
use MyAppWeb, :controller
alias MyApp.Accounts
def new(conn, _params) do
render(conn, "new.html")
end
def create(conn, %{"user" => %{"email" => email, "password" => password}}) do
case Accounts.authenticate_by_email_password(email, password) do
{:ok, user} ->
conn
|> put_flash(:info, "Welcome back!")
|> put_session(:user_id, user.id)
|> configure_session(renew: true)
|> redirect(to: "/")
{:error, :unauthorized} ->
conn
|> put_flash(:error, "Invalid email or password")
|> redirect(to: Routes.session_path(conn, :new))
end
end
def delete(conn, _params) do
conn
|> configure_session(drop: true)
|> put_flash(:info, "Signed out successfully")
|> redirect(to: "/")
end
end

Authentication Plug

Add to the router for protected routes:

defmodule MyAppWeb.Router do
# ...
pipeline :authenticate do
plug :require_authenticated_user
end
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :authenticate]
resources "/posts", PostController
end
defp require_authenticated_user(conn, _opts) do
case get_session(conn, :user_id) do
nil ->
conn
|> put_flash(:error, "Please sign in to access this page")
|> redirect(to: Routes.session_path(conn, :new))
|> halt()
user_id ->
assign(conn, :current_user, Accounts.get_user!(user_id))
end
end
end

Plugs (Middleware)

Plugs are composable modules that transform the connection:

defmodule MyAppWeb.Plugs.LoadCurrentUser do
import Plug.Conn
import Phoenix.Controller
alias MyApp.Accounts
def init(opts), do: opts
def call(conn, _opts) do
case get_session(conn, :user_id) do
nil ->
assign(conn, :current_user, nil)
user_id ->
user = Accounts.get_user!(user_id)
assign(conn, :current_user, user)
end
end
end

Running in Production with Docker

Create a Dockerfile:

# Build stage
FROM elixir:1.15-alpine AS builder
RUN apk add --no-cache build-base git
WORKDIR /app
ENV MIX_ENV=prod
# Install Hex and Rebar
RUN mix local.hex --force && mix local.rebar --force
# Copy dependency files
COPY mix.exs mix.lock ./
RUN mix deps.get --only prod
RUN mix deps.compile
# Copy application files
COPY lib lib
COPY priv priv
COPY assets assets
COPY config config
# Compile assets
RUN mix assets.deploy
# Create release
RUN mix release
# Runtime stage
FROM alpine:3.18 AS runtime
RUN apk add --no-cache libstdc++ openssl ncurses-libs
WORKDIR /app
COPY --from=builder /app/_build/prod/rel/my_app ./
ENV HOME=/app
ENV MIX_ENV=prod
CMD ["bin/my_app", "start"]

Key Takeaways

  1. Contexts organize business logic: Keep the web layer thin; put logic in contexts
  2. Pattern matching everywhere: Use it in function heads for cleaner code
  3. Pipe operator for readability: Chain transformations naturally
  4. Plugs are middleware: Compose request processing pipelines
  5. Supervision trees for reliability: Processes restart automatically on failure
  6. Ecto for data layer: Changesets provide explicit data validation

Phoenix combines functional programming elegance with practical web development patterns, making it excellent for building scalable, maintainable applications.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive