In this chapter, we're going to fetch the JSON data that we've generated. Only this time, we'll do it from our client app using the new Fetch API, and we'll learn how to use proper API authentication using JSON Web Tokens.
Most modern browsers support Fetch already, however, there are still some that haven't implemented it (most notably Safari and Internet Explorer). Therefore, we're going to use a polyfill to handle those older browsers in our app! Run this
npm install command in the
assets folder to get it:
Then, just like we did with our React and Babel dependencies, we need to whitelist the
whatwg-fetch library in our brunch configuration, so that we can use it on the client.
Now we simply import the library in our
app.js file, and we're ready to use the global Fetch-function everywhere!
Making our first requests
Let's see how Fetch works! We'll make our first request in our
componentDidMount hook, and fetch one of the users from our database there. Thanks to the work we did in the previous lesson, we can retrieve the user in JSON format by sending a simple
GET request to
/api/users/1 (remember that your own user's ID might not be , but something else).
Note that Fetch is a promise-based library, which is why we use
.catch() (if you're not too familiar with promises, check out this awesome write-up). In the code we just wrote, we send an asynchronous
/api/users/1. Then, we take the
response and we convert it to JSON using
return response.json() and finally, we log the result.
If you now load
Sweet! Our fetch request works! However, we don't really need this user info. What we really want to do is fetch the rooms from our API so that we can replace the mock data from
fake-data.js with data from the actual database!
Let's see what happens if we change our request URL to
Hm, it seems like something went wrong on our server when we tried to do that. Let's check the Phoenix log to get some more info:
Now we can see the source of the error: we're trying to get the ID of our logged-in user in the
get_counterpart/2 function. But since
nil, there's no ID to get!
nil is because we're currently only using Guardian's browser authentication, and not their API authentication. You'll notice that we can retrieve our rooms in JSON format if we go to
/api/rooms directly in our browser (which we did in the previous lesson), but if we're sending AJAX requests, it's a no-go. Let's fix this!
Using JWT tokens
Let's comment out the
componentDidMount hook in our component for now so that the error message doesn't distract us.
What we need to do now is extract the JSON Web Token (JWT) that Guardian generated automatically for us when we logged in, and manually send it in our Fetch request so that Phoenix knows that we're authenticated. Here's a picture illustrating this new flow:
Since the token was already generated by Guardian when we logged in a few lessons ago, the only thing we need to do now is to find this token in Elixir and give it to the browser somehow so that the browser can send it back again when it makes requests.
This is easy to do thanks to Phoenix's views! We simply open our existing
ChatView and we create a
jwt/1-function there which uses Guardian to return the generated JWT token:
Now we need to store this token somehow so that we can use it when we send our AJAX request. Other websites often store the tokens as cookies or in the user's local storage. However, since we'll only need the token for this particular page (
/messages) right now, we'll just store the token as a global variable by setting it to the
After doing this and refreshing the page, you should be able to type
Next, we need to make sure that we use this token when we make our request. The standardized way to do this (according to the OAuth 2.0 protocol) is to set an
Authorization header on the request with the string
"Bearer xxxxx" (where
xxxxx is the JWT token). It's usually good to follow conventions, so let's do that. Uncomment the
componentDidMount-hook, and set the headers:
If you refresh the page now... you'll still see the same error message, and it might look as if nothing has changed! However, if you open the "Network"-tab in your DevTools, and click on the request we just made, you'll notice that we're now sending out the
authorization header with the token, which is an important difference!
Using API authentication in Guardian
The last step to make our request work is to change the
:api pipeline in our
router.ex-file. Instead of using the browser session to verify the user, we want to use the header's Bearer token.
For this, we'll create a new custom pipeline module in the
lib/messengyr/auth folder. It's called
ApiPipeline and is very similar to the original
Awesome! Now we'll just do some error handling to make this perfect. For example, we don't want anyone to be able to fetch any rooms unless they're logged-in. Therefore, we'll use the
EnsureAuthenticated plug in
RoomController, just like we did previously in
We'll again use the already defined
"error.json"-template in our
ErrorView to render the error as JSON.
Our authentication now works great. The last step on the Elixir-side of things is to make some adjustments to the way we handle the request, and to the JSON that we return.
The first flaw in our
RoomController is that we currently return all the rooms that exist in the database. In reality, we only want to return the rooms that the user is a member of!
To do this, we'll create a new
list_user_rooms/1-function in our chat context. In this function, we can build a little Ecto query (which is like a traditional SQL statement, but with some syntactic sugar) where we join the
rooms-table with the
users-table, and then specify what we want:
Also, one missing piece of data in our
messages JSON is whether the message is outgoing or not (in other words, whether it was sent by the user or the counterpart). For this we'll create an
outgoing?/2 function in the
MessageView that simply checks if the logged-in user's ID is the same as the message author's ID:
Refresh the page one last time, and you'll see that we finally have all the data that we need in order to replace
Replacing the fake data
We now have the possiblity to finally use some real data on the client! To do this, we'll stop using the
DATA constant in
app.js and instead use React's state.
We start by creating a
constructor method where we set the initial state of the component (= an empty list of rooms and an empty list of message).
It's probably also a good idea to set the
defaultProps in our
ChatContainer to match this initial state (just in case the props passed down to these 2 components happens to be
undefined for some reason)! We can set these at the very end of our files, right before we export the component:
Next, we go back to
app.js and use the response from our Fetch request to mutate the state:
With this new state, we can pass down the relevant data to our
ChatContainer components as props:
Also, since the users in our JSON data actually have the field
avatarURL now, we can use it in our
img-tag inside the
Now refresh the page and pat yourself on the back!
One last thing – Moment.js
You might have noticed that the timestamp for the last sent message looks a bit ugly. Preferably, we'd want it to show a relative time (e.g.
2 minutes ago), instead of a timestamp (
...we whitelist it in our Brunch configuration...
...and tfinally we import it into
menu-message.js where we use it to format the timestamp for the