Lesson 15. Connecting Controllers and Models

So far, you’ve set up your Node.js application to handle data and store that data in a MongoDB database. With the help of Mongoose, you’ve structured your data with a model and schema. In this lesson, you connect your routes to your controllers and to these models so that you can start to save meaningful data based on your user’s URL requests. First, you build a new controller for subscriber routes. Then you will convert those routes to use JavaScript ES6-enabled promises. Adding promises gives more flexibility to your database calls now and as your application grows. Finally, you wrap up this lesson with new views and a form where subscribers can post their information.

This lesson covers

  • Connecting controllers to models
  • Saving data through a controller action
  • Implementing database queries with promises
  • Handling posted form data

Consider this

Your recipe application is taking shape with Mongoose models to represent data in your database. JavaScript, however, is asynchronous in your application, so database calls require callbacks to run upon completion. Callbacks can be messy, though, especially with complicated queries.

Luckily, you can use multiple other types of syntax to wrap your callbacks and handle returned data or errors in a more elegant way. Promises are a way to do that, and Mongoose offers support for using the promise syntax within your application.

15.1. Creating a controller for subscribers

Recall that controllers are the glue between your models (the data) and your views (the web page). Now that you have a model set up, you need a controller that handles external requests specifically looking for data related to your model. If someone requests the home path /, you can return a view following logic in the home controller. Now that someone may request to register as a subscriber, you need to implement a subscriber controller. Create a new file in your controllers folder called subscribersController.js.

Note

Conventionally, controllers are named in the plural version of your model. There’s no strict rule, and as you can see, you already have a homeController.js, but this controller doesn’t represent a model in the application.

This file needs access to mongoose and your Subscriber model, both of which can be required at the top of the file. Next, you can create a controller action for when a request is made to view all subscribers in your database. The code would look like listing 15.1. You require mongoose so that you have access to the tools needed to save your model to the database. Next, require the Subscriber model from your subscriber module so that you can integrate the model into your code logic; you no longer need any reference to the Subscriber model in main.js. getAllSubscribers will be accessible to any file that requires this module. You can use this exported callback function to return data from the database.

In this controller action, you use the Mongoose find method on the Subscriber model to tell MongoDB that you want an array of all the subscribers in your database.

Note

Using the find query method without any arguments is the same as an empty object ({}). Here, you using the empty object to make it clear that you want to get all subscribers with no conditions attached.

If an error occurs while reading from the database, send it to the next middleware function. Otherwise, set the data that comes back from MongoDB to the request object. Then this object can be accessed by the next function in the middleware chain.

Listing 15.1. Building your subscribers controller in subscribersController.js
const Subscriber = require("../models/subscriber");     1

exports.getAllSubscribers = (req, res, next) => {       2
  Subscriber.find( {}, (error, subscribers) => {        3
    if (error) next(error);                             4
    req.data = subscribers;                             5
    next();                                             6
  });
};

  • 1 Require the subscriber module.
  • 2 Export getAllSubscribers to pass data from the database to the next middleware function.
  • 3 Query with find on the Subscriber model.
  • 4 Pass an error to the next middleware function.
  • 5 Set data that comes back from MongoDB on request object.
  • 6 Continue to the next middleware function.
Note

Because the model is in a different folder, you need to use .. to indicate stepping out of your current folder before entering the models folder and requiring it.

Make sure that you still have Express.js installed and working properly. The next step is setting up the route in main.js. First, make sure to require the subscribers controller in main.js by using const subscribersController = require("./controllers/subscribers -Controller"). The route you use looks like the code in listing 15.2.

In this code, you’re looking for GET requests made to the /subscribers path. Upon getting a request, pass the request to your getAllSubscribers function in subscribersController.js. Because you aren’t doing anything with the data in that function, attach the results of your query to the request object, and pass it to the next middleware function. In this case, that function is a custom callback created to render the data in the browser.

Listing 15.2. Using the subscribers controller in main.js
app.get("/subscribers", subscribersController.getAllSubscribers,    1
 (req, res, next) => {
  console.log(req.data);                                            2
  res.send(req.data);                                               3
});

  • 1 Pass the request to the getAllSubscribers function.
  • 2 Log data from the request object.
  • 3 Render the data on the browser window.

Test this code by running npm start to relaunch your application. If everything worked as planned, you can visit http://localhost:3000/subscribers and see a list of all the subscribers in your database by name and email (figure 15.1).

Figure 15.1. Example browser response with subscriber data

You could immediately improve this action by responding with the data in a view instead of returning the data. Modify the action’s return statements and replace them with res.render from Express.js. The line to render a view called subscribers.ejs could look like res.render(subscribers”;, {subscribers: req.data}). The response makes a call to render a view called subscribers.ejs and passes the subscribers from the database to that view in a variable called subscribers. Now you need to build the view to display these subscribers.

Note

Ultimately, this page will be used by administrators of the application to see who has signed up for the recipe application. But right now, this page is public to anyone who visits its associated route.

