Lesson 15

Fetching JSON data

6

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.

Polyfilling Fetch

Not too long ago, JavaScript developers had to either learn how to use the cumbersome XMLHttpRequest API or use an external library like jQuery in order to get data asynchronously from a server. Today, there's a new JavaScript API called Fetch which makes it much easier!


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.

assets/brunch-config.js

Now we simply import the library in our app.js file, and we're ready to use the global Fetch-function everywhere!

assets/js/app.js

Making our first requests

Let's see how Fetch works! We'll make our first request in our App-component's 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 11, but something else).

assets/js/app.js

Note that Fetch is a promise-based library, which is why we use .then() and .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 GET-request to /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 /messages in your browser and open the JavaScript console, you should see your user being logged there:



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 /api/rooms instead:

assets/js/app.js


"Internal Server Error". Oh noez!


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 me is nil, there's no ID to get!


The reason me is 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.

assets/js/app.js

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:


The main takeaway from this picture is that, as long as we supply the server with a valid token when we make a request, we should always be able to retrieve the JSON we want.


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:

lib/messengyr_web/views/chat_view.ex

Now we can use this function in our EEx-template! Instead of just rendering it as text in an HTML-tag though, we're going to log it as a JavaScript string (because we need to be able to easily get it with JavaScript).

lib/messengyr_web/templates/chat/index.html.eex


Look, there's our 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 window object:

lib/messengyr_web/templates/chat/index.html.eex

After doing this and refreshing the page, you should be able to type window.jwtToken in your JavaScript console and confirm that the token has been saved



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:

assets/js/app.js

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 Pipeline module:

lib/messengyr/auth/api_pipeline.ex
lib/messengyr_web/router.ex


Refresh... and there's our room data! Yay!


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 ChatController:

lib/messengyr_web/controllers/api/room_controller.ex

We'll again use the already defined "error.json"-template in our ErrorView to render the error as JSON.


If you go to /api/rooms in your browser now, you should see the error message since the "Authorization" header is not set here.


Ecto queries

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:

lib/messengyr/chat/chat.ex
lib/messengyr_web/controllers/api/room_controller.ex

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:

lib/mesengyr_web/views/api/message_view.ex

Refresh the page one last time, and you'll see that we finally have all the data that we need in order to replace fake-data.js!


Rooms, check! Counterpart, check! Messages, check!


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).

assets/js/app.js

It's probably also a good idea to set the defaultProps in our MenuContainer and 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:

assets/js/components/menu-container.js
assets/js/components/chat-container.js

Next, we go back to app.js and use the response from our Fetch request to mutate the state:

assets/js/app.js

With this new state, we can pass down the relevant data to our MenuContainer and ChatContainer components as props:

assets/js/app.js

Also, since the users in our JSON data actually have the field avatarURL now, we can use it in our img-tag inside the MenuMessage component:

assets/js/components/menu-message.js

Now refresh the page and pat yourself on the back!


We did it! There's our database data!


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 (2017-02-21T08:59:57).


To do this, we can use a great little library called Moment.js. Again, we use the same process as with our other JavaScript plugins; we start by installing it via NPM in the assets folder:

...we whitelist it in our Brunch configuration...

assets/brunch-config.js

...and tfinally we import it into menu-message.js where we use it to format the timestamp for the sentAt variable:

assets/js/components/menu-message.js


Now you should have a nice relative time instead!


Comments

Peter Marreck

response here is a Promise object. response.json() is the JSON resolution of the promise, thus response.rooms will always be undefined. you have to pass it through an additional .then which return's response.json() and then the last "then" call here receives the json object, which .rooms will now be defined on

profile/avatar/default
Andreas

thanks a lot, this made it work! :)

profile/avatar/default
Jeffylube

.then((response) => { return response.json(); }) .then((response) => { // Get the rooms from the JSON: let rooms = response.rooms; // Mutate the state with "setState": this.setState({ rooms: rooms, messages: rooms[0].messages, }); })

Peter Marreck

I haven't done frontend in a while so I didn't know what a polyfill was; this seems like an explanation: https://remysharp.com/2010/10/08/what-is-a-polyfill