Lesson 27. Accessing Your API from Your Application

In this lesson, you change the way that you access JSON-formatted data by adding an API namespace. Then you modify your AJAX function to allow users to join courses directly from a modal. Last, you create the action to link users and courses through a new route.

This lesson covers

  • Creating an API namespace
  • Building a UI modal to fetch data asynchronously
  • Connecting models with MongoDB methods
Consider this

Users can now view course listings from any page on your application, but they want to do more than view that list. With AJAX requests, you can not only pull data asynchronously into the page, but also perform other actions, such as creating new records and editing existing records.

In this lesson, you explore ways in which you can make better use of your API and how AJAX can help.

27.1. Applying an API namespace

I discussed namespacing in lesson 26. Now you’re going to implement a namespace for API endpoints that return JSON data or perform actions asynchronously. To get started, create a new route module called apiRoutes.js in your routes folder. This module will contain all the API routes with JSON response bodies. Require this new module in index.js by adding const apiRoutes = require("./apiRoutes"). Then tell your router to use this module under the api namespace with router.use("/api", apiRoutes).

Note

You must add this new route above the home and error routes. Those routes are namespaced for /, meaning that any URL entered that doesn’t match a route name before reaching the error or home routes defaults to an error page.

Create your first route, and have it point to your coursesController.js. Add the code in listing 27.1 to apiRoutes.js. Require the Express.js router along with your courses controller at ../controllers/coursesController. Then point GET requests to the /courses path to the index action of coursesController.js and export the router, followed by respondJSON. As with your other error-handling middleware, tell this router to use errorJSON in case actions run earlier don’t return a response.

Note

If an action doesn’t explicitly respond to the client, the connection is still open, and the request continues to flow through the chain of middleware functions. Typically, this situation means that an error has occurred, and that error will propagate through until error-handling middleware catches it.

Listing 27.1. Adding a route to show all courses in apiRoutes.js
const router = require("express").Router(),
  coursesController =
 require("../controllers/coursesController");      1

router.get("/courses", coursesController.index,
 coursesController.respondJSON);                   2
router.use(coursesController.errorJSON);              3

module.exports = router;

  • 1 Require courses controller.
  • 2 Add the API route to the Express.js Router.
  • 3 Add API error-handling middleware.

To get this code to work, create the respondJSON and errorJSON actions in courses-Controller.js. Add the code in listing 27.2 to the courses controller for this action.

The index action in coursesController.js already attaches courses to the response’s locals object. Take that locals object and display it in JSON format instead of rendering the data in EJS. If an error occurs in the courses query, pass the error to your errorJSON action. Your normal errors controller actions respond only with browser views. If an error occurs, instead of redirecting to another page, respond with a status code of 500, indicating that an internal error has occurred.

Listing 27.2. Adding JSON responses for courses in coursesController.js
respondJSON: (req, res) => {                    1
  res.json({
    status: httpStatus.OK,
    data: res.locals
  });                                           2
},

errorJSON: (error, req, res, next) => {         3
  let errorObject;

  if (error) {
    errorObject = {
      status: httpStatus.INTERNAL_SERVER_ERROR,
      message: error.message
    };
  } else {
    errorObject = {
      status: httpStatus.INTERNAL_SERVER_ERROR,
      message: "Unknown Error."
    };
  }

  res.json(errorObject);
},

  • 1 Handle the request from previous middleware, and submit response.
  • 2 Respond with the response’s local data in JSON format.
  • 3 Respond with a 500 status code and error message in JSON format.
Note

You will also need to add const httpStatus = require("http-status-codes") to the top of coursesController.js.

Restart your application, and visit http://localhost:3000/api/courses in your browser to see course data in JSON. Having these routes and controllers separate from your web application routes and controllers prevents you from making mistakes in the future. As things stand now, you always want to render EJS or redirect if you visit /courses, and you always expect a JSON response from/api/courses.

With this new API namespace, route, and controller action in place, change the AJAX GET request in recipeApp.js to call /api/courses instead of /courses?format=json. Then remove the conditional block checking for the format query param in your courses indexView action. Restart your application, and check whether you can still load the course data in the modal.

Also, because you’re now returning your data wrapped in another JavaScript object containing your status code, you need to modify your AJAX call to handle returned data properly. Change the AJAX call in recipeApp.js as shown in the next listing.

Listing 27.3. Modifying AJAX call in recipeApp.js
$.get("/api/courses", (results = {}) => {
  let data = results.data;                     1
  if (!data || !data.courses) return;          2
  data.courses.forEach((course) => {           3
    $(".modal-body").append(
      `<div>
      <span class="course-title">
      ${course.title}
      </span>
      <div class='course-description'>
      ${course.description}
      </div>
      </div>`
    );
  });
});

  • 1 Set up a local variable to represent data.
  • 2 Check that the data object contains course information.
  • 3 Loop through course data, and add elements to modal.

Restart your application, and click the modal button to see that functionality hasn’t changed from the last section.

In the next section, you add more functionality to the modal to allow users to join courses.

Quick check 27.1