Create a file in your views folder called subscribers.ejs, and add the code in listing 15.3. Using the EJS template syntax, loop through the subscribers array passed in from the action you just created. For each subscriber, s, you can print some information. You print the name and email address of the subscriber in a paragraph tag.

Listing 15.3. Looping and printing subscribers in a subscribers.ejs
<%subscribers.forEach(s => { %>         1
  <p><%= s.name %></p>                  2
  <p><%= s.email %></p>
<% }); %>

  • 1 Loop through subscribers.
  • 2 Insert subscriber data into the view.

Your view at http://localhost:3000/subscribers should list your subscribers, as shown in figure 15.2.

Figure 15.2. Example browser view with listed subscriber data

In the next section, you add two more routes to handle information posted with a form.

Quick check 15.1

Q1:

From what module do you pass data to the view?

QC 15.1 answer

1:

You can pass data to the view from your controller. Within subscribersController.js, you pass an array of subscribers within the rendered subscribers.ejs.

 

15.2. Saving posted data to a model

So far, you should have data flowing in one direction when a request is made to your application’s web server. The next step is saving user-submitted data in the form of a subscriber object. Figure 15.3 shows the flow of information from a form to your database.

Figure 15.3. Flow from a web page form to your database

Recall that according to its schema, a subscriber object must contain name, email, and zipCode fields, so you should have a view with a form that contains these input fields. Change the form in contact.ejs to use the form shown in the next listing. The form will submit data to the /subscribe path via an HTTP POST request. The inputs of the form match the fields of the subscriber model.

Listing 15.4. Form to post subscriber data in contact.ejs
<form action="/subscribe" method="post">                   1
  <input type="text" name="name" placeholder="Name">
  <input type="text" name="email" placeholder="Email">
 <input type="text" name="zipCode" placeholder="Zip Code"
pattern="[0-9]{5}">
  <input type="submit" name="submit">
</form>

  • 1 Add a subscription form.

Because this form will display when contact.ejs is rendered, create a route to render this view when requests are made to the /contact path from the subscribers controller. You need to build a GET route for the /subscribe path and modify the existing POST route for the /contact path. These routes look like the code in listing 15.5.

The first route listens for requests made to /subscribe and uses the getSubscriptionPage callback in the subscribersController. The second route uses the saveSubscriber callback function only for requests made with the POST method.

Note

After these changes, you no longer need the contact form route handlers in homeController.js or their routes in main.js.

Listing 15.5. Routes for the subscriptions in main.js
app.get("/contact", subscribersController.getSubscriptionPage);   1
app.post("/subscribe", subscribersController.saveSubscriber);     2

  • 1 Add a GET route for the subscription page.
  • 2 Add a POST route to handle subscription data.

To complete your work here, create the getSubscriptionPage and saveSubscriber functions. Within subscribersController.js, add the code in listing 15.6. The first action renders an EJS page from the views folder. saveSubscriber collects data from the request and allows the body-parser package (installed in unit 2) to read the request body’s contents. A new model instance is created, mapping the subscriber’s fields to the request body parameters. As a final step, try to save the subscriber. If it fails, respond with the error that occurred. If it succeeds, respond with thanks.ejs.

Listing 15.6. Controller actions for subscription routes in subscribersController.js
exports.getSubscriptionPage = (req, res) => {     1
  res.render("contact");
};

exports.saveSubscriber = (req, res) => {          2
  let newSubscriber = new Subscriber({
    name: req.body.name,
    email: req.body.email,
    zipCode: req.body.zipCode
  });                                             3

  newSubscriber.save((error, result) => {         4
    if (error) res.send(error);
    res.render("thanks");
  });
};

  • 1 Add an action to render the contact page.
  • 2 Add an action to save subscribers.
  • 3 Create a new subscriber.
  • 4 Save a new subscriber.
Note

MongoDB returns the _id of the newly created subscriber. The result variable in the example contains this information.

You can try this code by filling out your own form at http://localhost/contact. Then visit http://localhost:3000/subscribers to see the list of subscribers, including your new post. In the next section, you add one more touch to your database queries by using JavaScript promises.

Quick check 15.2

Q1:

What middleware is needed in addition to Express.js to process data from a form?

QC 15.2 answer

1:

To easily parse the body of a request, you need the help of the express.json and express.urlencoded middleware function. These modules act as middleware between your request being received and processed fully with Express.js.

 

15.3. Using promises with Mongoose

ES6 made popular the idea of using promises to facilitate a chain of functions, usually callback functions, in asynchronous queries. A promise is a JavaScript object that contains information about the state of a function call and what the next call in the chain needs to see. Similar to middleware, promises can allow a function to start and patiently wait for it to complete before passing it off to the next callback function. Ultimately, promises offer a cleaner way of representing nested callbacks, and with database queries now introduced to your applications, your callback functions can get long.

Luckily, Mongoose is built to work with promises. All you need to do to get set up is let Mongoose know that you want to use native ES6 promises by adding mongoose.Promise = global.Promise near the top of main.js. Now with each query made, you can choose to return the normal database response or a promise containing that response. In listing 15.7, for example, a query to get all subscribers from the database returns a new promise with the database’s response.

