What is express and why use it ?

Pasted image 20240131025132.png

Installing Express

npm i express@4 : to install express v4

  • It is a convention to keep all the express configs in app.js

Creating a server, using get and post methods in Express :

app.js

const express = require('express');

const app = express(); // create express app

app.get('/', (req, res) => {
  res
    .status(200)
    .send('Hello from the server side')
    .json({ message: 'Hello from the server side', app: 'Natours' });
});

app.post('/', (req, res) => {
    res.send('You can post to this endpoint');
});

const port = 3000;
// listen for requests
app.listen(port, () => {
  console.log('App is running on port ' + port);
});

// routing basically means how an application’s endpoints (URLs) respond to client requests.

Pasted image 20240131133200.png
Pasted image 20240131133218.png

What is an API :

Pasted image 20240131135013.png

The Rest Architecture :

REST APIs

An API is two pieces of software communicating with each other. The type of API we might be the most familiar with is a web API, which sends data to a client whenever a request comes in. Other types might include the fs and http modules in Node: they are small pieces of software with which we can interact using their API. When we do DOM manipulation with Javascript, we are using the DOM’s API. When we add public methods to an object or class, we are creating an API for that class.

We’re mostly concerned with RESTful web APIs. REST stands for Representational State Transfer. REST architecture provides a smooth protocol with which developers can interact with APIs. REST APIs should follow a few principles:

  1. Separate the API into logical resources.
  2. Expose structured, resource-based URLS.
  3. Use the proper HTTP methods.
  4. Send data as JSON (usually).
  5. Be stateless.

Without REST conventions, we might have endpoints like /addNewTour/getTour/updateTour, or /deleteTour. In REST, we replace the verbs with the appropriate HTTP methods and then standardize the endpoint: POST /toursGET /toursPUT /tours/id or PATCH /tours/id, and DELETE /tours/id. Note that these endpoints are CRUD operations (Create, Read, Update, Delete).

We’re often going to be using JSON, but before we send JSON to a user, we usually rewrite in the JSend format, which is just an object with a "status" property and the original JSON inside a "data" property:

// JSON
{
  "someProperty": "some value",
  "anotherProperty": "another value"
}

// JSend
{
  "status": "success",
  "data": {
    "someProperty": "some value",
    "anotherProperty": "another value"
  }
}

This process of wrapping on object in another is called “enveloping,” and can mitigate security issues. Alternatives to JSend include the OData JSON protocol and JSON:API.

To make an API stateless, we make sure that all state (i.e. any data that might change) is handled on the client side, not the server side. The server should not have to remember previous requests to process current requests. For example, we should never create a GET tours/nextPage endpoint, because the server would have to remember the current page and calculate currentPage + 1. Instead, we should make an endpoint like GET tours/page/3.


Natours Project :

Terminal Commands :

npm init
npm i express
nodemon app.js

Returning the JSON data through the API :

const fs = require('fs');
const express = require('express');

const app = express(); // create express app

const tours = JSON.parse(fs.readFileSync(`${__dirname}/dev-data/data/tours-simple.json`));

app.get('/api/v1/tours', (req, res) => {
  res.status(200).json({
    status: 'success',
    data: {
      // tours: tours // or we can simply write "tours" because the key and object name is same
      tours
    }
  })
})

const port = 3000;
// listen for requests
app.listen(port, () => {
  console.log('App is running on port ' + port);
});

Pasted image 20240202114854.png

Code Block #1 :

const fs = require('fs');
const express = require('express');

const app = express(); // create express app

app.use(express.json()); // middleware is used to parse the body of the request, it can modify the incoming request data and then pass it to the next middleware in the stack

// JSON.parse() is used to convert JSON to object
const tours = JSON.parse(
  fs.readFileSync(`${__dirname}/dev-data/data/tours-simple.json`)
);

/* ------------------------------------------------------ */

app.get('/api/v1/tours', (req, res) => {
  res.status(200).json({
    status: 'success',
    results: tours.length,
    data: {
      // tours: tours // or we can simply write "tours" because the key and object name is same
      tours,
    },
  });
});

app.post('/api/v1/tours', (req, res) => {
  // console.log(req.params);
  const newId = tours[tours.length - 1].id + 1;
  const newTour = Object.assign({ id: newId }, req.body); // Obect.assign is used to merge two objects

  tours.push(newTour);

  // overwriting the file
  // we won't use writeFileSync because it is a synchronous function and it will block the event loop and we are inside a callback function
  // we are converting tours JSON to string because we can't write an object to a file
  fs.writeFile(
    `${__dirname}/dev-data/data/tours-simple.json`,
    JSON.stringify(tours),
    (err) => {
      // to send the response to the client that the tour has been created
      res.status(201).json({
        status: 'success',
        data: {
          tour: newTour,
        },
      });
    }
  );
});

