The Model Layer in Express.js

You may have noticed that we neglected many details in the previous lesson. For example,

javascript
1app.get("/user/:id", (req, res) => {
2  res.send("Show user");
3});

When a GET request is sent to route /users/:id, a response containing the message "Show user" will be returned. But of course, there is still a lot we need to do to actually display the user info.

For example, JavaScript will need to check the database and retrieve the right user data based on the id parameter. The retrieved raw data might need to be processed based on your project's specific needs. The processed data will then be embedded into an HTML page and transferred to the client as the response body.

The MVC architecture

In practice, a web application is often split into three layers: model, view, and controller, so that you don't have to put everything into one single file.

The model layer is in charge of interacting with the database, and it should provide methods to get, create, modify, and delete records in the corresponding database table.

The view layer is the frontend part of the app. It is just HTML, but with some programming features included, because sometimes you'll need to embed variables, if statements, and even loops inside the view layer, as you will see later.

The controller layer is your application's command center, connecting the model and view layers. In most cases, its job involves getting data from the database through the model layer, and passing it to the view layer to be rendered.

It might also go the other way, receiving data from the view layer, and passing it to the model so that data can be safely stored inside a database. In the process, the controller might need to perform certain actions on the retrieved data.

Most other frameworks require you to follow a strict file structure in order to implement this MVC architecture, but not Express.js. You can design your file structure however you want. As a demonstration, this is the structure we are going to use:

text
1.
2├── models
3├── controllers
4├── views
5├── libs
6├── routes
7├── package.json
8├── package-lock.json
9└── index.js

In this case, models, controllers, and views each contain the code for their corresponding layer. libs contains the extra libraries you might need. The routes directory contains the routers defined for the app, and finally, index.js is the entry point of our app.

Preparing the database

In this lesson, we are going to start with the model layer, which is in charge of interacting with the database. Previously we used a .txt file to store information, but that is not going to be enough this time, so first of all, you'll need a proper database.

For demonstration purposes, we are going with SQLite. It is a lightweight, single file-based SQL database. It is a great option for beginners to create their first practice project, because to create a SQLite database, all you need to do is install the corresponding command line tool.

If you are on Windows:

bash
1winget install -e --id SQLite.SQLite

If you are on macOS:

bash
1brew install sqlite

If you are on Ubuntu:

bash
1sudo apt install sqlite3

And then, create a .sqlite file.

bash
1touch database.sqlite
tree
1.
2├── controllers
3├── database.sqlite  <===
4├── index.js
5├── libs
6├── models
7├── package-lock.json
8├── package.json
9├── routes
10└── views

To view the SQLite database, you can use a VSCode extension such as SQLite Viewer, or any other tools you are familiar with.

Lastly, there is one more thing we must do, that is to install the sqlite3 NPM package in our project. This package allows JavaScript to work with SQLite databases.

bash
1npm install sqlite3

To use this package, go to libs and create a new file named db.js.

javascript
1import sqlite3 from "sqlite3";
2
3const db = new sqlite3.Database("./database.sqlite");
4
5export default db;

This example makes a connection to the SQLite database by creating a new instance of Database object, which gives you access to various methods that allow you to interact with the database. We will talk more about them later.

Of course, you can initialize a Database from inside the model itself, but this setup allows you to share the same Database instance across multiple models, which is much more convenient.

Next, go to the models directory and create a user.js file. This will be our user model. It is in charge of interacting with the corresponding users table.

models/user.js

javascript
1import db from "../libs/db.js";

Inside the user model, you'll need to import the Database instance we just created.

Initializing the users table

The first thing we need to do is to make sure a users table has ben initialized inside database.sqlite.

models/user.js

javascript
1db.serialize(() => {
2  db.run(
3    `CREATE TABLE IF NOT EXISTS users (
4      id INTEGER PRIMARY KEY AUTOINCREMENT,
5      username TEXT,
6      email TEXT
7    )`
8  );
9});

db.serialize(() => {}) makes sure that everything in the callback function is executed in sequence, and db.run() executes the given SQL query, which is a command language used to interact with relational databases such as SQLite. We did not discuss how to write SQL in this course, but the language itself is quite intuitive.

