Skip to content

IISSI2-IS/Lab2-Backend-Routing-Controllers

Repository files navigation

Introduction

We will learn how to define and implement the Endpoints of our backend and how endpoints are handled by Controllers (from the MVC pattern). Controllers run some parts of the business logic of our application.

Secondly, we will learn how an ORM software tool (the Sequelize package) will help us performing operations on the Maria Database from these controllers.

Prerequisites

Exercices

1. Use repository as template and clone

Press on "Use this template" to create your own repository based on this template. Afterwards clone your own repository by opening VScode and clone the base lab repository by opening Command Palette (Ctrl+Shift+P or F1) and Git clone this repository, or using the terminal and running

git clone <url>

It may be necessary to setup your git username by running the following commands on your terminal:

git config --global user.name "FIRST_NAME LAST_NAME"
git config --global user.email "MY_NAME@example.com"

In case you are asked if you trust the author, please select yes.

As in previous labs, it is needed to create a copy of the .env.example file, name it .env and include your environment variables.

Run npm install to download and install packages to the current project folder.

2. Remember project structure

You will find the following elements (some of them will appear in following labs). During this lab we will focus our attention on the routes and controllers folders

  • routes folder: where URIs are defined and referenced to middlewares and controllers
  • controllers folder: where business logic is implemented, including operations to the database
  • package.json: scripts for running the server and packages dependencies including express, sequelize and others. This file is usally created with npm init, but you can find it already in your cloned project.
    • In order to add more package dependencies you have to run npm install packageName --save or npm install packageName --save-dev for dependencies needed only for development environment (p. e. nodemon). To learn more about npm please refer to its documentation.
  • package-lock.json: install exactly the same dependencies in futures deployments. Notice that dependencies versions may change, so this file guarantees to download and deploy the exact same tree of dependencies.
  • backend.js: run http server, setup connections to Mariadb and it will initialize various components
  • .env.example: example environment variables.
  • models folder: where models entities are defined
  • migrations folder: where the database schema is defined
  • seeders folder: where database sample data is defined
  • middlewares folder: various checks needed such as authorization, permissions and ownership.
  • controllers\validation folder: validation of data included in client requests. One validation file for each entity
  • config folder: where some global config files are stored (to run migrations and seeders from cli)
  • example_api_client folder: will store test requests to our Rest API
  • .vscode folder: VSCode config for this project

3. Routes

Backend software can publish its functionalities through RESTFul services. These services follows the architectural patterns of the HTTP protocol. DeliverUS functionalities are explained at https://github.com/IISSI2-IS/DeliverUS-Backend/blob/main/README.md#functional-requirements

As an example, if the system provides CRUD operations over an entity, there should be an endpoint for each operation. HTTP POST endpoint to Create, HTTP GET to Read, HTTP PUT|PATCH to Update and HTTP DELETE to Delete.

Routes are usually created following some common patterns and good practices. For instance, for the CRUD operations on the Restaurant entity:

  • HTTP POST /restaurants to Create a restaurant. The controller method typically is named as create
  • HTTP GET /restaurants to Read all restaurants. The controller method typically is named as index
  • HTTP GET /restaurants/{restaurantId} to Read details of the restaurant with id=restaurantId (a path param). The controller method typically is named as show
  • HTTP PUT /restaurants/{restaurantId} to Update details of the restaurant with id=restaurantId (a path param). The controller method typically is named as update
  • HTTP DELETE /restaurants/{restaurantId} to Delete the restaurant with id=restaurantId (a path param). The controller method typically is named as destroy

Moreover, and endpoint may define some query params. These are usually intended to include some optional parameters in the request, such as implementing a search over the entity. For instance, if we want to query the orders filtered by status, a status query param should be defined.

3.1. Restaurant routes definition

In order to define routes in an Express Node.js application, we have to follow the following template:

app.route('/path') //the endpoint path
    .get( //the http verb that we want to be available at the previous path
      EntityController.index) // the function that will attend requests for that http verb and that path
    .post( //we can chain more http verbs for the same endpoint
      EntityController.create) // the function that will attend requests for that http verb and that path

DeliverUS project organizes its routes in the routes folder. We define routes for each entity in its own file. For instance, restaurant routes will be defined in the RestaurantRoutes.js file.

Complete the file RestaurantRoutes.js in order to define the endpoints for the following functional requirements:

  • Customer functional requirements:

    • FR1: Restaurants listing: Customers will be able to query all restaurants.
    • FR2: Restaurants details and menu: Customers will be able to query restaurants details and the products offered by them.
  • Owner functional requirements:

    • FR1: To Create, Read, Update and Delete (CRUD) Restaurants: Restaurantes are related to an owner, so owners can perform these operations to the restaurants owned by him. If an owner creates a Restaurant, it will be automatically related (owned) to him.
    • FR3: List orders of a Restaurant. An owner will be able to inspect orders of any of the restaurants owned by him. The order should include products related.
    • FR5: To Show a dashboard including some business analytics: #yesterdayOrders, #pendingOrders, #todaysServedOrders, #invoicedToday (€). Notice that the controller function that attends this request is OrderController.analytics)

3.2. Updating our Express app.

Notice that backend.js now includes a way of register routes by importing all routes definition files in the routes folder by the following sentences:

// require only admits one parameter, so we need to create an object composed of both parameters needed on routes
const requireOptions = { app: app }
require('./routes/')(requireOptions)