/* ------------------------------------------------------ */

// app.get('/api/v1/tours/:id:x:y?', ...}); => here id x and y are different parameters and 'y' is an OPTIONAL parameter.

app.get('/api/v1/tours/:id', (req, res) => {
  console.log(req.params);
  const id = req.params.id *1; // since the id is a string, we are converting it to a number by multiplying it by 1
  const tour = tours.find(el => el.id === id)
  
  // if(id > tours.length) // or
  if(!tour)
  {
    res.status(404).json({
      status: 'fail',
      message: 'Invalid ID'
    });
  }
  res.status(200).json({
    status: 'success',
    data: {
      tour,
    }
  });
});

/* ------------------------------------------------------ */

/* ------------------------ patch ----------------------- */

app.patch('/api/v1/tours/:id', (req,res) => {
  if(req.params.id*1 > tours.length)
  {
    return res.status(404).json({
      status: 'fail',
      message: 'Invalid ID'
    });
  }

  res.status(200).json({
    status: 'success',
    data: {
      tour: '<Updated tour here...>'
    }
  });
});

/* ------------------------------------------------------ */

/* ----------------------- delete ----------------------- */

app.delete('/api/v1/tours/:id', (req, res) => {
    if(req.params.id*1 > tours.length)
    {
      return res.status(404).json({  
        status: 'fail',
        message: 'Invalid ID'
      });
    }
  
    res.status(204).json({
      status: 'success',
      data: null // we are not sending any data as we have deleted the data
    });
  });

/* ------------------------------------------------------ */

const port = 3000;
// listen for requests
app.listen(port, () => {
  console.log('App is running on port ' + port);
});

we can optimize this code to make it look much better and cleaner by doing this :

const fs = require('fs');
const express = require('express');
const { create } = require('domain');

const app = express(); // create express app

app.use(express.json()); // middleware is used to parse the body of the request, it can modify the incoming request data and then pass it to the next middleware in the stack

// JSON.parse() is used to convert JSON to object
const tours = JSON.parse(
  fs.readFileSync(`${__dirname}/dev-data/data/tours-simple.json`)
);
/* ------------------------------------------------------ */

const getAllTours = (req, res) => {
  res.status(200).json({
    status: 'success',
    results: tours.length,
    data: {
      // tours: tours // or we can simply write "tours" because the key and object name is same
      tours,
    },
  });
};

const getTour = (req, res) => {
  console.log(req.params);
  const id = req.params.id * 1; // since the id is a string, we are converting it to a number by multiplying it by 1
  const tour = tours.find((el) => el.id === id);

  // if(id > tours.length) // or
  if (!tour) {
    res.status(404).json({
      status: 'fail',
      message: 'Invalid ID',
    });
  }
  res.status(200).json({
    status: 'success',
    data: {
      tour,
    },
  });
};

const createTour = (req, res) => {
  // console.log(req.params);
  const newId = tours[tours.length - 1].id + 1;
  const newTour = Object.assign({ id: newId }, req.body); // Obect.assign is used to merge two objects

  tours.push(newTour);

  // overwriting the file
  // we won't use writeFileSync because it is a synchronous function and it will block the event loop and we are inside a callback function
  // we are converting tours JSON to string because we can't write an object to a file
  fs.writeFile(
    `${__dirname}/dev-data/data/tours-simple.json`,
    JSON.stringify(tours),
    (err) => {
      // to send the response to the client that the tour has been created
      res.status(201).json({
        status: 'success',
        data: {
          tour: newTour,
        },
      });
    }
  );
};

const updateTour = (req, res) => {
  if (req.params.id * 1 > tours.length) {
    return res.status(404).json({
      status: 'fail',
      message: 'Invalid ID',
    });
  }

  res.status(200).json({
    status: 'success',
    data: {
      tour: '<Updated tour here...>',
    },
  });
};

const deleteTour = (req, res) => {
  if (req.params.id * 1 > tours.length) {
    return res.status(404).json({
      status: 'fail',
      message: 'Invalid ID',
    });
  }

  res.status(204).json({
    status: 'success',
    data: null, // we are not sending any data as we have deleted the data
  });
};

/* ------------------------------------------------------ */

// app.get('/api/v1/tours', getAllTours);
// app.post('/api/v1/tours', createTour);
// app.get('/api/v1/tours/:id', getTour);
// app.patch('/api/v1/tours/:id', updateTour);
// app.delete('/api/v1/tours/:id', deleteTour);

app.route('/api/v1/tours').get(getAllTours).post(createTour);
// this is exactly the same as the above two lines, i.e. : app.get('/api/v1/tours', getAllTours); and app.post('/api/v1/tours', createTour);

app
  .route('/api/v1/tours/:id')
  .get(getTour)
  .patch(updateTour)
  .delete(deleteTour);

