A few days ago, I read a post about Why you shouldn't use a web framework that gave me a bit of inspiration. Frameworks are great tools for full-fledged apps, but sometimes you don't need or want all the magic a framework provides.
Full disclosure though, a much better solution to what we're building is better done in Phoenix by passing --no-ecto --no-webpack
which would get you a much better, more stable, more efficient base in which to build upon.
But thats not the point of this tutorial. We're not fully replicating Phoenix, we're making this a super simple HTTP server. You can view the full implementation here.
Go ahead and open up a terminal and do mix new todo --sup
which will get us a nice little project started with a Supervision tree. That will allow us to start the GenServer automatically when the application is ran.
We'll start by building up the GenServer portion of it. We need to be able to do all CRUD functions in the app, so we'll make a file under todo/elixir/lib/todo/
called server.ex
.
server.ex
defmodule Todo.Server do
use GenServer
# Client
def start_link(opts) do
end
def list() do
end
def add(todo) do
end
def remove(id) do
end
def toggle(id) do
end
# Server
end
That lays out the client interface for us so that we can begin implementation. To start a GenServer, we call start
or start_link
which handles the call to init
.
server.ex
...
def start_link(opts) do
GenServer.start_link(__MODULE__, :ok, opts)
end
...
# Server
def init(:ok) do
{:ok, []}
end
Going back to our Terminal, we can run our app by typing iex -S mix
which will get a REPL. Inside, we can make sure our server starts by typing Todo.Server.start_link([])
, and you should get the following
iex(1)> Todo.Server.start_link([])
{:ok, #PID<0.135.0>}
The PID will likely be different for you, of course.
We can go ahead and make sure that the Todo Server starts when we run iex -S mix
. Go into lib/todo/application.ex
and to the start(_type, _args)
function already there.
application.ex
...
def start(_type, _args) do
children = [
{Todo.Server, [name: Todo.Server]}
]
...
Next, we'll implement the Add and List functions.
server.ex
...
def list() do
GenServer.call(__MODULE__, {:list})
end
def add(todo) do
GenServer.call(__MODULE__, {:add, todo})
end
...
# Server
...
def handle_call({:list}, _from, state) do
{:reply, state, state}
end
def handle_call({:add, todo}, _from, state) do
new_todo = %{name: todo, done: false, id: generate_id()}
new_state = state ++ [new_todo]
{:reply, new_state, new_state}
end
...
# Private
defp generate_id() do
# This generates a basic ID for us, ends up being something
# like "CeiGYfspjHJAld42hI3OY1lbulxGkyR8W-zqXpkSMwpilPFtv0uE5jCW229-k47J"
# which will not completely prevent collisions, but will reduce
# their chances.
:crypto.strong_rand_bytes(64)
|> Base.url_encode64()
|> binary_part(0, 64)
end
Notice that I'm using call
here instead of cast
. This is because when we make our HTTP server, it will reply with the list of Todos with every response. We're not going to be dealing with holding any state on the front end, so we can't just reply with an :ok
.
We can then go back to our terminal again and iex -S mix
. Our GenServer should boot up with the application, so no need to call start_link
this time. Lets try it out!
iex(1)> Todo.Server.add("Hello")
[
%{
done: false,
id: "B9aMiHTT5CAqDWY8JyxV6QbmlRdiPyLopThv_EHARk4EwRFCyRG_Ip4q5NJv7NSh",
name: "Hello"
}
]
iex(2)> Todo.Server.list()
[
%{
done: false,
id: "B9aMiHTT5CAqDWY8JyxV6QbmlRdiPyLopThv_EHARk4EwRFCyRG_Ip4q5NJv7NSh",
name: "Hello"
}
]
Your response should look something like that. If it does, great! Lets move on to our toggle(id)
function.
server.ex
...
def toggle(id) do
GenServer.call(__MODULE__, {:toggle, id})
end
...
def handle_call({:toggle, id}, _from, state) do
[todo] = Enum.filter(state, fn x -> x.id == id end)
toggled_todo = %{todo | done: !todo.done}
new_state =
state
|> Enum.map(fn x ->
if is_id?(x, id) do
toggled_todo
else
x
end
end)
{:reply, new_state, new_state}
end
...
# Private
defp is_id?(x, id), do: x.id == id
And again, we'll test it out.
iex(1)> [todo] = Todo.Server.add("test")
[
%{
done: false,
id: "KTH7L3yoHNS_-ciwdLpMbmCtW62VY8xS_VPT62yVxdXv_ZQDb1GROVmjBV6oEage",
name: "test"
}
]
iex(2)> Todo.Server.toggle(todo.id)
[
%{
done: true,
id: "KTH7L3yoHNS_-ciwdLpMbmCtW62VY8xS_VPT62yVxdXv_ZQDb1GROVmjBV6oEage",
name: "test"
}
]
Wonderful! Next up, remove(id)
.
server.ex
...
def remove(id) do
GenServer.call(__MODULE__, {:delete, id})
end
...
def handle_call({:delete, id}, _from, state) do
new_state =
state
|> Enum.filter(fn x ->
!is_id?(x, id)
end)
{:reply, new_state, new_state}
end
And we test..
iex(1)> [todo] = Todo.Server.add("test")
[
%{
done: false,
id: "RpfnxF-YJE_A4FgsQFtg1ODhRcY90lxkSG0U0DHlVDvAfN_zDOJOVXPEkjuBAPsz",
name: "test"
}
]
iex(2)> Todo.Server.remove(todo.id)
[]
iex(3)> Todo.Server.list()
[]
Great, that makes up our abilities to add, toggle, remove and list Todos in a GenServer app. Next, we'll make our HTTP server.