MEAN/Go-full-stack/Partie 1 - Build a simple Express server

De WikiSys
Aller à : navigation, rechercher

Sommaire

npm install

npm install -g @angular/cli
npm install --save express
npm install --save express-session
npm install --save body-parser 
npm install --save -g mongoose
npm install --save -g mongoose-unique-validator
npm install --save jsonwebtoken
npm install --save bcrypt

Partie 1 - Build a simple Express server

Set up your coding environment

Before you can start coding, there are a few tools you will need to install. Let's start by installing the Node runtime.

Install Node

Go to NodeJS.org and download then install the latest version of Node. This installs the Node JavaScript runtime, allowing you to run Node servers. It also installs the Node Package Manager, or npm , a tool that will be invaluable for installing the packages you need to build your projects.

Install Angular

To follow the project we will be working on in this course, you will also need the Angular CLI, so that you can run the development server the front end code will be running on. To install it, run the following from the command line:

npm install -g @angular/cli

Clone the front end app

Now it's time to create what will be your working directory for this course: you can call it something like go-full-stack , for example.

Once you have created that directory, you'll want to clone the code for the front-end app into a subdirectory called frontend. From within your working directory:

git clone https://github.com/OpenClassrooms-Student-Center/5614116-front-end-app.git frontend

You can then do the following:

cd frontend
npm install
ng serve

This will install all the dependencies needed by the front end app and will launch the development server. Now, if you navigate to http://localhost:4200 , you should see the following (assuming you've followed the steps above successfully):

A web page allowing the user to select which part of the course they are working on, so as to have the correct front end app loaded.

Start a Node server


What is Node?

Before we jump in and start writing code, what is Node? What is Express? Is there a difference?

Node is the runtime that allows us to write all of our server-side tasks — like business logic, data persistence, and security — in JavaScript.

It also adds functionalities that normal browser JavaScript doesn't have, like giving you access to the local filesystem, for example.

Express, simply put, is a framework that sits on top of Node, and makes Node servers much easier to build and maintain, as you will see as we advance through this course.

Initialize your project

From within your backend directory, run the terminal command npm init to initialize your project.

npm init

You can use the default options, or change them as you wish — however, your entry point should be server.js , which you will create shortly.

You can also initialize a Git repo at this point by running git init from within your backend folder.

Don't forget to create a .gitignore file containing the line node_modules so as not to push this large folder to your remote repo.

Create a server.js file inside your backend folder — this will contain your first Node server.

Start a basic server

To create a Node server in your server.js file, you will need the following code :

backend/server.js
const http = require('http');

const server = http.createServer((req, res) => {
    res.end('This is my server response!');
});

server.listen(process.env.PORT || 3000);

Here, you import the Node native HTTP package and use it to create a server, by passing a function which will be executed every time a call is made to that server.

That function receives the request and response objects as arguments.

In this example, you use the response's end method to send a string response back to the caller.

In the final line, set the server up to listen on either the port environment variable (potentially set by the platform to which you will deploy your server) or port 3000 for development.

Start this server by running node server from the command line.

To check that it is sending the correct response, use a browser window to navigate to http://localhost:3000 (assuming you've successfully completed the steps above).

Alternatively, you can use a testing tool like Postman to make a GET request (or any other kind for that matter, as our server does not differentiate for now!) to the same URL: http://localhost:3000 (again, assuming you've successfully completed the steps above).

Install nodemon

To make Node development much simpler, you may wish to install nodemon, by running

sudo npm install -g nodemon

from the command line. Now, instead of using node server to spin up your server, you can use nodemon server , which will watch your files for changes and restart the server, making sure it is always up to date.

Now that you know how to spin up a Node development server, in the next chapter we will add Express to the project to make building our API a walk in the park.

Create an Express app

Writing web servers in pure Node, while possible, is time-consuming and painstaking, as we have to manually parse every incoming request, for example.

Using the Express framework makes these tasks much simpler, allowing us to build out our APIs in half the time, so let's install it now.

Install Express

To add Express to your project, run the following from within your backend folder:

npm install --save express
cd backend/

Create a new app.js file which will contain your Express app:

backend/app.js
const express = require('express');
const app = express();

module.exports = app;

Run the Express app on the Node server

Go back to your server.js file, and modify it as follows:

backend/server.js;
const http = require('http');
const app = require('./app');

const port = process.env.PORT || 3000;
app.set('port', port);

const server = http.createServer(app);

server.listen(port);
console.log('Listening on port http://localhost:' + port);

Making a request to this server will throw an Express-generated 404 error, as our Express app has no way of responding yet. Let's set up a simple response just to make sure everything is working properly by making an addition to our app.js file:

backend/app.js
const express = require('express');

const app = express();

app.use((req, res) => {
   res.json({ message: 'Your request was successful!' }); 
});

module.exports = app;

If you try and make a request to your server, you should get back a JSON object containing the message we have specified.

Now that our Node server is correctly serving up our Express app, let's see how we can add functionality to that app. Let's set up a simple response just to make sure everything is working properly by making an addition to our app.js file:

Add some middleware

An Express app is basically a series of functions called middleware.

Each piece of middleware receives the request and response objects, and can read, parse, and manipulate them as necessary.

Express middleware also receives the next method, which allows that middleware to pass execution on to the next piece of middleware.

Let's see how all of that works.

This Express app contains four pieces of middleware:

backend/app.js
const express = require('express');
const app = express();

app.use((req, res, next) => {
    console.log('Request received');
    next();
});

app.use((req, res, next) => {
  res.status(201);
  next();
});

app.use((req, res, next) => {
  res.json({ message: 'Your request was successful!' });
  next();
});

app.use((req, res, next) => {
  console.log('Response sent successfully!');
});

module.exports = app;
  • the first logs "Request received" to the console, and hands on execution
  • the second adds a 201 status code to the response, and hands on execution
  • the third sends the JSON response, and hands on execution
  • the final piece of middleware logs "Response sent successfully" to the console

This is a very simple server that doesn't do much for now, but it illustrates how middleware works in an Express app.

Improve server.js

Before we move forward with the course, let's make a few improvements to our server.js file which will make it more stable and suitable for deployment:

backend/server.js
const http = require('http');
const app = require('./app');

const normalizePort = val => {
  const port = parseInt(val, 10);

  if (isNaN(port)) {
    return val;
  }
  if (port >= 0) {
    return port;
  }
  return false;
};

const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

const errorHandler = error => {
  if (error.syscall !== 'listen') {
    throw error;
  }
  const address = server.address();
  const bind = typeof address === 'string' ? 'pipe ' + address : 'port: ' + port;
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges.');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use.');
      process.exit(1);
      break;
    default:
      throw error;
  }
};

const server = http.createServer(app);

server.on('error', errorHandler);
server.on('listening', () => {
  const address = server.address();
  const bind = typeof address === 'string' ? 'pipe ' + address : 'port ' + port;
  console.log('Listening on ' + bind);
});

server.listen(port);

A quick run-through of what is going on here:

  • the normalizePort function returns a valid port, whether it is provided as a number or a string
  • the errorHandler function checks for various errors and handles them appropriately — it is then registered to the server
  • a "listening" event listener is also registered, logging the port or named pipe on which the server is running to the console

Our Node development server is now up and running properly, and our Express app is ready to have some proper functionality added to it.

Create a GET route

It's time to start adding the functionality our front-end app requires and watch the whole system take shape!

You should have your front-end app running in a browser (run ng serve from the frontend directory and navigate your browser to http://localhost:4200) and head into "Parts 1 + 2."

Send back stuff for sale

As you may have noticed, the front-end app currently shows "No stuff for sale" and also indicates an error in the console. This is because it is trying to reach out to our API (which does not exist yet!), and retrieve the stuff for sale. Let's try and make that stuff available.

In your app.js file, replace all the middleware with the following middleware:

backend/app.js
app.use('/api/all-stuff', (req, res, next) => {

  const stuff = [
    {
      _id: 'oeihfzeoi',
      title: 'My first thing',
      description: 'All of the info about my first thing',
      imageUrl: '',
      price: 4900,
      userId: 'qsomihvqios',
    },

    {
      _id: 'oeihfzeomoihi',
      title: 'My second thing',
      description: 'All of the info about my second thing',
      imageUrl: '',
      price: 2900,
      userId: 'qsomihvqios',
    },

  ];

  res.status(200).json(stuff);

The first difference you will note is the extra argument passed to the use method: we are passing it a string, corresponding to the endpoint for which we want this piece of middleware to be registered. In this case, that endpoint will be http://localhost:3000/api/all-stuff, as that is the URL being requested by the front end app.

In this middleware, we create an array of stuff with the specific data schema required by the front end. We then send that stuff as JSON data, along with a 200 status, for a successful request.

If you make a GET request to this endpoint from Postman, you will see that you receive the array of stuff. However, refreshing the browser doesn't seem to work. So what exactly is going on here?

CORS errors

CORS stands for Cross Origin Resource Sharing.

It is a standard that allows us to relax default security rules which prevent HTTP calls from being made between different servers.

In our case, we have two origins: localhost:3000 and localhost:4200, and we would like them to be able to communicate with each other.

For this, we need to add some headers to our response object.

Back in the app.js file, add the following middleware before your API route:

backend/app.js
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content, Accept, Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
  next();
});

This will allow all requests from all origins to access your API.

You can also now add valid image URLs to the stuff being sent back by the API, completing the GET route. If you now refresh the front end app, you should see your stuff for sale:

modifié /api/stuff => /api/all-stuff dans src/app/services/stuff.service.ts

Create a POST route

While we may not be able to store any data sent by the user for now, as we do not yet have a database set up, we can at least make sure that we are receiving data from the front end correctly. The front end app has a "Sell a thing" form, which sends a POST request, containing the thing for sale, to our api/stuff endpoint. Let's see how we can capture that data.

cd backend
npm install --save body-parser

add to app.js :

backend/app.js
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content, Accept, Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
  next();
});

/* receive things as a json Object */
app.use(bodyParser.json());

app.post('/api/all-stuff', (req, res, next) => {
    console.log(req.body);
    res.status(201).json({
	message: 'Thing created successfully!';
    });
});