/* ------------------------------------------------------ */

const port = 3000;
// listen for requests
app.listen(port, () => {
  console.log('App is running on port ' + port);
});

Middleware and the Request Response Cycle :

Pasted image 20240203200005.png

Middleware gets it name because it happens between the request and the response in Express. We can say that everything in Express is middleware, even our route definitions. All the middleware we use in our app, considered together, is called the “middleware stack.” If we have a middleware stack with multiple middlewares, the request and response objects go through each middleware step by step (in the order defined in the code). At the end of each middleware, the next() function is called so that the next middleware can execute. At the end of the stack, we call res.send(). Thus, we can think of hte middleware stack as a pipeline for our request and response objects to travel through.

Creating Middleware

To create middleware of our own, we again use the use() method and pass it a callback function with three arguments: reqres, and next (we can actually call these whatever we want, but those names are convention). Within the callback, we absolutely must call the next() function, lest we block the pipeline:

/* --------------------- middleware --------------------- */
  app.use((req, res, next) => {
    console.log('Hello from the middleware 👋');
    next(); // next is used to move to the next middleware in the stack
  });
/* ------------------------------------------------------ */

 If we want this middleware to apply to all our routes, we need to declare it before our route declarations. Suppose we wrote our code like this:

app
  .route('/api/v1/tours')
  .get(getAllTours)
  .post(createTour);

app.use((req, res, next) => {
  console.log('Hello from the middleware 👋');
  next();
});

app
  .route('/api/v1/tours/:id')
  .get(getTour)
  .patch(updateTour)
  .delete(deleteTour);

If we make a GET request to /api/vi/tours, our middleware would not be invoked because the res.status(200).json() call inside getAllTours() ends the request-response cycle.
However, if we make a request to /api/v1/tours/:id, our middleware will indeed be invoked now.

Pasted image 20240203203448.png

  • The time here is coming from the middleware
  • We had manipulated the req object before passing it further.

3rd party middlewares :

Morgan :

npm i morgan
app.use(morgan('dev')); // dev is used for development

Pasted image 20240203214141.png

  • Logs the request on the console.

Code Block :

const fs = require('fs');
const express = require('express');
const { create } = require('domain');
const morgan = require('morgan');
const { get } = require('http');

const app = express();

/* --------------------- middleware --------------------- */
app.use(morgan('dev')); // dev is used for development
app.use(express.json());

app.use((req, res, next) => {
  console.log('Hello from the middleware 👋');
  next();
});
/* ------------------------------------------------------ */

const tours = JSON.parse(
  fs.readFileSync(`${__dirname}/dev-data/data/tours-simple.json`)
);
/* ------------------------------------------------------ */

/* ------------------- route handlers ------------------- */

const getAllTours = (req, res) => {
  res.status(200).json({
    status: 'success',
    results: tours.length,
    data: {
      tours,
    },
  });
};

const getTour = (req, res) => {
  console.log(req.params);
  const id = req.params.id * 1;
  const tour = tours.find((el) => el.id === id);

  if (!tour) {
    res.status(404).json({
      status: 'fail',
      message: 'Invalid ID',
    });
  }
  res.status(200).json({
    status: 'success',
    data: {
      tour,
    },
  });
};

const createTour = (req, res) => {
  const newId = tours[tours.length - 1].id + 1;
  const newTour = Object.assign({ id: newId }, req.body);

  tours.push(newTour);

  fs.writeFile(
    `${__dirname}/dev-data/data/tours-simple.json`,
    JSON.stringify(tours),
    (err) => {
      res.status(201).json({
        status: 'success',
        data: {
          tour: newTour,
        },
      });
    }
  );
};

const updateTour = (req, res) => {
  if (req.params.id * 1 > tours.length) {
    return res.status(404).json({
      status: 'fail',
      message: 'Invalid ID',
    });
  }

  res.status(200).json({
    status: 'success',
    data: {
      tour: '<Updated tour here...>',
    },
  });
};

const deleteTour = (req, res) => {
  if (req.params.id * 1 > tours.length) {
    return res.status(404).json({
      status: 'fail',
      message: 'Invalid ID',
    });
  }

  res.status(204).json({
    status: 'success',
    data: null,
  });
};

const getAllUsers = (req, res) => {
  res.status(500).json({
    status: 'error',
    message: 'This route is not yet defined !',
  });
};
const getUser = (req, res) => {
  res.status(500).json({
    status: 'error',
    message: 'This route is not yet defined !',
  });
};
const createUser = (req, res) => {
  res.status(500).json({
    status: 'error',
    message: 'This route is not yet defined !',
  });
};
const updateUser = (req, res) => {
  res.status(500).json({
    status: 'error',
    message: 'This route is not yet defined !',
  });
};
const deleteUser = (req, res) => {
  res.status(500).json({
    status: 'error',
    message: 'This route is not yet defined !',
  });
};
/* ------------------------------------------------------ */
/* ----------------------- Routes ----------------------- */

