Categories
Express JavaScript Nodejs

Basic Routing with Express

Routing is the most important part of a back end application. Express allows us to route URLs to our route handler code easily.

In this article, we’ll look at how to create basic routes with Express.

Basic Routing

Routing is where an Express application response to a client request from a URL or path and a specific HTTP request method, like GET or POST.

Each route in Express can have one more handler function, which are executed when the route is matched.

The general definition of a route takes the following format:

app.METHOD(PATH, HANDLER);

app is the Express app instance.

The METHOD is an HTTP request method in lowercase. Possible methods include GET, POST, PUT, and DELETE.

PATH is the path for the route. HANDLER is the handler function that’s run when the route is matched.

For example, we can write:

app.get('/', (req, res) => {  
  res.send('Hello World!')  
})

To display 'Hello World!' on the screen.

If we want our app to accept a POST request, we can use app.post as follows:

app.post('/', (req, res) => {  
  res.send('Received POST request');  
})

We can test this with an HTTP client like Postman by send a POST request to the URL that our app is running on. Then we should get:

Received POST request

In the response body.

Likewise, we can do the same for PUT and DELETE requests as follows:

app.put('/', (req, res) => {  
  res.send('Got a PUT request at /user')  
})

app.delete('/', (req, res) => {  
  res.send('Got a DELETE request')  
})

Notice that in each route handler, we have a req and res parameter. The req has the request object which has the URL, headers and other fields.

The res object lets us render a response back to the client-side.

Request Object

The req parameter we have in the route handlers above is the req object.

It has some properties that we can use to get data about the request that’s made from the client-side. The more important ones are listed below.

req.baseUrl

The req.baseUrl property holds the base URL of the router instance that’s mounted.

For example, if we have:

const express = require('express');
const app = express();  
const greet = express.Router();  
greet.get('/', (req, res) => {  
  console.log(req.baseUrl);  
  res.send('Hello World');  
})

app.use('/greet', greet);

app.listen(3000, () => console.log('server started'));

Then we get /greet from the console.log .

req.body

req.body has the request body. We can parse JSON bodies with express.json() and URL encoded requests with express.urlencoded() .

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))
app.post('/', (req, res) => {  
  res.json(req.body)  
})

app.listen(3000, () => console.log('server started'));

Then when we make a POST request with a JSON body, then we get back the same that we sent in the request.

req.cookies

We can get cookies that are sent by the request with the req.cookies property.

req.hostname

We can get the hostname from the HTTP header with req.hostname .

When the trust proxy setting doesn’t evaluate to false , then Express will get the value from the X-Forwarded-Host header field. The header can be set by the client or by the proxy.

If there’s more than one X-Forwarded-Host header, then the first one will be used.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.json(req.hostname)  
})

app.listen(3000, () => console.log('server started'));

Then we get the domain name that the app is hosted in if there’re no X-Forwarded-Host headers and trust proxy doesn’t evaluate to false .

req.ip

We can get the IP address that the request is made from with this property.

req.method

The method property has the request method of the request, like GET, POST, PUT or DELETE.

req.params

params property has the request parameters from the URL.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/:name/:age', (req, res) => {  
  res.json(req.params)  
})

app.listen(3000, () => console.log('server started'));

Then when we pass in /john/1 as the parameter part of the URL, then we get:

{  
    "name": "john",  
    "age": "1"  
}

as the response from the route above.

req.query

The query property gets us the query string from the request URL parsed into an object.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.json(req.query)  
})

app.listen(3000, () => console.log('server started'));

Then when we append ?name=john&age=1 to the end of the hostname, then we get back:

{  
    "name": "john",  
    "age": "1"  
}

from the response.

Response Object

The response object has some useful methods to let us return various kinds of responses.

res.append

The append method lets us attach response headers to our responses.

For example, if we have the following code:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.append('Link', 'http://localhost/', 'http://localhost:3000'])  
  res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly')  
  res.append('Warning', 'Alert')  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

Then when we go to Postman, we should see the same data returned in the Headers tab of the response when we look at the data.

Note that we have to run res.send() to actually send the response.

res.attachment

res.attachment let us add a file to the response. It doesn’t send the response.

For example, we can use it as follows:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))
app.get('/', (req, res) => {  
  res.attachment('../public/foo.txt');  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

Then if we have a foo.txt in the public folder, then the file will be downloaded if we make the request to the route.

Note that again we have res.send() to actually send the response.

res.cookie

res.cookie lets us add a cookie to the response.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.cookie('name', 'foo', { domain: 'repl.it', path: '/', secure: true })  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

Then we send a cookie with name foo to the client. We can check in Postman under the Cookies link in the top right corner.

res.download

res.download sends a file response to the server.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))
app.get('/', (req, res) => {  
  res.download('./public/foo.txt');  
})
app.listen(3000, () => console.log('server started'));

Then when a request is made to this route, then we’ll get a file downloaded.

res.json

res.json lets us send a JSON response to the client. The parameter can be any JSON type, including object, array, string, Boolean, number, or null.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.json({ message: 'hi' });  
})

app.listen(3000, () => console.log('server started'));

Then we get:

{"message":"hi"}

as the response.

res.redirect

We can use this to redirect to another URL with the string passed in. For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))
app.get('/', (req, res) => {  
  res.redirect('http://medium.com'));  
})

app.listen(3000, () => console.log('server started'));

Then we’ll see the content of http://medium.com when we make the request to the route above.

res.status

res.status lets us send a status code response. We can use it with the end , send , or sendFile methods by calling them after calling status .

For example, if we have:

const express = require('express')  
const app = express()app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.status(403).end();  
})
app.listen(3000, () => console.log('server started'));

Then we get a 403 response.

Conclusion

Adding routes is simple with Express. We just have to tell it the URL and method to listen for, and the route handler to handle requests that match them.

We can get query strings and URL parameters with the Request object.

Then we can send a status, text, or file according to our preference with the Response object.

Categories
JavaScript Nodejs

Node.js’ fs Module — Getting File Information

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, being able to manipulate files is a basic feature.

Fortunately, Node.js has an fs module built into its library. It has many functions that can help with manipulating files and folders. File and directory operations that are supported include basic ones like manipulating and opening files in directories.

Likewise, it can do the same for files. It can do this both synchronously and asynchronously. It has an asynchronous API that has functions that support promises. Also, it can show statistics for a file.

Almost all the file operations that we can think of can be done with the built-in fs module.

In this article, we will use the functions in the fs module to get data about a file located in the fs.Stats object with the fs.stat(), fs.lstat(), and fs.fstat() functions.

To get information about a file, we can use the fs.Stats object, which is returned by the fs.stat(), fs.lstat(), and fs.fstat() functions, and their synchronous counterparts.

They can display numeric data as bigint as it’s passed in as an option key with its value set to true. It has nanosecond-precision properties suffixed with Ns.

The stat function takes a path object which can be a string, Buffer, or a URL object as the first argument.

A second argument is an object that can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

The stat function runs asynchronously. Its synchronous counterpart is the statSync function, which takes the same first two arguments without the callback function. statSync returns the file information as an object.

lstat is similar to stat, but it doesn’t follow the symbolic link. It takes a path object which can be a string, Buffer, or a URL object as the first argument.

A second argument is an object which can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

When the path that’s passed in is a symbolic link, then it gives the information about the symbolic link. lstat runs asynchronously, so that the data is retrieved in an indeterminate amount of time.

Its synchronous counterpart, the lstatSync function, takes the same arguments as the lstat function without the callback function and returns the Stat object which has the file information.

The fstat function is similar to the stat function. It takes a path object which can be a string, Buffer, or an URL object as the first argument.

The second argument is an object which can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

The only difference between stat and fstat is that it takes a file descriptor instead of a path object.

We can get the file descriptor from the callback that’s accepted by the fs.open function and its promise and synchronous counterparts, fsPromises.open, and fs.opensync.

To use the fs.stat function, we can use it like in the following code:

const fs = require("fs");
fs.stat("./files/file.txt", (err, stat) => {  
  if (err) throw err;  
  console.log(stat);  
});

Then, if we run the code above, we get something like the following output:

Stats {  
  dev: 3605029386,  
  mode: 33206,  
  nlink: 1,  
  uid: 0,  
  gid: 0,  
  rdev: 0,  
  blksize: 4096,  
  ino: 22799473115106240,  
  size: 0,  
  blocks: 0,  
  atimeMs: 1572569358035.625,  
  mtimeMs: 1572569358035.625,  
  ctimeMs: 1572569358035.625,  
  birthtimeMs: 1572569358035.625,  
  atime: 2019-11-01T00:49:18.036Z,  
  mtime: 2019-11-01T00:49:18.036Z,  
  ctime: 2019-11-01T00:49:18.036Z,  
  birthtime: 2019-11-01T00:49:18.036Z  
}

As we can see, the Stats object has many properties. The data properties are listed above. It also has a few function properties.

The data properties in the Stats object means the following:

  • dev — The numeric identifier of the device storing the given file. It can be a number or a bigInt.
  • ino — The “inode” number of the file. It’s a number that contains basic information about a file, directory, or other file system object. It can be a number or a bigInt.
  • mode — Bit-field description of the file type and mode. It can be a number or a bigInt.
  • nlink — Number of hard links that exist for the file. It can be a number or a bigInt.
  • uid — The numeric user identifier of the user that owns the file. Applicable to POSIX systems only. It can be a number or a bigInt.
  • gid — The numeric group identifier of the user that owns the file. Applicable to POSIX systems only. It can be a number or a bigInt.
  • rdev — Numeric device identifier of the file if it’s a special file. A file is special if it’s used for I/O. For example, page files and hibernation files are considered special files. It can be a number or a bigInt.
  • size — The size of the file in bytes. It can be a number or a bigInt.
  • blksize — The block size for a file system I/O. It can be a number or a bigInt.
  • blocks — The number of blocks allocated to the file. It can be a number or a bigInt.
  • atimeNs — The timestamp indicating when the file was last accessed in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • mtimeNs — The timestamp indicating when the file was last modified in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • ctimeNs — The timestamp indicating when the file was last changed in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • birthtimeNs — The timestamp indicating when the file was created in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • atime — The timestamp indicating when the file was last accessed in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • mtime — The timestamp indicating when the file was last modified in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • ctime — The timestamp indicating when the file was last changed in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • birthtime — The timestamp indicating when the file was created in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.

The Stats object also has the following function properties to check for the basic information about a file:

  • isBlockDevice() — This is a function with a boolean return value that returns true if the file is a block device. A block device refers to a file that represents the device that stores files in blocks and also retrieves them as such.
  • isCharacterDevice() — This is a function with a boolean return value that returns true if the file is a character device. A character device refers to a file that represents the device that provided unbuffered, direct access to the hardware device. They don’t have to allow programs to read or write a single character at a time.
  • isDirectory() — This is a function with a boolean return value that returns true if the item is a directory.
  • isFIFO() — This is a function with a boolean return value that returns true if the item is a first-in-first-out pipe. FIFO pipe means that the first bits of a file going into the device will be the same ones that come out when retrieved. It only allows for unidirectional communication.
  • isFile() — This is a function with a boolean return value that returns true if the item is a file.
  • isSocket() — This is a function with a boolean return value that returns true if the item is a socket. A socket is a special file that enables communication between two processes. It can send data and file descriptors across a domain socket. It can do bidirectional communication.
  • isSymbolicLink() — This is a function with a boolean return value that returns true if the item is a symbolic link. A symbolic link is a reference to another file or directory in the form of an absolute or relative path.

To use the synchronous version of the stat function, the statSync function, we can write something like the following code:

const fs = require("fs");
const stat = fs.statSync("./files/file.txt");  
console.log(stat);

The Stat object is returned directly from the statSync function.

To use the fstat function, we have to get the file descriptor first, which we can do with the open function and its variants. For example, if we want to use the open function to get the file descriptor, we can write the following:

const fs = require("fs");
fs.open("./files/file.txt", "r", (err, fd) => {  
  if (err) throw err;  
  fs.fstat(fd, (err, stat) => {  
    if (err) throw err;  
    console.log(stat);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

We can use fstat with the promise version of the open function like in the following code:

const fsPromises = require("fs").promises;  
const fs = require("fs");
(async () => {  
  const fdObj = await fsPromises.open("./files/file.txt", "r");  
  fs.fstat(fdObj.fd, (err, stat) => {  
    if (err) throw err;  
    console.log(stat);  
    fs.close(fdObj.fd, err => {  
      if (err) throw err;  
    });  
  });  
})();

The promise version of the open function returns a promise that resolves to an object with the file descriptor inside it. We can use the fd property to get the file descriptor and pass it into the fstat function.

Likewise, with the lstat function, we can call it as in the following code:

const fs = require("fs");
fs.lstat("./files/file.txt", (err, stat) => {  
  if (err) throw err;  
  console.log(stat);  
});

The lstat does almost everything as stat does, except that it gets the data of a symbolic link instead of following it, so we will get output similar to the ones above.

The fs.stat(), fs.lstat(), and fs.fstat() functions are very useful for getting data about files, directories, and symbolic links.

They can get us the Stat object which has lots of information that we can use, including access time, modified time, whether the file is a special file, the device it’s stored on, and many other pieces of information through its value and function properties.

Data can be displayed as numbers or bigInt if they’re numerical and timestamps are available in both milliseconds and nanoseconds for extra precision.

Categories
JavaScript Nodejs

Node.js’s ‘fs’ Module: Writing Files and Directories

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, the ability to manipulate files is a basic feature.

Fortunately, Node.js has a fs module built into its library. It has many functions that can help with manipulating files and folders. It supports basic file and directory operations, like manipulating and opening files in directories.

It can do the same for files. It can do this both synchronously and asynchronously. It has an asynchronous API with functions that support promises. Also, it can show statistics for a file.

Almost all the file operations that we can think of can be done with the built-in fs module. In this piece, we’ll use the functions in the fs module to write files with the write family of functions to write files.


fs.Write

There are two versions of the write functions, one for writing text to disk and another for writing binary data to disk.

The text version of write function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor — a number that identifies the file.

The second argument is a string that’s written to the file. If the value passed in is not a string, it is converted to one.

The third argument is the position in which the file writing starts. If the value passed in isn’t a number, then it starts in the current position.

The fourth argument is a string that has the character encoding of the file to be written, which defaults to be utf8. The last argument is a callback function with three parameters.

The first is the err object which has the error object and it’s not null if there’s an error.

The second parameter is the written parameter, an integer which specifies how many bytes are written to the file system. It’s not necessarily the same as the number of string characters written.

The third parameter is the string parameter that has the string that was written.

On Linux, positional writes don’t work in the append model. On Windows, if the file descriptor is 1, which stands for the standard output, then strings that have non-ASCII characters won’t be rendered properly by default.

To use the write function, we can use the open function to get the file descriptor of the file you want to write to first, then we can write to the file by passing in the file descriptor to the write function. For example, if we want to write to the file with the path ./files/file.txt , we can write something like this:

const fs = require("fs");
fs.open("./files/file.txt", "r+", (err, fd) => {  
  if (err) throw err;  
  fs.write(fd, "abc", 0, "utf8", (err, written, string) => {  
    console.log(err, written, string);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

When we run the code above, we should get output that looks something like this:

null 3 abc

In the code above, we first open the file with the open function. We pass in the r+ flag so that we can write to the file. Then we get the file descriptor fd in the callback function that we passed into the open function. With the fd file descriptor, we can pass it into the write function.

In the second argument of the write function we specified that we want to write the string abc to the file. In the third argument, we specified that we want to write it at position 0, the fourth argument specifies that the character encoding of the string should be UTF-8.

The callback in the last argument would get us the result of the write. From there, we know from the output that three bytes and the string ‘abc’ were written to the file.

Other than the r+ flag, there are many other possible system flags, including:

  • 'a' — Opens a file for appending, which means adding data to the existing file. The file is created if it does not exist.
  • 'ax' — Like 'a' but an exception is thrown if the path exists.
  • 'a+' — Open file for reading and appending. The file is created if it doesn’t exist.
  • 'ax+' — Like 'a+' but an exception is thrown if the path exists.
  • 'as' — Opens a file for appending in synchronous mode. The file is created if it does not exist.
  • 'as+' — Opens a file for reading and appending in synchronous mode. The file is created if it does not exist.
  • 'r' — Opens a file for reading. An exception is thrown if the file doesn’t exist.
  • 'r+' — Opens a file for reading and writing. An exception is thrown if the file doesn’t exist.
  • 'rs+' — Opens a file for reading and writing in synchronous mode.
  • 'w' — Opens a file for writing. The file is created (if it does not exist) or overwritten (if it exists).
  • 'wx' — Like 'w' but fails if the path exists.
  • 'w+' — Opens a file for reading and writing. The file is created (if it does not exist) or overwritten (if it exists).
  • 'wx+' — Like 'w+' but an exception is thrown if the path exists.

The binary version of the write function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor which is a number that identifies the file.

The second argument is the buffer object which can be of type Butter, TypedArray or DataView.

The third argument is the offset , which determines the part of the buffer to be written.

The fourth argument is the length argument that specifies the number of bytes being written, the last argument is the position which is an integer which describes the position in which the write function will start writing.

The final argument is a callback function — a function that takes the err parameter, which has the error object. If an error occurs, the second is the bytesWritten parameter which gets us the number of bytes written to disk, the third is the buffer object, which has the binary data which was written to disk.

For example, we can use it as in the following code:

const fs = require("fs");
fs.open("./files/binaryFile", "w", (err, fd) => {  
  if (err) throw err;  
  fs.write(fd, new Int8Array(8), 0, 8, 0, (err, bytesWritten, buffer) => {  
    console.log(err, bytesWritten, buffer);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

We get the following output if we run it:

null 8 Int8Array [  
  0, 0, 0, 0,  
  0, 0, 0, 0  
]

fs.writeSync

The synchronous version of the write function is the writeSync function. There’s one version for writing binary data and one for writing text data.

The text version of thewriteSync function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor which is a number that identifies the file.

The second argument is a string that will be written to the file. If the value passed in is not a string, it will be converted to one.

The third argument is the position in which the file writing starts. If the value passed in isn’t a number, then it starts in the current position.

The fourth argument is a string that has the character encoding of the file be written, which defaults to be utf8. The number of bytes written is returned.

We can use the text version of writeSync function as in the following code:

const fs = require("fs");
const fd = fs.openSync("./files/file.txt", "r+");  
const numBytesWritten = fs.writeSync(fd, "abc", 0, "utf8");  
console.log(numBytesWritten);

We should get 3 in the console.log statement since 3 bytes were written.

The binary data version of the writeSync function takes five arguments. It takes a file descriptor as the first argument. The second argument is the buffer object, which can be a Buffer, TypedArray or DataView object.

The third argument is the offset, which is the integer which specifies the part of the buffer to which the writeSync function will start writing.

The fourth argument is the length argument, which specifies the number of bytes being written.

The last argument is the position, which is an integer that describes the position in which the writeSync function will start writing. The number of bytes written is returned.

We can use the binary version of the writeSync function as in the following code:

const fs = require("fs");
const fd = fs.openSync("./files/binaryFile", "w");  
const numBytesWritten = fs.writeSync(fd, new Int8Array(8), 0, 8, 0);  
console.log(numBytesWritten);

We should get 8 in the console.log statement since 8 bytes were written.


fs.writeFile

We can write files in a less complicated way with the fs.writeFile function. It takes four arguments.

The first argument is the reference to the file, which can be a string path, a Buffer object, a URL object or a file descriptor, which is an integer that identifies the file.

The second argument is the data to be written to the file, which can be a string, a Buffer object, a TypedArray, or a DataView object. The third arguments is for passing in an object with a few options.

There are three things that can be set: the encoding, the mode, and the flag:

  • The encoding specifies the character encoding of the text being written which defaults to utf8 .
  • The mode is an integer which defaults to 0o666. The mode sets the file permission and the sticky bits but only if the file was already created. 0o666 means both readable and writable.
  • The flag option specifies the read or writes options of the file being written. The ones listed above are all valid for this argument. The last argument is the callback function, which is a function that takes an err object which is not null if an error exists.

For example, if we want to write to the file with the path ./files/file.txt with the writeFile function, we can write something like this:

const fs = require("fs");
fs.writeFile(  
  "./files/file.txt",  
  "abc",  
  { encoding: "utf8", mode: 666, flag: "w" },  
  err => console.log(err)  
);

In the first argument, we pass in the string path of the file. Then, in the second argument, we passed in the content to be written. The third argument has the encoding options, file permission of the file being written with the mode option, and the flag to set the mode that the file is open with. If the file is written successfully. The console.log statement above should output null and that you can see the content of the file you wrote onto the disk.

When a file descriptor is passed into the first argument of the writeFile function, the behavior is slightly different from passing in other objects to reference the file. The file is not replaced if a file descriptor is passed into the first argument.

This means that if the writeFile function is called multiple times with the file descriptor, then the item that’s written to the file will be appended to the file instead of overwriting the existing content. So if the first writeFile call passes in the file descriptor 1 and content a and the second writeFile call passes in the file descriptor 1 and content b, then we get ab in the file with file descriptor 1.


fs.writeFileSync

There’s a synchronous version of writeFile called writeFileSync, which takes the same arguments as the writeFile function except without the callback. It returns undefined . We can use it as in the following function:

const fs = require("fs");
fs.writeFileSync("./files/file.txt", "abc", {  
  encoding: "utf8",  
  mode: 666,  
  flag: "w"  
});

After running the code above, you should see the content written to disk in the file with the given path.

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, being able to manipulate files is a basic feature. Fortunately, Node.js has a fs module built into its library.

It has many functions that can help with manipulating files and folders. In this piece, we used the functions in the fs module to write files with the write family of functions to write files. We used the open and write functions to first open the file to get the file descriptor, then to write the content to the file. It has separate versions to work with text and binary files.

There’s also a synchronous version of the function called writeSync which takes the same arguments without the callbacks. It also has a text and binary version. To make writing to files more convenient, we can use the writeFile and the writeFileSync function, which can write both text and binary files given reference to a file like a path string or a URL object. writeFile is asynchronous and writeFileSync is synchronous.

Categories
JavaScript Nodejs React

How To Build A Chat App With React, Socket.io, And Express

WebSockets is a great technology for adding real time communication to your apps. It works by allowing apps to send events to another app, passing data along with it. This means that users can see new data on their screen without manually retrieving new data, allowing better interactivity and making the user experience easier for the user. HTTP also has a lot of overhead with sending data that not all apps need like headers, this increases the latency of the communication between apps.

Socket.io is a library that uses both WebSockets and HTTP requests to allow apps to send and receive data between each other. Sending data between apps is almost instant. It works by allow apps to emit events to other apps and the apps receiving the events can handle them the way they like. It also provides namespacing and chat rooms to segregate traffic.

One the best uses of WebSockets and Socket.io is a chat app. Chat apps requires real time communication since messages are sent and received all the time. If we use HTTP requests, we would have to make lots of requests repeatedly to do something similar. It will be very slow and taxing on computing and networking resources if we send requests all the time to get new messages.

In this article, we will build a chat app that allows you to join multiple chat rooms and send messages with different chat handles. Chat handle is the username you use for joining the chat. We will use React for front end, and Express for back end. Socket.io client will be used on the front end and Socket.io server will be used on the back end.

To start we make an empty folder for our project and then inside the folder we make a folder called backend for our back end project. Then we go into the backend folder and run the Express Generator to generate the initial code for the back end app. To do this, run npx express-generator . Then in the same folder, run npm install to install the packages. We will need to add more packages to our back end app. We need Babel to use the latest JavaScript features, including the import syntax for importing modules, which is not yet supported by the latest versions of Node.js. We also need the CORS package to allow front end to communicate with back end. Sequelize is needed for manipulate our database, which we will use for storing chat room and chat message data. Sequelize is a popular ORM for Node.js. We also need the dotenv package to let us retrieve our database credentials from environment variables. Postgres will be our database system of choice to store the data.

We run npm i @babel/cli @babel/core @babel/node @babel/preset-env cors dotenv pg pg-hstore sequelize sequelize-cli socket.io to install the packages. After installing the packages, we will run npx sequelize-cli init in the same folder to add the code needed to use Sequelize for creating models and migrations.

Now we need to configure Babel so that we can run our app with the latest JavaScript syntax. First, create a file called .babelrc in the backend folder and add:

{  
    "presets": [  
        "@babel/preset-env"  
    ]  
}

Next we replace the scripts section of package.json with:

"scripts": {  
    "start": "nodemon --exec npm run babel-node --  ./bin/www",  
    "babel-node": "babel-node"  
},

Note that we also have to install nodemon by running npm i -g nodemon so that the app will restart whenever file changes, making it easier for us to develop the app. Now if we run npm start , we should be able to run with the latest JavaScript features in our app.

Next we have to change config.json created by running npx sequelize init . Rename config.json to config.js and replace the existing code with:

require("dotenv").config();  
const dbHost = process.env.DB_HOST;  
const dbName = process.env.DB_NAME;  
const dbUsername = process.env.DB_USERNAME;  
const dbPassword = process.env.DB_PASSWORD;  
const dbPort = process.env.DB_PORT || 5432;

module.exports = {  
  development: {  
    username: dbUsername,  
    password: dbPassword,  
    database: dbName,  
    host: dbHost,  
    port: dbPort,  
    dialect: "postgres",  
  },  
  test: {  
    username: dbUsername,  
    password: dbPassword,  
    database: "chat_app_test",  
    host: dbHost,  
    port: dbPort,  
    dialect: "postgres",  
  },  
  production: {  
    use_env_variable: "DATABASE_URL",  
    username: dbUsername,  
    password: dbPassword,  
    database: dbName,  
    host: dbHost,  
    port: dbPort,  
    dialect: "postgres",  
  },  
};

This is allow us to read the database credentials from our .env located in the backend folder, which should look something like this:

DB_HOST='localhost'  
DB_NAME='chat_app_development'  
DB_USERNAME='postgres'  
DB_PASSWORD='postgres'

Now that we have our database connection configured, we can make some models and migrations. Run npx sequelize model:generate --name ChatRoom --attributes name:string to create the ChatRooms table with the name column and the ChatRoom model in our code along with the associated migration. Next we make the migration and model for storing the messages. Run npx sequelize model:generate --name ChatRoomMessages --attributes author:string,message:text,chatRoomId:integer . Note that in both commands, we use singular word for the model name. There should also be no spaces after the comma in the column definitions.

Next we add a unique constraint to the name column of the ChatRooms table. Create a new migration by running npx sequelize-cli migration:create add-unique-constraint-for-chatroom-name to make an empty migration. Then in there, put:

"use strict";

module.exports = {  
  up: (queryInterface, Sequelize) => {  
    return queryInterface.addConstraint("ChatRooms", ["name"], {  
      type: "unique",  
      name: "unique_name",  
    });  
  }, 

  down: (queryInterface, Sequelize) => {  
    return queryInterface.removeConstraint("ChatRooms", "unique_name");  
  },  
};

After all that is done, we run npx sequelize-cli db:migrate to run the migrations.

Next in bin/www , we add the code for sending and receiving events with Socket.io. Replace the existing code with:

#!/usr/bin/env node
/**
 * Module dependencies.
 */
const app = require("../app");
const debug = require("debug")("backend:server");
const http = require("http");
const models = require("../models");
/**
 * Get port from environment and store in Express.
 */
const port = normalizePort(process.env.PORT || "3000");
app.set("port", port);
/**
 * Create HTTP server.
 */
const server = http.createServer(app);
const io = require("socket.io")(server);
io.on("connection", socket => {
  socket.on("join", async room => {
    socket.join(room);
    io.emit("roomJoined", room);
  });
  socket.on("message", async data => {
    const { chatRoomName, author, message } = data;
    const chatRoom = await models.ChatRoom.findAll({
      where: { name: chatRoomName },
    });
    const chatRoomId = chatRoom[0].id;
    const chatMessage = await models.ChatMessage.create({
      chatRoomId,
      author,
      message: message,
    });
    io.emit("newMessage", chatMessage);
  });
});
/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port);
server.on("error", onError);
server.on("listening", onListening);
/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort(val) {
  const port = parseInt(val, 10);
  if (isNaN(port)) {
    // named pipe
    return val;
  }
  if (port >= 0) {
    // port number
    return port;
  }
  return false;
}
/**
 * Event listener for HTTP server "error" event.
 */
function onError(error) {
  if (error.syscall !== "listen") {
    throw error;
  }
  const bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
// handle specific listen errors with friendly messages
  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;
  }
}
/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
  const addr = server.address();
  const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
  debug("Listening on " + bind);
}

so that the app will listen to connect from clients, and let the join rooms when the join event is received. We process messages received with the message event in this block of code:

socket.on("message", async data => {  
    const { chatRoomName, author, message } = data;  
    const chatRoom = await models.ChatRoom.findAll({  
      where: { name: chatRoomName },  
    });  
    const chatRoomId = chatRoom\[0\].id;  
    const chatMessage = await models.ChatMessage.create({  
      chatRoomId,  
      author,  
      message: message,  
    });  
    io.emit("newMessage", chatMessage);  
  });

and emit a newMessage event once the message sent with the message event is saved by getting the chat room ID and saving everything to the ChatMessages table.

In our models, we have to create a has many relationship between the ChatRooms and ChatMessages table by changing our model code. In chatmessage.js , we put:

'use strict';
module.exports = (sequelize, DataTypes) => {
  const ChatMessage = sequelize.define('ChatMessage', {
    chatRoomId: DataTypes.INTEGER,
    author: DataTypes.STRING,
    message: DataTypes.TEXT
  }, {});
  ChatMessage.associate = function(models) {
    // associations can be defined here
    ChatMessage.belongsTo(models.ChatRoom, {
      foreignKey: 'chatRoomId',
      targetKey: 'id'
    });
  };
  return ChatMessage;
};

to make the ChatMessages table belong to the ChatRooms table.

In ChatRoom.js , we put:

"use strict";  
module.exports = (sequelize, DataTypes) => {  
  const ChatRoom = sequelize.define(  
    "ChatRoom",  
    {  
      name: DataTypes.STRING,  
    },  
    {}  
  );  
  ChatRoom.associate = function(models) {  
    // associations can be defined here  
    ChatRoom.hasMany(models.ChatMessage, {  
      foreignKey: "chatRoomId",  
      sourceKey: "id",  
    });  
  };  
  return ChatRoom;  
};

so that we make each ChatRoom have many ChatMessages .

Next we need to add some routes to our back end for getting and setting chat rooms, and getting messages messages. Create a new file called chatRoom.js in the routes folder and add:

const express = require("express");
const models = require("../models");
const router = express.Router();
/* GET users listing. */
router.get("/chatrooms", async (req, res, next) => {
  const chatRooms = await models.ChatRoom.findAll();
  res.send(chatRooms);
});
router.post("/chatroom", async (req, res, next) => {
  const room = req.body.room;
  const chatRooms = await models.ChatRoom.findAll({
    where: { name: room },
  });
  const chatRoom = chatRooms[0];
  if (!chatRoom) {
    await models.ChatRoom.create({ name: room });
  }
  res.send(chatRooms);
});
router.get("/chatroom/messages/:chatRoomName", async (req, res, next) => {
  try {
    const chatRoomName = req.params.chatRoomName;
    const chatRooms = await models.ChatRoom.findAll({
      where: {
        name: chatRoomName,
      },
    });
    const chatRoomId = chatRooms[0].id;
    const messages = await models.ChatMessage.findAll({
      where: {
        chatRoomId,
      },
    });
    res.send(messages);
  } catch (error) {
    res.send([]);
  }
});
module.exports = router;

The /chatrooms route get all the chat rooms from the database. The chatroom POST route adds a new chat room if it does not yet exist by looking up any existing one by name. The /chatroom/messages/:chatRoomName route gets the messages for a given chat room by chat room name.

Finally in app.js , we replace the existing code with:

var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var indexRouter = require("./routes/index");
var chatRoomRouter = require("./routes/chatRoom");
var app = express();
const cors = require("cors");

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors());
app.use("/", indexRouter);
app.use("/chatroom", chatRoomRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

and add our chat room routes by adding:

app.use("/chatroom", chatRoomRouter);

Now that back end is done, we can build our front end. Go to the project’s root folder and run npx create-react-app frontend . This create the initial code for front end with the packages installed. Next we need to install some packages ourselves. Run npm i axios bootstrap formik react-bootstrap react-router-dom socket.io-client yup to install our Axios HTTP client, Bootstrap for styling, React Router for routing URLs to our pages, and Formik and Yup for easy form data handling and validation respectively.

After we installed our packages, we can write some code. All files we change are in the src folder except when the path is mentioned explicitly. First, in App.js , we change the existing code to the following:

import React from "react";  
import { Router, Route, Link } from "react-router-dom";  
import HomePage from "./HomePage";  
import TopBar from "./TopBar";  
import { createBrowserHistory as createHistory } from "history";  
import "./App.css";  
import ChatRoomPage from "./ChatRoomPage";  
const history = createHistory();function App() { return (  
    <div className="App">  
      <Router history={history}>  
        <TopBar />  
        <Route path="/" exact component={HomePage} />  
        <Route path="/chatroom" exact component={ChatRoomPage} />  
      </Router>  
    </div>  
  );  
}

export default App;

To define our routes and include the top bar in our app, which will build later. Then in App.css , replace the existing code with:

.App {  
  margin: 0 auto;  
}

Next create a new page called ChatRoomPage.js and add the following:

import React from "react";
import { useEffect, useState } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import io from "socket.io-client";
import "./ChatRoomPage.css";
import { getChatRoomMessages, getChatRooms } from "./requests";
const SOCKET_IO_URL = "http://localhost:3000";
const socket = io(SOCKET_IO_URL);
const getChatData = () => {
  return JSON.parse(localStorage.getItem("chatData"));
};
const schema = yup.object({
  message: yup.string().required("Message is required"),
});
function ChatRoomPage() {
  const [initialized, setInitialized] = useState(false);
  const [messages, setMessages] = useState([]);
  const [rooms, setRooms] = useState([]);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    const data = Object.assign({}, evt);
    data.chatRoomName = getChatData().chatRoomName;
    data.author = getChatData().handle;
    data.message = evt.message;
    socket.emit("message", data);
  };
  const connectToRoom = () => {
    socket.on("connect", data => {
      socket.emit("join", getChatData().chatRoomName);
    });
    socket.on("newMessage", data => {
      getMessages();
    });
    setInitialized(true);
  };
  const getMessages = async () => {
    const response = await getChatRoomMessages(getChatData().chatRoomName);
    setMessages(response.data);
    setInitialized(true);
  };
  const getRooms = async () => {
    const response = await getChatRooms();
    setRooms(response.data);
    setInitialized(true);
  };
  useEffect(() => {
   if (!initialized) {
      getMessages();
      connectToRoom();
      getRooms();
    }
  });
  return (
    <div className="chat-room-page">
      <h1>
        Chat Room: {getChatData().chatRoomName}. Chat Handle:{" "}
        {getChatData().handle}
      </h1>
      <div className="chat-box">
        {messages.map((m, i) => {
          return (
            <div className="col-12" key={i}>
              <div className="row">
                <div className="col-2">{m.author}</div>
                <div className="col">{m.message}</div>
                <div className="col-3">{m.createdAt}</div>
              </div>
            </div>
          );
        })}
      </div>
      <Formik validationSchema={schema} onSubmit={handleSubmit}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="handle">
                <Form.Label>Message</Form.Label>
                <Form.Control
                  type="text"
                  name="message"
                  placeholder="Message"
                  value={values.message || ""}
                  onChange={handleChange}
                  isInvalid={touched.message && errors.message}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.message}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Send
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}
export default ChatRoomPage;

This contains our main chat room code. The user will see the content of this page after going through the home page where they will fill out their chat handle and chat room name. First we connect to our Socket.io server by running const socket = io(SOCKET_IO_URL); Then, we connect to the given chat room name , which we stored in local storage in the connectToRoom function. The function will have the handler for the connect event, which is executed after the connect event is received. Once the event is received, the the client emits the join event by running socket.emit(“join”, getChatData().chatRoomName); , which sends the join event with our chat room name. Once the join event is received by the server. It will call the socket.join function in its event handler. Whenever the user submits a message the handleSubmit function is called, which emits the message event to our Socket.io server. Once the message is delivered to the server, it will save the message to the database and then emit the newMessage event back to the front end. The front end will then get the latest messages using the route we defined in back end using an HTTP request.

Note that we send the chat data to the server via Socket.io instead of HTTP requests, so that all users in the chat room will get the same data right away since the newMessage event will be broadcasted to all clients.

We create a file called ChatRoom.css , then in the file, add:

.chat-room-page {
  width: 90vw;
  margin: 0 auto;
}
.chat-box {
  height: calc(100vh - 300px);
  overflow-y: scroll;
}

Next we create the home page, which is the first page that the users sees when the user first opens the app. It is where the user will enter their chat handle and the name of the chat room. Create a file called HomePage.js and add:

import React from "react";
import { useEffect, useState } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import { Redirect } from "react-router";
import "./HomePage.css";
import { joinRoom } from "./requests";
const schema = yup.object({
  handle: yup.string().required("Handle is required"),
  chatRoomName: yup.string().required("Chat room is required"),
});
function HomePage() {
  const [redirect, setRedirect] = useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    localStorage.setItem("chatData", JSON.stringify(evt));
    await joinRoom(evt.chatRoomName);
    setRedirect(true);
  };
  if (redirect) {
    return <Redirect to="/chatroom" />;
  }
  return (
    <div className="home-page">
      <h1>Join Chat</h1>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={JSON.parse(localStorage.getItem("chatData") || "{}")}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="handle">
                <Form.Label>Handle</Form.Label>
                <Form.Control
                  type="text"
                  name="handle"
                  placeholder="Handle"
                  value={values.handle || ""}
                  onChange={handleChange}
                  isInvalid={touched.handle && errors.handle}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.firstName}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="chatRoomName">
                <Form.Label>Chat Room Name</Form.Label>
                <Form.Control
                  type="text"
                  name="chatRoomName"
                  placeholder="Chat Room Name"
                  value={values.chatRoomName || ""}
                  onChange={handleChange}
                  isInvalid={touched.chatRoomName && errors.chatRoomName}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.chatRoomName}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Join
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}
export default HomePage;

Once the user enters the data into the form, it will be checked if they are filled in and once they are, a request will be sent to back end to add the chat room if it is not there. We also save the filled in data to local storage and redirect the user to the chat room page, where they will connect to the chat room with the name that they entered.

Both forms are built with React Bootstrap’s Form component.

Next we create a file called HomePage.css and add:

.home-page {  
    width: 90vw;  
    margin: 0 auto;  
}

to add some margins to our page.

Then we create a file called requests.js in the src folder to add the code for making the requests to our server for manipulating chat rooms and getting chat messages. In the file, add the following code:

const APIURL = "http://localhost:3000";  
const axios = require("axios");  
export const getChatRooms = () => axios.get(`${APIURL}/chatroom/chatrooms`);
export const getChatRoomMessages = chatRoomName =>  
  axios.get(`${APIURL}/chatroom/chatroom/messages/${chatRoomName}`);
export const joinRoom = room =>  
  axios.post(`${APIURL}/chatroom/chatroom`, { room });

Finally, in we create the top bar. Create a file called TopBar.js and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
  const { pathname } = location;
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Chat Room App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={pathname == "/"}>
            Join Another Chat Room
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}
export default withRouter(TopBar);

We create the top bar using the Navbar widget provided by React Bootstrap with a link to the home page. We wrap the component with the withRouter function so that we get the location object from React Router.

Categories
JavaScript Nodejs

Use JSON Web Tokens to Make a Secure Web App

With single page front end apps and mobile apps being more popular than ever, the front end is decoupled from the back end. Since almost all web apps need authentication, there needs to be a way for front end or mobile apps to store user identity data in a secure fashion.

JSON Web Tokens (JWT) is one of the most common ways to store authentication data on front end apps. With Node.js there are popular libraries that can generate and verify the JWT by check for its authenticity by check against a secret key stored in the back end and also check for expiry date.

The token is encoded in a standard format that is understood by most apps. It usually contains user identity data like user ID, user name, etc. It is given to the user when the user can successfully complete authentication.

In this piece, we will build an app that uses JWT to store authentication data. On the back end, we will use the Express framework, which runs on Node.js, and the jsonwebtoken package for generating and verify the token. For the front end, we will use the Angular framework and the @auth0/angular-jwt module for Angular. In our app, when the user enters user name and password and they are in our database, then a JWT will be generated from our secret key and returned to the user, and stored on the front end app in local storage. Whenever the user needs to access authenticated routes on the back end, they will need the token. There will be a function in the back end app called middleware to check for a valid token. A valid token is one that is not expired and verifies to be valid against our secret key. There will also be signed up, and user credential settings pages in addition to the login page.


Now that we have our plan, we can begin by creating the front and back end app folders. Make one for each. Then we start writing the back end app. First, we install some packages and generate our Express skeleton code. We run npx express-generator to generate the code. Then we need to install some packages. We do that by running npm i @babel/register express-jwt sequelize bcrypt sequelize-cli dotenv jsonwebtoken body-parser cors . @babel/register allows us to use the latest JavaScript features. express-jwt generates the JWT and verifies it against a secret.bcrypt does the hashing and salting of our passwords. sequelize is our ORM for doing CRUD. cors allows our Angular app to communicate with our back end by allowing cross-domain communication. dotenv allows us to store environment variables in an .env file. body-parser is needed for Express to parse JSON requests.

Then we make our database migrations. Run npx sequelize-cli init to generate the skeleton code for our database to object mapping. Then we run:

npx sequelize-cli model:generate --name User --attributes username:string, password:string, email:string

We make another migration and put:

'use strict';

module.exports = {  
  up: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.addConstraint(  
        "Users",  
        ["email"],  
        {  
          type: "unique",  
          name: 'emailUnique'  
        }), 
      queryInterface.addConstraint(  
        "Users",  
        ["userName"],  
        {  
          type: "unique",  
          name: 'userNameUnique'  
        }),  
  }, 

  down: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.removeConstraint(  
        "Users",  
        'emailUnique'  
      ), 
      queryInterface.removeConstraint(  
        "Users",  
        'userNameUnique'  
      ),  
    ])  
  }  
};

This makes sure we don’t have duplicate entries with the same username or email.

This creates the User model and will create the Users table once we run npx sequelize-cli db:migrate .

Let’s write some code. Put the following in app.js:

require("babel/register");  
require("babel-polyfill");  
require('dotenv').config();  
const express = require('express');  
const bodyParser = require('body-parser');  
const cors = require('cors');  
const user = require('./controllers/userController');  
const app = express();
app.use(cors())  
app.use(bodyParser.urlencoded({ extended: true }));  
app.use(bodyParser.json());

app.use((req, res, next) => {  
  res.locals.session = req.session;  
  next();  
});  
  
app.use('/user', user);

app.get('*', (req, res) => {  
  res.redirect('/home');  
});

app.listen((process.env.PORT || 8080), () => {  
  console.log('App running on port 8080!');  
});

We need:

require("babel/register");  
require("babel-polyfill");

to use the latest features in JavaScript.

And we need:

require('dotenv').config();

to read our config in an .env file.

This is the entry point. We will create userController in controllers folder shortly.

app.use(‘/user’, user); routes any URL beginning with user to the userController file.

Next, we add the userController.js file:

const express = require('express');  
const bcrypt = require('bcrypt');  
const router = express.Router();  
const models = require('../models');  
const jwt = require('jsonwebtoken');  
import { authCheck } from '../middlewares/authCheck';

router.post('/login', async (req, res) => {  
    const secret = process.env.JWT_SECRET;  
    const userName = req.body.userName;  
    const password = req.body.password;  
    if (!userName || !password) {  
        return res.send({  
            error: 'User name and password required'  
        })  
    }  
    const users = await models.User.findAll({  
        where: {  
            userName  
        }  
    }) 

    const user = users[0];  
    if (!user) {  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    } 

    try {  
        const compareRes = await bcrypt.compare(password, user.hashedPassword);  
        if (compareRes) {  
            const token = jwt.sign(  
                {  
                    data: {  
                        userName,  
                        userId: user.id  
                    }  
                },  
                secret,  
                { expiresIn: 60 * 60 }  
            );  
            return res.send({ token });  
        }  
        else {  
            res.status(401);  
            return res.send({  
                error: 'Invalid username or password'  
            });  
        }  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    }});

router.post('/signup', async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const password = req.body.password;  
    try {  
        const hashedPassword = await bcrypt.hash(password, 10)  
        await models.User.create({  
            userName,  
            email,  
            hashedPassword  
        })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }  
});

