Module
Elixir is a functional programming language. Module is used to group functions, like namespace in Clojure.
defmodule Calculator do
@moduledoc """
A simple module
"""
@doc """
Adds two numbers together.
## Examples
iex> Calculator.add(2, 3)
5
"""
def add(a, b) do
a + b
end
end
import
Modules can be used in other modules directly like Calculator.add.
With import, you can get rid of the prefix.
defmodule MyApp do
import Calculator
def sum(a, b, c) do
# Call add directly instead of Calculator.add
add(add(a, b), c)
end
end
Only import specific functions:
defmodule MyApp do
import Calculator, only: [add: 2]
end
use
use is more powerful.
When use SomeModule is encountered, the __using__/1 macro defined in that module will be called, which can then inject code into the current module.
defmodule Greeter do
defmacro __using__(opts) do
quote do
def hello(name) do
"Hello, #{name}!"
end
def greeting_type do
unquote(opts[:type] || "formal")
end
end
end
end
Use it in another module:
defmodule MyApp do
use Greeter, type: "casual"
def greet(name) do
hello(name) <> " Welcome to #{greeting_type()} chat."
end
end
Module Resolving
When you import or use a module, Elixir doesn't directly look for
files - it looks for compiled modules that are available in the
current compilation context. Loading paths can be inspected using :code.get_path.
iex -S mix
:code.get_path
Elixir follows a naming convention:
- Module names use
PascalCase - Filenames use
snake_case.ex - Nested modules (like
MyApp.User) map to directories (my_app/user.ex)
Basic Types
Boolean
In Elixir, the only values considered falsy are false and nil, same as Clojure.
String
Double quotes (") create a binary string (UTF-8 encoded)
msg = "鯖の塩焼き"
is_binary(msg)
# true
byte_size(msg)
# 15
String.at(msg, 1)
# "の"
byte_size("über")
# 5
Single quotes (') create a character(integer) list, which is deprecated, use ~c if you want a list.
chars = ~c"atoz"
is_list(chars)
# true
hd(chars)
# 97
heredoc
"""
Hello,
Alice
"""
# "Hello,\nAlice\n"
String concatenation
name = "Alice"
"Hello, " <> name <> "!"
# "Hello, Alice!"
String interpolation
name = "Alice"
"Hello, #{name}!"
# "Hello, Alice!"
interpolation not work with ~S and ~C
name = "Alice"
~S"Hello, #{name}!"
# "Hello, \#{name}!"
The String module
import String
name = "Alice"
"Hello, #{upcase(name)}!"
# "Hello, ALICE!"
contains? "abc", "b"
# true
trim " hello\n"
# hello
replace "hello", "e", "a"
# "hallo"
Sigils
Sigils in Elixir are a mechanism for working with textual representations using a compact and specialized syntax. They provide a convenient way to create and manipulate different types of data.
Syntax
~<letter><delimiter><content><delimiter><modifiers>
common <delimiter> are ", ', (, [, {, <, /, but you are free to use other chars.
Built-in Sigils
String: ~s and ~S
one reason to use ~s is that it's easier to include double quotes:
~s(double "quotes")
# "double \"quotes\""
uppercased sigils like ~S ignores interpolation or escaping
~S(\n #{})
# "\\n \#{}"
Character List: ~c and ~C
hd(~c"a")
# 97
Regular Expression: ~r and ~R
"a" =~ ~r/^[a-z]$/
# true
Word List: ~w and ~W
~w(split by space)
# ["split", "by", "space"]
~w(a for atom)a
# [:a, :for, :atom]
Date/Time Sigils: ~D, ~T, ~U, ~N
~D[1970-01-01]
# ~D[1970-01-01]
Custom Sigils
defmodule HTMLSigil do
def sigil_h(string, _opts) do
string
|> String.replace("&", "&")
|> String.replace("<", "<")
|> String.replace(">", ">")
end
end
import HTMLSigil
html = ~h(<div>Hello & welcome</div>)
# "<div>Hello & welcome</div>"
Date and Time
Elixir has four primary date/time types:
Date- Just dateTime- Just timeNaiveDateTime- Date and time without timezoneDateTime- Date and time with timezone
Date.new(2025, 5, 2)
# {:ok, ~D[2025-05-02]}
Date.new(2025, 5, 32)
# {:error, :invalid_date}
Time.new(23, 59, 59)
# {:ok, ~T[23:59:59]}
Time.new(24, 0, 0)
# {:error, :invalid_time}
NaiveDateTime.new(2025, 5, 2, 23, 59, 59)
# {:ok, ~N[2025-05-02 23:59:59]}
DateTime.new(Date.utc_today(), Time.utc_now())
# {:ok, ~U[2025-05-02 23:59:59.802817Z]}
DateTime.utc_now()
# ~U[2025-05-02 23:59:59.931917Z]
Manipulations
Date.diff(~D[2025-05-02], ~D[2025-05-08])
# -6
Date.add(~D[2025-05-02], 365)
# ~D[2026-05-02]
DateTime.compare(~U[2025-05-02 23:59:59Z], DateTime.utc_now())
# :lt
Formating
Calendar.strftime(~U[2025-05-02 23:59:59Z], "%Y-%m-%d %H:%M:%S")
# "2025-05-02 23:59:59"
DateTime.shift_zone(~U[2025-05-02 23:59:59Z], "Asia/Tokyo")
# {:error, :utc_only_time_zone_database}
Anonymous Functions
fn a, b -> a + b end
There is more concise form:
&(&1 + &2)
Which reminds me of the Clojure anonymous function shorthand:
#(+ % %2)
The fn ... end form is more versatile, it supports pattern matching
in arguments, and allows multiple clauses.
handle_result = fn
{:ok, value} -> "Success: #{value}"
{:error, reason} -> "Error: #{reason}"
end
To call a anonymous function directly:
sum = fn a, b -> a + b end
sum.(2, 3)
If you are curious why there is a dot. There is a lenthy explaination.
But usually you don't use anoymous functions this way. In most cases, anonymous functions are passed to other functions:
Enum.reduce([1, 2, 3, 4], 0, &(&1 + &2))
Pipe Operator
Pipe operator |>:
import String
" hello "
|> trim
|> upcase
Similar to Clojure's threading macro:
(require '[clojure.string :refer [trim upper-case]])
(-> " hello "
trim
upper-case)
It is worth mentioning that Clojure's threading macro is more versertile with variations like ->>, as-> and cond->.
Collections
Tuple
tp = {:ok, "value"}
Tuple elements are usually accessed via pattern matching:
{:ok, value} = tp
value
# "value"
But they can also be accessed using elem:
elem(tp, 0)
# :ok
Lists
xs = [1, 2, 3]
hd(xs)
# 1
tl(xs)
# [2, 3]
[first | rest] = xs
rest
# [2, 3]
[first | rest]
# [1, 2, 3]
xs ++ [4]
# [1, 2, 3, 4]
Accessing elements by index is an O(n):
Enum.at(xs, 1)
# 2
Enum.at(xs, 3)
# nil
Enum.at(xs, 3, :default_val)
# :default_val
Operations map, filter on Lists are more common:
Enum.map([1, 2, 3], fn x -> x * 2 end)
# [2, 4, 6]
Enum.filter([1, 2, 3], &rem(&1, 2) == 0)
# [2]
Enum.reduce([1, 2, 3, 4], 0, &+/2)
# 10
Keyword Lists
A keyword list is a list of two-element tuples where the first element (the key) is an atom.
foo = [{:a, 1}, {:a, 2}, {:b, nil}]
# [a: 1, a: 2, b: nil]
Elixir's keyword lists are conceptually very similar alists in Emacs Lisp.
'((first-name . "John") (last-name . "Doe") (age . 30))
Elixir provides a more concise syntax sugar for keyword lists:
foo = [a: 1, a: 2, b: nil]
# [a: 1, a: 2, b: nil]
hd(foo)
# {:a, 1}
foo[:a]
# 1
Keyword.get_values(foo, :a)
# [1, 2]
Keyword.has_key?(foo, :b)
# true
foo[:c]
# nil
Keyword Lists are usually small(access is O(n)).
A great use case is for passing optional named arguments to functions:
String.split "hello world ", " ", trim: true
# ["hello", "world"]
defmodule Greeter do
def greet(name, opts \\ []) do
greeting = Keyword.get(opts, :greeting, "Hello")
"#{greeting}, #{name}!"
end
end
Greeter.greet("Alice")
# "Hello, Alice!"
Greeter.greet("Alice", greeting: "Hi")
# "Hi, Alice!"
This is usually achieved by using a map in other languages. In Clojure it's keyword arguments:
(defn greet [name & {:keys [greeting] :or {greeting "Hello"}}]
(str greeting ", " name "!"))
(greet "Alice" )
; "Hello, Alice!"
(greet "Alice" :greeting "Hi")
; "Hi, Alice!"
Map
map = %{:name => "Alice", "age" => 7, 7 => nil, true => false}
# %{7 => nil, true => false, :name => "Alice", "age" => 7}
map["age"]
# 7
map[:name]
# "Alice"
map.name
# "Alice"
map[:no_such_key]
# nil
Map.get(map, :no_such_key, :value)
# :value
Map.fetch(map, :name)
# {:ok, "Alice"}
Map.fetch(map, :no_such_key)
# :error
Map.new([{:name, "Alice"}, {map, nil}])
# %{
# :name => "Alice",
# %{7 => nil, true => false, :name => "Alice", "age" => 7} => nil
# }
map = %{key: 1}
# %{key: 1}
%{map | key: 2}
# %{key: 2}
%{map | k2: 1}
# ** (KeyError) key :k2 not found in: %{key: 1}
Map.put(map, :key, 2)
# %{key: 2}
Map.put(map, :key2, 2)
# %{key: 1, key2: 2}
%{a: 1, b: 2} |> Enum.map(fn {k, v} -> {k, v * 2} end)
# [a: 2, b: 4]
%{a: 1, b: 2} |> Enum.map(fn {k, v} -> {k, v * 2} end) |> Map.new
%{a: 2, b: 4}
MapSet
set = MapSet.new(["a", "b", "c"])
MapSet.member?(set, "b")
# true
MapSet.put(set, "b")
# MapSet.new(["a", "b", "c"])
MapSet.delete(set, "d")
# MapSet.new(["a", "b", "c"])
MapSet.union(set, MapSet.new(["c", "d"]))
# MapSet.new(["a", "b", "c", "d"])
MapSet.intersection(set, MapSet.new(["c", "d"]))
# MapSet.new(["c"])
MapSet.difference(set, MapSet.new(["c", "d"]))
# MapSet.new(["a", "b"])
MapSet.subset?(MapSet.new(["c"]), set)
# true
Struct
defmodule User do
defstruct name: "", age: nil
end
user = %User{name: "Alice", age: 7}
user.age
# 7
%User{ user | age: 8}
# %User{name: "Alice", age: 8}
defmodule User do
defstruct [:name, :age]
def child?(%User{age: age}) do
is_integer(age) and age < 18
end
end
User.child?(%User{name: "Alice", age: 7})
# true
Working with enumerable data types
Enum provides a wide range of functions for working with enumerable
data types. Combined with pipe operator, it can pretty clear and
concise.
List comprehension is even more concise for specific operations, namely map and filter.
1..10
|> Enum.filter &rem(&1, 2) == 0
|> Enum.map &(&1*&1)
for x <- 1..10, rem(x, 2) == 0, do: x * x
There is also into:
%{a: 1, b: 2}
|> Enum.map(fn {k, v} -> {k, v * 2} end)
|> Map.new
for {k, v} <- %{a: 1, b: 2}, into: %{}, do: {k, v * 2}
Pattern Matching
Elixir has first-class pattern matching support.
Control Flow
case {:ok, 1} do
{:ok, value} -> value
:error -> 0
end
In Rust:
#![allow(unused)] fn main() { println!("{}", match Some(1) { Some(value) => value, None => 0 }); }
Function Parameters
defmodule Greeter do
def greet({:ok, name}), do: "Hello, #{name}"
def greet(:error), do: "Error occurred"
end
Greeter.greet({:ok, "Alice"})
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } fn greet(result: Result<&str, &str>) -> String { match result { Result::Ok(name) => format!("Hello, {}", name), Result::Err(_) => "Error occurred".to_string(), } } println!("{}", greet(Result::Ok("Alice"))); }
Guards
defmodule Number do
def sign(n) when n > 0, do: :positive
def sign(n) when n < 0, do: :negative
def sign(0), do: :zero
end
#![allow(unused)] fn main() { fn sign(n: i32) -> &'static str { match n { x if x > 0 => "positive", x if x < 0 => "negative", _ => "zero", } } println!("{}", sign(-1)); }
Wildcards
case value do
{:ok, v} -> IO.puts("Got #{v}")
_ -> IO.puts("Unknown")
end
match value {
Some(v) => println!("Got {}", v),
_ => println!("Unknown"),
}
It is worth mentioning that Rust has very useful compile-time exhaustiveness check, which is based on type system.
Error Handling
Pattern Matching
case File.read("example.txt") do
{:ok, content} ->
IO.puts("File content: #{content}")
{:error, reason} ->
IO.puts("Error reading file: #{reason}")
end
with
with is handy for match result of a serial of operations:
with {:ok, file} <- File.open("example.txt"),
content = IO.read(file),
:ok <- File.close(file),
{:ok, data} <- Jason.decode(content) do
data
else
{:error, reason} ->
IO.puts("Error: #{reason}")
_ ->
IO.puts("Unknown error occurred")
end
with allows you to focus on the successful path of the computation. It is called "Railway-oriented programming".
This is a common approach in functional programming languages and others where exceptions aren't the primary error handling mechanism.
In haskell there is do and >>=.
In rust to work with Result<T, E> and Option<T> there is ? operator.
In JavaScript, it's Promise chaining using .then().
try/rescue
For catching exceptions(not common in idiomatic Elixir):
try do
String.to_integer("not_a_number")
rescue
ArgumentError -> "Invalid argument"
e in RuntimeError -> "Runtime error: #{e.message}"
_ -> "Unknown error"
after
# like finally in other languages
IO.puts("Operation attempted")
end
Handling process termination
# trap to prevent the parent process from exiting
Process.flag(:trap_exit, true)
spawn_link(fn ->
exit("I am exiting")
end)
Task
task = Task.async(fn ->
File.read!("missing_file.txt")
end)
case Task.yield(task, 5000) || Task.shutdown(task) do
{:ok, result} ->
result
nil ->
"Timeout"
{:exit, reason} ->
IO.puts inspect(reason)
end
Polymorphism
Protocol
Protocol is a manchanism for dispatching behavior based on data type, kind of like interface in other languages.
defprotocol Speak do
def speak(data)
end
defmodule Dog do
defstruct [:name]
end
defimpl Speak, for: Dog do
def speak(%Dog{name: name}), do: "#{name} says woof!"
end
Speak.speak(%Dog{name: "Rex"})
# "Rex says woof!"
Elixir's Protocol is conceptually similar to Clojure's protocol:
(defprotocol Speak
(speak [this]))
(defrecord Dog [name])
(extend-type Dog
Speak
(speak [{:keys [name]}]
(str name " says woof!")))
(let [rex (->Dog "Rex")]
(speak rex))
Behavior
Behaviour is a formal way to define interfaces that modules should implement.
defmodule MyBehaviour do
@callback hello(String.t()) :: String.t()
end
defmodule MyModule do
@behaviour MyBehaviour
@impl MyBehaviour
def hello(name), do: "Hello, #{name}"
end
Behaviour conformance is checked at compile time. The compiler will warn if any required callback is missing or arity doesn't match.
Macros
defmodule Demo do
defmacro unless(condition, do: expression) do
quote do
if !unquote(condition), do: unquote(expression)
end
end
end
Similar to Clojure's:
(ns demo)
(defmacro unless [condition body]
`(if (not ~condition)
~body))
Spec
defmodule App1.Math do
@spec sum(integer(), integer()) :: integer()
def sum(a, b) do
a + b
end
end
Custom Type
defmodule App1.User do
@type t() :: %__MODULE__{id: integer(), name: String.t()}
defstruct name: "", id: 0
end
defmodule App1.Greeter do
alias App1.User
@spec greet(User.t()) :: String.t()
def greet(%User{name: name}) do
"Hello, #{name}"
end
end
Hex
hex is the package manager and repository for Elixir (and Erlang) libraries and packages.
Login
mix hex.user register
or
mix hex.user auth
Create a new package
mix new my_pkg
mix.exs
defmodule MyPkg.MixProject do
use Mix.Project
def project do
[
name: "MyPkg",
app: :my_pkg,
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
description: description(),
package: package(),
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ex_doc, "~> 0.29", only: :dev, runtime: false},
]
end
defp description() do
"""
A Elixir library that ...
"""
end
defp package() do
[
licenses: ["MIT"],
links: %{
"GitHub" => "https://github.com/foo/mypkg"
}
]
end
end
Publish
mix hex.build
mix hex.publish
Routing
Start postgres:
docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres
Create a new project:
mix phx.new app1 --live
cd app1
mix ecto.create
mix phx.server
Take a look at router.ex:
defmodule App1Web.Router do
use App1Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {App1Web.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", App1Web do
pipe_through :browser
get "/", PageController, :home
end
end
use App1Web, :router
This will inject some code defined in app1_web.ex in to current Router module:
defmodule App1Web do
# ...
def router do
quote do
use Phoenix.Router, helpers: false
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
# ...
end
pipeline & plug
pipeline and plug come from Plug, which is Phoenix's middleware mechanism.
A Plug manipulates a connection, in other languages and frameworks
middleware usually manipulates Request and Response.
A Plug can be a function or a module. Let's take module as an example:
defmodule App1Web.Plugs.SayHello do
import Plug.Conn
def init(default), do:
IO.puts "SayHello Plug is Ready"
end
def call(conn, _opts) do
IO.puts "Hello from the plug!"
Plug.Conn.put_resp_header(conn, "x-say-hello", "Hello")
conn
end
end
And obviously a pipeline is a collection of middlewares. Insert the middleware to the top of the list:
pipeline :browser do
plug App1Web.Plugs.SayHello
# ...
end
It is worth mentioning that Phoenix provides compile-time checked verified routes. Errors such as misspelled route names or incorrect parameters when generating paths/URLs will be caught by the compiler.
Controller
Phoenix adopt an architectural pattern similar to the MVC pattern.
In controller we need to find out:
- How to access request data, like headers, body and query parameters.
- How to invoke models.
- How to render template.
Take a look at lib/app1_web/controllers/page_controller.ex:
defmodule App1Web.PageController do
use App1Web, :controller
def home(conn, _params) do
render(conn, :home, layout: false)
end
end
Take a look at lib/app1_web.ex:
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: App1Web.Layouts]
use Gettext, backend: App1Web.Gettext
import Plug.Conn
unquote(verified_routes())
end
end
Accessing Request Data
_params is a map of query string and form inputs:
def home(conn, _params) do
render(conn, :home, layout: false)
end
Try to add query string with curl:
curl 'localhost:4000?key=1&key=2'
It will be printed on the command line:
[debug] Processing with App1Web.PageController.home/2
Parameters: %{"key" => 2}
Pipelines: [:browser]
See if array works:
curl 'localhost:4000?key[]=1&key[]=2'
%{"key" => ["1", "2"]}
Add a routing rule for POST in router.ex:
post "/", PageController, :home
curl -d 'foo=bar' 'localhost:4000?key[]=1&key[]=2'
%{"foo" => "bar", "key" => ["1", "2"]}
JSON also works:
curl -d '{"ok":true}' -H 'content-type: application/json' \
'localhost:4000?key[]=1&key[]=2'
%{"key" => ["1", "2"], "ok" => true}
Rendering Template
The controler simply called render, add passed :home.
In lib/app1_web/controllers/page_html.ex, it doesn't do much, just
called embed_templates and passed the director path.
defmodule App1Web.PageHTML do
use App1Web, :html
embed_templates "page_html/*"
end
The embed_templates macro expands to following:
def home(assigns) do
~H"""
<.flash_group flash={@flash} />
...
"""
end
And the home function will be called by render.
View
- How to pass variables to template?
- How to handle lists?
- How to handle branching?
- How to escape dangerous content?
Passing Variables
Take a look at lib/app1_web/controllers/page_html/home.html.heex. Replace it with following content:
<div>{ @message }</div>
Variables can be passed directly to render:
render(conn, :home, layout: false, message: "Hello")
You can also assign variables to conn:
conn
|> assign(:user, %{:name => "Alice"})
|> render(:home, layout: false)
<div>{@user.name}</div>
It is worth mentioning that assign is imported from Plug.Conn and it's immutable in behavior.
Render a list
<ul>
<%= for user <- @users do %>
<li>{user.name}</li>
<% end %>
</ul>
Or use the :for attribute, which is more concise:
<ul :for={user <- @users}>
<li>{user.name}</li>
</ul>
Branching
<%= if @logged_in do %>
<p>Welcome back!</p>
<% else %>
<p>Please log in.</p>
<% end %>
Elixir is more expressive than average programming languages, and it
offers case and cond:
<%= case @user.role do %>
<% "admin" -> %>
<p>You're an admin.</p>
<% "member" -> %>
<p>You're a member.</p>
<% _ -> %>
<p>Unknown role.</p>
<% end %>
<%= cond do %>
@user.age < 13 ->
<p>You're a child.</p>
@user.age < 20 ->
<p>You're a teenager.</p>
true ->
<p>You're an adult.</p>
<% end %>
Escaping
Dangerous or untrusted content (like user input) must be escaped to prevent XSS attacks.
Phoenix's HEEx templates automatically escape content by default.
To render trusted content without excapping:
<%= raw("<b>Check you console!</b>") %>
<%= raw("<script>console.log('code executing!')</script>") %>
Layouts
Layouts are a way to define a shared HTML structure (like headers, navbars, footers) that wraps around your templates.
In lib/app1_web/components/layouts.ex, embed_templates is called
to embed the template under the layouts folder as functions.
To render with the default layout:
render(conn, :home)
Custom Layout
Create a custom layout at lib/app1_web/components/layouts/admin.html.heex.
<header><h1>Admin Panel</h1></header>
<main><%= @inner_content %></main>
<footer><p>© 2025 Demo</p></footer>
The custom layout will also be wrapped with the root layout. So no need for a full html structure.
Specify the layout with put_layout:
conn
|> put_layout(html: :admin)
|> render(:home)
Component
<.button>Submit</.button>
Define custom component:
defmodule App1Web.CoreComponents do
use Phoenix.Component
def alert(assigns) do
~H"""
<div class={"alert alert-#{@type || "info"}"}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end
<.alert type="error"><div>Something went wrong!</div></.alert>
Context
The Model in traditional MVC frameworks are splited into context and schema in Phoenix.
Scaffolding
Following command will generate a full CRUD API for a Post resource inside a Blog context.
mix phx.gen.json Blog Post posts title:string content:text
The command will genrate a bunch of files,
* lib/app1_web/controllers/post_controller.ex
* lib/app1_web/controllers/post_json.ex
* lib/app1_web/controllers/changeset_json.ex
* lib/app1_web/controllers/fallback_controller.ex
* lib/app1/blog/post.ex
* lib/app1/blog.ex
...
more db migration and other test related files
Add a routing rule to the "/api" scope in lib/app1_web/router.ex:
resources "/posts", PostController, except: [:new, :edit]
Then run mix ecto.migrate to apply the database schema changes.
The API should be ready.
curl localhost:4000/api/posts
Will return:
{"data":[]}
Context
What is a context? Take a look at lib/app1/blog.ex. It containers following functions:
list_postsget_postcreate_postupdate_postdelete_postchange_post
And in those functions, App1.Repo is invoke to talk to database:
def list_posts do
Repo.all(Post)
end
Here Post is a schema module defined in lib/app1/blog/post.ex.
In Phoenix, it's
Controller → Context → Repo → Schema
In Spring, it's
Controller → Service → Repository → Entity
Schema
Schema definition is stright forward, what's interesting here is changeset:
defmodule App1.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
field :content, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :content])
|> validate_required([:title, :content])
end
end
changeset from Ecto is for validation and transformation. It will
called in context before inserting the data into database.
def create_post(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
Try to call API with invalid request body:
curl -id '{"post": {}}' -H 'content-type: application/json'\
localhost:4000/api/posts
Will return:
HTTP/1.1 422 Unprocessable Entity
...
{"errors":{"title":["can't be blank"],"content":["can't be blank"]}}
Ecto
Ecto is an ORM, or more precisely a "language integrated query" (LINQ) library for Elixir.
Schema
defmodule App1.User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
timestamps() # Adds :inserted_at and :updated_at fields
end
end
Changeset
Change set is a way to track and validate changes to data before persisting them.
castselects the allowed fields from input.validate_*functions add errors if rules are broken.changesetfunction is usually the place to define these rules, you may use separated functions for insert and update or other purpose.
defmodule App1.User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
timestamps() # Adds :inserted_at and :updated_at fields
end
def changeset(user, attrs) do
user
|> Ecto.Changeset.cast(attrs, [:name, :email, :age])
|> Ecto.Changeset.validate_required([:name, :email])
|> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)
|> Ecto.Changeset.unique_constraint(:email, name: :users_email_index)
end
end
Create a Changeset:
alias App1.User
%User{}
|> User.changeset(%{name: "Alice"})
#Ecto.Changeset<
action: nil,
changes: %{name: "Alice"},
errors: [email: {"can't be blank", [validation: :required]}],
data: #App1.User<>,
valid?: false,
...
>
changeset.valid? is false and changeset.errors contains the validation messages. Typically these errors will returned to the user (e.g., in a web form).
Fix the Changeset:
%User{}
|> User.changeset(%{name: "Alice", email: "alice@example.com", age: 30})
#Ecto.Changeset<
action: nil,
changes: %{name: "Alice", email: "alice@example.com", age: 30},
errors: [],
data: #App1.User<>,
valid?: true,
...
>
Now changeset.valid? is true and changeset.errors is empty. And this changeset can be inserted into the database using Repo.insert(changeset).
Repo
A repository module is needed to interact with the database.
defmodule App1.Repo do
use Ecto.Repo,
otp_app: :app1,
adapter: Ecto.Adapters.Postgres
end
In config/config.exs:
config :app1, App1.Repo,
url: System.get_env("DATABASE_URL"),
Inserting
alias App1.Repo
%User{}
|> User.changeset(%{name: "Alice", email: "alice@example.com", age: 30})
|> Repo.insert() # Returns {:ok, user} or {:error, changeset}
Updating
{:ok, user} = Repo.get(User, 1)
user
|> User.changeset(%{age: 7})
|> Repo.update()
User
|> Query.where(id: 1)
|> Repo.update_all(inc: [age: 1])
Query
Repo.all(App1.User)
Repo.get_by(App1.User, email: "alice@example.com")
Use the from macro:
import Ecto.Query
from u in User,
where: u.age > 18,
order_by: u.name,
select: [u.id, u.email]
|> Repo.all
Since from is a macro, you need to unquote variables using ^:
from u in User,
where: u.age > ^min_age
Tackle the N+1 Problem
users = Repo.preload(Repo.all(User, :posts))
Channels
Channels are Phoenix's abstraction of WebSockets:
defmodule MyAppWeb.RoomChannel do
use Phoenix.Channel
def join("room:lobby", _message, socket) do
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
{:error, %{reason: "unauthorized"}}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body})
{:noreply, socket}
end
end
Scaling across multiple servers
Channels use Phoenix.PubSub under the hood to broadcast messages
between processes. By default, it's local to the node and can be
configured for distributed systems,
In config.exs:
config :my_app, MyAppWeb.Endpoint,
pubsub_server: MyApp.PubSub
config :my_app, MyApp.PubSub,
adapter: Phoenix.PubSub.Redis,
url: "redis://localhost:6379"
LiveView
Modern web applications usually render views in the browser using JavaScript. LiveView renders HTML on the server and pushes changes to the client through diffs over WebSockets.
A Counter
defmodule App1Web.CounterLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
def handle_event("increment", _, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def render(assigns) do
~H"""
<div>
<p>Count: { @count }</p>
<button phx-click="increment">+</button>
</div>
"""
end
end
Add a routing rule under scope "/":
live "/counter", CounterLive
The counter is now ready at http://localhost:4000/counter.
Anatomy of a LiveView Component
The LiveView component is a module with specific functions like
mount, render and handle_event. Similar to React's class
component, those are called lifecycle callbacks, as they are invoked
at different stages.
-
mount: Invoked once when the component is first added to the page or the LiveView process is started. It's used to initialize the component's state (assigns). -
handle_event: Handles events originating from the client (e.g., button clicks). -
render: Called to generate HTML. -
update: Called whenever the assigns passed to the component from its parent LiveView change. It's an opportune place to update the component's internal state based on the new assigns.
A Rough Comparation with React
Life Cycle Callbacks:
| LiveView Component | React Class Component |
|---|---|
| mount(params, session, socket) | componentDidMount() |
| handle_event(event, params, socket) | custom functions |
| render(assigns) | render() |
| update(assigns, socket) | componentDidUpdate(prevProps, prevState) |
Event Handling:
| LiveView Component | React Class Component |
|---|---|
| phx-click | onClick |
| phx-change | onChange |
State Management:
| LiveView Component | React Class Component |
|---|---|
| assign(socket, state) | this.setState(state) |
| update(socket, key, fn) | this.setState(fn) |
Thoughts on LiveView
The ultimate goal of LiveView is to create dynamic server-rendered applications without writing JavaScript.
It is kind of a unique and intereting solution from Phoenix.
- States are kept on the server, not in the browser, rendering is also done on the server.
- After rendering, a diff with previous rendering result is done and only the diff is sent to the browser, in order to minimize the payload.
Why is the idea not adapted in other frameworks?
- It relies on WebSocket, which is cheap in Phoenix but a luxury for a lot of other frameworks.
- It's a back-end oriented solution, the JavaScript community is front-end centric, and LiveView solves a problem that they don't have.
LiveView is a good fit for
- Chats, monitoring(real-time updates)
- Dashboards, admin panels(limited number of concurrent users, relative simple interaction)
- Developers with limited front-end skills
Not suitable for
- UI-heavy apps with complex states
- Apps with extreme high-concurrency usage
- Mobile-first apps on unstable networks, or apps with offline usage
Debugging
printing
op()
|> IO.inspect(label: "op", limit: :infinity)
op()
|> dbg()
tracing
:dbg.tracer()
:dbg.p(:all, :c) # :c for call trace
:dbg.tpl(App1Web.PageController, :home, 2, []) # tpl for trace process local
:dbg.stop_clear()
breaking point
IEx.pry starts an interactive IEx session at a specific point during code execution.
- You can insert
IEx.pry()into your code. - When execution reaches
IEx.pry(), it pauses and drops you into an IEx shell. - Inside the
prysession, you can inspect local variables, evaluate expressions, and resume execution.
IEx.break!
The iex --dbg pry command line option configures IEx to automatically enter a pry session when a breakpoint is hit or certain errors occur.
iex --dbg pry -S mix
IEx.break! will leads to a pry session when the breakpoint is hit.
IEx.break!(App1Web.PageController, :home, 2)
recon
REmote CONtrol, it can be used in prod env.
defmodule M, do: (def incr(n), do: n + 1)
:recon_trace.calls({M, :incr, :_}, 3)
M.incr(0)
The maching pattern here is {mod, fn, arity}, and it limited to 3 times.
:recon_trace.calls/2 returns 0 if instruction is not successful, this happens when the module is not loaded or the function spec is incorrect.
maching parameters
Only tracing calls where paramters are [1]:
:recon_trace.calls({M, :incr, [{[1], [], []}]}, 3)
M.incr(0)
M.incr(1)
tracing result
:recon_trace.calls({M, :incr, [{[:_], [], [{:return_trace}]}]}, 3)
# or simply:
:recon_trace.calls({M, :incr, :return_trace}, 3)
M.incr(0)
12:24:58.761345 <0.658.0> 'Elixir.M':incr(0)
1
Recon tracer rate limit tripped.
tracing all calls to modules matching a prefix
prefix = "Elixir.App1Web."
:code.all_loaded()
|> Enum.map(fn {mod, _file} -> mod end)
|> Enum.filter(&String.starts_with?(Atom.to_string(&1), prefix))
|> Enum.each(&:recon_trace.calls({&1, :_, :return_trace}, 100))
Test
ExUnit
ExUnit is Elixir's built-in testing framework. It's integrated with Mix.
A basic example
1. Code to Test (lib/my_math.ex)
defmodule MyMath do
def add(a, b) do
a + b
end
end
2. Test File (test/my_math_test.exs)
defmodule MyMathTest do
use ExUnit.Case
doctest MyMath # Run doctests found in lib/my_math.ex (optional but good practice)
test "adds two numbers" do
assert MyMath.add(1, 2) == 3
assert MyMath.add(0, 0) == 0
assert MyMath.add(-1, 1) == 0
end
end
3. Running Tests
mix test
Mix will compile your code and run all files matching test/*_test.exs. To test a single file:
mix test test/my_math_test.exs
To get coverage reporting:
mix --cover test
More
describesetup
Mock
Mock is a testing library that allows you to temporarily replace the behavior of modules or functions during tests.
Deploy
Create a simple app
To create a new Phoenix project, open your terminal and run the following command:
mix phx.new ping_pong_app --no-html --no-ecto --no-dashboard
Navigate into your new project directory:
cd ping_pong_app
~/pg/cloud/ping_pong_app/lib/ping_pong_app_web/controllers/page_controller.ex
defmodule PingPongAppWeb.Controllers.PageController do
use PingPongAppWeb, :controller
def index(conn, _params) do
data = %{
message: "Hello, world!"
}
json(conn, data)
end
end
~/pg/cloud/ping_pong_app/lib/ping_pong_app_web/router.ex
scope "/api", PingPongAppWeb do
pipe_through :api
get "/ping", Controllers.PageController, :index
end
Prepare a Dockerfile
~/pg/cloud/ping_pong_app/Dockerfile
FROM elixir:1.16.0-alpine as builder
RUN apk add --no-cache build-base git openssl-dev ca-certificates
WORKDIR /app
COPY mix.exs mix.lock ./
COPY config config/
COPY lib lib/
COPY priv priv/
COPY assets assets/
RUN mix deps.get --only prod
RUN mix deps.compile
ENV MIX_ENV=prod
RUN mix compile
RUN mix release
FROM alpine:3.18
RUN apk add --no-cache openssl ncurses-libs libstdc++
WORKDIR /app
COPY --from=builder /app/_build/prod/rel/ping_pong_app ./
ENV PORT=8080
EXPOSE 8080
CMD ["/app/bin/ping_pong_app", "start"]
google cloud
gcloud auth login
gcloud projects list
echo 'export GCLOUD_PROJECT_ID=<your project id>'
direnv allow
gcloud config set project $GCLOUD_PROJECT_ID
Build and Push Image
docker buildx create --name mybuilder --use
docker buildx inspect --bootstrap
brew install docker-credential-helper
gcloud auth configure-docker
docker buildx build \
--platform linux/amd64 \
-t gcr.io/$GCLOUD_PROJECT_ID/ping-pong-app:v1 \
--push .
docker push gcr.io/$GCLOUD_PROJECT_ID/ping-pong-app:v1
Create a GKE cluster
gcloud container clusters create ping-pong-cluster \
--zone us-central1-c \
--num-nodes 1 \
--machine-type e2-micro \
--enable-ip-alias
Authenticates your local kubectl CLI with the GKE cluster:
gcloud components install gke-gcloud-auth-plugin
gcloud container clusters get-credentials ping-pong-cluster --zone us-central1-c
kubectl get node
Prepare k8s configuration
~/pg/cloud/ping_pong_app/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ping-pong-deployment
labels:
app: ping-pong-app
spec:
replicas: 1
selector:
matchLabels:
app: ping-pong-app
template:
metadata:
labels:
app: ping-pong-app
spec:
containers:
- name: ping-pong-app
image: gcr.io/YOUR_PROJECT_ID/ping-pong-app:v1 # Replace with your project ID
ports:
- containerPort: 8080
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "8080"
~/pg/cloud/ping_pong_app/k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: ping-pong-service
spec:
selector:
app: ping-pong-app # Selects pods with this label
ports:
- protocol: TCP
port: 80 # The port the service exposes
targetPort: 8080 # The port on the pod to forward traffic to
type: LoadBalancer # Creates an external IP address
Deploy
kubectl apply -f k8s/
deployment.apps/ping-pong-deployment created
service/ping-pong-service created
kubectl get pods -l app=ping-pong-app
kubectl get pods -l app=ping-pong-app -o jsonpath='{.items[0].metadata.name}'
kubectl logs YOUR_POD_NAME
Visit your app
kubectl get service ping-pong-service
Clean up
kubectl delete -f k8s/
gcloud container clusters delete ping-pong-cluster --zone us-central1-c
gcloud container images list
gcloud container images list-tags gcr.io/$GCLOUD_PROJECT_ID/ping-pong-app
gcloud container images delete gcr.io/$GCLOUD_PROJECT_ID/ping-pong-app:v1