When the require method receives a folder path (instead of a file) it looks for an index.js file. If present, this file usually requires (imports) all the files of the folder. In this case, this means all the routes files of the folder.

4. Controllers.

Controllers are the main components of the business logic layer. Functionalities and business rules may be implemented on controllers specially according to the MVC architectural pattern. DeliverUS project organizes its controllers in the controllers folder. We define controllers for the business logic related to each entity in its own file. For instance, restaurant controller will be defined in the RestaurantController.js file.

Each controller method receives a request req and a response res object. Request object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on (see https://expressjs.com/en/4x/api.html#req for more details). In our project we will need the following attributes from the request:

  • req.body represents the data that comes from the client (usually a Json document or the a form sent as multipart/form-data when files are needed).
  • req.params represents path params. For instance if we defined a :restaurantId param, we will have access to it by req.params.restaurantId.
  • req.file or req.files represents files appended to a multipart/form-data request. For instance, req.files.logo would give access to a file param named logo.
  • req.query represents query params. For instance, if a request includes a status query param, it will be accessed by req.query.status.
  • req.user represents the logged in user that made the request. We will learn more about this in lab3.

Response object represents the HTTP response that an Express app sends when it gets an HTTP request (see https://expressjs.com/en/4x/api.html#res. In our project we will need the following methods from the res object:

  • res.json(entityObject) returns the object entityObject to the client as a json document with the HTTP 200 status code. For instance: res.json(restaurant) will return the restaurant object as json document.
  • res.status(500).send(err) returns the err object (typically including some kind of error message) and a HTTP 500 status code to the client.

HTTP Code status used in this project are:

  • 200. Requests attended successfully.
  • 401. Wrong credentials.
  • 403. Request forbidden (not enough privileges).
  • 404. Requested resource was not found.
  • 422. Validation error.
  • 500. General error.

For more information about HTTP Status code see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

4.1. Restaurant controller methods.

4.1.1 Create method to Create entity

Typically, we expect that the body of the req includes a json document with all the needed information to create a new element of the entity. To access this document we use the req.body attribute.

Sequelize offers a way of creating new elements, the Model.build method that receives a json object that includes the needed fields for buliding a new element and then a Model.save method to store it in the corresponding database table.

For the RestaurantController we can do this by using the following snippets:

const newRestaurant = Restaurant.build(req.body)

Then, we have to include the logged in owner as the restaurant userId. Notice that until next lab, the system does not implement authentication, so the following line will NOT be included during this lab, so the restaurant will not be related to any owner. We will fix this next lab.

 // newRestaurant.userId = req.user.id // authenticated user

Next, we have to check if files are present. When we create a restaurant, we may include a heroImage and a logoImage. To this end we will check if each file is present, and if so, we store the path of the file in the corresponding database table field.

if (typeof req.files?.heroImage !== 'undefined') {
  newRestaurant.heroImage = req.files.heroImage[0].path
}
if (typeof req.files?.logo !== 'undefined') {
  newRestaurant.logo = req.files.logo[0].path
}

Finally, we can save the new created restaurant. Our newRestaurant variable is built, but not saved. To this end, sequelize offers a save method for each model. As this is an I/O operation, we don't want to block the system, so the save method returns a promise. We will use the await/async syntax to make our code more readable. We can use the following snippet:

try {
      const restaurant = await newRestaurant.save()
      res.json(restaurant)
    }
catch (err) {
    if (err.name.includes('ValidationError')) { //The database may return some kind of error.
        res.status(422).send(err)
    } else {
        res.status(500).send(err)
    }
}

4.1.2 Index methods to Read entity.

Implement the FR1: Restaurants listing: Customers will be able to query all restaurants. To this end, you can use the Sequelize Model.findAll method.

Implement the FR3: List orders of a Restaurant. An owner will be able to inspect orders of any of the restaurants owned by him. The order should include products related. To this end, you can use the Sequelize Model.findAll method including a where clause.

4.1.3 Show methods to return entity details.

Implement the FR2: Restaurants details and menu: Customers will be able to query restaurants details and the products offered by them. To this end, you will receive a req.params.restaurantId identifying the restaurant. You can use the Sequelize Model.findByPk method. Notice that you will need to include its products. Follow this syntax:

const restaurant = await Restaurant.findByPk(req.params.restaurantId,
      {
        include: [
          {
            model: Product,
            as: 'products'
          }
       ]
      })

Next, return the restaurant by using res.json() method. Surround this code with the corresponding try and catch (you may not find a restaurant with the received id). In this case return a HTTP 404 status code.

4.1.4 Update method to modify entity.

As done for the creation, check if files are present. Then use the Model.update method. This method returns the number of rows modified. In case of success, you should return the updated restaurant element by querying the database (findByPk) after the success update. This function follows the same steps that when creating a restaurant.

4.1.5 Destroy method to remove entity.

Use the Model.destroy method. You need to specify a where clause to remove only the req.params.restaurantId restaurant. Destroy returns the number of destroyed elements. Return an info message.

5. Test Restaurant routes and controllers

Open ThunderClient extension (https://www.thunderclient.io/), and reload the collections by clicking on Collections → menu→ reload. Collections are stored at

Click on Collections folder and you will find a set of requests with tests for all endpoints. Run all the collection, you will find at the right side if a test is successful or not. Some requests perform more than one test.

References

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published