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.
- Keep in mind we are developing the backend software needed for DeliverUS project. Please, read project requirements found at: https://github.com/IISSI2-IS-2025
- The template project includes EsLint configuration so it should auto-fix formatting problems as soon as a file is saved.
- The template project also includes the complete model of the App, which was completed in the previous lab.
Click on "Use this template" in GitHub and "Create a new repository" to create your own repository based on this template. Afterwards, clone your own repository by opening VScode and clone the previously created repository by opening Command Palette (Ctrl+Shift+P or F1) and Git clone this repository, or using the terminal and running
git clone <url>Alternatively, you can use the Source Control button in the left-sided bar and click on Clone Repository button.
In case you are asked if you trust the author, please select yes.
It may be necessary to setup your git username by running the following commands on your terminal, in order to be able to commit and push:
git config --global user.name "FIRST_NAME LAST_NAME"
git config --global user.email "[email protected]"As in previous labs, it is needed to create a copy of the .env.example file, name it .env and include your environment variables.
Open a terminal a run npm run install:backend to install dependencies. A folder node_modules will be created under the DeliverUS-Backend folder.
All the elements related to the backend subsystem are located in DeliverUS-Backend folder. You will find the following elements (during this lab we will focus our attention on the routes and controllers folders):
src/routesfolder: where URIs are defined and referenced to middlewares and controllerssrc/controllersfolder: where business logic is implemented, including operations to the databasepackage.json: scripts for running the server and packages dependencies including express, sequelize and others. This file is usally created withnpm 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 --saveornpm install packageName --save-devfor dependencies needed only for development environment. To learn more about npm please refer to its documentation.
- In order to add more package dependencies you have to run
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..env.example: example environment variables.src/backend.js: run http server, setup connections to Mariadb and it will initialize various componentssrc/modelsfolder: where models entities are definedsrc/databasefolder: where all the logic for creating and populating the database is locatedsrc/database/migrationsfolder: where the database schema is definedsrc/database/seedersfolder: where database sample data is defined
src/controllers/validationfolder: validation of data included in client requests. One validation file for each entitysrc/middlewaresfolder: various checks needed such as authorization, permissions and ownership.src/configfolder: where some global config files are stored (to run migrations and seeders from cli)src/testfolder: will store unit test requests to our Rest API, using the SuperTest module.
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-2025#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 /restaurantsto Create a restaurant. The controller method typically is named ascreateHTTP GET /restaurantsto Read all restaurants. The controller method typically is named asindexHTTP GET /restaurants/{restaurantId}to Read details of the restaurant with id=restaurantId (a path param). The controller method typically is named asshowHTTP PUT /restaurants/{restaurantId}to Update details of the restaurant with id=restaurantId (a path param). The controller method typically is named asupdateHTTP DELETE /restaurants/{restaurantId}to Delete the restaurant with id=restaurantId (a path param). The controller method typically is named asdestroy
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.
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 pathDeliverUS project organizes its routes in the src/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.
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 src/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.bodyrepresents 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.paramsrepresents path params. For instance if we defined a:restaurantIdparam, we will have access to it byreq.params.restaurantId.req.queryrepresents query params. For instance, if a request includes astatusquery param, it will be accessed byreq.query.status.req.userrepresents 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 objectentityObjectto 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.json(message)returns a stringmessageto the client as a JSON document with HTTP 200 status code.res.status(500).send(err)returns theerrobject (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
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 we will temporarily include a fixed value for the userId. We will fix this next lab.
// newRestaurant.userId = req.user.id // authenticated user
newRestaurant.userId = 1Finally, 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) {
res.status(500).send(err)
}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 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 and its restaurant category. Remember that products should be sorted according to the order field value. You can use the following code snippet to perform the query:
const restaurant = await Restaurant.findByPk(req.params.restaurantId, {
attributes: { exclude: ['userId'] },
include: [{
model: Product,
as: 'products',
include: { model: ProductCategory, as: 'productCategory' }
},
{
model: RestaurantCategory,
as: 'restaurantCategory'
}],
order: [[{model:Product, as: 'products'}, 'order', 'ASC']],
}
)Next, return the restaurant by using res.json() method that receives the object to be returned. Surround this code with the corresponding try and catch In case that an exception is raised, you should return the HTTP status code 500 in the catch block by using the methods res.status(httpCode).send(error).
Use the Model.update method. In case of success, you should return the updated restaurant element by querying the database (using the method findByPk) after the update.
This method follows the same steps that when creating a restaurant.
Use the Model.destroy method. You need to specify a where clause to remove only the restaurant identified by req.params.restaurantId . Destroy returns the number of destroyed elements. Return an info message.
Open a terminal and run:
npm run test:backend
By now, only the restaurant routes are tested. We will add more tests in following labs.
- Node.js docs: https://nodejs.org/en/docs/
- Express docs: https://expressjs.com/
- Sequelize docs: https://sequelize.org/master/manual/getting-started.html