- Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
- Its written using Node.js and its the most popular.
- Express allows for rapid development of Node.js applications.
- It also makes it easy to impliment the MVC architecture.
- Piece of s/w which can be used by another s/w for purposes of communication.
- RESTFul APIs- APIs following the REST architecture.
- Resources should contain resources BUT not actions that can be performed on them.
- Middleware is software that lies between an operating system and the applications running on it. Essentially functioning as hidden translation layer, middleware enables communication and data management for distributed applications.
- Stands between request and response.
-
In express everything is middleware.
-
Example of middleware
-
![image]
-
Middleware that appears first in code, is executed before the one that appears later.
-
Order of code is important in express.
-
At the end of each middleware function, next() function is called, allowing for execution of the next middleware.
-
Request and Response objects created in the beggining go through each middleware.
-
The last middleware function, its ussually a router handler, thus we dont call the next() function to move to next middleware instead we send response data back to the client, thus finishing Request-Response cycle.
-
![image]
-
For us to use middleware in node.js, we rely on
use()
function. -
e.g
app.use(express.json());
- This middleware applies to every request since we havent specified any route.
- Eaxmple 1
app.use((req, res, next) => {
console.log('Hello from the middleware!!');
// We need to call next() to avoid Request-Response cycle being stuck
next();
});
- Example 2
app.use((req, res, next) => {
req.requestTime = new Date().toISOString();
next(); //call next middleware in the callstack
});
- Order of code is very important in express.
const tourRouter = express.Router();
const userRouter = express.Router();
app.use('/api/v1/tours', tourRouter);
app.use('/api/v1/users', userRouter);
tourRouter.route('/').get(getAllTours).post(createTour);
tourRouter
.route('/:id')
.get(getTour)
.post(createTour)
.patch(updateTour)
.delete(deleteTour);
app.route('/').get(getAllUsers).post(createUser);
app.route('/:id').get(getUser).patch(updateUser).delete(deleteUser);
- Param Middleware is one that only runs for certain parameters. i.e it runs when we have a certain parameter in our url.
- Global Variables used to define the environment in which node app is running.
app.get('env')
- gives us the environment variables
- Starting mongo shell
mongosh
- Create new DB
use natours-test
or switch to new DB. - Creating a collection (
tours
) and inserting data in BSON format
db.tours.insertOne({name:"The Forest Hiker",price: 297,rating:4.7})
- viewing contents of a collection
db.tours.find()
- Output
[
{
_id: ObjectId("631aea8a871f5067afc4c3ef"),
name: 'The Forest Hiker',
price: 297,
rating: 4.7
}
]
ObjectId("631aea8a871f5067afc4c3ef")
unique identifier of the above object.show dbs
- shows all the databases
admin 40.00 KiB
config 108.00 KiB
local 72.00 KiB
natours-test 40.00 KiB
- Show collections(similar to tables) -
show collections
natours-test> show collections
tours
- Quit mongo shell -
quit()
- Creating multiple documents.
db.tours.insertMany([{name:"The Sea Explorer",price:497,rating:4.8},{name:"The Snow Adventurer", price: 997, rating: 4.9,difficulty:"easy"}])
db.tours.find({name: "The Forest Hiker"})
db.tours.find({difficulty:"easy"})
{name: "The Forest Hiker"} and {difficulty:"easy"}
are the search criteria.
db.tours.find({price: {$lte: 500}})
$
is preserved for mongo operatorslte
stands for less than or equal to
db.tours.find({price: {$lte: 500},rating: {$gte: 4.8}})
- The above query works if both conditions are true, (AND query)
db.tours.find({$or: [ {price: {$lt: 500}},{rating:{$gte: 4.8}} ]})
lt
- less thangte
greater than or equal to
db.tours.find({$or: [ {price: {$gt: 500}},{rating:{$gte: 4.8}} ]},{name: 1})
- The command above only displays the
name
.
[
{
_id: ObjectId("631aeda0d5dbcff2c6a727af"),
name: 'The Sea Explorer'
},
{
_id: ObjectId("631aeda0d5dbcff2c6a727b0"),
name: 'The Snow Adventurer'
}
]
- Updating single document
db.tours.updateOne({name: "The Snow Adventurer"}, {$set: {price: 597}})
- Updating many/multiple documents
db.tours.updateMany({price: {$gt: 500},rating: {$gte: 4.8}},{$set: {premium: true}})
- To replace contents of a document, we use
replaceOne()
- Deleting multiple documents.
db.tours.deleteMany({rating: {$lt: 4.8}})
- Delete one document
db.tours.deleteOne({rating: {$lt: 4.8}})
- Delete all documents
db.tours.deleteMany({})
- parsing empty object.
- Lets use the most popular mongoDB Driver,
mongoose
const mongoose = require('mongoose');
dotenv.config({ path: './config.env' });
// console.log(app.get('env'));
// console.log(process.env);
const DB = process.env.DATABASE.replace(
'<PASSWORD>',
process.env.DATABASE_PASSWORD
);
// .connect(process.env.DATABASE_LOCAL, {
mongoose
.connect(DB, {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false,
})
.then(() => {
// console.log(con);
console.log('DB Connection Successful');
});
-
We create a model in mongoose in order to create documents using it and also query, update and delete thse documents, i.e perform CRUD(CREATE,READ,UPDATE,DELETE) operations.
-
To create a model we need a schema. We create models out of mongoose schema.
- Model layer is concerned with the application layer and the business logic.
- Controller layer - Function of the controller is to handler the applications' requests, interact with models and send response to clients. This is basically known as the
application logic
. - The View Layer - is necessary if we have GUI in our app. (Presentation Logic)
- sample url for filtering
http://127.0.0.1:3000/api/v1/tours?difficulty=easy&duration[gte]=5&price[lt]=1500
- Sorting in Ascending Order
http://127.0.0.1:3000/api/v1/tours?sort=price
http://127.0.0.1:3000/api/v1/tours?sort=duration
- Sorting in descending Order
http://127.0.0.1:3000/api/v1/tours?sort=-price
http://127.0.0.1:3000/api/v1/tours?sort=-duration
http://127.0.0.1:3000/api/v1/tours?fields=name,duration,difficulty,price
select
in the case below makescreatedAt
unavailable to the client on fetching the api.
createdAt: {
type: Date,
default: Date.now(),
select: false,
},
- Create the route
router.route('/top-5-cheap').get(tourController.aliasTopTours,tourController.getAllTours)
- Create the handler (using middleware)
exports.aliasTopTours = (req,res,next) => {
req.query.limit = '5';
req.query.sort = '-ratingsAverage,price';
req.query.fields = 'name,price,ratingAverage,summary,difficulty';
next();
}
- Its a powerful and useful mongodb framework for data aggregation.
- An aggregation pipeline consists of one or more stages that process documents: Each stage performs an operation on the input documents. For example, a stage can filter documents, group documents, and calculate values. The documents that are output from a stage are passed to the next stage.
- we can use it to calculate averages, min and max values, distances and many more.
//aggregation pipeline use case
exports.getTourStats = async (req, res) => {
try {
const stats = await Tour.aggregate([
//match stage
{
$match: { ratingsAverage: { $gte: 4.5 } },
},
//group stage
{
$group: {
// _id: '$ratingsAverage',
// _id: '$difficulty',
_id: { $toUpper: '$difficulty' },
numTours: { $sum: 1 },
numRatings: { $sum: '$ratingsQuantity' },
avgRating: { $avg: '$ratingsAverage' },
avgPrice: { $avg: '$price' },
minPrice: { $min: '$price' },
maxPrice: { $max: '$price' },
},
},
//sorting stage (we use variable names that are used up in the GROUP stage)
{
//1 here represents ascending
$sort: { avgPrice: 1 },
},
//Example below is to demonstrate we can actualy repeat stages
// {
// $match: { _id: { $ne: 'EASY' } },
// },
]);
res.status(200).json({
status: 'success',
data: {
stats,
},
});
} catch (err) {
res.status(404).json({
status: 'fail',
message: err,
});
}
};
router.route('/tour-stats').get(tourController.getTourStats);
- Deconstructs an array field from the input documents to output a document for each element. Each output document replaces the array with an element value.
- For each input document, outputs n documents where n is the number of array elements and can be zero for an empty array.
exports.getMonthlyPlan = async (req, res) => {
try {
const year = req.params.year * 1; //2021
const plan = await Tour.aggregate([
{
$unwind: '$startDates',
},
{
$match: {
startDates: {
$gte: new Date(`${year}-01-01`),
$lte: new Date(`${year}-12-31`),
},
},
},
{
$group: {
_id: { $month: '$startDates' }, //group by month
numTourStats: { $sum: 1 },
tours: { $push: '$name' },
},
},
]);
res.status(200).json({
status: 'success',
data: {
plan,
},
});
} catch (err) {
res.status(404).json({
status: 'fail',
message: err,
});
}
};
http://127.0.0.1:3000/api/v1/tours/monthly-plan/:year
- Passes along the documents with the requested fields to the next stage in the pipeline. The specified fields can be existing fields from the input documents or newly computed fields.
exports.getMonthlyPlan = async (req, res) => {
try {
const year = req.params.year * 1; //2021
const plan = await Tour.aggregate([
//unwind
{
$unwind: '$startDates',
},
//match
{
$match: {
startDates: {
$gte: new Date(`${year}-01-01`),
$lte: new Date(`${year}-12-31`),
},
},
},
//group
{
$group: {
_id: { $month: '$startDates' }, //group by month
numTourStats: { $sum: 1 },
tours: { $push: '$name' },
},
},
{
$addFields: { month: '$_id' },
},
//project
{
$project: {
_id: 0, //works with 1 and 0, for 0 it means _id wont showup
},
},
//sorting
{
$sort: { numTourStats: -1 },
},
//limit
{
$limit: 12, //limit to 12 outputs
},
]);
res.status(200).json({
status: 'success',
data: {
plan,
},
});
} catch (err) {
res.status(404).json({
status: 'fail',
message: err,
});
}
};
- In Mongoose, a
virtual
is a property that isNOT
stored in MongoDB. Virtuals are typically used for computed properties on documents. - These are fields that we can define in our schema though we cant save them on DB.
- we must explicitly define in our schema that we want virtual properties in our output.
- Defining a virtual property
tourSchema.virtual('durationWeeks').get(function () {
return this.duration / 7;
});
- We also need to explicitly define in our schema that we need virtuals displayed in our output.
- You add the
options
below as the second argument forconst tourSchema = new mongoose.Schema()
.
{
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
- Mongoose has 4 types of middleware:
document middleware
,model middleware
,aggregate middleware
, andquery middleware
. - We define middleware on schema.
- Middleware that can act on the currently processed documents.
- we can have middleware run before and after certain event.
- runs only for
.save() and .create()
- we can have multiple pre and post
document middleware
.
- The callback will be called before an actual document is saved on the database.
tourSchema.pre('save', function (next) {
// console.log(this);
//this at this point is the currently processed document
this.slug = slugify(this.name, { lower: true });
next();
});
- Executed after all pre middleware are executed
tourSchema.post('save', function (doc, next) {
console.log(doc);
next();
});
- Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing plugins.
- Allows us to run functions before or after certain query is executed.
this
keyword will point on the current query
// tourSchema.pre('find', function (next) {
// this.find({ secretTour: { $ne: true } });
// next();
// });
/^find/ - all strings that start with find
tourSchema.pre(/^find/, function (next) {
this.find({ secretTour: { $ne: true } });
this.start = Date.now();
next();
});
tourSchema.post(/^find/, function (docs, next) {
console.log(`Query took ${Date.now() - this.start} milliseconds`);
console.log(docs);
next();
});
- Aggregate middleware executes when you call exec() on an aggregate object.
- Aggregation middleware is a natural complement to query middleware, it lets you apply a lot of the use cases for hooks like pre('find') and post('updateOne') to aggregation.
- It allows us to add hooks before or after an aggregation happens.
this
keyword is going to point at the aggregation object
tourSchema.pre('aggregate', function (next) {
this.pipeline().unshift({ $match: { secretTour: { $ne: true } } });
console.log(this.pipeline());
next();
});
If we parse an argument into next(), express automatically knows that the argument is an error
- To define error handling middleware,all we need it to give it 4 arguments (
error, request, response, next
)
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
res.status(err.statusCode).json({
status: err.status,
message: err.message,
});
});
- then for catching the error in any route , create the error like below
const err = new Error(`Can't find ${req.originalUrl} on this server`);
err.status = 'fail';
err.statusCode = 404;
next(err);
console.log(err.stack);
- shows where the error happened
-
Password salting adds a random string to a password in way that even if a 2 different users enters similar passwords, they won't look similar
-
Password Hashing is basically encryption.
-
JWT, or JSON Web Token, is an open standard used to share security information between two parties — a client and a server. Each JWT contains encoded JSON objects, including a set of claims. JWTs are signed using a cryptographic algorithm to ensure that the claims cannot be altered after the token is issued.
-
REST APIS should be stateless. No state should be saved in a server while a request is being sent, processed and responded to.
- This module lets you authenticate endpoints using a JSON web token. It is intended to be used to secure RESTful endpoints without sessions.
- A cookie is a small piece of text that a server can send to a client and then the client stores it and send it along with future requests to the same server
- prevent same IP from making many requests into API thus preventing attacks such as DOS and Brute Force attacks
const rateLimit = require('express-rate-limit'); //rate limiting
-
cleaning all data that comes to app from malicious code. Code that is trying to attack our application
-
Data Sanitization against NoSQL query injection
app.use(mongoSanitize());
- Data sanitization against XSS
- cleans any user input from malicious html code with some js attached to it
app.use(xss());
const hpp = require('hpp'); //http parameter pollution
- prevent parameter pollution
app.use(
hpp({
whitelist: [
'duration',
'ratingsQuantity',
'ratingsAverage',
'maxGroupSize',
'difficulty',
'price',
],
})
);
- It clears up query string
-
Data Modelling is the process of taking unstructured data generated from a real world scenario and the structure it into a logical data model in a database. We do that according to a set of criteria.
- Endpoint where user can retrieve their own information.
- Below is the published API for Natours
https://documenter.getpostman.com/view/16390985/2s8YCXMxQe
- This api documentation feature exists in postman to help devs document their APIs
- Its a template engine which is commonly used with express.
- Its white space sensitive.
- Types of comments in pug
// h1 The Park Camper
- The comment is seen in dev tools//- h1 The Park Camper
- Comment isn't seen in dev tools
https://web-production-c886.up.railway.app/
-Hosted inrailway.app
https://natours-api.blinx.co.ke