Lesson 29. Capstone: Implementing an API

Confetti Cuisine raves about the user interaction with the application. To encourage more users to enroll in their courses, however, they’d like me to add more data on individual pages. More specifically, they want me to include a modal on every page that lists the offered courses and a link to enroll in each one.

To accomplish this task, I’m going to make requests to my application server by using Ajax on the client side. By making an asynchronous call to my server behind the scenes, I won’t need to load the course data until the user clicks a button to enroll. This change to use Ajax should help with the initial page-load time and ensure that course data is up to date when the user views it.

First, I’m going to need to modify my application layout view to include a partial containing the Embedded JavaScript (EJS) for my modal. Next, I’m going to create the client-side JavaScript code to request for course data. To get this data to appear, I need to create an API endpoint to respond with course data as JSON. When I have that endpoint working, I’ll add an action to handle enrolling users in courses and respond with JSON upon completion. This endpoint will allow users to enroll in classes from any page without needing to leave or refresh the page they’re on.

Before I begin, I’m going to restructure my routes to pave the way for my new API endpoints.

29.1. Restructuring routes

To start with the application’s improvements, I’ll move my routes into their own modules to clean up my main application file. As this application grows, the routes will increase as well. I’d like future developers on this project to be able to locate the routes they need easily. Because my routes for each model resource are already RESTful—meaning that the route paths take my application’s models and CRUD functions into consideration—the restructuring process is much simpler. My new application structure will separate my routes based on controller name, as shown in figure 29.1.

Figure 29.1. Application structure with routes folder

First, I create a new routes folder at the root level of my application directory. Within that folder, I create three modules to hold my models’ respective routes:

  • userRoutes.js
  • courseRoutes.js
  • subscriberRoutes.js

Next, I move all the user routes out of main.js and into userRoutes.js. This new routes file resembles the code in listing 29.1.

Note

I’ll also move my home and error routes into their own home: Routes.js and errorRoutes.js, respectively.

At the top of this file, I require the Express.js Router and usersController.js. These two modules allow me to attach my routes to the same object across my application and link those routes to actions in the users controller. Then I apply the get, post, put, and delete routes for users, which include the routes for CRUD actions as well as the routes to sign in and log in. Before I continue, I remove all occurrences of the text users in the route path. Instead, I’ll apply these routes under the users namespace later. These routes are bound to the router object, which I export with this module to make it available to other modules in the project.

Listing 29.1. User routes in userRoutes.js
const router = require("express").Router(),                      1
  usersController = require("../controllers/usersController");

router.get("/", usersController.index,
 usersController.indexView);                                    2
router.get("/new", usersController.new);
router.post("/create", usersController.validate,
 usersController.create, usersController.redirectView);
router.get("/login", usersController.login);
router.post("/login", usersController.authenticate);
router.get("/logout", usersController.logout,
 usersController.redirectView);
router.get("/:id/edit", usersController.edit);
router.put("/:id/update", usersController.update,
 usersController.redirectView);
router.get("/:id", usersController.show,
 usersController.showView);
router.delete("/:id/delete", usersController.delete,
 usersController.redirectView);

module.exports = router;                                         3

  • 1 Require the Express.js Router and usersController.
  • 2 Define user routes on the router object.
  • 3 Export the router object from the module.

Then I apply the same strategy to the other model routes and export the router object in each module. Exporting the router object allows any other module to require these routes. My routes are better organized, with each module requiring only the controllers it needs to use. To get these routes accessible in main.js, I create a new file called index.js in the routes folder. This file requires all relevant routes so that they can be accessed in one place. Then I’ll require index.js in main.js.

Note

All remaining middleware in main.js should be applied to app.use and should no longer use router.

I start by requiring the Express.js Router along with all my route modules. In this example, I include model routes and routes for errors and my home controller. router.use tells my router to use the first parameter as the namespace and the second parameter as the routes module specific to that namespace. At the end of the file, I export my router object, which now contains all the previously defined routes. The code in index.js is shown in the next listing.

