What is express and why use it ?
npm i express@4
: to install express v4
app.js
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.
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:
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 /tours
, GET /tours
, PUT /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
.
npm init
npm i express
nodemon app.js
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);
});
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 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.
To create middleware of our own, we again use the use()
method and pass it a callback function with three arguments: req
, res
, 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.
req
object before passing it further.npm i morgan
app.use(morgan('dev')); // dev is used for development
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);
});
/* ------------------------------------------------------ */
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);
/:id
is our parameter.app.use('/api/v1/tours', tourRouter);
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.127.0.0.1:3000/api/v1/tours/2
we'll get the above output.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);
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'
})
}
app.use(express.static(`${__dirname}/public`)); // path to the public folder where the static files are stored
Now we can access the HTML file :
http://127.0.0.1:3000/overview.html
and we do not use public/overview.html
here. Just using /overview
will sufficeprocess.env
npm i dotenv
Changing Port :
process.env.NODE_ENV
that is written in the app.js
file.