Q1:

Why do you create a new folder for API controllers?

QC 27.1 answer

1:

Having a separate folder for API controllers and actions makes it easier to split the application in two. One part of the application serves data with a visual aspect, and the other serves data to sources looking for the raw data.

 

27.2. Joining courses via modal

Listing the courses in a modal is a great accomplishment. In this section, you improve the modal even more by allowing users to join a course asynchronously through the modal. Add a button that allows users to join the course. Through AJAX, you submit a request to an API endpoint where a controller action attempts to add the user to the course and responds with a success or failure message in JSON.

First, add the link to join the course by adding the HTML code in listing 27.4 to the bottom of the HTML rendered from the original AJAX call in recipeApp.js. This button needs a custom class join-button and can be placed next to the course title in the modal. It also needs the data-id set to ${course._id}, which allows you to know which course listing you selected.

Note

The data attribute in HTML is helpful in situations like these. You can mark each button with a data-id attribute so that each button’s unique ID matches some corresponding course ID.

Listing 27.4. Adding a button to join a course in recipeApp.js
<button class="join-button" data-id="${course._id}">
  Join
</button>                   1

  • 1 Add a button with target-class join-button to join a course.

If you restart the application now, you should see a button next to each course item, as shown in figure 27.1. These buttons don’t have any functionality yet, though.

Figure 27.1. Adding a join button

To get these buttons to work, change the code in recipeApp.js to use the code in listing 27.5. In this example, you create a function called addJoinButtonListener that sets up a click-event listener for each button with the class join-button. You need to call this function right after the AJAX request completes because you want to attach the listener to the buttons after they’re created on the page. To do this, append a then block to the AJAX request.

Note

AJAX functions use promises, so you can chain then and catch blocks to the end of requests to run code after you get a response. The success block behaves the same way.

In addJoinButtonListener, you grab the target of the click—the button—and then pull the data ID you set earlier with the course’s ID. With this information, you can make a new AJAX GET request to the /api/courses/:id/join endpoint. For this request to work, you need to make sure that the user is logged in. This route allows you to target specific courses to join by using the course ID.

The route and action that handle that request return the JSON value success: true if you’re able to add the user to the course. If you’re successful, change the text and color of the button to indicate that the user has joined by adding a new joined-button class and removing the old join-button class. This swapping of classes allows you to style each button with different style rules in recipe_app.css and also prevents the click event from triggering another request. If you don’t see the color of the button change, make sure that you’re targeting the correct button class. If joining the course results in an error, change the button’s text to tell the user to try again.

Note

The variable $button has only the $ in front to indicate that it represents a jQuery object. This syntax is stylistic and conventional but not required to get your code to work.

Listing 27.5. Adding an event listener to each button in recipeApp.js
$(document).ready(() => {
  $("#modal-button").click(() => {
    $(".modal-body").html("");
    $.get("/api/courses", (results = {}) => {
      let data = results.data;
      if (!data || !data.courses) return;
      data.courses.forEach((course) => {
        $(".modal-body").append(
          `<div>
          <span class="course-title">
          ${course.title}
          </span>
          <button class="join-button" data-id="${course._id}">
          Join
          </button>
          <div class="course-description">
          ${course.description}
          </div>
          </div>`
        );
      });
    }).then(() => {
      addJoinButtonListener();                                     1
    });
  });
});

let addJoinButtonListener = () => {                                2
  $(".join-button").click((event) => {
    let $button = $(event.target),
      courseId = $button.data("id");                               3
  $.get(`/api/courses/${courseId}/join`, (results = {}) => {       4
    let data = results.data;
    if (data && data.success) {                                    5
      $button
        .text("Joined")
        .addClass("joined-button")
        .removeClass("join-button");
    } else {
      $button.text("Try again");
    }
    });
  });
}

  • 1 Call addJoinButtonListener to add an event listener on your buttons after the AJAX request completes.
  • 2 Create the event listener for the modal button.
  • 3 Grab the button and button ID data.
  • 4 Make an AJAX request with the course’s ID to join.
  • 5 Check whether the join action was successful, and modify the button.

Now your application is prepared to send an AJAX request and handle its response when the join button is clicked. In the next section, you create the API endpoint to handle this request.

Quick check 27.2

Q1:

Why do you need to call the addJoinButtonListener function after the modal contents are created?

QC 27.2 answer

1:

addJoinButtonListener sets an event listener for a specific class within the modal contents. To set the listener, you must first create the content in the modal.

 

27.3. Creating an API endpoint to connect models

To complete the course modal, you need to create a route to handle requests made for the current user to join a course. To do so, add router.get("/courses/:id/join", courses-Controller.join, coursesController.respondJSON) to apiRoutes.js. This route allows get requests to go through a join action and feed results to your respondJSON action, which returns to the client. At the top of coursesController.js, require the User model with const User = require("../models/user"). Then, in coursesController.js, add the join action in listing 27.6.

