REST API & Implementation | CRUD

REST API & Implementation | CRUD

What are we going to do today?

  • We are going to take our basic Node.js project setup and clone it.

  • We are going to write our first set of CRUD APIs.

  • We will be using the ORM to interact with the database.

  • We will write 3-4 APIs that can help us implement basic REST using Sequelize and Express.

Observations

  • How will I use the same template to get started?

  • How will we actually use the power of Sequelize and Sequelize CLI?

Steps to clone

  • git clone https://github.com/shubhamkhuntia/Base-Node-Project-Template.git

  • npm install

  • Inside Git Bash: vim .env

  • Enter the port and then press : and then press x and enter.

  • Go inside the src folder and run npx sequelize init. If it shows an error, delete the file and try the command again with --force.

  • Insert the database name and password.

We are ready!

  • sequelize db:create: This will create a brand new database according to our specifications.

  • Every table is called a model. So if we want to create a table, we need to create a model. npx sequelize model:create --name Airplane --attributes modelNumber:string,capacity:integer This will create a model (table) named Airplane. Without any space, we provide columnName:columnType. It automatically creates an Airplane class and gives us an associate function where we can define associations.

    • By doing this, it doesn't actually create tables. It only creates model files and migration files, which are used for version control.

    • Migration is like git add ., not git commit. It doesn't create versions until you commit. Migration is similar to versions in your table.

    • Migration files indicate the changes that will happen in the next commit.

    • The changes that are going to happen are defined in the up function. The up function uses await queryInterface. If you don't want to use an ORM and want to connect to MySQL in a raw fashion, you need to set up a query interface object. With Sequelize, this is done automatically. Sequelize adds some properties itself, like ID, createdAt, updatedAt.

    • Sequelize shows that in the next migration commit, a table will be created with these properties. If you want to make any changes, this is the time to do it.

    • You can revert changes, but it requires several steps.

    • I added a bunch of constraints in the model number in airplane.js. You need to add those constraints in migrations as well because Sequelize supports two levels of constraints: database level and JavaScript level.

    • If you try to add an airplane without a capacity in JS code, it will throw an error.

    • If you put the constraint in the migration, the table will also have that constraint. How do you commit the database? npx sequelize db:migrate: This applies any pending migrations to the database. How does it know what the pending migrations are? It tracks the applied migrations using a unique migration number (file name).

  • We can undo a migration using sequelize db:migrate:undo. If you want to undo all migrations, you can use undoall.

  • When you add a migration, the async up function is applied.

  • When you undo a migration, the async down function is applied. It simply drops the table.

Let's say in a migration, you added a table. Now, if your other team members want to have the same table, they can simply do a git pull, run npx sequelize db:migrate, and the pending migrations in their system will be applied.

For every change in the model, there will be migration files. When we push to GitHub, we will also push the migration files. If you download this project, you just need to run db:create and then npx sequelize db:migrate to create all the tables in the MySQL database. The schema will be transferred, but not the data.

Models are like classes for JavaScript. You can perform the same operations on models as you do with tables in the database. Programmatically, you will interact with models, but internally, when you query a model, you are actually querying the corresponding table.

If you make any changes to your model, you should create a migration for it as well. Otherwise, you will see the changes in the JavaScript code, but not in the table. Migrations are used to port changes to the databases.

Always create a new migration file. Modifying an old migration will make your life difficult.

Everything we are doing in the database level stuff can be done without migrations as well. Sequelize provides support for establishing without migrations. There's something called db sync in Sequelize. When you create a model and set it up using raw Sequelize, you can use db sync. The only problem is that it doesn't maintain versions of your database schema. You can't make incremental changes.

There can be multiple models and only one migration. You can generate and then run db:migrate.

Repositories

Controllers should not directly talk to models. Services have business logic and don't talk to models. So, who talks to models? Your repository talks to models, and that's why we are going to write a bunch of code in our repository.

There could be complex operations that you have to perform. Let's say you have a movie booking application. You have a theater model, movie model, and user model. Of course, for every model, you should have create, read, update, and delete functions. Why do you want to make the same effort again and again? We will create a generic CRUD repository that can handle the basic operations, and if you want to perform something complex, you can write a specific function for it.

We have created a CRUD repository class. The constructor of this class takes a model and stores it in our this.model variable.

// Create a new user
const jane = await User.create({ firstName: "Jane", lastName: "Doe" });
console.log("Jane's auto-generated ID:", jane.id);
const { Op } = require("sequelize");
Post.destroy({
  where: {
    authorId: {
      [Op.or]: [12, 13]
    }
  }
});
// DELETE FROM post WHERE authorId = 12 OR authorId = 13;
// Change everyone without a last name to "Doe"
await User.update({ lastName: "Doe" }, {
  where: {
    lastName: null
  }
});

Why are we using classes here? Yes, we can use functions, but we are going to inherit a class using this. If we have to do it using prototypes, we would need to use the .prototype.prototype syntax.

Whenever I have to write any raw query, custom query, joins, or anything specific to queries using Sequelize, I will go to this file.

If you're extending from a particular class and need to access some properties of that class, you need to manually call the constructor of that class.

Services

Now, if you remember, controllers pass on to services. Services use the repository to interact with databases. This file will contain a bunch of logic.

Technically, you can avoid using a class here if you want, because I'm not making a CRUD service that can handle all the basic operations already. If you want, you can create a function instead of a class.

I will create a new airplane repository object. I can create it as a global object or a local object. There won't be much of a performance difference as JavaScript has its own garbage collector.

Controllers

Here we will require the service because we will always interact with the service.

We simply called the service. Nothing more, nothing less. We got the request, passed it to the service. The service is going to perform all the computations, do the DB interactions with the repository, and everything else. It will give me back my airplane (data), and then the controller will structure the output response. This is the only use case of the controller. To trigger this controller, we need to register it in a route.

If somebody wants to make a post request, let's see what the flow will be:

localhost:3000/api/v1/airplanes

Currently, we will not be able to read the request body. Everything crashed at the controller. By default, ExpressJS doesn't know how to read the request body. There's a simple way to handle it. We need to inform ExpressJS that for incoming requests, if there's a request body, please read it like JSON. In earlier versions of Express, Express used to have an inbuilt library for this. Then, after some time, they extracted it into a separate library called body-parser. Now, they have brought it back inside Express. body-parser is still a separate library, but you don't need it now. Express has again implemented it.

All we have to do is use app.use() before registering any routes. app.use() registers a middleware for all the upcoming routes mentioned below it.

express.json(): This middleware helps to parse the incoming JSON request body. Without it, Express won't be able to parse the request body.

express.urlencoded(): Sometimes there might be special characters in your URL that are encoded in a particular type of encoding. This function takes an object as an argument with the property "extended". If you set it to true, it will use one library; if false, it will use another library. There isn't much difference.

Voila! We have implemented our first CRUD API.

<u>FLOW</u>

API Routes -> v1 Routes -> Airplane Routes || This was route registration || -> Inside the controller -> Service -> Repository || In the repository, it's internally executing an SQL query || -> The response is given back to the controller -> The controller returns the response JSON.

Special thanks and credits to my exceptional instructor, Sanket Singh, for his invaluable guidance and expertise.