sql
1CREATE TABLE IF NOT EXISTS users (
2  id INTEGER PRIMARY KEY AUTOINCREMENT,
3  username TEXT,
4  email TEXT
5)

In this case, a new users table will be created if it doesn't exist already. The users table has the following structure.

text
1+---------+----------------+-------------------+
2|   id    |      email     |      username     |
3+---------+----------------+-------------------+
4| INTEGER |      TEXT      |       TEXT        |
5+---------+----------------+-------------------+

The User class

Next, your users table needs a schema, meaning the table should have a fixed structure. In our case, you should make sure every user item has an id, a username, and an email. We are using a JavaScript class to organize our code:

models/user.js

javascript
1class User {
2  constructor(id, username, email) {
3    this.id = id;
4    this.username = username;
5    this.email = email;
6  }
7
8  static getAll() {. . .} // Get all user items in the table.
9  static getById() {. . .} // Get a single user item based on id.
10  static create() {. . .} // Create a new user, and save it in the database.
11  static update() {. . .} // Update an existing user item.
12  static delete() {. . .} // Remove a user item from the database.
13}

Besides the constructor function, we are also going to create five utility methods. Of course, you may need other methods depending on the specific requirements of your project, such as getByUsername(), getByEmail(), and so on. After going over this lesson, you should be able to create them with ease.

Creating a new user

Right now, our database is empty, so let's start by talking about how to add new records to the database.

models/user.js

javascript
1static create(username, email, callback) {
2  const sql = "INSERT INTO users (username, email) VALUES (?, ?)";
3  db.run(sql, [username, email], function (err) {
4    callback(null, this.lastID);
5  });
6}

The create() method accepts three arguments, username, email, and callback. The callback is a function that should be executed after the new user has been inserted into the database.

Inside create(), we are using the run() method under Database to insert the new user record. The run() method has the following syntax:

javascript
1db.run(sql, [param1, param2, ...], callback);

The first argument, sql, accepts an SQL query.

sql
1INSERT INTO users (username, email) VALUES (?, ?)

This command says we are going to insert a new user into the users table. The columns username and email each correspond to a placeholder (?). Their values are provided in the second argument of run() in the form of an array.

create methods

Wait, our schema defines the users table to have an id, a username, and an email, so where is the id? The answer is quite simple, they are automatically created. When you insert a new record, it will be added to the end of the table, and the id will simply increment by 1.

Lastly, the run() method accepts a callback function, which will run after the SQL query has been executed. If something goes wrong, an error object will be passed to the callback function (err). If not, err will be null.

We did not implement an error handling mechanism in this example for simplicity, but you should definitely do that in production.

Inside the callback function, the function callback(), which is passed to the create() method, will be executed.

callback

Create the corresponding route

So what exactly is this callback(), and why do we need it here? To answer this question, we need to think about how you may use this User.create() method. Go back to the entry point of our app, index.js, and add a new router for our create() method:

index.js

javascript
1import User from "./models/user.js";
2
3app.use(express.json()); // Allows Express.js to parse the request body
4
5app.post("/users", (req, res) => {
6  const { username, email } = req.body;
7  User.create(username, email, (err, userId) => {
8    res.status(201).json({ id: userId, message: "User created successfully" });
9  });
10});

First of all, we need to retrieve the username and email from the request body. This step requires us to use the express.json() function, which we are going to discuss later.

The username and email are then passed to the create() method, so that create() can save them to the database. The third argument will become the callback() function like this:

from route to model

In this example, we left out error handling, so null is assigned to the argument err, but you should include it in your project, especially in a production environment.

You may have noticed something strange here. What exactly is this.lastID? We did not define a lastID in the constructor function, so where did it come from?

This is a common mistake for beginners to JavaScript, due to the fact that this exists outside of an object, which is a special feature in JavaScript.

As we've discussed previously in the JavaScript Methods lesson, this is context dependent, meaning the this keyword in different functions points to different things. And in this case, this exists in the context of the callback function.

this and the callback function

And it has a lastID property that stores the id of the item that has just been saved.

One more thing to be noted is that you cannot use arrow functions here if you wish to access this.lastID, because arrow functions do not have a this keyword.