Listing 29.2. All routes in index.js
const router = require("express").Router(),           1
  userRoutes = require("./userRoutes"),
  subscriberRoutes = require("./subscriberRoutes"),
  courseRoutes = require("./courseRoutes"),
  errorRoutes = require("./errorRoutes"),
  homeRoutes = require("./homeRoutes");

router.use("/users", userRoutes);                     2
router.use("/subscribers", subscriberRoutes);
router.use("/courses", courseRoutes);
router.use("/", homeRoutes);
router.use("/", errorRoutes);

module.exports = router;                              3

  • 1 Require the Express.js Router and route modules.
  • 2 Define namespaces for each route module.
  • 3 Export the complete router object.

With these routes reorganized, I’ll still be able to access my index of courses and individual courses at the /courses and /courses/:id paths, respectively. Because my routes are more organized, I have room to introduce new route modules without complicating my code structure. To import these routes into the application, I need to require index.js at the top of main.js by using const router = require("./routes/index"). This router object replaces the one I had before. Then I tell my Express.js app to use this router in the same way that I told the router to use previously defined routes by making sure that app.use("/", router) is in main.js.

Note

I also need to remove my require lines for all controllers in main.js, as they’re no longer referenced in that module.

With this new routing structure in place, my application continues to function as before. I can start implementing my API modifications by creating the modal that will display courses.

29.2. Adding the courses partial

To create a modal, I use the default bootstrap modal HTML, which provides the code for a button that displays a simple modal in the center of the screen. I add that code to a new file called _coursesModal.ejs in my courses folder. The underscore distinguishes the names of partials from regular views.

This partial, which contains only the modal code shown in the next listing, needs to be included in my layout.ejs file. I include the partial as a list item in my navigation bar, with <%- include courses/_coursesModal %>.

Listing 29.3. Code for modal in _coursesModal.ejs
<button id="modal-button" type="button"
 data-toggle="modal"
 data-target="#myModal"> Latest Courses</button>        1

<div id="myModal" class="modal fade" role="dialog">        2
  <div class="modal-dialog">
    <h4 class="modal-title">Latest Courses</h4>
    <div class="modal-body">
    </div>
    <div class="modal-footer">
      <button type="button" data-dismiss="modal">Close</button>
    </div>
  </div>
</div>

  • 1 Add the button to open modal.
  • 2 Add code for the modal window.
Note

I also need to make sure that the JavaScript files for bootstrap and jQuery are added to my public/js folder and imported into my layout.ejs through script tags. Otherwise, my modal won’t animate on the screen. I can download the latest jQuery code from https://code.jquery.com and bootstrap code from https://www.bootstrapcdn.com.

When I restart my application, I see a button in my navigation bar, which opens an empty modal when clicked (figure 29.2).

Figure 29.2. Modal button in layout navigation

The next step is populating this modal by using course data with AJAX and a new API endpoint.

29.3. Creating the AJAX function

One way to access application data without needing to refresh my web page is to make an asynchronous Ajax request to my server. This request occurs behind the scenes on the browser used by the application’s clients and originates from the client’s JavaScript file in the public folder.

To get this Ajax function to work, I need to ensure that jQuery is added to my project and linked from the layout file, because I’ll use some of its methods to populate my modal. Then, through my custom confettiCuisine.js file in my public/js folder, I can add the code in listing 29.4. I can reference this file in layout.ejs using the following script tag: <script type="text/javascript" src="js/confettiCuisine.js"></script>.

This Ajax function runs only when the Document Object Model (DOM) is loaded and the modal button is clicked. I handle the click event by making a GET request to my API endpoint at /api/courses. This request is equivalent to making a GET request to http://localhost:3000/api/courses in my web browser and receiving a page of JSON data. I’ll create this route soon.

Next, I handle the results in the response through the results object. Within this object, I expect to see a data object. If there’s no data or course object, I return to exit the function. I parse the data object for JSON and loop through its array of contents to populate my modal. For each item in my data object, I display the title, cost, and description within HTML tags.