app.use('/api/all-stuff', (req, res, next) => {
 ...

Receive stuff from the front-end app

While we may not be able to store any data sent by the user for now, as we do not yet have a database set up, we can at least make sure that we are receiving data from the front end correctly.

The front end app has a "Sell a thing" form, which sends a POST request, containing the thing for sale, to our api/stuff endpoint.

Let's see how we can capture that data.

To handle the POST request coming from the front-end app, we'll need to be able to extract the JSON object from the request — we will need the body-parser package.

Install it as a production dependency using npm  :

npm install --save body-parser

Import it in your app.js file:

const bodyParser = require('body-parser');

And set its json function as global middleware for your app, just after setting the response headers:

app.use(bodyParser.json());

Now that body-parser has parsed the request body, we can write the following POST middleware:

app.post('/api/all-stuff', (req, res, next) => {
  console.log('req.body', req.body);
  res.status(201).json({
    message: 'Thing created successfully!'
  });
});

Make sure you place the POST route above the middleware for GET requests, as the GET logic will currently intercept all requests to your /api/all-stuff endpoint — placing the POST route beforehand will intercept POST requests, preventing them from reaching the GET middleware.

Now, if you fill in the form in the front-end app and submit it, you should see the object you just created logged to your Node console!

Set up your database

So far, we have not been able to persist any data, or make our app properly dynamic. All that is about to change, however, as we integrate the database layer for our server: MongoDB.

While it is possible to download and run MongoDB on your own machine (see the MongoDB website for details), we will be using the free tier of MongoDB Atlas — the database-as-a-service — for this course.

Set up MongoDB Atlas

nous ne l'utilisons pas
cd ~/Angular/go-full-stack/backend
backend/models/db_config.js
 module.exports = {
    DB: 'mongodb://localhost:27017/stuffs_db'
 };
backend/app.js
const db_config = require('./models/db_config');

mongoose.Promise = global.Promise;

mongoose.connect(config.DB, { useNewUrlParser: true, useNewUrlParser: true })
    .then(
	() => {console.log('Database is connected')}
    )
    .catch ((error) => {
	console.log('Can not connect to the database');
	console.lerror(error);
    });

Connect your API to your MongoDB cluster

From your MongoDB Atlas, click the Connect button, and choose "Connect your application." You can then select "driver 3.6 or later" and copy the SRV address that is generated for you.

Back in your project, install the Mongoose package by running:

cd backend
npm install --save mongoose

from within the backend folder.

Once it has finished installing, import it into your app.js file by adding the following constant:

backend/app.js
 const mongoose = require('mongoose'); 
 mongoose.connect(config.DB, { useNewUrlParser: true, useNewUrlParser: true })
    .then(
	() => {console.log('Database is connected')}
    )
    .catch ((error) => {
	console.log('Can not connect to the database');
	console.error(error);
    });

After saving this, and restarting your server if necessary, you should see "Successfully connected to MongoDB Atlas" logged to the console.

Your API is now connected to your database cluster, and we can start creating some server routes to take advantage of this.

Create a data schema

One of the advantages of using Mongoose to manage our MongoDB database is that we can implement strict data schemas to make our app more robust.

Let's start by creating a Thing schema for every Thing put up for sale on our app.

Create a Thing schema

Inside your backend folder, create a new folder called models, and within that new folder, a file called thingModel.js  :

backend/thingModel.js
const mongoose = require('mongoose');

const thingSchema = mongoose.Schema({
  title: { type: String, required: true },
  description: { type: String, required: true },
  imageUrl: { type: String, required: true },
  userId: { type: String, required: true },
  price: { type: Number, required: true },
});

module.exports = mongoose.model('Thing', thingSchema);

Here, what we are doing is:

  • creating a data schema which contains the fields we want for each Thing, their type, and whether or not they are a required field
  • exporting that schema as a Mongoose model, making it available for our Express app

This model will not only allow us to enforce our data structure — it also makes read and write operations to the database far simpler, as you will see in the next chapters.

Save and retrieve data

Using the Thing Mongoose model we created in the previous chapter, we are going to leverage Mongoose to make saving and retrieving data to and from the database a walk in the park.

Let's start by properly implementing our POST route.

Saving Things to the database

To be able to use our new Mongoose model in our app, we'll need to import it in the app.js file:

backend/app.js
const Thing = require('./models/thingModel');

Now replace the logic in your POST route with the following:

backend/app.js
app.post('/api/all-stuff', (req, res, next) => {
    const thing = new Thing({ /* _id not needed */
    title: req.body.tit
    description: req.body.description,
    imageUrl: req.body.imageUrl,
    price: req.body.price,
    userId: req.body.userId
  });
  thing.save().then(  /* a promise *
    () => {
      res.status(201).json({
        message: 'Post saved successfully!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
});

Here, create a new instance of your Thing model, passing it a JavaScript object containing all of the information it needs from the parsed request body.

That model has a save() method which simply saves your Thing to the database.

The save() method returns a promise, so in our then() block, we send back a success response, and in our catch() block, we send back an error response with the error thrown by Mongoose.

Retrieving the list of Things for sale

Now we can implement our GET route to return all of the Things in the database:

app.use('/api/all-stuff', (req, res, next) => {
  Thing.find().then( /* returns a promise */
    (things) => {
      res.status(200).json(things);
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
});

Here, we use the find() method on our Mongoose model to return an array containing all of the Things in our database.

Now, if you add a new Thing , it should appear immediately in your Stuff for Sale page.

However, if you click on one of the Things, the single item view does not work, as it is trying to make a different GET call to find an individual thing.

Let's implement that route now.

Retrieving a specific Thing

Let's add another route to our app, just after our POST route:

app.get('/api/all-stuff/:id', (req, res, next) => {
/* :id parametrized variable */
    Thing.findOne({
	_id: req.params.id
    }).then(
	(thing) => {
	    res.status(200).json(thing);
	}
    ).catch(
	(error) => {
	    res.status(404).json({
		error: error
	    });
	}
    );
});

In this route:

  • we use the get() method to only react to GET requests to this endpoint
  • we use a colon in front of the dynamic segment of the route to make it accessible as a parameter
    • :id corresponds to req.params.id
  • we then use the findOne() method on our Thing model to find the single Thing with the same _id as the request parameter (the one clicked on in the list of Things).
  • that Thing is then returned in a promise, and sent to the front end
  • if no Thing is found or an error occurs, we send a 404 error to the front end, along with the thrown error

Our app is really starting to take shape now. In the next chapter, we will implement our Modify and Delete buttons to complete the Thing part of our API.

Complete the CRUD with update and delete

Update an existing Thing

Let's add another route to our app, just below our individual GET route — this time, it will respond to PUT requests:

app.put('/api/all-stuff/:id', (req, res, next) => {

    const thing = new Thing({ /* get fields from req */
	_id: req.params.id, /* to overwrite the _id */
	title: req.body.title,
	description: req.body.description,
	imageUrl: req.body.imageUrl,
	price: req.body.price,
	userId: req.body.userId
    });
    
    Thing.updateOne({_id: req.params.id}, thing).then(
	() => {
	    res.status(201).json({
		message: 'Thing updated successfully!'
	    });
	}
    ).catch(
	(error) => {
	    res.status(400).json({
		error: error
	    });
	}
    );
})

Here, we leverage the updateOne() method on our Thing model, allowing us to update the Thing corresponding to the object we pass as a first argument — here, we use the id parameter passed in the request — and replace it with the Thing passed as a second argument.

Using the new keyword with a Mongoose model creates a new _id field by default. In this case, that would throw an error, as we would be trying to modify an immutable field on a database document. Therefore, we must use the id parameter from the request to set our Thing up with the same _id as before.

Try out our new route by clicking on a Thing in the app, then its Modify button, and submitting a modified Thing !

Deleting a Thing

Time to add one last route — the DELETE route:


app.delete('/api/all-stuff/:id', (req, res, next) => {

    Thing.deleteOne({_id: req.params.id}).then(
	() => {
	    res.status(200).json({
		message: 'Deleted!'
	    });
	}
    ).catch(
	(error) => {
	    res.status(400).json({
		error: error
	    });
	}
    );
});

Our model's deleteOne() method works like findOne() and updateOne() , in that we pass it an object corresponding to the document we want to delete. We then send either a success or a failure response to the front end as appropriate.

Congratulations! Our app now allows the full customer journey through adding, viewing, updating, and deleting Things for sale!

Optimize the back end's structure

Before we get stuck into the complex subject that is authentication, we are going to reorganize the structure of our back end to make it easier to understand and to maintain. While it is technically possible to leave all of our routing and business logic in our app.js file, it can quickly become far too large to maintain easily, so let's make things a bit more modular.

Set up routing

The first thing we are going to do is separate our routing logic.

Create a new folder inside your backend folder called routes , and create a file inside it called stuff.routes.js , which will contain the logic for our stuff routes:

backend/routes/stuff.routes.js
const express = require('express');

const router = express.Router();

module.exports = router;

Here, we are creating an Express router.

Until now, we have been registering our routes directly to our app. What we will now do instead is register them to our Express router, and then register that router to the app.

It's time to cut all of our routes from app.js and paste them inside our router.

Be careful to replace all occurrences of app with router, as we are registering the routes to our router:

backend/app.js
const express = require('express');
const router = express.Router();

const Thing = require('../models/thing');

router.post('/', (req, res, next) => {
  const thing = new Thing({
    title: req.body.title,
    description: req.body.description,
    imageUrl: req.body.imageUrl,
    price: req.body.price,
    userId: req.body.userId
  });
  thing.save().then(
    () => {
      res.status(201).json({
        message: 'Post saved successfully!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
});

router.get('/:id', (req, res, next) => {
  Thing.findOne({
    _id: req.params.id
  }).then(
    (thing) => {
      res.status(200).json(thing);
    }
  ).catch(
    (error) => {
      res.status(404).json({
        error: error
      });
    }
  );
});

router.put('/:id', (req, res, next) => {
  const thing = new Thing({
    _id: req.params.id,
    title: req.body.title,
    description: req.body.description,
    imageUrl: req.body.imageUrl,
    price: req.body.price,
    userId: req.body.userId
  });
  Thing.updateOne({_id: req.params.id}, thing).then(
    () => {
      res.status(201).json({
        message: 'Thing updated successfully!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
});

router.delete('/:id', (req, res, next) => {
  Thing.deleteOne({_id: req.params.id}).then(
    () => {
      res.status(200).json({
        message: 'Deleted!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
});

router.get('/' +
  '', (req, res, next) => {
  Thing.find().then(
    (things) => {
      res.status(200).json(things);
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
});

module.exports = router;

Each route segment should have /api/all-stuff removed from it. If that would empty a route string, be sure to leave a slash / (see code above).

You should also replace the final app.use() with app.get() , as that route only concerns GET requests.

We now need to register our new router in our app.js file. First, we need to import it:

const stuffRoutes = require('./routes/stuff');

We then register it as we would a single route. We want to register our router for all requests to /api/all-stuff , so:

app.use('/api/all-stuff', stuffRoutes);

Now, if you refresh the front end app (stay in the section for Parts 1 + 2 for now), everything should still function as before.

Set up controllers

To make our structure even more modular, and our code easier to read and to maintain, we are going to separate our routes' business logic into controllers.

Create a new controllers folder in your backend folder, and create another stuff.js file — this one will be our stuff controller.

Let's copy our first piece of business logic from our POST route to our controller:

cd ~/Angular/go-full-stack/backend
mkdir controllers
create stuffCtrl.js
backend/controllers/stuffCtrl.js
const Thing = require('../models/thing');

exports.createThing = (req, res, next) => {
    
    const thing = new Thing({
	title: req.body.title,
	description: req.body.description,
	imageUrl: req.body.imageUrl,
	price: req.body.price,
	userId: req.body.userId
    });
    
    thing.save().then(
	() => {
	    res.status(201).json({
		message: 'Post saved successfully!'
      });
	}
    ).catch(
	(error) => {
	    res.status(400).json({
		error: error
	    });
	}
    );
};

Here, we are exposing the logic from our POST route as a function called createThing() . To implement this back in our route, we need to import our controller, and then register createThing :

Dans routes/stuff.js:

const stuffCtrl = require('../controllers/stuff');

router.get('/', stuffCtrl.getAllStuff);

We can now do the same for all of our other routes. Here is the final controller:

const Thing = require('../models/thing');

exports.createThing = (req, res, next) => {
  const thing = new Thing({
    title: req.body.title,
    description: req.body.description,
    imageUrl: req.body.imageUrl,
    price: req.body.price,
    userId: req.body.userId
  });
  thing.save().then(
    () => {
      res.status(201).json({
        message: 'Post saved successfully!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
};

exports.getOneThing = (req, res, next) => {
  Thing.findOne({
    _id: req.params.id
  }).then(
    (thing) => {
      res.status(200).json(thing);
    }
  ).catch(
    (error) => {
      res.status(404).json({
        error: error
      });
    }
  );
};

exports.modifyThing = (req, res, next) => {
  const thing = new Thing({
    _id: req.params.id,
    title: req.body.title,
    description: req.body.description,
    imageUrl: req.body.imageUrl,
    price: req.body.price,
    userId: req.body.userId
  });
  Thing.updateOne({_id: req.params.id}, thing).then(
    () => {
      res.status(201).json({
        message: 'Thing updated successfully!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
};

exports.deleteThing = (req, res, next) => {
  Thing.deleteOne({_id: req.params.id}).then(
    () => {
      res.status(200).json({
        message: 'Deleted!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
};

And our final router:

const express = require('express');
const router = express.Router();

const stuffCtrl = require('../controllers/stuff');

router.get('/', stuffCtrl.getAllStuff);
router.post('/', stuffCtrl.createThing);
router.get('/:id', stuffCtrl.getOneThing);
router.put('/:id', stuffCtrl.modifyThing);
router.delete('/:id', stuffCtrl.deleteThing);

module.exports = router;

As you can see, this makes our router file easier to understand. It is clear which routes are available at which endpoints and the descriptive names given to our controller functions make it easier to understand what each route does.

While not absolutely necessary for every project, structuring code in a modular manner like this is a good habit to get into, as it really does make maintenance far easier.

Now that's all ready, let's start implementing user authentication.

Prepare the database for authentication info

Understand safe password storage

Over the next few chapters, we will be implementing email and password based authentication to our API.

This will mean storing user passwords in our database in some way or another. What we certainly do not want to do is store them as plain text: anyone who gained access to our database would have a full list of everyone's login information. What we will do instead is store each user's password as a hash, or encrypted string.

The encryption package we will be using, bcrypt , uses a one-way algorithm to encrypt and create a hash of our users' passwords, which we will then store in that user's database document.

When a user tries to sign in, we will use bcrypt to create a new hash with the entered password, and then compare it to the hash stored in the database.

These two hashes will not be the same — that would be insecure, as hackers could simply guess passwords until the hashes matched — but bcrypt can tell if both hashes were generated using the same initial password. This will allow us to correctly implement safe and secure password storage and verification.

The first step in implementing authentication will be to create a database model for our user information.

Create a user model

To make sure that two users cannot use the same email address, we will be using the unique keyword. However, the errors thrown by MongoDB by default can be tricky to unpack, so to make our lives easier, we will be installing a validation package to pre-validate information before saving:

cd ~/Angular/go-full-stack/backend/
npm install --save mongoose-unique-validator

With that package installed, we can now build our our user model:

backend/models/userModel.js
const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');

const userSchema = mongoose.Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true }
});

userSchema.plugin(uniqueValidator);

module.exports = mongoose.model('User', userSchema);

The unique value in our schema, along with the mongoose-unique-validator passed as a plugin, will ensure that no two users can share the same email address.

Now that our model is ready, in the next chapter, we are going to start using it to save new users to our database, and to enforce password encryption.

Create new users

Set up authentication routes

Let's start by building the infrastructure we will need for our authentication routes. We will need a controller and a router, and then we will need to register that router with our Express app.

First of all, create a user.js in your controllers folder:

exports.signup = (req, res, next) => {
 
};
 
exports.login = (req, res, next) => {
 
};

We will be implementing these functions very soon — for now, let's finish building the routes.

Create another user.js file, this time in your routes folder:

const express = require('express');
 
const router = express.Router();
 
const userCtrl = require('../controllers/user');
 
router.post('/signup', userCtrl.signup);
router.post('/login', userCtrl.login);
 
module.exports = router

The routes provided are the ones expected by the front-end app.

Remember that the route segment shown here is only the final segment, as the rest of the route address will be declared in our Express app.

Now let's register our router with our app.js First, import the router:

const userRoutes = require('./routes/user');

And then register it:

app.use('/api/all-stuff', stuffRoutes);
app.use('/api/auth', userRoutes);

Our routes are now ready, so it's time to start implementing the business logic.

Create new users

We will be needing the bcrypt encryption package for our signup function, so let's install it to our project:

cd ~/Angular/go-full-stack/backend
npm install --save bcrypt

We can now import it to our controller, and implement our signup function (don't forget to import your user model!):

signup

backend/controllers/userCtrl.js
const bcrypt = require('bcrypt');
const User = require('../models/user');

exports.signup = (req, res, next) => {
  bcrypt.hash(req.body.password, 10).then(
    (hash) => {
      const user = new User({
        email: req.body.email,
        password: hash
      });

      user.save().then(
        () => {
          res.status(201).json({
            message: 'User added successfully!'
          });
        }
      ).catch(
        (error) => {
          res.status(500).json({
            error: error
          });
        }
      );
    }
  );
};

In this function:

  • we call bcrypt's hash function on our password, and ask it to salt the password 10 times (the higher the value here, the longer the function will take, but the more secure the hash — for more information, check out bcrypt's documentation)
  • this is an asynchronous function which returns a promise, where we receive the produced hash
  • in our then block, we create a new user and save it to the database, returning a success response if successful, and any errors with an error code if not

In the next chapter, we will implement our login function to check user credentials to allow them to log in.

Check a user's credentials

Implement the login function

Now that we can create new users in the database, we need a way to check whether a user trying to sign in has valid credentials by implementing our login function:

backend/controllers/userCtrl.js
exports.login = (req, res, next) => {

    User.findOne({ email: req.body.email }).then(
	(user) => {
	    if (!user) {
		return res.status(401).json({
		    error: new Error('User not found!')
		});
	    }
	    bcrypt.compare(req.body.password, user.password).then(
		(valid) => {
		    if (!valid) {
			return res.status(401).json({
			    error: new Error('Incorrect password!')
			});
		    }
		    res.status(200).json({
			userId: user._id,
			token: 'token'
		    });
		}
	    ).catch(
		(error) => {
		    res.status(500).json({
			error: error
		    });
		}
	    );
	}
    ).catch(
	(error) => {
	    res.status(500).json({
		error: error
	    });
	}
    );
};

In this function:

  • we use our Mongoose model to check if the email entered by the user corresponds to an existing user in the database
    • if it does not, we return a 401 Unauthorized error
    • if it does, we move on
  • we use bcrypt's compare function to compare the user entered password with the hash saved in the database
    • if they do not match, we return a 401 Unauthorized error
    • if they match, our user has valid credentials
  • if our user has valid credentials, we return a 200 response containing the user ID and a token, which for now is a generic string

Before using the "Part 3" section of the front end app, delete all stuff for sale from within the "Parts 1+2" section, as you will not be able to modify them from now on otherwise — they were created using a generic user ID, and will, therefore, be untouchable by any users created from now on.

In the next chapter, you will discover token-based authentication — what it's for, how it works, and how we will be applying it in our app to secure our API properly.

Create authentication tokens

To be able to create and verify authentication tokens, we will need a new package:

cd ~/Angular/go-full-stack/backend
npm install --save jsonwebtoken

We will then import it in controllers/user.js:

const jwt = require('jsonwebtoken');

And use it in our login function:

exports.login = (req, res, next) => {
  User.findOne({ email: req.body.email }).then(
    (user) => {
      if (!user) {
        return res.status(401).json({
          error: new Error('User not found!')
        });
      }
      bcrypt.compare(req.body.password, user.password).then(
        (valid) => {
          if (!valid) {
            return res.status(401).json({
              error: new Error('Incorrect password!')
            });
          }
          const token = jwt.sign(
            { userId: user._id },
            'RANDOM_TOKEN_SECRET',
            { expiresIn: '24h' });
          res.status(200).json({
            userId: user._id,
            token: token
          });
        }
      ).catch(
        (error) => {
          res.status(500).json({
            error: error
          });
        }
      );
    }
  ).catch(
    (error) => {
      res.status(500).json({
        error: error
      });
    }
  );
}

Here:

  • we use jsonwebtoken's sign function to encode a new token
  • that token contains the user's ID as a payload
  • we use a temporary development secret string to encode our token (to be replaced with a much longer, random string for production)
  • we set the token's validity time to 24 hours
  • we send the token back to the front end with our response

You can now use the Chrome DevTools Network tab to check that, once logged in, every request coming from the front end contains an "Authorization" header, with the keyword "Bearer" and a long encoded string: this is our token.

In the next and final chapter, we will create a piece of middleware to check for and verify this token and its contents to make sure that only authorized requests get access to the routes we want to protect.

token

Le token

  • Cette solution authentifie notre utilisateur à l’aide d’un token signé qui est envoyé dans toutes les requêtes adressées à notre serveur.
  • Le token est d’abord généré par le backend qui l’envoie lors de l’authentification d’un utilisateur, pour chaque requête, le serveur va comparer le token qu’il a généré avec celui qu’il reçoit généralement à l’aide d’un middleware afin de s’assurer de l’identité de l’utilisateur.

Set up authentication middleware

Implement authentication middleware


We are now going to create the middleware which will protect selected routes, and ensure that a user is authenticated before allowing their requests to go through.

Create a new middleware folder, and an auth.js file inside it:

cd ~Angular/go-full-stack/backend/
mkdir middleware
cd middleware

create auth.js

middleware/auth.js
const jwt = require('jsonwebtoken');

module.exports = (req, res, next) => {
  try {
    const token = req.headers.authorization.split(' ')[1];
    const decodedToken = jwt.verify(token, 'RANDOM_TOKEN_SECRET');
    const userId = decodedToken.userId;
    if (req.body.userId && req.body.userId !== userId) {
      throw 'Invalid user ID';
    } else {
      next();
    }
  } catch {
    res.status(401).json({
      error: new Error('Invalid request!')
    });
  }
};

In this middleware:

  • because many things can go wrong, we are putting everything inside a try...catch bloc
  • we extract the token from the incoming request's Authorization header — remember that it will also contain the Bearer keyword, so we use the split function to get everything after the space in the header — and any errors thrown here will wind up in the catch block
  • we then use the verify function to decode our token — if the token is not valid, this will throw an error
  • we extract the user ID from our token
  • if the request contains a user ID, we compare it to the one extracted from the token — if they are not the same, we throw an error
  • otherwise, all is well, and our user is authenticated — we pass execution along using the next() function

Now we need to apply this middleware to routes/stuff.js , which are the ones we want to protect. In our stuff router:

const express = require('express');
const router = express.Router();

const auth = require('../middleware/auth');

const stuffCtrl = require('../controllers/stuff');

router.get('/', auth, stuffCtrl.getAllStuff);
router.post('/', auth, stuffCtrl.createThing);
router.get('/:id', auth, stuffCtrl.getOneThing);
router.put('/:id', auth, stuffCtrl.modifyThing);
router.delete('/:id', auth, stuffCtrl.deleteThing);


module.exports = router;

We simply import our middleware and pass it as an argument to the routes we wish to protect.

Now, from the front end, you should be able to login and use the app normally. To check that unauthorized requests do not work, you can use an app like Postman to pass a request without an Authorization header — the API will refuse access and send a 401 response.

Congratulations! Your API now implements token-based authentication, and is properly secure.

Accept incoming files with multer

In this final part of the course, we are going to implement file uploads, to allow users to upload images of the things they want to sell.

We will be doing this using multer , a package which allows us to handle incoming files in HTTP requests.

Let's start by installing multer and building a piece of middleware to handle those incoming files.

Set up file handling middleware

npm install --save multer

Now we can create a new piece of middleware in our middleware folder called multer-config.js :

cd backend/middleware

multer-config.js
const multer = require('multer');

const MIME_TYPES = {
  'image/jpg': 'jpg',
  'image/jpeg': 'jpg',
  'image/png': 'png'
};

const storage = multer.diskStorage({

  destination: (req, file, callback) => {
    callback(null, 'images'); /* where to save file */
  },

  filename: (req, file, callback) => {
    const name = file.originalname.split(' ').join('_');
    const extension = MIME_TYPES[file.mimetype];
    callback(null, name + Date.now() + '.' + extension); /* null : error */
  }
});

module.exports = multer({storage: storage}).single('image'); /* single file */

In this middleware:

  • we create a storage constant — to be passed to multer as configuration — which contains the logic necessary for telling multer where to save incoming files
  • the destination function tells multer to save files in the images folder
  • the filename function tells multer to use the original name, replacing any spaces with underscores and adding a Date.now() timestamp, as the file name; it then uses the MIME type map constant to resolve the appropriate file extension
  • we then export the fully configured multer , passing it our storage constant, and telling it that we will be handling uploads of single image files

Before we can apply our middleware to our stuff routes, we will need to modify them slightly, as the structure of the incoming data is not quite the same when there are files and JSON data.


Modify routes to take files into account (multer)

For our middleware to work on our routes, we will need to modify them slightly, as a request containing a file from the front end will have a different format.

Modify the POST route

First, let's add our multer middleware to our POST route in our stuff router:

middleware/stuff.routes.js
const express = require('express');
const router = express.Router();

const auth = require('../middleware/auth');
const multer = require('../middleware/multer-config');

const stuffCtrl = require('../controllers/stuff');

router.get('/', auth, stuffCtrl.getAllStuff);
router.post('/', auth, multer, stuffCtrl.createThing); /* note order */
router.get('/:id', auth, stuffCtrl.getOneThing);
router.put('/:id', auth, stuffCtrl.modifyThing);
router.delete('/:id', auth, stuffCtrl.deleteThing);

module.exports = router;

The order of middleware is important! If we were to place multer before the authentication middleware, even unauthenticated requests with images would have their images saved to the server. Make sure you place multer after

We now need to update our controller to correctly handle the new incoming request:

controllers/stuffCtrl.js
exports.createThing = (req, res, next) => {
  req.body.thing = JSON.parse(req.body.thing);
  const url = req.protocol + '://' + req.get('host');
  const thing = new Thing({
    title: req.body.thing.title,
    description: req.body.thing.description,
    imageUrl: url + '/images/' + req.file.filename,
    price: req.body.thing.price,
    userId: req.body.thing.userId
  });
  thing.save().then(
    () => {
      res.status(201).json({
        message: 'Post saved successfully!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
};

So what are we doing here?

  • to add a file to the request, the front_end needed to send the request data as form-data as opposed to JSON — the request body contains a thing string, which is simply a stringified thing object — we therefore need to parse it using JSON.parse() to get a usable object
  • we also need to resolve the full URL for our image, as req.file.filename only contains the filename segment — we use req.protocol to get the first segment ( 'http' , in this case); we add the '://' , and then use req.get('host') to resolve the server host ('localhost:3000' in this case); we finally add '/images/' and the filename to complete our URL

If you save the controller as is, and try out the app (remember to use the Part 4 section!), you will see that almost everything works. The only issue we get is a 404 error when trying to fetch the image, even though it looks like our URL is correct. So what's happening here?

Well, we are making a GET request to http://localhost:3000/images/<image-name>.jpg. It seems simple, but remember that our Express app is running on localhost:3000 and we haven't told it how to react to requests going to that endpoint, so it is returning a 404 error. We therefore need to tell our app.js how to handle those requests, serving up our static images folder.

We will need a new import in app.js :

Modify the PUT route

Modifying our PUT route is slightly more complicated, as we have to take into account both possibilities:

  • the user has updated the image,
  • or they have not.
  • In the first case, we will be receiving form-data and file;
  • in the second case, we will just be receiving JSON data.

First things first though, let's add multer as middleware to our PUT route:

routes/stuff.routes.js
const express = require('express');
const router = express.Router();

const auth = require('../middleware/auth');
const multer = require('../middleware/multer-config');

const stuffCtrl = require('../controllers/stuff');

router.get('/', auth, stuffCtrl.getAllStuff);
router.post('/', auth, multer, stuffCtrl.createThing);   /* post route modified */
router.get('/:id', auth, stuffCtrl.getOneThing);
router.put('/:id', auth, multer, stuffCtrl.modifyThing); /* put route modified */
router.delete('/:id', auth, stuffCtrl.deleteThing);

module.exports = router;

Now we need to modify our modifyThing() function to check whether or not we have received a new file, and to react accordingly:

controllers/stuffCtrl.js
exports.modifyThing = (req, res, next) => {
  let thing = new Thing({ _id: req.params._id });
  if (req.file) {
    const url = req.protocol + '://' + req.get('host');
    req.body.thing = JSON.parse(req.body.thing);
    thing = {
      _id: req.params.id,
      title: req.body.thing.title,
      description: req.body.thing.description,
      imageUrl: url + '/images/' + req.file.filename,
      price: req.body.thing.price,
      userId: req.body.thing.userId
    };
  } else {
    thing = {
      _id: req.params.id,
      title: req.body.title,
      description: req.body.description,
      imageUrl: req.body.imageUrl,
      price: req.body.price,
      userId: req.body.userId
    };
  }
  Thing.updateOne({_id: req.params.id}, thing).then(
    () => {
      res.status(201).json({
        message: 'Thing updated successfully!'
      });
    }
  ).catch(
    (error) => {
      res.status(400).json({
        error: error
      });
    }
  );
};

In this modified version of the function:

  • we first create a new instance of our Thing model with the received _id so as not to cause problems when trying to update that Thing in the database
  • if we receive a new file with the request (via multer ), we handle the form-data and generate the image URL
  • if we do not receive a new file, we simply capture the request body JSON
  • we save the updated Thing to the database

Congratulations! Our app now correctly handles file uploads both when putting new things up for sale, and when modifying existing ones.

Expand the back end's delete function

Modify the DELETE route

As a final touch to the file handling in our back end, let's make sure that whenever a Thing is deleted from the database, the corresponding image file also gets deleted.

In our stuff controller, we need a new import — the Node fs package:

const fs = require('fs');fs

stands for file system, and gives us access to functions which allow us to modify the file system, including functions for deleting files.

Now we can modify our deleteThing() function:

controllers/stuffCtrl.js
exports.deleteThing = (req, res, next) => {
  Thing.findOne({_id: req.params.id}).then(
    (thing) => {
      const filename = thing.imageUrl.split('/images/')[1];
      fs.unlink('images/' + filename, () => {
        Thing.deleteOne({_id: req.params.id}).then(
          () => {
            res.status(200).json({
              message: 'Deleted!'
            });
          }
        ).catch(
          (error) => {
            res.status(400).json({
              error: error
            });
          }
        );
      });
    }
  );
};

In this function:

  • we use the ID we receive as a parameter to access the corresponding Thing in the database;
  • we use the fact that we know there is an /images/ segment in our image URL to separate out the file name;
  • we then use the fs package's unlink function to delete that file, passing it the file to be deleted and the callback to be executed once that file has been deleted;
  • in the callback, we implement the original logic, deleting the Thing from the database.

Our API can now successfully handle all CRUD operations containing files: when a user creates a new Thing , updates an existing Thing , or deletes a Thing !

Let's recap!

You're nearly finished. Well done!

Let's have a look at what you've learned:

Are you ready to handle user files?