const tourRouter = express.Router(); // we can use tourRouter as a middleware
const userRouter = express.Router();

tourRouter
  .route('/') // here we don't have to specify the full path, because we already did that when we mounted the router
  .get(getAllTours)
  .post(createTour);

tourRouter.route('/:id').get(getTour).patch(updateTour).delete(deleteTour);

userRouter.route('/').get(getAllUsers).post(createUser);

userRouter.route('/:id').get(getUser).patch(updateUser).delete(deleteUser);

app.use('/api/v1/tours', tourRouter); // Mounting a new router on a route, means that we want to use tourRouter for all the routes that start with /api/v1/tours
app.use('/api/v1/users', userRouter);

/* ------------------------------------------------------ */

/* ------------------ Start the server ------------------ */
const port = 3000;
// listen for requests
app.listen(port, () => {
  console.log('App is running on port ' + port);
});
/* ------------------------------------------------------ */

A Better File Structure :

Pasted image 20240208030151.png
Pasted image 20240208030202.png
Pasted image 20240208030210.png
Pasted image 20240208030225.png
Pasted image 20240208030237.png
Pasted image 20240208030305.png
Pasted image 20240208030314.png
Pasted image 20240208030331.png
Pasted image 20240208030340.png

Param Middleware :

It is a middleware that only runs for certain parameters. That is when we have some certain parameters in our URL.
For example :

router.route('/:id').get(tourController.getTour).patch(tourController.updateTour).delete(tourController.deleteTour);
  • here /:id is our parameter.
  • with the base URL being : app.use('/api/v1/tours', tourRouter);
  • The app.param() function is used to add the callback triggers to route parameters. It is commonly used to check for the existence of the data requested related to the route parameter.
  • Pasted image 20240208132727.png
    • on sending request to 127.0.0.1:3000/api/v1/tours/2 we'll get the above output.
    • Also, this param middleware is only applicable on the tours URL (as we have used router.param(...) ).
const router = express.Router(); // we can use tourRouter as a middleware

// here val is the value of the parameter in question
router.param('id', (req, res, next, val) => {
  console.log(`Tour id is : ${val}`);
  next();
});

router
  .route('/') // here we don't have to specify the full path, because we already did that when we mounted the router
  .get(tourController.getAllTours)
  .post(tourController.createTour);

router.route('/:id').get(tourController.getTour).patch(tourController.updateTour).delete(tourController.deleteTour);

To check if the id requested is valid or not, we can use a separate param middleware to check for the validity of the id and if it is VALID then pass it onto "next" middlewares :

exports.checkID = (req, res, next, val) => {
  if (req.params.id * 1 > tours.length) {
    console.log(`Tour id is : ${val}`);
    // if we do not have this return here, then express would sent this response back (res.status wala response), then it will hit the next() function and then it will send the response again, which will result in an error

    // hence if the return statement is executed the next() function will not be executed

    return res.status(404).json({
      status: 'fail',
      message: 'Invalid ID',
    });
  }
  next();
};

now we are ready to use the checkID function in tourRoutes.js :

router.param('id', tourController.checkID);

Multiple Middleware functions :

router
  .route('/') // here we don't have to specify the full path, because we already did that when we mounted the router
  .get(tourController.getAllTours)
  .post(tourController.checkBody, tourController.createTour);
exports.checkBody = (req, res, next) => {
  if (!req.body.name || !req.body.price) {
    return res.status(400).json({
      status: 'fail',
      message :'Missing name or price'
    })
  }
  • The checkBody is responsible whether the next middleware should or should not be called.

Serving Static Files like HTML with express :

Pasted image 20240209011544.png

  • We cannot access this html file because we do not have a route defined to handle the request.
  • If we want to access something from out file system then we have to use a built in middleware :
    Pasted image 20240209011750.png
app.use(express.static(`${__dirname}/public`)); // path to the public folder where the static files are stored

Now we can access the HTML file :
Pasted image 20240209011938.png

  • NOTE that the URL is http://127.0.0.1:3000/overview.html and we do not use public/overview.html here. Just using /overview will suffice

Environment Variables :

Pasted image 20240210141708.png

Pasted image 20240212192143.png

  • process.env

Pasted image 20240212192443.png

  • Pasted image 20240212192451.png

Creating a config env file :

Pasted image 20240212193020.png

  • npm i dotenv
    Pasted image 20240212194631.png

Pasted image 20240212194832.png

Changing Port :
Pasted image 20240212195215.png

Pasted image 20240212195435.png

  • We are writing in this order because we require to read the process.env.NODE_ENV that is written in the app.js file.
    • Pasted image 20240212195918.png