Poyters
Published on

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

  1. Elixir
  2. Phoenix
  3. PostgreSQL
  4. Phoenix with phx.gen.auth for the auth part
  5. 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


Additional resources