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/DeliverUS-Backend/blob/main/README.md
- Software requirements for the developing environment con be found at: https://github.com/IISSI2-IS/DeliverUS-Backend#readme
- 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.
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.
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 controllerscontrollers
folder: 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 --save
ornpm install packageName --save-dev
for dependencies needed only for development environment (p. e. nodemon). 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.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 definedmigrations
folder: where the database schema is definedseeders
folder: where database sample data is definedmiddlewares
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 entityconfig
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
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 ascreate
HTTP GET /restaurants
to Read all restaurants. The controller method typically is named asindex
HTTP GET /restaurants/{restaurantId}
to Read details of the restaurant with id=restaurantId (a path param). The controller method typically is named asshow
HTTP PUT /restaurants/{restaurantId}
to Update details of the restaurant with id=restaurantId (a path param). The controller method typically is named asupdate
HTTP 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 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
)
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.
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 byreq.params.restaurantId
.req.file
orreq.files
represents files appended to a multipart/form-data request. For instance,req.files.logo
would give access to a file param namedlogo
.req.query
represents query params. For instance, if a request includes astatus
query param, it will be accessed byreq.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 objectentityObject
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 theerr
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
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)
}
}
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.
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.
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.
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.
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.
- Node.js docs: https://nodejs.org/en/docs/
- Express docs: https://expressjs.com/
- Sequelize docs: https://sequelize.org/master/manual/getting-started.html
- ThunderClient: https://www.thunderclient.io/