To the side of each course listing, I link a button to an enrollment route for that course. I create a function called addJoinButtonListener to add an event listener on each course listing after its elements are added to the DOM. That function listens for a click event on the join button, marked with the .join-button class. When that button is clicked, I make another AJAX request through my API namespace to /api/courses/${courseId}/join for the specific course listing I selected. If my server returns a response saying that I was successfully added to the course, I change the color and text of the button. Using the ternary operator ${course.joined ? "joined-button" : "join-button" }, I determine the class of the button’s styling, depending on the value of course.joined. I’ll create this property on each course object to let my user interface know whether the currently logged-in user has already joined the course.

Listing 29.4. Creating an Ajax function to retrieve course data in confettiCuisine.js
$(document).ready(() => {                                        1
  $("#modal-button").click(() => {                               2
    $(".modal-body").html("");                                   3
    $.get(`/api/courses`, (results = {}) => {                    4

      let data = results.data;
      if (!data || !data.courses) return;

      data.courses.forEach((course) => {                         5
        $(".modal-body").append(
          `<div>
              <span class="course-cost">$${course.cost}</span>
                <span class="course-title">
                   ${course.title}
              </span>
              <button class="${course.joined ? "joined-button" :
   "join-button"} btn btn-info btn-sm" data-id="${course._id}">
                   ${course.joined ? "Joined" : "Join"}
              </button>
              <div class="course-description">
                 ${course.description}
              </div>
        </div>`
        );                                                       6
      });
    }).then(() => {
      addJoinButtonListener();                                   7
    });
  });
});

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

  • 1 Wait for the DOM to load.
  • 2 Handle a click event on the modal button.
  • 3 Reset the modal body’s contents to an empty string.
  • 4 Fetch course data via an AJAX GET request.
  • 5 Loop through each course, and append to the modal body.
  • 6 Link to enroll the current user.
  • 7 Call addJoinButtonListener to add an event listener on the course listing.
  • 8 Make an API call to join the selected course.

To get this code to work, I need to create two new API endpoints. One endpoint retrieves course data as JSON; the other handles my requests to enroll users at /api/course/${courseId}/join. I’ll add these endpoints in the next section.

29.4. Adding an API endpoint

Now that my Confetti Cuisine application is configured to communicate with two new API endpoints, I need to create the routes to handle these requests. The first step is adding the routes to my index.js file in the routes folder. For the AJAX request, I need a specific route under an api namespace because I want requests to go to /api/courses, not only /courses. To accomplish this task, I create apiRoutes.js within the routes folder with the code in listing 29.5.

This file requires the Express.js Router and my coursesController. Then I have that router object handle GET requests made to the /courses path. This route gets the course listing from the index action in the courses controller. Then the course listing goes through a filterUserCourses middleware function to mark the courses that the current user has already joined, and results are sent back through the respondJSON function. Under the api namespace, this path is /api/courses. The second route handles GET requests to a new action called join. I have one more piece of middleware for this API. I make reference to the errorJSON action, which handles all errors resulting from any of the routes in this API. Last, I export the router.