router.put('/updateUser', authCheck, async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const token = req.headers.authorization;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        await models.User.update({  
            userName,  
            email  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

router.put('/updatePassword', authCheck, async (req, res) => {  
    const token = req.headers.authorization;  
    const password = req.body.password;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        const hashedPassword = await bcrypt.hash(password, saltRounds)  
        await models.User.update({  
            hashedPassword  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

module.exports = router;

The login route searches for the User entry, then, if it’s found, checks for the hashed password with the compare function of bcrypt. If both are successful, a JWT is generated. The signup route gets JSON payload of username and password and saves it. Note that there is hashing and salting on the password before saving. Passwords should not be stored as plain text. bcrypt.hash takes two arguments. This first is the plain text password, and the second is the number of salt rounds.

updatePassword route is an authenticated route. It checks for the token and, if it’s valid, will continue to save the user’s password by searching for the User with the user ID that matches the decoded token. We will add the authCheck middleware next.

Create a middlewares folder and create authCheck.js inside it.

const jwt = require('jsonwebtoken');  
const secret = process.env.JWT_SECRET;export const authCheck = (req, res, next) => {  
    if (req.headers.authorization) {  
        const token = req.headers.authorization;  
        jwt.verify(token, secret, (err, decoded) => {  
            if (err) {  
                res.send(401);  
            }  
            else {  
                next();  
            }  
        });  
    }  
    else {  
        res.send(401);  
    }  
}

This allows us to check for authentication in authenticated routes without repeating code. We place if in between the URL and our main route code in each authenticated route by importing this and referencing it.

We make an .env file of the root of the back end app folder, with this content:

DB_HOST='localhost'  
DB_NAME='login-app'  
DB_USERNAME='db-username'  
DB_PASSWORD='db-password'  
JWT_SECRET='secret'

The back end app is now complete. Now we move on to the front end Angular app.

Switch to your front end app folder. To build the Angular app, you need the Angular CLI.

To install it, run npm i -g @angular/cli in your Node.js command prompt. Then, run ng new frontend to generate the skeleton code for your front end app.

Also, install @angular/material according to the Angular documentation.

After that, replace the default app.module.ts with the following:

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';  
import {  
  MatButtonModule,  
  MatCheckboxModule,  
  MatInputModule,  
  MatMenuModule,  
  MatSidenavModule,  
  MatToolbarModule,  
  MatTableModule,  
  MatDialogModule,  
  MAT_DIALOG_DEFAULT_OPTIONS,  
  MatDatepickerModule,  
  MatSelectModule,  
  MatCardModule  
} from '@angular/material';  
import { MatFormFieldModule } from '[@angular/material](http://twitter.com/angular/material)/form-field';  
import { AppRoutingModule } from './app-routing.module';  
import { AppComponent } from './app.component';  
import { StoreModule } from '@ngrx/store';  
import { reducers } from './reducers';  
import { FormsModule } from '@angular/forms';  
import { TopBarComponent } from './top-bar/top-bar.component';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';  
import { SessionService } from './session.service';  
import { HttpReqInterceptor } from './http-req-interceptor';  
import { UserService } from './user.service';  
import { CapitalizePipe } from './capitalize.pipe';

@NgModule({  
  declarations: [  
    AppComponent,  
    TopBarComponent,  
    HomePageComponent,  
    LoginPageComponent,  
    SignUpPageComponent,  
    SettingsPageComponent,  
  ],  
  imports: [  
    BrowserModule,  
    AppRoutingModule,  
    StoreModule.forRoot(reducers),  
    BrowserAnimationsModule,  
    MatButtonModule,  
    MatCheckboxModule,  
    MatFormFieldModule,  
    MatInputModule,  
    MatMenuModule,  
    MatSidenavModule,  
    MatToolbarModule,  
    MatTableModule,  
    FormsModule,  
    HttpClientModule,  
    MatDialogModule,  
    MatDatepickerModule,  
    MatMomentDateModule,  
    MatSelectModule,  
    MatCardModule,  
    NgxMaterialTimepickerModule  
  ],  
  providers: [  
    SessionService,  
    {  
      provide: HTTP_INTERCEPTORS,  
      useClass: HttpReqInterceptor,  
      multi: true  
    },  
    UserService,  
    {  
      provide: MAT_DIALOG_DEFAULT_OPTIONS,  
      useValue: { hasBackdrop: false }  
    },  
  ],  
  bootstrap: [AppComponent],  
})  
export class AppModule { }

This creates all the dependencies and components which we will add. In order to make authenticated requests easy with our token, we create an HTTP request interceptor by creating http-req-interceptor.ts:

import { Injectable } from '@angular/core';  
import {  
    HttpEvent,  
    HttpInterceptor,  
    HttpHandler,  
    HttpResponse,  
    HttpErrorResponse,  
    HttpRequest  
} from '@angular/common/http';  
import { Observable } from 'rxjs';  
import { environment } from '../environments/environment'  
import { map, filter, tap } from 'rxjs/operators';  
import { Router } from '@angular/router';

@Injectable()  
export class HttpReqInterceptor implements HttpInterceptor {  
    constructor(  
        public router: Router  
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
        let modifiedReq = req.clone({}); 
        if (localStorage.getItem('token')) {  
            modifiedReq = modifiedReq.clone({  
                setHeaders: {  
                    authorization: localStorage.getItem('token')  
                }  
            });  
        } 
        return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => {  
            if (event instanceof HttpResponse) {}  
        });  
    }  
}

We set the token in all requests except the login request.

In our environments/environment.ts, we have:

export const environment = {  
  production: false,  
  apiUrl: 'http://localhost:8080'
};

This points to our back end’s URL.

Now we need to make our side nav. We want to add @ngrx/store to store the side nav’s state. We install the package by running npm install @ngrx/store --save. We add our reducer by running ng add @ngrx/store to add our reducers.

We add menu-reducers.ts to set the state centrally in our flux store:

const TOGGLE_MENU = 'TOGGLE_MENU';function menuReducer(state, action) {  
    switch (action.type) {  
        case TOGGLE_MENU:  
            state = action.payload;  
            return state;  
        default:  
            return state  
    }  
}

export { menuReducer, TOGGLE_MENU };

To link our reducer to other parts of the app, put the following in index.ts:

import { menuReducer } from './menu-reducer';  
import { tweetsReducer } from './tweets-reducer';export const reducers = {  
  menu: menuReducer,  
};

To get our Material Design look, add the following to style.css:

/* You can add global styles to this file, and also import other style files */  
@import "~@angular/material/prebuilt-themes/indigo-pink.css";  
body {  
  font-family: "Roboto", sans-serif;  
  margin: 0;  
}

form {  
  mat-form-field {  
    width: 95vw;  
    margin: 0 auto;  
  }  
}

.center {  
  text-align: center;  
}

Between the head tags in index.html, we add:

<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Then we add a service for the user functions, by running ng g service user . The will create user.service.ts . We then put:

import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { environment } from 'src/environments/environment';  
import { Router } from '[@angular/router](http://twitter.com/angular/router)';  
import { JwtHelperService } from "@auth0/angular-jwt";

const helper = new JwtHelperService();

@Injectable({  
  providedIn: 'root'  
})  
export class UserService { constructor(  
    private http: HttpClient,  
    private router: Router  
  ) { } 

  signUp(data) {  
    return this.http.post(`${environment.apiUrl}/user/signup`, data);  
  } 

  updateUser(data) {  
    return this.http.put(`${environment.apiUrl}/user/updateUser`, data);  
  } 

  updatePassword(data) {  
    return this.http.put(`${environment.apiUrl}/user/updatePassword`, data);  
  } 

  login(data) {  
    return this.http.post(`${environment.apiUrl}/user/login`, data);  
  } 

  logOut() {  
    localStorage.clear();  
    this.router.navigate(['/']);  
  } 

  isAuthenticated() {  
    try {  
      const token = localStorage.getItem('token');  
      const decodedToken = helper.decodeToken(token);  
      const isExpired = helper.isTokenExpired(token);  
      return !!decodedToken && !isExpired;  
    }  
    catch (ex) {  
      return false;  
    }  
  }}

Each function requests a subscription for HTTP request except for the isAuthenticated function which is used to check for the token’s validity.

We also need routing for our app so we can see the pages when we go to the URLs listed below. In app-routing.module.ts, we put:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { TweetsPageComponent } from './tweets-page/tweets-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { PasswordResetRequestPageComponent } from './password-reset-request-page/password-reset-request-page.component';  
import { PasswordResetPageComponent } from './password-reset-page/password-reset-page.component';  
import { IsAuthenticatedGuard } from './is-authenticated.guard';const routes: Routes = [  
  { path: 'login', component: LoginPageComponent },  
  { path: 'signup', component: SignUpPageComponent },  
  { path: 'settings', component: SettingsPageComponent, canActivate: [IsAuthenticatedGuard] },  
  { path: '**', component: HomePageComponent }];

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }

Now we create the parts that are referenced in the file above. We need to prevent people from access authenticated routes without a token, so we need a guard in Angular. We make that by running ng g guard isAuthenticated . This generates is-authenticated.guard.ts.

We put the following in is-authenticated.guard.ts :

import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';  
import { Observable } from 'rxjs';  
import { UserService } from './user.service';

@Injectable({  
  providedIn: 'root'  
})  
export class IsAuthenticatedGuard implements CanActivate {  
  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  canActivate(  
    next: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot  
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {  
    const isAuthenticated = this.userService.isAuthenticated();  
    if (!isAuthenticated) {  
      localStorage.clear();  
      this.router.navigate(['/']);  
    }  
    return isAuthenticated;  
  }}

This uses our isAuthenticated function from UserService to check for a valid token. If it’s not valid, we clear it and redirect it back to the home page.

Now we create the forms for logging in setting the user data after logging. We run ng g component homePage, ng g component loginPage, ng g component topBar, ng g component signUpPage, and ng g component settingsPage . These are for the forms and the top bar components.

The home page is just a static page. We should have home-page.component.html and home-page.component.tsgenerated after running the commands in our last paragraph.

In home-page.component.html we put:

<div class="center">  
    <h1>Home Page</h1>  
</div>

Now we make our login page. In login-page.component.ts, we put:

<div class="center">  
    <h1>Log In</h1>  
</div>  
<form #loginForm='ngForm' (ngSubmit)='login(loginForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='loginData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='loginData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Log In</button>  
    <a mat-raised-button routerLink='/passwordResetRequest'>Reset Password</a>  
</form>

In login-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';
[@Component](http://twitter.com/Component)({  
  selector: 'app-login-page',  
  templateUrl: './login-page.component.html',  
  styleUrls: ['./login-page.component.scss']  
})  
export class LoginPageComponent implements OnInit {  
  loginData: any = <any>{}; 

  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { } 

  ngOnInit() {  
  } 

  login(loginForm: NgForm) {  
    if (loginForm.invalid) {  
      return;  
    }  
    this.userService.login(this.loginData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/settings']);  
      }, err => {  
        alert('Invalid username or password');  
      })  
  }  
}

We make sure that all fields are filled. If they are, the login data will be sent and the token will be saved to local storage if authentication is successful. Otherwise, an error alert will be displayed.

Then in our sign up page, sign-up-page.component.html, we put:

<div class="center">  
    <h1>Sign Up</h1>  
</div>  
<br>  
<form #signUpForm='ngForm' (ngSubmit)='signUp(signUpForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='signUpData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='signUpData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='signUpData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Sign Up</button>  
</form>

and in sign-up-page.component.ts, we put:

import { Component, OnInit } from '[@angular/core](http://twitter.com/angular/core)';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';  
import _ from 'lodash';

@Component({  
  selector: 'app-sign-up-page',  
  templateUrl: './sign-up-page.component.html',  
  styleUrls: ['./sign-up-page.component.scss']  
})  
export class SignUpPageComponent implements OnInit {  
  signUpData: any = <any>{}; constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() {  
  } 

  signUp(signUpForm: NgForm) {  
    if (signUpForm.invalid) {  
      return;  
    }  
    this.userService.signUp(this.signUpData)  
      .subscribe(res => {  
        this.login();  
      }, err => {  
        console.log(err);  
        if (  
          _.has(err, 'error.error.errors') &&  
          Array.isArray(err.error.error.errors) &&  
          err.error.error.errors.length > 0  
        ) {  
          alert(err.error.error.errors[0].message);  
        }  
      })  
  } 

  login() {  
    this.userService.login(this.signUpData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/tweets']);  
      })  
  }  
}

These two pieces of code get the sign-up data and send it to the back end, which will save the file if they are all valid.

Similarly, in the settings-page.component.html:

<div class="center">  
    <h1>Settings</h1>  
</div>  
<br>  
<div>  
    <h2>Update User Info</h2>  
</div>  
<br>  
<form #updateUserForm='ngForm' (ngSubmit)='updateUser(updateUserForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='updateUserData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='updateUserData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update User Info</button>  
</form>  
<br><div>  
    <h2>Update Password</h2>  
</div>  
<br>  
<form #updatePasswordForm='ngForm' (ngSubmit)='updatePassword(updatePasswordForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='updatePasswordData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update Password</button>  
</form>  
<br>
<div *ngIf='currentTwitterUser.id' class="title">  
    <h2>Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Different Twitter Account</button>  
    </div>  
</div>  
<div *ngIf='!currentTwitterUser.id' class="title">  
    <h2>Not Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Twitter Account</button>  
    </div>  
</div>

In settings-page.component.html, we put:

import { Component, OnInit } from '@angular/core';  
import { ActivatedRoute, Router } from '@angular/router';  
import { SessionService } from '../session.service';  
import { NgForm } from '@angular/forms';  
import { UserService } from '../user.service';

@Component({  
  selector: 'app-settings-page',  
  templateUrl: './settings-page.component.html',  
  styleUrls: ['./settings-page.component.scss']  
})  
export class SettingsPageComponent implements OnInit {  
  currentTwitterUser: any = <any>{};  
  elements: any[] = [];  
  displayedColumns: string[] = ['key', 'value'];  
  updateUserData: any = <any>{};  
  updatePasswordData: any = <any>{}; constructor(  
    private sessionService: SessionService,  
    private userService: UserService,  
    private router: Router  
  ) {  
  
  } 

  ngOnInit() {  
  
  } 

  updateUser(updateUserForm: NgForm) {  
    if (updateUserForm.invalid) {  
      return;  
    }  
    this.userService.updateUser(this.updateUserData)  
      .subscribe(res => {  
        alert('Updated user info successful.');  
      }, err => {  
        alert('Updated user info failed.');  
      })  
  } 

  updatePassword(updatePasswordForm: NgForm) {  
    if (updatePasswordForm.invalid) {  
      return;  
    }  
    this.userService.updatePassword(this.updatePasswordData)  
      .subscribe(res => {  
        alert('Updated password successful.');  
      }, err => {  
        alert('Updated password failed.');  
      })  
  }  
}

Similar to other pages, this sends a request payload for changing user data and password to our back end.

Finally, to make our top bar, we put the following in top-bar.component.html:

<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    Twitter Automator  
</mat-toolbar>

And in top-bar.component.ts:

import { Component, OnInit } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from '../reducers/menu-reducer';

@Component({  
  selector: 'app-top-bar',  
  templateUrl: './top-bar.component.html',  
  styleUrls: ['./top-bar.component.scss']  
})  
export class TopBarComponent implements OnInit {  
  menuOpen: boolean; constructor(  
    private store: Store<any>  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  ngOnInit() {  
  } 

  toggleMenu() {  
    this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen });  
  }  
}

In app.component.ts, we put:

import { Component, HostListener } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from './reducers/menu-reducer';  
import { UserService } from './user.service';

@Component({  
  selector: 'app-root',  
  templateUrl: './app.component.html',  
  styleUrls: ['./app.component.scss']  
})  
export class AppComponent {  
  menuOpen: boolean; 

  constructor(  
    private store: Store<any>,  
    private userService: UserService  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  isAuthenticated() {  
    return this.userService.isAuthenticated();  
  }

  @HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

  logOut() {  
    this.userService.logOut();  
  }  
}

and in app.component.html, we have:

<mat-sidenav-container class="example-container">  
    <mat-sidenav mode="side" [opened]='menuOpen'>  
        <ul>  
            <li>  
                <b>  
                    Twitter Automator  
                </b>  
            </li>  
            <li>  
                <a routerLink='/login' *ngIf='!isAuthenticated()'>Log In</a>  
            </li>  
            <li>  
                <a routerLink='/signup' *ngIf='!isAuthenticated()'>Sign Up</a>  
            </li>  
            <li>  
                <a href='#' (click)='logOut()' *ngIf='isAuthenticated()'>Log Out</a>  
            </li>  
            <li>  
                <a routerLink='/tweets' *ngIf='isAuthenticated()'>Tweets</a>  
            </li>  
            <li>  
                <a routerLink='/settings' *ngIf='isAuthenticated()'>Settings</a>  
            </li>  
        </ul>
    </mat-sidenav>  
    <mat-sidenav-content>  
        <app-top-bar></app-top-bar>  
        <div id='content'>  
            <router-outlet></router-outlet>  
        </div>  
    </mat-sidenav-content>  
</mat-sidenav-container>

This allows us to toggle our side navigation menu. Note that we have:

@HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

to detect clicks out the side nav. If we click outside, i.e. not clicking on any element with those classes, then we close the menu. this.store.dispatch propagates the closed state to all components.