Testing the create() method

Finally, let's test if our setup works. At this point, this should be your user.js model:

javascript
1import db from "../libs/db.js";
2
3db.serialize(() => {
4  db.run(
5    `CREATE TABLE IF NOT EXISTS users (
6      id INTEGER PRIMARY KEY AUTOINCREMENT,
7      username TEXT,
8      email TEXT
9    )`
10  );
11});
12
13class User {
14  constructor(id, username, email) {
15    this.id = id;
16    this.username = username;
17    this.email = email;
18  }
19
20  static create(username, email, callback) {
21    const sql = "INSERT INTO users (username, email) VALUES (?, ?)";
22    db.run(sql, [username, email], function (err) {
23      callback(null, this.lastID);
24    });
25  }
26}
27
28export default User;

And this should be your index.js:

javascript
1import express from "express";
2import User from "./models/user.js";
3
4const app = express();
5const port = 3001;
6
7app.use(express.json());
8
9app.post("/users", (req, res) => {
10  const { username, email } = req.body;
11  User.create(username, email, (err, userId) => {
12    res.status(201).json({ id: userId, message: "User created successfully" });
13  });
14});
15
16app.listen(port, () => {
17  console.log(`App listening on port ${port}. Visit http://localhost:${port}.`);
18});

Start the server by running the following command:

bash
1node index.js
text
1App listening on port 3001. Visit http://localhost:3001.

A new users table should be created because the db.serialize() function will be executed first. But now, the table is still empty.

empty users table

Next, open Thunder Client and make a new POST request to http://localhost:3001/users. Inside the request body, you must provide the username and email, so that they can be accessed on the server side.

json
1{
2  "username": "John",
3  "email": "john@example.com"
4}

request body

Click send, and a new user record should be added to the database.

new user item

Getting a single user by id

Next, let's talk about how to retrieve the user record we just created.

models/user.js

javascript
1static getById(id, callback) {
2  const sql = "SELECT * FROM users WHERE id = ?";
3  db.get(sql, [id], (err, row) => {
4    const user = new User(row.id, row.username, row.email);
5    callback(null, user);
6  });
7}

In this case, we use the db.get() method to get a single row from the users table. The method works just like db.run(), as it also accepts an SQL query, an array of parameters, and a callback function.

The only difference is that if the SQL query executes without error, the retrieved row will be passed to the callback function. In our example, we use the retrieved data to initialize a new instance of User, which is then passed to the function callback().

As for the corresponding route handler:

index.js

javascript
1app.get("/users/:id", (req, res) => {
2  const id = req.params.id;
3  User.getById(id, (err, user) => {
4    res.json(user);
5  });
6});

get single user

Getting all users

models/user.js

javascript
1static getAll(callback) {
2  const sql = "SELECT * FROM users";
3  db.all(sql, (err, rows) => {
4    const users = rows.map(
5      (row) => new User(row.id, row.username, row.email)
6    );
7    callback(null, users);
8  });
9}

The db.all() method is used to retrieve multiple rows from the database.

index.js

javascript
1app.get("/users", (req, res) => {
2  User.getAll((err, users) => {
3    res.json(users);
4  });
5});

get all users

Updating an existing user

models/user.js

javascript
1static update(id, username, email, callback) {
2  const sql = "UPDATE users SET username = ?, email = ? WHERE id = ?";
3  db.run(sql, [username, email, id], function (err) {
4    callback(null);
5  });
6}

index.js

javascript
1app.put("/users/:id", (req, res) => {
2  const id = req.params.id;
3  const { username, email } = req.body;
4  User.update(id, username, email, () => {
5    res.json({ message: "User updated successfully" });
6  });
7});

update a user

The corresponding record in the database should be updated.

updated database

Deleting a user

models/user.js

javascript
1static delete(id, callback) {
2  const sql = "DELETE FROM users WHERE id = ?";
3  db.run(sql, [id], function (err) {
4    callback(null);
5  });
6}

index.js

javascript
1app.delete("/users/:id", (req, res) => {
2  const id = req.params.id;
3  User.delete(id, () => {
4    res.json({ message: "User deleted successfully" });
5  });
6});

delete a user