Rewriting this action with a promise still allows querying of all subscribers in the database. Within the query, instead of rendering a view immediately, return a promise that contains data on whether to resolve by rendering a view or reject by logging an error. By using the exec call following find, you’re invoking your query to return a promise.

Note

Without using exec, you’re still able to use then and catch to handle follow-up commands. Without exec, however, you won’t have an authentic promise—only Mongoose’s version of a promise query. Some Mongoose methods, however, such as save, return a promise and won’t work with exec. You can read more about the distinctions at http://mongoosejs.com/docs/promises.html.

If an error occurs in the process, the error propagates down the promise chain to the catch block. Otherwise, data returned from the query passes on to the next then block. This promise-chain procedure follows the promise convention of rejecting or resolving code in a promise block to determine what code should be run next (figure 15.4).

Figure 15.4. Promise chain in Mongoose.js

When the promise is complete, it calls next to use any following middleware in Express.js. You chain on a then method to tell the promise to perform this task immediately after the database responds. This then block is where you render your view. Next, the catch method is chained to handle any errors rejected in the promise.

Note

then is used only in the context of promises. next is used in a middleware function. If both are used, as in listing 15.7, you’re waiting for a promise to resolve with then and later calling next to go to another middleware function.

You can add as many then chains as you like, ultimately telling your promise to run the code within that block when everything else is complete. The final then block logs a message to your console to let you know that the promise completed.

Listing 15.7. Using promises to get all subscribers in subscribersController.js
exports.getAllSubscribers = (req, res) => {       1
  Subscriber.find({})
    .exec()                                       2

    .then((subscribers) => {                      3
      res.render("subscribers", {
        subscribers: subscribers
      });                                         4
    })
    .catch((error) => {                           5
      console.log(error.message);
      return [];
    })
    .then(() => {                                 6
      console.log("promise complete");
    });
};

  • 1 Rewrite the getAllSubscribers action.
  • 2 Return a promise from the find query.
  • 3 Send saved data to the next then code block.
  • 4 Serve results from the database.
  • 5 Catch errors that are rejected in the promise.
  • 6 End the promise chain with a log message.

You may also modify your save command in saveSubscriber to use promises as shown in the following listing. In this example, exec isn’t needed.

Listing 15.8. Modifying saveSubscriber to use promises in subscribers-Controller.js
newSubscriber.save()
  .then(result => {                  1
    res.render("thanks");
  })
  .catch(error => {
    if (error) res.send(error);
  });

  • 1 Save a new subscriber with a promise return.

Last, if you want to add data in bulk to your application in development instead of tediously entering new subscribers through the contact form, you can create a module for that purpose. Create seed.js in your project directory, and add the code in listing 15.9. This file makes a connection to your local database and loops through an array of subscribers to create. First, clear the existing subscriber database with remove. Then the promise library’s Promise.all waits for all new subscriber documents to be created before printing log messages.

Listing 15.9. Creating new data in seed.js
const mongoose = require("mongoose"),
  Subscriber = require("./models/subscriber");

mongoose.connect(                             1
  "mongodb://localhost:27017/recipe_db",
  { useNewUrlParser: true }

);

mongoose.connection;

var contacts = [
  {
    name: "Jon Wexler",
    email: "[email protected]",
    zipCode: 10016
  },
  {
    name: "Chef Eggplant",
    email: "[email protected]",
    zipCode: 20331
  },
  {
    name: "Professor Souffle",
    email: "[email protected]",
    zipCode: 19103
  }
];

Subscriber.deleteMany()
  .exec()                                     2
  .then(() => {
    console.log("Subscriber data is empty!");
  });

var commands = [];

contacts.forEach((c) => {                     3
    commands.push(Subscriber.create({
      name: c.name,
      email: c.email
    }));
});

Promise.all(commands)                         4
  .then(r => {
    console.log(JSON.stringify(r));
    mongoose.connection.close();
  })
  .catch(error => {
    console.log(`ERROR: ${error}`);
  });

  • 1 Set up the connection to the database.
  • 2 Remove all existing data.
  • 3 Loop through subscriber objects to create promises.
  • 4 Log confirmation after promises resolve.

You can run this file by entering node seed.js in terminal in each subsequent lesson to avoid having an empty or inconsistent database. I talk more about how to use seed data in unit 8.

Quick check 15.3

Q1:

True or false: using exec on a Mongoose query is the same as running a query that returns a new promise.

QC 15.3 answer

1:

True. exec is designed to run a query and return a promise if promises are configured with your Mongoose setup.

 

Summary

In this lesson, you learned how to connect your models with controller actions. You also made a complete connection between models, views, and controllers by loading a list of subscribers from your database. At the end of the lesson, you were introduced to promises used with Mongoose and Node.js. In lesson 16, you take everything you learned in this unit and build a database for an application in the capstone exercise. In unit 4, you’ll take these steps further by building more-robust models and actions for doing more than saving and viewing data.

Try this

Try converting your other controller actions to use promises. You can also chain other Mongoose query methods, such as where and order. Each method passes a promise to the next command.

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

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