Listing 29.5. Creating an API route in apiRoutes
const router = require("express").Router(),                 1
  coursesController = require("../controllers/
 coursesController");

router.get("/courses", coursesController.index,
 coursesController.filterUserCourses,
 coursesController.respondJSON);                         2
router.get("/courses/:id/join", coursesController.join,
 coursesController.respondJSON);                         3
router.use(coursesController.errorJSON);                    4

module.exports = router;

  • 1 Require the Express.js Router and coursesController.
  • 2 Create a route for the courses data endpoint.
  • 3 Create a route to join a course by ID.
  • 4 Handle all API errors.

Next, I need to add this router to the router defined in index.js. I require apiRoutes.js into index.js by adding const apiRoutes = require("./apiRoutes"). I add router.use ("/api", apiRoutes) to index.js to use the routes defined in apiRoutes.js under the /api namespace. I’ve already created the index action to fetch the courses from my database. Now I need to create the filterUserCourses, respondJSON, and errorJSON actions in my courses controller so that I can return my data in JSON format. To do so, I add the code in the following listing to coursesController.js.

Listing 29.6. Creating an action to enroll users in courses in coursesController.js
respondJSON: (req, res) => {                         1
  res.json({
    status: httpStatus.OK,
    data: res.locals
  });
},
errorJSON: (error, req, res, next) => {              2
  let errorObject;
  if (error) {
    errorObject = {
      status: httpStatus.INTERNAL_SERVER_ERROR,
      message: error.message
    };
  } else {
    errorObject = {
      status: httpStatus.OK,
      message: "Unknown Error."
    };
  }
  res.json(errorObject);
},
filterUserCourses: (req, res, next) => {             3
  let currentUser = res.locals.currentUser;
  if (currentUser) {
    let mappedCourses = res.locals.courses.map((course) => {
      let userJoined = currentUser.courses.some((userCourse) => {
        return userCourse.equals(course._id);
      });
      return Object.assign(course.toObject(), {joined: userJoined});
    });
    res.locals.courses = mappedCourses;
    next();
  } else {
    next();
  }
}

  • 1 Return a courses array through the data property.
  • 2 Return an error message and status code of 500 if an error occurs.
  • 3 Check whether the user is logged in and return an array of courses with joined property reflecting user association.

With these new endpoints in place, I can restart my application and see the course listings populate my modal when the navigation button is clicked (figure 29.3).

Figure 29.3. Showing course listing through modal in browser

Note

While testing that this API endpoint works, I need to comment out my route to join until the action is added to my courses controller. Otherwise, my application will complain that it’s looking for a callback that doesn’t exist.

The last phase is creating a route and action to handle users who are looking to enroll in a class and filter the course listing to reflect those users who have already joined.

29.5. Creating an action to enroll users

To enroll a user in a cooking class, I need the current user’s ID and the selected course’s ID. I can get the user’s ID from the user object on the request, provided by passport. I need to use req.user._id or the currentUser variable I created the last time I worked on this project (lesson 25). I also have easy access to the course ID through the RESTful route. The course ID is the second element in the route’s path. My second route, '/courses/:id/join' in apiRoutes.js, points to the join action in my courses controller.

The last step is adding a controller action to enroll the user in the selected course. I start by creating a new action called join and defining local variables for the course and user IDs. Because I’m referencing the User model in this controller, I need to require that model in coursesController.js by adding const User = require("../models/user"). Then I check whether a user is signed in. If not, I return an error message in JSON format.

Note

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

If the user is logged in, I use the Mongoose findByIdAndUpdate query method to search for the user by the user object, the currentUser, and the MongoDB array update operator $addToSet to insert the selected course into the user’s courses list. This association signifies an enrollment. I accomplish all these tasks by using the code in listing 29.7.

Note

$addToSet ensures that no duplicate values appear in the courses array. I could have used the MongoDB $push operator to add the course ID to the user’s courses array, but this operator may have allowed users to enroll in the same course multiple times by accident.

Listing 29.7. Creating an action to enroll users in courses in coursesController.js
join: (req, res, next) => {
  let courseId = req.params.id,
    currentUser = req.user;                    1

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

  • 1 Define local variables for course and user IDs.
  • 2 Check whether the user is logged in.
  • 3 Find and update the user to connect the selected course.
  • 4 Continue to next middleware.
  • 5 Continue to error middleware with an error message if the user failed to enroll.

With this action in place, I can restart the application. When I try to enroll in a course before logging in, I see the message in figure 29.4.

Figure 29.4. Trying to enroll before logging in

After I successfully log in and click the button to join a course, the screen resembles figure 29.5. Also, after joining a course, I can refresh my window and still see my joined status preserved in the modal.

Figure 29.5. Successfully enrolling in a course

With a new API namespace, I can open this application to more Ajax requests and other applications that want to access Confetti Cuisine’s raw JSON data. I could secure the API, but doing so isn’t required for this small change.

Now that I’ve implemented a new feature to allow users to enroll in courses, I’ll work on improving other parts of the application that may benefit from single-page asynchronous calls to my API.

Summary

In this capstone exercise, I improved the Confetti Cuisine application experience by introducing an Ajax request to a new API endpoint. I started by reorganizing my application’s routes and separating the web routes from the API routes. Then I created an Ajax function on the client-side JavaScript to populate a modal with course-listing results from a custom API endpoint. Last, I created a route and action to allow users to enroll in courses from any page in the application. With this new improvement in place, Confetti Cuisine’s marketing team feels better about informing users and encouraging them to join their classes.

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

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