Full stack application with Elixir, GraphQL & React: API setup and data structures

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

Hello,

thank you for the great video, and really congratulations, I'm looking forward the next parts of your tutorial.

Just a quick note: at minute ~10:30 you create the atom :playground, but because of your emacs configuration it looks like the atom name is :\playground, this had me scratching my head for a bit... maybe it's worth pointing this out for others?

👍︎︎ 3 👤︎︎ u/gnagnone 📅︎︎ Apr 12 2020 🗫︎ replies

Hey uh, why is it set as content for kids? I can't save it to watch later because of that...

👍︎︎ 1 👤︎︎ u/RoosterCrab 📅︎︎ Apr 12 2020 🗫︎ replies

Yay for more Elixir tutorials!

My next series is going to be on GraphQL and Absinthe also, but it's not going to involve any React. Glad to see a niche filled on YT. The vast majority of dev YouTubers abandon Elixir due to the small audience size.

👍︎︎ 1 👤︎︎ u/alchemistcamp 📅︎︎ Apr 13 2020 🗫︎ replies
Captions
Hi guys, in this series I'd like to show you how to build a full stack application using Elixir and React. On the backend, we will first build a GraphQL API using the Phoenix framework and Absinthe, which is a full-fledged GraphQL implementation for Elixir, and we will be using Postgres for the database. Then we will create a single-page application using React with hooks, and we will be talking to the API using apollo-boost. The application we're going to build in this series will be a To-do list. It's a very simple project in terms of features, but we can use it to cover many important topics that we can build upon in more advanced projects. Now, in order to follow along with this tutorial, you need to have Elixir, Node, and Postgres installed on your machine. You can check if you have Elixir installed by typing "iex --version". Here you can see that I am running Elixir version 1.10.2. Let's do the same for Node: type "node --version". You can see that I am running Node version 12.16.0. And now for Postgres: "psql --version", and I have Postgres version 12.2 installed. If any of these dependencies is missing, you should take a pause and install it before you go ahead. The way you do it depends on the operating system you are using. If you are running macOS, then you are in luck, because you can just install all of these using Homebrew. And if you're using a distribution of GNU/Linux, such as Ubuntu or Debian, you should be able to find some versions of Elixir and Node in the system repositories, but they will likely be outdated, so when in doubt, please refer to official installation guides. Another way to install Elixir and Node is using asdf, which is a universal version manager. If you'd like to install many versions of Node and Elixir, I highly recommend you take a look into asdf. In the following part of this video, I'm goint to assume that you already have all of these installed. Now, in order to create a new project with Phoenix, you first have to install the "mix phx.new" generator, which creates new applications. In order to install it, we will go to Hexdocs, into the documentation of Phoenix, and we will take a look into "Installation Guide", and down there in this document, there is a command that you have to copy and paste in your terminal. Now, this command will install the "phx.new" generator, in fact I already had it installed. And we can now use this command we just installed to create a new project. The command is "mix phx.new", and then what follows is the name of the project, I will call it "todo_list". I press Enter, and the generator has just created several files for us, and then it offers to install dependencies. These dependencies will be Elixir dependencies as well as dependencies for Node, because the Phoenix framework comes with batteries included and it has Webpack configured. Now, while these dependencies are downloading, we can go to a new tab, and take a look at the files we have just created. I open them up in Emacs, but you can use any editor you like. The structure of a new Phoenix application is fairly simple. There is the "assets" directory, which contains the front end files. There is "config" we use for configuration. Dependencies is where Elixir dependencies are installed. Lib is where our application lives, and the "priv" directory is for private files of the applications, and there is "test" for unit tests, "formatter.exs" is the configuration file for the Elixir code formatter. "mix.exs" is where we configure our dependencies. We will now install two Elixir libraries that we will need to build a GraphQL API. Let's open up the "mix.exs" that is located in the project root. When we scroll down, almost to the bottom of the file, we will see a single function called "deps". This is the function that defines our dependencies. It returns a list of tuples, and each one of these tuples represents a single library with a version number. So, let's jump to line 45, and after the tuple that requires "plug_cowboy", I add a comma, and in the new line I add a tuple, the first element of the tuple is the name of the dependency as an atom, so first a colon and then "absinthe", and as of this recording, the latest version of Absinthe is 1.4.16. And another library I have to install is "absinthe_plug", and this is a library that connects Absinthe, the GraphQL engine, with Phoenix. The latest version of this library is 1.4.7. And if you are not sure what the latest version is, you can check on hex.pm, and in my case, I will type "absinthe_plug", and we can see that the latest version is indeed 1.4.7. So let's jump to the terminal and type "mix deps.get", which will fetch the libraries, and before we start implementing a GraphQL API, we also need to create a database for our application. So, the command to do this is "mix ecto.create", and it will first compile our application, and in this command, "Ecto" is the name of a library that talks to the database, casts our data to correct types, and does validation, so it's kind of like an ORM, but we can't really say it's an ORM because Elixir is not an object-oriented programming language and therefore there are no objects. So we can see that the database has already been created. Now that the database has been created, we can start crafting our GraphQL API. So I'll go back to the editor and open the file tree, and we can see that in the "lib" directory, in this "todo_list_web" subfolder, we have files that are related to our application's web layer, in this directory I will create a directory called "api". And inside this directory, I will create a module called "schema.ex". This file will be the main file of our GraphQL schema. It has to be an Elixir module, so I will type "defmodule TodoListWeb.Api.Schema". This module has to be using "Absinthe.Schema". And in order to create the first meaningful Absinthe schema, we need to define at least one query. Absinthe doesn't use GraphQL for definition, it uses Elixir macros that get converted to actual GraphQL schemas. So you write everything in Elixir code. There is a macro called "query", where we define a "Hello, world" query, and this will be just a query that returns a string. In order for this to work, we need to define a resolver. There is a "resolve" macro that accepts a function that can have arity of either 2 or 3. In this case we don't care about the arguments to the query, so we can just pass underscores for bindings. Then we have to return a tuple, and the first element is the atom ":ok", and the second one will be the actual string "Hello, world!". We save the file, and now we need to plug this schema into our router, so we go to the file "lib/todo_list_web/router.ex". And down there, in the line 22, I'll uncomment this. There are two routes that we need to provide, one will be "POST /api", and it should point to the schema that we just created. There is a plug called "Absinthe.Plug", and it accepts a [keyword] list of options, the first option will be "schema: TodoListWeb.Api.Schema". Another route will be the GraphQL playground, which we probably want. I will do it on the same endpoint, but with GET method. So, get "/", the plug is called "Absinthe.Plug.GraphiQL", because it used to provide GraphiQL, but obviously, we don't really want to use GraphiQL anymore, because it's slightly dated. So we will define it in a moment. We have to give it a schema, it's the same module, and "interface: :playground". Oops. And let's see if this compiles. I see. Here, I typed "Absinthe.Plug.GraphiQL", but because this scope gets a second argument, "TodoListWeb", then "TodoListWeb" will be prepended to the plug names. So I have to delete this argument. Let's see if this compiles right now. It does. I will spin up a server and when we go to the browser, at localhost:4000 we see the application, and if we go to /api, we should be able to see our GraphQL Playground. On the right, we have our schema, and introspection shows that there is in fact this "hello" query just defined. Let's try to run a query against the schema. And it does indeed return "Hello, world!". So the next step will be creating some data structures, so we can store our to-dos. Phoenix has a lot of code generators that can simplify this task. There is one in particular that we will use right now, it's called "mix phx.gen.context", and it will create a context, which is a module that interacts with data, and I will call it "Todos". The second argument of this generator command is the name of the actual schema, I will call it "Item". The third argument is the table name, it will be the plural form of the schema, which is "items". What follows is the list of columns in the table, and the first one will be the content, which is a string. The last one will be a timestamp which indicates whether the to-do has been completed or not. I will call it "completed_at", and the type is "utc_datetime". And we can go back to the editor and see the migration that was created. It does indeed look OK, but I will add "null: false", so the column will not be nullable. And I will open the schema that we have just generated and it does look OK. However, I just noticed that it does require us to specify "completed_at" which is not what we want. So let's delete this and save it. And now we can run the database migration, by using "mix ecto.migrate", and the next thing that we're going to do is create a query that fetches all todo_items from the database. In order to do this, let's go back to "schema.ex", and in here I will create a new type, I will call it :todo_item, and let's define its fields. First, I will add a field for its ID, in order to see its primary key, it's a non-nullable :id. This corresponds to "ID!" in GraphQL. The next one will be content, which is a non-nullable string. And the last one is a timestamp, but we don't really want to show the timestamp to the user, so we will make it "field :is_completed", and it's a non-nullable boolean. In order to return this from the database, we will need to provide a resolver function, and once again, we use the "resolve" macro, and if we give it a function with the arity of 3, then the first argument will be the object itself, and we can pattern-match on the "completed_at" timestamp. The second argument is the arguments, which we don't need, and the third argument is the context. Let's return "{:ok, !is_nil(completed_at)}". The next thing we have to do in order to actuall fetch these to-do items will be to provide another query. I will do it in here. "field :todo_items", and it's a non-nullable list of non-nullable todo items. This corresponds in GraphQL to "[TodoItem!]!" In here, let's provide a resolver, I will provide a separate resolver module for this later on. For now, let's just pass a function, and here we also don't care about the arguments and the context, so let's return "{:ok, TodoList.Todos.list_items()}". This is a function from the module that we have just generated using "mix phx.gen.context". If you are not sure what this function does, you can open "iex -S mix", and this will open a console. In this console, we can see what this function does. There's this module, TodoList.Todos, and it has the function "list_items/0". We see that what this function does is query the database, selecting everything from the table "items". This looks like this is exactly what we want. So let's jump back to the API Playground, and I will reload the window, so we can have a newer GraphQL schema. Oh, it's not here... what happened? Let's see if it works. I will create a new query. No, it's not working. This is peculiar. Oh, I see. I think I misspelled this right now. It should be "resolve". Let's see if it helps. It does indeed help, now we have this "todoItems" query and it does have a non-nullable content, non-nullable ID, and "isCompleted" as a boolean. So, let's query all of these. And we do get an empty array, because obviously, we don't have any to-do items yet. So, in the next step, we can create some to-do items in the database. I suggest that you do it in IEx, because it's the simplest way to do it quickly. We have this "TodoList.Todos" module, and I will alias it, so we can access it more quickly. And now "Todos.create_item" and we pass it some arguments and I will create a to-do with the argument "content": "Make a video about GraphQL, Elixir, and React". And we see that Ecto has inserted this item into the database. So if I go back to the GraphQL playground, we can now see that the query is already returning the correct item. Another thing we would like to do is to create a mutation to create these items. In the schema file, I will use the mutation macro to create mutations. So I will create a mutation called "create_todo_item", and for now let's just return a boolean. It accepts just the text of this to-do item, and it has to be a non-nullable string. And let's write a simple resolver, I will use the 2-arity version, and I pattern-match the content like this, we don't care about the second argument. I will say "case"... We have this TodoList.Todos module, I will alias this module right now so we can use it more easily in this module. And down there I will substitute it with just "Todos". And in here, line 22, in the resolver, I will say "Todos.create_item(%{content: content})". If it returns "{:ok, %Todos.Item{}}", then let's return "{:ok, true}", and if it returns anything else, let's return "{:ok, false}". So now let's jump to the GraphQL Playground and I will open a new tab and create a mutation. I will make this a bit bigger, so you can see it better. "mutation createTodoItem", and this accepts "$content" as a non-nullable string. And in here we create a to-do item. "createTodoItem" with "content: $content", and this field only returns a Boolean, which is a scalar. If we run it now, it will say that we didn't provide the content, so let's add this content right now in here. And in this "content" variable I will write, let's say, "Write a decent mutation". I run this mutation, and it does indeed create a to-do item, and we can see in the log that it did indeed create an item. So if we jump back to the "todoItems" query we created before, it should return the new item. And the last mutation we're going to need will be marking a to-do item as completed. This will also be a mutation in this file, and let's say ":mark_todo_item_completed". Or we could also just say "toggle", because sometimes we may want to do it with two states. When the to-do item is completed, we want to mark it as not completed, and if it's not completed, we want to make it completed. So I'll call it ":toggle_todo_item", and let's return the actual to-do item. It may be nullable, because we may pass some invalid data to the mutation. It will accept the ID of the to-do item, and it has to be a non-nullable ID. And the resolver function will be a 2-arity function, we make it accept %{id: item_id}, and we don't care about the second argument. In here, I suggest that we write a separate function to resolve it. I will go back to the Todos context, and I will create a new function for this. Define "toggle_todo_item_by_id", and this will accept the to-do item's ID, and I will add a guard, so we can be sure that it is either a binary or a number, so we can see that it's an actual ID. "when is_binary(todo_item_id) or is_integer(todo_item_id)". Inside this function, I will write it like this: "case Repo.get!(Item, todo_item_id) do" I will write it like this: "Repo.get(...)", so it returns nil instead of raising and exception. If it returns nil, then we just return nil, and if it returns and item, let's update this item. I will update_item(item, ...). Ummm... I will write a separate function to just toggle it. "def toggle_item", if the item is completed... or rather if it has "completed_at: nil", it means it's not completed, so I will update the item to say completed_at and let's pass the current time. I think you can get it like this: "DateTime.utc_now". Yes, it works. DateTime.utc_now, and if the item has some completed_at, it means that it's already completed, so we can pattern-match on the actual item, because this field can only have two possible values, either a timestamp or nil. So let's update the item with "completed_at: nil". And in here, I will just call toggle_item(item). In the resolver function, we will need to return a tuple, let's say "{:ok, Todos.toggle_todo_item_by_id}"... I'll rename it to "toggle_item_by_id" because it's more consistent with the rest of the API. We pass it the item_id. OK, let's see if it works. We have the todoItems list. And I still haven't made a video about GraphQL, Elixir, and React, really, but I have already written a decent mutation, at least I think so, so I will write another one and see if I can mark this first to-do item as completed. So I will reload the browser tab so we can see if the GraphQL schema has been updated. It is. I will go back to the new mutation, and I will make it accept an ID, which is non-nullable, and I will "toggleTodoItem" with the id of $id. And it should return content and isCompleted, and maybe the ID. Let's see if it works. It complains that I didn't provide any ID. So, in the query variables below, I will add the ID of 2, and then run the query. And we get an error. Let's see what happened. I think I know what happened. This is because in the "toggle_item/1" I used the "update_item/2" function, which uses "Repo.update/2", and "Repo.update/2" by definition returns a tuple. It either returns a tuple with :ok as the first element, or :error as the first element. So, this "toggle_item/1" will return a tuple, and this branch would return nil, so I will convert it to return "{:ok, nil}", and the toggle_item I will leave as is, so in the resolver, I remove the first :ok, because our function in the context will already return a tuple. And let's see if that works. Oops, what happened? It did work, but... OK, it did update it, I see what happened. The first time we ran this mutation, it has already updated the item, but we just didn't get the correct result from the mutation. And the second time we ran it, it returned nil, because it toggled it, and the third time over it also returns the correct completed_at. I think this wraps up this video, and to wrap it up, we have created a new Phoenix application, we have installed absinthe and absinthe_plug, we have set up the Absinthe schema to work with the Phoenix router, and we have created one query and two mutations. Thank you guys, and if you liked this video, please like it and subscribe, and let me know if you have any suggestions in the comments below. Thank you!
Info
Channel: Make Programming Fun Again
Views: 9,628
Rating: 4.8793101 out of 5
Keywords:
Id: 8MCmTiB3g6c
Channel Id: undefined
Length: 31min 47sec (1907 seconds)
Published: Sun Apr 12 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.