In this join action, you get the current logged-in user and the course’s ID from the URL params. If a currentUser exists, use the Mongoose findByIdAndUpdate to locate the user object and update its courses array to contain the target course ID. Here, you use the MongoDB $addToSet method, which ensures that the array has no duplicate IDs. If you’re successful, add a success property to the response’s locals object, which in turn is passed to respondJSON and passed back to the client. In case the user isn’t logged in or an error occurs while updating the user’s association, pass an error to be handled by your error-handling middleware.

Listing 27.6. Creating an action to join a course in coursesController.js
join: (req, res, next) => {                         1
  let courseId = req.params.id,
    currentUser = req.user;                         2

  if (currentUser) {                                3
    User.findByIdAndUpdate(currentUser, {
      $addToSet: {
        courses: courseId                           4
      }
    })
      .then(() => {
        res.locals.success = true;                  5
        next();
      })
      .catch(error => {
        next(error);                                6
      });
  } else {
    next(new Error("User must log in."));           7
  }
}

  • 1 Add the join action to let users join a course.
  • 2 Get the course id and current user from the request.
  • 3 Check whether a current user is logged in.
  • 4 Update the user’s courses field to contain the targeted course.
  • 5 Respond with a JSON object with a success indicator.
  • 6 Respond with a JSON object with an error indicator.
  • 7 Pass an error through to the next middleware function.

With this action in place, restart your application, and try joining courses in the modal. If you’re not signed in, you may see the Try Again text appear over the button. Otherwise, depending on your custom styling, your button should turn green and change text for every button you click, as shown in figure 27.2.

Figure 27.2. Example modal after a course has been joined

You can improve the user experience by letting users know whether they’re already part of one or more courses in the modal.

Given your application structure and model schemas, you can filter your results by adding the middleware function filterUserCourses to coursesController.js, as shown in listing 27.7. In this code, you’re checking whether a user is logged in before you continue. If a user is logged in, use the map function on your array of courses. Within this function, look at each course and check whether its _id is found in your logged-in user’s array of courses. The some function returns a Boolean value to let you know if a match occurs. If a user has joined a course with ID 5a98eee50e424815f0517ad1, for example, that ID should exist in currentUser.courses, and the userJoined value for that course is true. Last, convert the courses Mongoose document object to JSON so that you can append an additional property by using Object.assign. This property, joined, lets you know in the user interface whether the user previously joined the course. If no user is logged in, call next to pass along the unmodified course results.

Listing 27.7. Adding an action to filter courses in coursesController.js
filterUserCourses: (req, res, next) => {
  let currentUser = res.locals.currentUser;
  if (currentUser) {                                                1
    let mappedCourses = res.locals.courses.map((course) => {        2
      let userJoined = currentUser.courses.some((userCourse) => {
        return userCourse.equals(course._id);                       3
      });
      return Object.assign(course.toObject(), {joined: userJoined});
    });
    res.locals.courses = mappedCourses;
    next();
  } else {
    next();
  }
}

  • 1 Check whether a user is logged in.
  • 2 Modify course data to add a flag indicating user association.
  • 3 Check whether the course exists in the user’s courses array.

To use this middleware function, you need to add it to your APU route for /courses before you return the JSON response. The route will look like router.get("/courses", coursesController.index, coursesController.filterUserCourses, coursesController .respondJSON), where coursesController.filterUserCourses sits after your query for courses in coursesController.index.

The last step is changing the client-side JavaScript in recipeApp.js to check whether the current user has already joined the course and modifying the button in the course listing modal. In listing 27.8, you use a ternary operator in the button’s class attribute and main text content. These operators check whether the course data’s joined property is true or false. If the property is true, create the button to indicate that the user has already joined. Otherwise, display a button inviting users to join.

Listing 27.8. Adding dynamic button styling in recipeApp.js
<button class='${course.joined ? "joined-button" : "join-button"}'
 data-id="${course._id}">                                        1
  ${course.joined ? "Joined" : "Join"}                              2
</button>

  • 1 Add the appropriate class to reflect join status.
  • 2 Add the button’s text to reflect join status.

After applying these changes, relaunch your application and log in. The color and text of your course-listing buttons will correctly reflect the status of your associations in the database.

Note

If you experience problems maintaining a logged-in account, make sure to use sessions and cookies prior to initializing passport and your custom middleware.

Quick check 27.3

Q1:

Why do you need to use the findByIdAndUpdate method?

QC 27.3 answer

1:

The findByIdAndUpdate Mongoose method combines the find and update methods, so you can conveniently perform a single step to update a user document.

 

Summary

In this lesson, you learned how to modify your namespacing structure to accommodate an API for JSON data responses. You also improved your courses modal by allowing users to join specific courses without needing to change pages. Through the AJAX requests and API endpoints you created, more of your application’s functionality can move to a single page and away from individual views for each action. In lesson 28, I discuss some ways in which you can secure your API.

Try this

With this new API in place, you’ll want to create endpoints for every route that might return data. You may want to add every index and show action to the controllers in the api directory, for example.

Create those actions and one additional action to create a user, and return JSON with a confirmation of success or failure instead of a rendered view.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.238.159