Building a performant real-time web app with Ember Fastboot and Phoenix (Part 5)
Building a CRUD resource
This article is part 5 in a series. You should first read the previous parts leading up to this, or it may not make much sense!
Part 1: The meeting of two well-aligned, opinionated frameworks
Part 2: Users and Authentication (API)
Part 2.1: Some small adjustments to your API
Part 3: Users and Authentication (UI)
Part 4: Logging in to our API & ember-simple-auth
Part 5: Building a CRUD resource
Part 6: Animating your UI with liquid-fire
Part 7: Room UI & Messages
Now it’s time for us to start taking advantage of the alignment of our two opinionated frameworks, by creating a couple of CRUD resources. For our chat app, we want “rooms” and “messages”.
Some high-level requirements
- Rooms should “belong” to a user, and only that user should be able to update (i.e., change the name) or delete the room
- Messages don’t need to be deletable or updatable. But timestamps are important
- Common API requests we may make are “get all messages for this room” or “get all rooms owned by this user”. Let’s make sure to create DB indices where appropriate.
- We want to insulate our UI from the particulars of our API whenever possible.
Building Rooms in Phoenix
We’ll be making use of Ecto, a simple (but powerful) Elixir data persistence library. Phoenix does a lot for us through code generation. Make sure you’re in your API project, and run the following in your terminal
mix phoenix.gen.json Room rooms name:string owner_id:references:users
You’ll see that a bunch of files were generated. Let’s take a look at a few of these.
Open up ./web/models/room.ex and take a look
There are a couple of things going on here.
line 2 — We’re “using” the base model module from Phoenix.Web. If you look at ./web/web.ex this will make a little more sense
Essentially, this is where we get our basic functionality from, and it’s one way that inheritance works in Elixir.
line 4–9: We’re defining the way we expect our DB schema to look. In this case we have one string field, one reference to another table via a foreign key, and a pair of timestamps (inserted_at, updated_at) that are automatically set up for us
line 11–12: we distinguish between fields that are required and fields that are optional. When we make a changeset (first step in creating/updating the DB), the proposed changes are validated against these lists of attributes
line 14–17: the changeset method will make more sense later on as we start using it. Just for fun, let’s add one line here to make sure that chat room names are 4 or more characters long.
If you’re not used to the pipe forward operator (|>), it’s a very simple concept that’s used all over Elixir.
Pipe-forward simply means “use the value on the left side of the pipe, as the first argument in the function on the right side of the pipe”
As a concrete example, the following two methods are equivalent, pipe-forward just makes code that involves chaining a lot of functions together a lot more readable
If you go back to your terminal, you may notice that it left you some instructions about adding the resource to your router. Let’s go ahead and do that.
Open up ./web/router.ex and add one line in the scope you’re piping through your api_auth pipeline.
resources “rooms”, RoomController, except: [:new, :edit]
Your router should now look like this
What’s happening in this one line is, we’re defining a collection of routes that are handled by our RoomController (another thing that our mix phoenix.gen.json command built for us). The new and edit routes are typically only used in non-API scenarios, so they’re disabled using the “except” option.
Open up ./web/controllers/room_controller.ex — you’ll see that a lot of code has been generated for you already. This is an already working set of CRUD (Create, Read, Update, Delete) functionality! We just need to make a couple of changes in order to meet our requirements above (and some common sense requirements)
Only logged-in users should be able to access rooms
Add the following plug at the top of the module, just like we did for our UserController (./web/controllers/user_controller.ex)
plug Guardian.Plug.EnsureAuthenticated, handler: Peepchat.AuthErrorHandler
This will ensure that only authenticated users will be able to invoke the methods in this controller.
saving the owner of a room upon creation
When a user creates a room, we want to associate their user with it. We could ask them to pass their user ID, but this allows for the possibility that they could pass an id other than theirs. A less error-prone approach would be to use the currently logged in user’s ID directly, like this
only the owner of a room should be able to delete or update a room. An easy way to meet this requirement is to build the current user’s ID into the query again. Users who attempt to delete rooms that aren’t theirs will get a 404 in response. This is awesome for two reasons
- It’s easy to implement because we’re just adding a constraint to the DB query (rather than performing a more complex authorization check)
- In a lot of situations, it’s a good idea to return a 404 instead of a 403 (Unauthorized) anyway, so that attackers can’t identify an addressable resource from a non-existent resource as easily
This is all accomplished pretty easily with Ecto
Our implemented update and delete actions now should look something like this
You’ll notice that we have to use the pin operator (^) when building DB queries that use variables. Read more about that here if you’re interested.
One of our requirements is to be able to get a list of rooms owned by a particular user. Pattern matching makes this really easy in Phoenix — we just will end up defining two index actions.
These two methods can co-exist quite nicely, the first one will be used in the event that the second argument is an object with an “owner_id” property, otherwise the second one will be used.
All-in-all, our controller should look like this now
We are done with the controller for now
Room Controller Tests
Phoenix has generated some basic tests controller for us allready in ./test/controllers/room_controller_test.exs, but we are still missing some important things
- Ensure that authenticated users can access the room resource (collection of endpoints)
- Ensure that only owners are allowed to update or delete rooms
- Basic tests for the “list of rooms owned by a specific user” endpoint
ExUnit, the Elixir test framework that comes with Phoenix, is a topic worthy of its own set of articles, but we’ll just touch on what we need to know for now. Open up ./test/controllers/room_controller_test.exs.
You’ll notice that the general structure of the test file looks like this:
We have a setup function that is called before every test runs, and it returns a two-value tuple. This second value is passed to each test. The first thing we need to do is update this setup method so that it effectively logs a test user in. Let’s update our setup method to this
Next, let’s deal with tests for our list of rooms. We want to seed our tests with some data, and an easy way to do this is with a private method inside our test case
Here we create three rooms belonging to the currently logged in user, and two more belonging to another user. Now we can test our two scenarios pretty effectively, in that a request to
should return 5 results, and a request to
should return 3 (we’ll see how to deal with path params vs queryparams later). Implementing these two tests is now quite simple
Note that I’m just checking for the length of the array here — that’s because deserialization of an individual object is handled by our ja_serializer library. Thus, if we have coverage for our show action, we should have reasonable test coverage for deserialization of “room” objects for this view covered. let’s do that next
The remaining tests are mostly set up for us. We just need to make some adjustments due to our use of JSON-API contracts. Here’s what my test module looks like when all is done
We’ll need to take a quick detour to enhance our user controller a little bit, just by adding basic show and index actions. While we’re at it, let’s get rid of a deprecation warning having to do with the property key used to pass data to ja_serializer views
This will set the stage for API endpoints like
To make these available, head back to the router and update the scope that uses your authenticated API pipeline
This will enable the new user-related endpoints, as well as the “rooms owned by a user” endpoint we already have in place in the room_controller.
To check out the new routing table, run the mix task
You should see something like
We have to modify the user view so that the user object contains a link to the relationship for the rooms the user owns. ja_serializer makes it really easy to define a “has many” relationship. There’s more than one way to express this relationship, but we’ll opt for a “link” — meaning a URL that’ll return the appropriate list of objects.
And of course we need to define a view for a room object as well
One thing to observe here is that a “belongs to” relationship is defined as has_one. Again, we’ll opt for using a “link” to define this relationship. the resulting JSON representation for a user will now be something like this
and a room will be something like this
Just for fun, let’s add a DB constraint ensuring that the name of a room is unique across our whole app. Database changes are accomplished by a schema migration, which is a (usually) reversible discrete change to the structure of your database. When other developers check out your code change (including your migration), they can simply replay it to update their own database.
Open the DB migration file that Phoenix created as part of the mix phoenix.gen.json task. It should be named something like ./priv/repo/migrations/20160427094427_create_room.exs, although the number (a timestamp) that the filename begins with will be different.
We’ll add just one new line at the bottom of the migration to create this name uniqueness DB constraint
And to have model-level validation error messages, we’ll want to add validation constraint to our room model (./web/models/room.ex)
It’s now time to run our DB migration. Head over to your terminal and run:
and now run your tests
They should all be passing. This is a great time to open up a PR and merge your work into master. If you want to compare your work to mine, here are all of my changes
Contribute to peepchat-api development by creating an account on GitHub.github.com
Building rooms in Ember
Head on over to your UI project, and let’s get started. We’ll get some mileage out of ember-cli’s code generation by running these commands
ember g model room name:string owner:belongs-to:user
ember g model user email:string password:string password_confirmation:string rooms:has-many:room
This will set up a new Room model, with an owner “belongsTo” relationship of type User, and update the existing User model, to add a rooms “hasMany” relationship of type Room.
Now, let’s think about the UI we want to build
To start building this UI, the next thing we’ll focus on is the leaf-level active route for the “logged in” page: ./app/routes/app/index.js. A simple way to think about what’s needed here is to go back to the core purposes of a route
- To load and provide data to the active template(s)
- To handle actions that pertain to the aforementioned data
The data that we’ll need for this page consists of two things
- The list of rooms (asynchronous data)
- Something representing a new record, originating from the client side (synchronous data)
The actions we’ll need to be able to handle are
- createRoom — needs no argument, and will use the route’s “currentModel” state to either persist data, or render error messages into the UI
- removeRoom — needs a Room record as an argument
For the data, we’ll make use of Ember’s promise library: RSVP.js. The RSVP.hash method is great for resolving an object that may consist of a mix of values and eventual values (a.k.a Promises). A simple example of how this might work
Now that we know how this works, we can define the overall structure of our route.
We’ll want to make use of our ember-cli-flash awesomeness, so we should inject the appropriate service onto this route. Here’s what my route ends up looking like
One aspect of this implementation that may be interesting to those unfamiliar with ember-data is that I’m waiting to create the ember-data record until we’re right about to send out the API request. The reason I’m doing this is because the live array returned from the invocation of
would include this result, even before the user hits the create button! Here’s what that would look like
For this same reason, I’m explicitly unloading the record from the store in the event that there was a problem creating it.
Now we just have to update the template associated with this route, and some styles. Here’s what mine looks like
And the styles I added to my app/styles/app.scss file.
Now, a logged in user should be able to create and destroy their rooms. Now is another good time to commit your changes, open a PR and merge it into your master branch once tests pass. Here’s what my changes on the UI side look like, if you’d care to compare