Rapid prototyping: Building a TODO App in Phoenix
- Authors
Updated at 6/27/2024
Welcomeaaa, developers! In this tutorial, we're diving into the world of web development with Phoenix Elixir to build a powerful, straightforward TODO application with full authorization, views and DB. Whether you're new to Phoenix or looking to expand your skills, you're in the right place.
Phoenix is renowned for its speed and simplicity in creating web applications, and we're about to experience that firsthand. Believe it or not, by the time you finish this tutorial, you'll have a fully functional TODO app, up and running in less than 20 minutes. Yes, you read that right – less than 20 minutes!
Prequeries
- Elixir
- Phoenix
- PostgreSQL
- Base knowledge about the MVC
To install Elixir on your system, visit the Elixir docs and follow the installation instruction for your operating system.
Technology stack
- Elixir
- Phoenix
- PostgreSQL
- Phoenix with
phx.gen.auth
for the auth part - Ecto
Start with Phoenix framework
Run the following command to install Phoenix on your system:
mix archive.install hex phx_new
To generate your Phoenix app, use the following command:
mix phx.new todo_app
You can replace the todo_app, by your application name.
When prompted to Fetch and install dependencies? [Yn]
, select yes to install all deps.
Reload the application, and you should now have a fully prepared authentication layer!
PostgreSQL
At this point, you need to have the PostgreSQL installed on your machine (or using Docker). I won't cover this step here. If you need help, please refer to the tutorials below:
Update database credentials
OK! So you have an installed PostgreSQL. Please go to the config/dev.exs
file, and you should see something like that:
import Config
# Configure your database
config :todo_app, TodoApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "todo_app_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
...
Update the highlighted lines of code and provide a real username
and password
for your database. Then ron the following command to create your database:
mix ecto.create
Great! At this point, we can run the Phoenix app, to do that, use:
mix phx.server
It should look like the following:
Visit: http://localhost:4000
MVC
And now, we are about to generate MVC structure, type in terminal:
mix phx.gen.html Todo Task tasks name:string completed:boolean
The mix phx.gen.html command generates views, schemas, database adjustments, CRUD operations, templates and so on.
We pass several arguments to the phx.gen.html
generator, starting with the resource's context (Todo), followed by the schema module name (Task), and finally, the plural name of the schema (the name to be used as the schema table name; tasks).
The name:string
and completed:boolean
are the two fields that will be created in the tasks table in the database.
Now, let's adjust the router to incorporate our new view. Navigate to the lib/todo_app_web/router.ex
file and update the root scope.
defmodule TodoAppWeb.Router do
use TodoAppWeb, :router
...
scope "/", TodoAppWeb do
pipe_through :browser
get "/", PageController, :home
resources "/tasks", TaskController
end
...
end
Next, we need to adjust our changes to the database by running migrations:
mix ecto.migrate
Finally, you can visit the route localhost:4000/tasks
and we have a pretty functional application. How fast!
Authentication
For authentication, I'm opting to use mix phx.gen.auth, a tool provided by the Phoenix. It's a convenient solution that automates the entire authentication infrastructure setup within a Phoenix web application. This command generates modules, controllers, views, templates, database migrations, and other necessary files and configurations to enable user authentication.
Run:
mix phx.gen.auth Accounts User users
When prompted to Do you want to create a LiveView based authentication system? [Yn]
, select yes.
Since this generator installed additional dependencies in mix.exs
, let's fetch those:
mix deps.get
And adjust the changes in the database by running migrations:
mix ecto.migrate
Reload the application, and the authentication layer should be ready!
Protect against unauthorized users
Let's ensure that our entire app requires authentication for access, meaning only logged-in users can use it. To implement this, let's proceed to the lib/todo_app_web/router.ex
file and update the pipeline accordingly:
scope "/", TodoAppWeb do
pipe_through [:browser, :require_authenticated_user]
get "/", PageController, :home
resources "/tasks", TaskController
end
In the same way, you can select which route should be restricted. For instance, /tasks route instead of /.
Make relations between users and tasks
Currently, only logged-in users can view tasks. However, these tasks are accessible to all users once logged in. I aim to personalize this experience further by restricting access so that each user can manage and view only their own tasks.
First I need to create a relationship between tasks and user. Let's create a migration file:
mix ecto.gen.migration add_user_id_to_tasks
The command generated a file named YYYYMMMDD_add_ser_id_to_tasks in the /priv/repo/migrations/
directory.
Replace the contents of this file with such code:
defmodule MyApp.Repo.Migrations.AddUserIdToTasks do
use Ecto.Migration
def change do
alter table(:tasks) do
add :user_id, references(:users)
end
end
end
And run migration:
mix ecto.migrate
Great! Now we need to modify our CRUD to respond only to related tasks. Visit the lib/todo_app/todo.ex
file and add there a method responsible for returning only related tasks to the user:
defmodule TodoApp.Todo do
...
def list_user_tasks(user) do
Task
|> where([t], t.user_id == ^user.id)
|> Repo.all()
end
...
end
It simply collects tasks from the database in which the user id matches the currently logged-in (by session token) user id.
Cool, but at the moment we don't have the user_id
field in the task changeset and schema, let's change it in the lib/todo_app/todo/task.ex
file:
defmodule TodoApp.Todo.Task do
use Ecto.Schema
import Ecto.Changeset
schema "tasks" do
field :name, :string
field :completed, :boolean, default: false
belongs_to :user, TodoApp.Accounts.User
timestamps(type: :utc_datetime)
end
def changeset(task, attrs) do
task
|> cast(attrs, [:name, :completed, :user_id])
|> validate_required([:name, :completed, :user_id])
end
end
Did you forget something? The task currently has a user_id field, but it is empty! We should add a value to this field when creating the task! Go to the lib/todo_app_web/controllers/task_controller.ex
file and modify the create method:
def create(conn, %{"task" => task_params}) do
# Fetch user data from current connection
user = conn.assigns.current_user
# Create map with passed task_params from the form and add to them user id
task_params_with_id = Map.put(task_params, "user_id", user.id)
case Todo.create_task(task_params_with_id) do
{:ok, task} ->
conn
|> put_flash(:info, "Task created successfully.")
|> redirect(to: ~p"/tasks/#{task}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
The method that returns only related tasks is ready, now it's time to use it. Let's go to lib/todo_app_web/controllers/task_controller.ex
and update the index method:
defmodule TodoAppWeb.TaskController do
use TodoAppWeb, :controller
alias TodoApp.Todo
alias TodoApp.Todo.Task
...
def index(conn, _params) do
# Fetch user data from current connection
user = conn.assigns.current_user
# Retrieve tasks associated only with the transferred user
tasks = Todo.list_user_tasks(user)
render(conn, :index, tasks: tasks)
end
...
end
And at this point you should see... error! It happens only if you have previously created some tasks. It should look more or less like this:
[error] ** (Ecto.QueryError) lib/todo_app/todo.ex:26: field `user_id` in `where` does not exist in schema TodoApp.Todo.Task in query[...]
This happens because Ecto tries to retrieve tasks from the database, but there is no data in the user_id column. The simplest solution is to delete all records from the Tasks table in the database.
*If you want to, you can also just update the user_id records in the database by proper user id.
Once you've done that, you should be able to create several tasks and from now on, each user only has their own tasks!
Summary
Do you see how empowering it is? In just about 20 minutes, you've built a fully functional application, complete with front and back-end components. With a comprehensive authentication system and your own database, the possibilities for expansion and customization are endless. Now, armed with this foundation, you're ready to dive deeper into Phoenix & Elixir development. Explore further, experiment, and watch your ideas come to life with ease and speed.
Repository with a finished project: link